private static Query<JsonNode> buildQuery(CentralDogmaBeanConfig config) { checkArgument(config.path().get().endsWith(".json"), "path: %s (expected: ends with '.json')", config.path().get()); return Query.ofJsonPath(config.path().get(), config.jsonPath().orElse("$")); } }
/** * Creates a new instance. */ public <U> FileWatcher(CentralDogma client, ScheduledExecutorService executor, String projectName, String repositoryName, Query<U> query, Function<? super U, ? extends T> function) { super(client, executor, projectName, repositoryName, requireNonNull(query, "query").path()); this.query = unsafeCast(query); this.function = unsafeCast(requireNonNull(function, "function")); }
/** * Converts the specified {@code request} to {@link Optional} which contains {@link Query} when * the request has a valid file path. {@link Optional#EMPTY} otherwise. */ @Override public Object convertRequest(ServiceRequestContext ctx, AggregatedHttpMessage request, Class<?> expectedResultType) throws Exception { final String path = getPath(ctx); final Optional<Iterable<String>> jsonPaths = getJsonPaths(ctx); if (jsonPaths.isPresent()) { return Optional.of(Query.ofJsonPath(path, jsonPaths.get())); } if (isValidFilePath(path)) { return Optional.of(Query.of(QueryType.IDENTITY, path)); } return Optional.empty(); }
@Override protected Query doForward(com.linecorp.centraldogma.common.Query<?> query) { switch (query.type()) { case IDENTITY: return new Query(query.path(), QueryType.IDENTITY, Collections.emptyList()); case JSON_PATH: return new Query(query.path(), QueryType.JSON_PATH, query.expressions()); } throw new Error(); }
@Override int weigh(Entry<?> value) { int weight = 0; weight += query.path().length(); for (String e : query.expressions()) { weight += e.length(); } if (value != null && value.hasContent()) { weight += value.contentAsText().length(); } return weight; }
/** * Performs the specified {@link Query}. * * @return the {@link Entry} on a successful query. * The specified {@code other} on a failure due to missing entry. * * @see #get(Revision, Query) */ default <T> CompletableFuture<Entry<T>> getOrNull(Revision revision, Query<T> query) { requireNonNull(query, "query"); requireNonNull(revision, "revision"); return getOrNull(revision, query.path()).thenApply(entry -> { if (entry == null) { return null; } final EntryType entryType = entry.type(); if (!query.type().supportedEntryTypes().contains(entryType)) { throw new QueryExecutionException("unsupported entry type: " + entryType); } @SuppressWarnings("unchecked") final T entryContent = (T) entry.content(); try { return Entry.of(entry.revision(), query.path(), entryType, query.apply(entryContent)); } catch (CentralDogmaException e) { throw e; } catch (Exception e) { throw new QueryExecutionException(e); } }); }
@Test @SuppressWarnings("unchecked") public void jsonPathQuery() throws JsonParseException { final Repository repo = setMockNames(newCachingRepo()); final Query<JsonNode> query = Query.ofJsonPath("/baz.json", "$.a"); final Entry<JsonNode> queryResult = Entry.ofJson(new Revision(10), query.path(), "{\"a\": \"b\"}"); doReturn(new Revision(10)).when(delegateRepo).normalizeNow(new Revision(10)); doReturn(new Revision(10)).when(delegateRepo).normalizeNow(HEAD); // Uncached when(delegateRepo.getOrNull(any(), any(Query.class))).thenReturn(completedFuture(queryResult)); assertThat(repo.get(HEAD, query).join()).isEqualTo(queryResult); verify(delegateRepo).getOrNull(new Revision(10), query); verifyNoMoreInteractions(delegateRepo); // Cached clearInvocations(delegateRepo); assertThat(repo.get(HEAD, query).join()).isEqualTo(queryResult); assertThat(repo.get(new Revision(10), query).join()).isEqualTo(queryResult); verify(delegateRepo, never()).getOrNull(any(), any(Query.class)); verifyNoMoreInteractions(delegateRepo); }
@Override public <T> CompletableFuture<Entry<T>> getOrNull(Revision revision, Query<T> query) { requireNonNull(revision, "revision"); requireNonNull(query, "query"); final Revision normalizedRevision; try { normalizedRevision = normalizeNow(revision); } catch (Exception e) { return CompletableFutures.exceptionallyCompletedFuture(e); } if (query.type() == QueryType.IDENTITY) { // If the query is an IDENTITY type, call find() so that the caches are reused in one place when // calls getOrNull(), find() and mergeFiles(). final String path = query.path(); final CompletableFuture<Entry<?>> future = find(revision, path, FIND_ONE_WITH_CONTENT).thenApply(findResult -> findResult.get(path)); return unsafeCast(future); } final CompletableFuture<Object> future = cache.get(new CacheableQueryCall(repo, normalizedRevision, query)) .thenApply(result -> result != CacheableQueryCall.EMPTY ? result : null); return unsafeCast(future); }
@Test public void testJsonConversion() { TestUtil.assertJsonConversion(Query.ofText("/foo.txt"), Query.class, '{' + " \"type\": \"IDENTITY\"," + " \"path\": \"/foo.txt\"" + '}'); TestUtil.assertJsonConversion(Query.ofJsonPath("/bar.json", "$..author", "$..name"), Query.class, '{' + " \"type\": \"JSON_PATH\"," + " \"path\": \"/bar.json\"," + " \"expressions\": [ \"$..author\", \"$..name\" ]" + '}'); } }
/** * Queries a file at the specified revision and path with the specified {@link Query}. */ <T> CompletableFuture<Entry<T>> getFile(String projectName, String repositoryName, Revision revision, Query<T> query);
@Test public void singleDiff() { final Repository repo = setMockNames(newCachingRepo()); final Query<String> query = Query.ofText("/foo.txt"); final Change<String> change = Change.ofTextUpsert(query.path(), "bar"); doReturn(new RevisionRange(10, 1)).when(delegateRepo).normalizeNow(HEAD, INIT); doReturn(new RevisionRange(10, 1)).when(delegateRepo).normalizeNow(new Revision(10), INIT); doReturn(new RevisionRange(10, 1)).when(delegateRepo).normalizeNow(new Revision(-1), new Revision(-10)); doReturn(new RevisionRange(10, 1)).when(delegateRepo).normalizeNow(new Revision(10), new Revision(-10)); // Uncached when(delegateRepo.diff(any(), any(), any(Query.class))).thenReturn(completedFuture(change)); assertThat(repo.diff(HEAD, INIT, query).join()).isEqualTo(change); verify(delegateRepo).diff(INIT, new Revision(10), query); verifyNoMoreInteractions(delegateRepo); // Cached clearInvocations(delegateRepo); assertThat(repo.diff(HEAD, new Revision(-10), query).join()).isEqualTo(change); assertThat(repo.diff(HEAD, INIT, query).join()).isEqualTo(change); assertThat(repo.diff(new Revision(10), new Revision(-10), query).join()).isEqualTo(change); assertThat(repo.diff(new Revision(10), INIT, query).join()).isEqualTo(change); verify(delegateRepo, never()).diff(any(), any(), any(Query.class)); verifyNoMoreInteractions(delegateRepo); }
@Test public void diffFile() throws Exception { doAnswer(invocation -> { final AsyncMethodCallback<DiffFileResult> callback = invocation.getArgument(5); callback.onComplete(new DiffFileResult(ChangeType.UPSERT_TEXT, "some_text")); return null; }).when(iface).diffFile(any(), any(), any(), any(), any(), any()); assertThat(client.getDiff("project", "repo", new Revision(1), new Revision(3), Query.ofText("/a.txt")).get()) .isEqualTo(Change.ofTextUpsert("/a.txt", "some_text")); verify(iface).diffFile(eq("project"), eq("repo"), any(), any(), any(), any()); }
/** * Applies the specified {@link Query} to the {@link Entry#content()} of the specified {@link Entry} and * returns the query result. * * @throws IllegalStateException if the specified {@link Entry} is a directory * @throws QuerySyntaxException if the syntax of specified {@link Query} is invalid * @throws QueryExecutionException if an {@link Exception} is raised while applying the specified * {@link Query} to the {@link Entry#content()} */ static <T> Entry<T> applyQuery(Entry<T> entry, Query<T> query) { requireNonNull(query, "query"); entry.content(); // Ensure that content is not null. final EntryType entryType = entry.type(); final QueryType queryType = query.type(); if (!queryType.supportedEntryTypes().contains(entryType)) { throw new QueryExecutionException("Unsupported entry type: " + entryType + " (query: " + query + ')'); } if (queryType == IDENTITY) { return entry; } else if (queryType == JSON_PATH) { return Entry.of(entry.revision(), query.path(), entryType, query.apply(entry.content())); } else { throw new QueryExecutionException("Unsupported entry type: " + entryType + " (query: " + query + ')'); } }
@Override int weigh(Entry<?> value) { int weight = 0; weight += query.path().length(); for (String e : query.expressions()) { weight += e.length(); } if (value != null && value.hasContent()) { weight += value.contentAsText().length(); } return weight; }
@Override protected Query doForward(com.linecorp.centraldogma.common.Query<?> query) { switch (query.type()) { case IDENTITY: return new Query(query.path(), QueryType.IDENTITY, Collections.emptyList()); case JSON_PATH: return new Query(query.path(), QueryType.JSON_PATH, query.expressions()); } throw new Error(); }
@Override public <T> CompletableFuture<Entry<T>> getOrNull(Revision revision, Query<T> query) { requireNonNull(revision, "revision"); requireNonNull(query, "query"); final Revision normalizedRevision; try { normalizedRevision = normalizeNow(revision); } catch (Exception e) { return CompletableFutures.exceptionallyCompletedFuture(e); } if (query.type() == QueryType.IDENTITY) { // If the query is an IDENTITY type, call find() so that the caches are reused in one place when // calls getOrNull(), find() and mergeFiles(). final String path = query.path(); final CompletableFuture<Entry<?>> future = find(revision, path, FIND_ONE_WITH_CONTENT).thenApply(findResult -> findResult.get(path)); return unsafeCast(future); } final CompletableFuture<Object> future = cache.get(new CacheableQueryCall(repo, normalizedRevision, query)) .thenApply(result -> result != CacheableQueryCall.EMPTY ? result : null); return unsafeCast(future); }
@Test public void testJsonConversion() { assertJsonConversion(new SaveNamedQueryCommand(1234L, Author.SYSTEM, "foo", "bar", true, "qux", Query.ofText("/first.txt"), "plaintext comment", Markup.PLAINTEXT), Command.class, Query.ofJsonPath("/second.json", "$..author", "$..name"), "markdown comment", Markup.MARKDOWN), Command.class,
/** * Queries a file at two different revisions and returns the diff of the two {@link Query} results. */ <T> CompletableFuture<Change<T>> getDiff(String projectName, String repositoryName, Revision from, Revision to, Query<T> query);
@Test public void watchFileTimedOut() throws Exception { doAnswer(invocation -> { AsyncMethodCallback<WatchFileResult> callback = invocation.getArgument(5); callback.onComplete(new WatchFileResult()); return null; }).when(iface).watchFile(any(), any(), any(), any(), anyLong(), any()); assertThat(client.watchFile("project", "repo", new Revision(1), Query.ofText("/a.txt"), 100).get()).isNull(); verify(iface).watchFile(eq("project"), eq("repo"), any(), any(), eq(100L), any()); } }
/** * Returns a newly-created {@link Query} that applies a series of expressions to the content. * * @param type the type of the {@link Query} * @param path the path of a file being queried on * @param expressions the expressions to apply */ static Query<?> of(QueryType type, String path, @Nullable String... expressions) { requireNonNull(type, "type"); switch (type) { case IDENTITY: return new IdentityQuery<>(path); case JSON_PATH: requireNonNull(expressions, "expressions"); return ofJsonPath(path, expressions); default: throw new IllegalArgumentException("Illegal query type: " + type.name()); } }