diff --git a/client/java-armeria-legacy/src/main/java/com/linecorp/centraldogma/client/armeria/legacy/LegacyCentralDogma.java b/client/java-armeria-legacy/src/main/java/com/linecorp/centraldogma/client/armeria/legacy/LegacyCentralDogma.java index 2b6495a3cf..af4e4285fe 100644 --- a/client/java-armeria-legacy/src/main/java/com/linecorp/centraldogma/client/armeria/legacy/LegacyCentralDogma.java +++ b/client/java-armeria-legacy/src/main/java/com/linecorp/centraldogma/client/armeria/legacy/LegacyCentralDogma.java @@ -20,7 +20,6 @@ import static com.google.common.collect.ImmutableMap.toImmutableMap; import static com.google.common.collect.ImmutableSet.toImmutableSet; import static com.linecorp.centraldogma.internal.Util.unsafeCast; -import static com.linecorp.centraldogma.internal.Util.validatePathPattern; import static com.linecorp.centraldogma.internal.Util.validateRepositoryName; import static java.util.Objects.requireNonNull; @@ -58,6 +57,7 @@ import com.linecorp.centraldogma.common.Markup; import com.linecorp.centraldogma.common.MergeQuery; import com.linecorp.centraldogma.common.MergedEntry; +import com.linecorp.centraldogma.common.PathPattern; import com.linecorp.centraldogma.common.ProjectExistsException; import com.linecorp.centraldogma.common.ProjectNotFoundException; import com.linecorp.centraldogma.common.PushResult; @@ -168,16 +168,16 @@ public CompletableFuture purgeRepository(String projectName, String reposi @Override public CompletableFuture unremoveRepository(String projectName, String repositoryName) { + validateProjectAndRepositoryName(projectName, repositoryName); return run(callback -> { - validateProjectAndRepositoryName(projectName, repositoryName); client.unremoveRepository(projectName, repositoryName, callback); }); } @Override public CompletableFuture> listRepositories(String projectName) { + validateProjectName(projectName); final CompletableFuture> future = run(callback -> { - validateProjectName(projectName); client.listRepositories(projectName, callback); }); return future.thenApply(list -> convertToMap( @@ -190,8 +190,8 @@ public CompletableFuture> listRepositories(String pr @Override public CompletableFuture> listRemovedRepositories(String projectName) { + validateProjectName(projectName); return run(callback -> { - validateProjectName(projectName); client.listRemovedRepositories(projectName, callback); }); } @@ -199,9 +199,9 @@ public CompletableFuture> listRemovedRepositories(String projectName @Override public CompletableFuture normalizeRevision(String projectName, String repositoryName, Revision revision) { + validateProjectAndRepositoryName(projectName, repositoryName); + requireNonNull(revision, "revision"); final CompletableFuture future = run(callback -> { - validateProjectAndRepositoryName(projectName, repositoryName); - requireNonNull(revision, "revision"); client.normalizeRevision(projectName, repositoryName, RevisionConverter.TO_DATA.convert(revision), callback); }); @@ -210,16 +210,15 @@ public CompletableFuture normalizeRevision(String projectName, String @Override public CompletableFuture> listFiles(String projectName, String repositoryName, - Revision revision, String pathPattern) { + Revision revision, PathPattern pathPattern) { + validateProjectAndRepositoryName(projectName, repositoryName); + requireNonNull(revision, "revision"); + requireNonNull(pathPattern, "pathPattern"); final CompletableFuture> future = run(callback -> { - validateProjectAndRepositoryName(projectName, repositoryName); - requireNonNull(revision, "revision"); - validatePathPattern(pathPattern, "pathPattern"); - client.listFiles(projectName, repositoryName, RevisionConverter.TO_DATA.convert(revision), - pathPattern, callback); + pathPattern.patternString(), callback); }); return future.thenApply(list -> list.stream().collect(toImmutableMap( com.linecorp.centraldogma.internal.thrift.Entry::getPath, @@ -229,9 +228,9 @@ public CompletableFuture> listFiles(String projectName, S @Override public CompletableFuture> getFile(String projectName, String repositoryName, Revision revision, Query query) { + requireNonNull(query, "query"); return maybeNormalizeRevision(projectName, repositoryName, revision).thenCompose(normRev -> { final CompletableFuture future = run(callback -> { - requireNonNull(query, "query"); client.getFile(projectName, repositoryName, RevisionConverter.TO_DATA.convert(normRev), QueryConverter.TO_DATA.convert(query), callback); @@ -285,15 +284,14 @@ private static Entry entryAsText(Query query, Revision normRev, String @Override public CompletableFuture>> getFiles(String projectName, String repositoryName, - Revision revision, String pathPattern) { - + Revision revision, PathPattern pathPattern) { + requireNonNull(pathPattern, "pathPattern"); return maybeNormalizeRevision(projectName, repositoryName, revision).thenCompose(normRev -> { final CompletableFuture> future = run(callback -> { - validatePathPattern(pathPattern, "pathPattern"); client.getFiles(projectName, repositoryName, RevisionConverter.TO_DATA.convert(normRev), - pathPattern, callback); + pathPattern.patternString(), callback); }); return future.thenApply(list -> convertToMap(list, e -> EntryConverter.convert(normRev, e), Entry::path, Function.identity())); @@ -303,11 +301,11 @@ public CompletableFuture>> getFiles(String projectName, Str @Override public CompletableFuture> mergeFiles(String projectName, String repositoryName, Revision revision, MergeQuery mergeQuery) { + validateProjectAndRepositoryName(projectName, repositoryName); + requireNonNull(revision, "revision"); + requireNonNull(mergeQuery, "mergeQuery"); final CompletableFuture future = run(callback -> { - validateProjectAndRepositoryName(projectName, repositoryName); - requireNonNull(revision, "revision"); - requireNonNull(mergeQuery, "mergeQuery"); client.mergeFiles(projectName, repositoryName, RevisionConverter.TO_DATA.convert(revision), MergeQueryConverter.TO_DATA.convert(mergeQuery), @@ -339,30 +337,27 @@ public CompletableFuture> getHistory(String projectName, String repositoryName, Revision from, Revision to, - String pathPattern) { + PathPattern pathPattern) { + validateProjectAndRepositoryName(projectName, repositoryName); + requireNonNull(from, "from"); + requireNonNull(to, "to"); + requireNonNull(pathPattern, "pathPattern"); final CompletableFuture> future = - run(callback -> { - validateProjectAndRepositoryName(projectName, repositoryName); - requireNonNull(from, "from"); - requireNonNull(to, "to"); - validatePathPattern(pathPattern, "pathPattern"); - - client.getHistory(projectName, repositoryName, - RevisionConverter.TO_DATA.convert(from), - RevisionConverter.TO_DATA.convert(to), - pathPattern, callback); - }); + run(callback -> client.getHistory(projectName, repositoryName, + RevisionConverter.TO_DATA.convert(from), + RevisionConverter.TO_DATA.convert(to), + pathPattern.patternString(), callback)); return future.thenApply(list -> convertToList(list, CommitConverter.TO_MODEL::convert)); } @Override public CompletableFuture> getDiff(String projectName, String repositoryName, Revision from, Revision to, Query query) { + validateProjectAndRepositoryName(projectName, repositoryName); + requireNonNull(from, "from"); + requireNonNull(to, "to"); + requireNonNull(query, "query"); final CompletableFuture future = run(callback -> { - validateProjectAndRepositoryName(projectName, repositoryName); - requireNonNull(from, "from"); - requireNonNull(to, "to"); - requireNonNull(query, "query"); client.diffFile(projectName, repositoryName, RevisionConverter.TO_DATA.convert(from), RevisionConverter.TO_DATA.convert(to), @@ -402,17 +397,18 @@ public CompletableFuture> getDiff(String projectName, String repos } @Override - public CompletableFuture>> getDiffs(String projectName, String repositoryName, - Revision from, Revision to, String pathPattern) { + public CompletableFuture>> getDiff(String projectName, String repositoryName, + Revision from, Revision to, PathPattern pathPattern) { + validateProjectAndRepositoryName(projectName, repositoryName); + requireNonNull(from, "from"); + requireNonNull(to, "to"); + requireNonNull(pathPattern, "pathPattern"); final CompletableFuture> future = run(callback -> { - validateProjectAndRepositoryName(projectName, repositoryName); - requireNonNull(from, "from"); - requireNonNull(to, "to"); - validatePathPattern(pathPattern, "pathPattern"); client.getDiffs(projectName, repositoryName, RevisionConverter.TO_DATA.convert(from), - RevisionConverter.TO_DATA.convert(to), pathPattern, callback); + RevisionConverter.TO_DATA.convert(to), pathPattern.patternString(), + callback); }); return future.thenApply(list -> convertToList(list, ChangeConverter.TO_MODEL::convert)); } @@ -421,16 +417,14 @@ public CompletableFuture>> getDiffs(String projectName, String re public CompletableFuture>> getPreviewDiffs(String projectName, String repositoryName, Revision baseRevision, Iterable> changes) { + validateProjectAndRepositoryName(projectName, repositoryName); + requireNonNull(baseRevision, "baseRevision"); + requireNonNull(changes, "changes"); final CompletableFuture> future = - run(callback -> { - validateProjectAndRepositoryName(projectName, repositoryName); - requireNonNull(baseRevision, "baseRevision"); - requireNonNull(changes, "changes"); - client.getPreviewDiffs( - projectName, repositoryName, - RevisionConverter.TO_DATA.convert(baseRevision), - convertToList(changes, ChangeConverter.TO_DATA::convert), callback); - }); + run(callback -> client.getPreviewDiffs( + projectName, repositoryName, + RevisionConverter.TO_DATA.convert(baseRevision), + convertToList(changes, ChangeConverter.TO_DATA::convert), callback)); return future.thenApply(LegacyCentralDogma::convertToChangesModel); } @@ -446,15 +440,15 @@ public CompletableFuture push(String projectName, String repositoryN public CompletableFuture push(String projectName, String repositoryName, Revision baseRevision, Author author, String summary, String detail, Markup markup, Iterable> changes) { + validateProjectAndRepositoryName(projectName, repositoryName); + requireNonNull(baseRevision, "baseRevision"); + requireNonNull(author, "author"); + requireNonNull(summary, "summary"); + requireNonNull(detail, "detail"); + requireNonNull(markup, "markup"); + requireNonNull(changes, "changes"); + checkArgument(!Iterables.isEmpty(changes), "changes is empty."); final CompletableFuture future = run(callback -> { - validateProjectAndRepositoryName(projectName, repositoryName); - requireNonNull(baseRevision, "baseRevision"); - requireNonNull(author, "author"); - requireNonNull(summary, "summary"); - requireNonNull(detail, "detail"); - requireNonNull(markup, "markup"); - requireNonNull(changes, "changes"); - checkArgument(!Iterables.isEmpty(changes), "changes is empty."); client.push(projectName, repositoryName, RevisionConverter.TO_DATA.convert(baseRevision), AuthorConverter.TO_DATA.convert(author), summary, @@ -468,15 +462,17 @@ public CompletableFuture push(String projectName, String repositoryN @Override public CompletableFuture watchRepository(String projectName, String repositoryName, Revision lastKnownRevision, - String pathPattern, - long timeoutMillis) { + PathPattern pathPattern, + long timeoutMillis, + boolean errorOnEntryNotFound) { + // Legacy client does not support 'errorOnEntryNotFound' + validateProjectAndRepositoryName(projectName, repositoryName); + requireNonNull(lastKnownRevision, "lastKnownRevision"); + requireNonNull(pathPattern, "pathPattern"); final CompletableFuture future = run(callback -> { - validateProjectAndRepositoryName(projectName, repositoryName); - requireNonNull(lastKnownRevision, "lastKnownRevision"); - validatePathPattern(pathPattern, "pathPattern"); client.watchRepository(projectName, repositoryName, RevisionConverter.TO_DATA.convert(lastKnownRevision), - pathPattern, timeoutMillis, + pathPattern.patternString(), timeoutMillis, callback); }); return future.thenApply(r -> { @@ -491,12 +487,12 @@ public CompletableFuture watchRepository(String projectName, String re @Override public CompletableFuture> watchFile(String projectName, String repositoryName, Revision lastKnownRevision, Query query, - long timeoutMillis) { - + long timeoutMillis, boolean errorOnEntryNotFound) { + // Legacy client does not support 'errorOnEntryNotFound' + validateProjectAndRepositoryName(projectName, repositoryName); + requireNonNull(lastKnownRevision, "lastKnownRevision"); + requireNonNull(query, "query"); final CompletableFuture future = run(callback -> { - validateProjectAndRepositoryName(projectName, repositoryName); - requireNonNull(lastKnownRevision, "lastKnownRevision"); - requireNonNull(query, "query"); client.watchFile(projectName, repositoryName, RevisionConverter.TO_DATA.convert(lastKnownRevision), QueryConverter.TO_DATA.convert(query), diff --git a/client/java-armeria-legacy/src/test/java/com/linecorp/centraldogma/client/armeria/legacy/LegacyCentralDogmaTest.java b/client/java-armeria-legacy/src/test/java/com/linecorp/centraldogma/client/armeria/legacy/LegacyCentralDogmaTest.java index 656fd8dc13..ac9f31d568 100644 --- a/client/java-armeria-legacy/src/test/java/com/linecorp/centraldogma/client/armeria/legacy/LegacyCentralDogmaTest.java +++ b/client/java-armeria-legacy/src/test/java/com/linecorp/centraldogma/client/armeria/legacy/LegacyCentralDogmaTest.java @@ -54,6 +54,7 @@ import com.linecorp.centraldogma.common.Markup; import com.linecorp.centraldogma.common.MergeQuery; import com.linecorp.centraldogma.common.MergeSource; +import com.linecorp.centraldogma.common.PathPattern; import com.linecorp.centraldogma.common.PushResult; import com.linecorp.centraldogma.common.Query; import com.linecorp.centraldogma.common.Revision; @@ -242,7 +243,7 @@ void listFiles() throws Exception { callback.onComplete(ImmutableList.of(entry)); return null; }).when(iface).listFiles(anyString(), anyString(), any(), anyString(), any()); - assertThat(client.listFiles("project", "repo", new Revision(1), "/a.txt").get()) + assertThat(client.listFiles("project", "repo", new Revision(1), PathPattern.of("/a.txt")).get()) .isEqualTo(ImmutableMap.of("/a.txt", EntryType.TEXT)); verify(iface).listFiles(anyString(), anyString(), any(), anyString(), any()); } @@ -256,7 +257,7 @@ void getFiles() throws Exception { callback.onComplete(ImmutableList.of(entry)); return null; }).when(iface).getFiles(anyString(), anyString(), any(), anyString(), any()); - assertThat(client.getFiles("project", "repo", new Revision(1), "path").get()) + assertThat(client.getFiles("project", "repo", new Revision(1), PathPattern.of("path")).get()) .isEqualTo(ImmutableMap.of("/b.txt", Entry.ofText(new Revision(1), "/b.txt", "world"))); verify(iface).getFiles(anyString(), anyString(), any(), anyString(), any()); } @@ -274,12 +275,13 @@ void getHistory() throws Exception { ImmutableList.of(new TChange("/a.txt", ChangeType.UPSERT_TEXT).setContent("content"))))); return null; }).when(iface).getHistory(any(), any(), any(), any(), any(), any()); - assertThat(client.getHistory("project", "repo", new Revision(1), new Revision(3), "path").get()) + assertThat(client.getHistory("project", "repo", new Revision(1), new Revision(3), + PathPattern.of("path")).get()) .isEqualTo(ImmutableList.of(new Commit(new Revision(1), new Author("name", "name@sample.com"), Instant.parse(TIMESTAMP).toEpochMilli(), "summary", "detail", Markup.PLAINTEXT))); - verify(iface).getHistory(eq("project"), eq("repo"), any(), any(), eq("path"), any()); + verify(iface).getHistory(eq("project"), eq("repo"), any(), any(), eq("/**/path"), any()); } @Test @@ -291,9 +293,10 @@ void getDiffs() throws Exception { callback.onComplete(ImmutableList.of(change)); return null; }).when(iface).getDiffs(any(), any(), any(), any(), any(), any()); - assertThat(client.getDiffs("project", "repo", new Revision(1), new Revision(3), "path").get()) + assertThat(client.getDiff("project", "repo", new Revision(1), new Revision(3), PathPattern.of("path")) + .get()) .isEqualTo(ImmutableList.of(Change.ofTextUpsert("/a.txt", "content"))); - verify(iface).getDiffs(eq("project"), eq("repo"), any(), any(), eq("path"), any()); + verify(iface).getDiffs(eq("project"), eq("repo"), any(), any(), eq("/**/path"), any()); } @Test @@ -395,7 +398,8 @@ void watchRepository() throws Exception { callback.onComplete(new WatchRepositoryResult().setRevision(new TRevision(42))); return null; }).when(iface).watchRepository(any(), any(), any(), anyString(), anyLong(), any()); - assertThat(client.watchRepository("project", "repo", new Revision(1), "/a.txt", 100).get()) + assertThat(client.watchRepository("project", "repo", new Revision(1), + PathPattern.of("/a.txt"), 100, false).get()) .isEqualTo(new Revision(42)); verify(iface).watchRepository(eq("project"), eq("repo"), any(), eq("/a.txt"), eq(100L), any()); } @@ -407,7 +411,8 @@ void watchRepositoryTimedOut() throws Exception { callback.onComplete(new WatchRepositoryResult()); return null; }).when(iface).watchRepository(any(), any(), any(), anyString(), anyLong(), any()); - assertThat(client.watchRepository("project", "repo", new Revision(1), "/a.txt", 100).get()) + assertThat(client.watchRepository("project", "repo", new Revision(1), + PathPattern.of("/a.txt"), 100, false).get()) .isNull(); verify(iface).watchRepository(eq("project"), eq("repo"), any(), eq("/a.txt"), eq(100L), any()); } @@ -421,7 +426,8 @@ void watchFile() throws Exception { .setContent("foo")); 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()) + assertThat(client.watchFile("project", "repo", new Revision(1), + Query.ofText("/a.txt"), 100, false).get()) .isEqualTo(Entry.ofText(new Revision(42), "/a.txt", "foo")); verify(iface).watchFile(eq("project"), eq("repo"), any(), any(), eq(100L), any()); } @@ -434,7 +440,7 @@ void watchFileTimedOut() throws Exception { 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(); + Query.ofText("/a.txt"), 100, false).get()).isNull(); verify(iface).watchFile(eq("project"), eq("repo"), any(), any(), eq(100L), any()); } } diff --git a/client/java-armeria/src/main/java/com/linecorp/centraldogma/client/armeria/ArmeriaCentralDogma.java b/client/java-armeria/src/main/java/com/linecorp/centraldogma/client/armeria/ArmeriaCentralDogma.java index a49749aca2..195ff9224d 100644 --- a/client/java-armeria/src/main/java/com/linecorp/centraldogma/client/armeria/ArmeriaCentralDogma.java +++ b/client/java-armeria/src/main/java/com/linecorp/centraldogma/client/armeria/ArmeriaCentralDogma.java @@ -22,7 +22,6 @@ import static com.google.common.collect.ImmutableMap.toImmutableMap; import static com.google.common.collect.ImmutableSet.toImmutableSet; import static com.linecorp.centraldogma.internal.Util.unsafeCast; -import static com.linecorp.centraldogma.internal.Util.validatePathPattern; import static com.linecorp.centraldogma.internal.api.v1.HttpApiV1Constants.PROJECTS_PREFIX; import static com.linecorp.centraldogma.internal.api.v1.HttpApiV1Constants.REMOVED; import static com.linecorp.centraldogma.internal.api.v1.HttpApiV1Constants.REPOS; @@ -52,13 +51,11 @@ import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.JsonNodeType; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Streams; -import com.google.common.math.IntMath; import com.google.common.math.LongMath; import com.linecorp.armeria.client.Clients; @@ -91,6 +88,7 @@ import com.linecorp.centraldogma.common.Markup; import com.linecorp.centraldogma.common.MergeQuery; import com.linecorp.centraldogma.common.MergedEntry; +import com.linecorp.centraldogma.common.PathPattern; import com.linecorp.centraldogma.common.ProjectExistsException; import com.linecorp.centraldogma.common.ProjectNotFoundException; import com.linecorp.centraldogma.common.PushResult; @@ -398,19 +396,14 @@ private static Revision normalizeRevision(AggregatedHttpResponse res) { @Override public CompletableFuture> listFiles(String projectName, String repositoryName, - Revision revision, String pathPattern) { + Revision revision, PathPattern pathPattern) { try { validateProjectAndRepositoryName(projectName, repositoryName); requireNonNull(revision, "revision"); - validatePathPattern(pathPattern, "pathPattern"); + requireNonNull(pathPattern, "pathPattern"); final StringBuilder path = pathBuilder(projectName, repositoryName); - path.append("/list"); - if (pathPattern.charAt(0) != '/') { - path.append("/**/"); - } - path.append(encodePathPattern(pathPattern)); - path.append("?revision=").append(revision.major()); + path.append("/list").append(pathPattern.encoded()).append("?revision=").append(revision.major()); return client.execute(headers(HttpMethod.GET, path.toString())) .aggregate() @@ -471,21 +464,19 @@ private static Entry getFile(Revision normRev, AggregatedHttpResponse res @Override public CompletableFuture>> getFiles(String projectName, String repositoryName, - Revision revision, String pathPattern) { + Revision revision, PathPattern pathPattern) { try { validateProjectAndRepositoryName(projectName, repositoryName); requireNonNull(revision, "revision"); - validatePathPattern(pathPattern, "pathPattern"); + requireNonNull(pathPattern, "pathPattern"); // TODO(trustin) No need to normalize a revision once server response contains it. return maybeNormalizeRevision(projectName, repositoryName, revision).thenCompose(normRev -> { final StringBuilder path = pathBuilder(projectName, repositoryName); - path.append("/contents"); - if (pathPattern.charAt(0) != '/') { - path.append("/**/"); - } - path.append(encodePathPattern(pathPattern)); - path.append("?revision=").append(normRev.major()); + path.append("/contents") + .append(pathPattern.encoded()) + .append("?revision=") + .append(normRev.major()); return client.execute(headers(HttpMethod.GET, path.toString())) .aggregate() @@ -592,17 +583,17 @@ private static MergedEntry mergeFiles(AggregatedHttpResponse res) { @Override public CompletableFuture> getHistory(String projectName, String repositoryName, Revision from, Revision to, - String pathPattern) { + PathPattern pathPattern) { try { validateProjectAndRepositoryName(projectName, repositoryName); requireNonNull(from, "from"); requireNonNull(to, "to"); - validatePathPattern(pathPattern, "pathPattern"); + requireNonNull(pathPattern, "pathPattern"); final StringBuilder path = pathBuilder(projectName, repositoryName); path.append("/commits/").append(from.text()); path.append("?to=").append(to.text()); - path.append("&path=").append(encodeParam(pathPattern)); + path.append("&path=").append(pathPattern.encoded()); return client.execute(headers(HttpMethod.GET, path.toString())) .aggregate() @@ -668,17 +659,17 @@ private static Change getDiff(AggregatedHttpResponse res) { } @Override - public CompletableFuture>> getDiffs(String projectName, String repositoryName, Revision from, - Revision to, String pathPattern) { + public CompletableFuture>> getDiff(String projectName, String repositoryName, Revision from, + Revision to, PathPattern pathPattern) { try { validateProjectAndRepositoryName(projectName, repositoryName); requireNonNull(from, "from"); requireNonNull(to, "to"); - validatePathPattern(pathPattern, "pathPattern"); + requireNonNull(pathPattern, "pathPattern"); final StringBuilder path = pathBuilder(projectName, repositoryName); path.append("/compare"); - path.append("?pathPattern=").append(encodeParam(pathPattern)); + path.append("?pathPattern=").append(pathPattern.encoded()); path.append("&from=").append(from.text()); path.append("&to=").append(to.text()); @@ -802,23 +793,19 @@ public CompletableFuture push(String projectName, String repositoryN @Override public CompletableFuture watchRepository(String projectName, String repositoryName, - Revision lastKnownRevision, String pathPattern, - long timeoutMillis) { + Revision lastKnownRevision, PathPattern pathPattern, + long timeoutMillis, boolean errorOnEntryNotFound) { try { validateProjectAndRepositoryName(projectName, repositoryName); requireNonNull(lastKnownRevision, "lastKnownRevision"); - validatePathPattern(pathPattern, "pathPattern"); + requireNonNull(pathPattern, "pathPattern"); checkArgument(timeoutMillis > 0, "timeoutMillis: %s (expected: > 0)", timeoutMillis); final StringBuilder path = pathBuilder(projectName, repositoryName); - path.append("/contents"); - if (pathPattern.charAt(0) != '/') { - path.append("/**/"); - } - path.append(encodePathPattern(pathPattern)); + path.append("/contents").append(pathPattern.encoded()); return watch(lastKnownRevision, timeoutMillis, path.toString(), QueryType.IDENTITY, - ArmeriaCentralDogma::watchRepository); + ArmeriaCentralDogma::watchRepository, errorOnEntryNotFound); } catch (Exception e) { return exceptionallyCompletedFuture(e); } @@ -840,7 +827,7 @@ private static Revision watchRepository(AggregatedHttpResponse res, QueryType un @Override public CompletableFuture> watchFile(String projectName, String repositoryName, Revision lastKnownRevision, Query query, - long timeoutMillis) { + long timeoutMillis, boolean errorOnEntryNotFound) { try { validateProjectAndRepositoryName(projectName, repositoryName); requireNonNull(lastKnownRevision, "lastKnownRevision"); @@ -859,7 +846,7 @@ public CompletableFuture> watchFile(String projectName, String repo } return watch(lastKnownRevision, timeoutMillis, path.toString(), query.type(), - ArmeriaCentralDogma::watchFile); + ArmeriaCentralDogma::watchFile, errorOnEntryNotFound); } catch (Exception e) { return exceptionallyCompletedFuture(e); } @@ -881,10 +868,12 @@ private static Entry watchFile(AggregatedHttpResponse res, QueryType quer private CompletableFuture watch(Revision lastKnownRevision, long timeoutMillis, String path, QueryType queryType, - BiFunction func) { + BiFunction func, + boolean errorOnEntryNotFound) { final RequestHeadersBuilder builder = headersBuilder(HttpMethod.GET, path); builder.set(HttpHeaderNames.IF_NONE_MATCH, lastKnownRevision.text()) - .set(HttpHeaderNames.PREFER, "wait=" + LongMath.saturatedAdd(timeoutMillis, 999) / 1000L); + .set(HttpHeaderNames.PREFER, "wait=" + LongMath.saturatedAdd(timeoutMillis, 999) / 1000L + + ", notify-entry-not-found=" + errorOnEntryNotFound); try (SafeCloseable ignored = Clients.withContextCustomizer(ctx -> { final long responseTimeoutMillis = ctx.responseTimeoutMillis(); @@ -968,31 +957,6 @@ private static String encodeParam(String param) { } } - @VisibleForTesting - static String encodePathPattern(String pathPattern) { - // We do not need full escaping because we validated the path pattern already and thus contains only - // -, ' ', /, *, _, ., ',', a-z, A-Z, 0-9. - // See Util.isValidPathPattern() for more information. - int spacePos = pathPattern.indexOf(' '); - if (spacePos < 0) { - return pathPattern; - } - - final StringBuilder buf = new StringBuilder(IntMath.saturatedMultiply(pathPattern.length(), 2)); - for (int pos = 0;;) { - buf.append(pathPattern, pos, spacePos); - buf.append("%20"); - pos = spacePos + 1; - spacePos = pathPattern.indexOf(' ', pos); - if (spacePos < 0) { - buf.append(pathPattern, pos, pathPattern.length()); - break; - } - } - - return buf.toString(); - } - /** * Encodes the specified {@link JsonNode} into a byte array. */ diff --git a/client/java-armeria/src/main/java/com/linecorp/centraldogma/client/armeria/CentralDogmaEndpointGroup.java b/client/java-armeria/src/main/java/com/linecorp/centraldogma/client/armeria/CentralDogmaEndpointGroup.java index bd23721d20..f18f59d2a0 100644 --- a/client/java-armeria/src/main/java/com/linecorp/centraldogma/client/armeria/CentralDogmaEndpointGroup.java +++ b/client/java-armeria/src/main/java/com/linecorp/centraldogma/client/armeria/CentralDogmaEndpointGroup.java @@ -88,7 +88,10 @@ public static CentralDogmaEndpointGroup of(CentralDogma centralDogma, String projectName, String repositoryName, Query query, EndpointListDecoder endpointListDecoder) { - return ofWatcher(centralDogma.fileWatcher(projectName, repositoryName, query), endpointListDecoder); + return ofWatcher(centralDogma.forRepo(projectName, repositoryName) + .watcher(query) + .start(), + endpointListDecoder); } /** diff --git a/client/java-armeria/src/test/java/com/linecorp/centraldogma/client/armeria/ArmeriaCentralDogmaTest.java b/client/java-armeria/src/test/java/com/linecorp/centraldogma/client/armeria/ArmeriaCentralDogmaTest.java index fee99332f3..ec7b1c9ed8 100644 --- a/client/java-armeria/src/test/java/com/linecorp/centraldogma/client/armeria/ArmeriaCentralDogmaTest.java +++ b/client/java-armeria/src/test/java/com/linecorp/centraldogma/client/armeria/ArmeriaCentralDogmaTest.java @@ -15,7 +15,6 @@ */ package com.linecorp.centraldogma.client.armeria; -import static com.linecorp.centraldogma.client.armeria.ArmeriaCentralDogma.encodePathPattern; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -29,7 +28,6 @@ import com.linecorp.centraldogma.common.Change; import com.linecorp.centraldogma.common.InvalidPushException; import com.linecorp.centraldogma.common.PushResult; -import com.linecorp.centraldogma.common.Revision; import com.linecorp.centraldogma.testing.junit.CentralDogmaExtension; class ArmeriaCentralDogmaTest { @@ -42,31 +40,15 @@ protected void scaffold(CentralDogma client) { } }; - @Test - void testEncodePathPattern() { - assertThat(encodePathPattern("/")).isEqualTo("/"); - assertThat(encodePathPattern(" ")).isEqualTo("%20"); - assertThat(encodePathPattern(" ")).isEqualTo("%20%20"); - assertThat(encodePathPattern("a b")).isEqualTo("a%20b"); - assertThat(encodePathPattern(" a ")).isEqualTo("%20a%20"); - - // No new string has to be created when escaping is not necessary. - final String pathPatternThatDoesNotNeedEscaping = "/*.zip,/**/*.jar"; - assertThat(encodePathPattern(pathPatternThatDoesNotNeedEscaping)) - .isSameAs(pathPatternThatDoesNotNeedEscaping); - } - @Test void pushFileToMetaRepositoryShouldFail() throws UnknownHostException { final CentralDogma client = new ArmeriaCentralDogmaBuilder() .host(dogma.serverAddress().getHostString(), dogma.serverAddress().getPort()) .build(); - assertThatThrownBy(() -> client.push("foo", - "meta", - Revision.HEAD, - "summary", - Change.ofJsonUpsert("/bar.json", "{ \"a\": \"b\" }")) + assertThatThrownBy(() -> client.forRepo("foo", "meta") + .commit("summary", Change.ofJsonUpsert("/bar.json", "{ \"a\": \"b\" }")) + .push() .join()) .isInstanceOf(CompletionException.class) .hasCauseInstanceOf(InvalidPushException.class); @@ -78,11 +60,9 @@ void pushMirrorsJsonFileToMetaRepository() throws UnknownHostException { .host(dogma.serverAddress().getHostString(), dogma.serverAddress().getPort()) .build(); - final PushResult result = client.push("foo", - "meta", - Revision.HEAD, - "summary", - Change.ofJsonUpsert("/mirrors.json", "[]")) + final PushResult result = client.forRepo("foo", "meta") + .commit("summary", Change.ofJsonUpsert("/mirrors.json", "[]")) + .push() .join(); assertThat(result.revision().major()).isPositive(); } diff --git a/client/java-armeria/src/test/java/com/linecorp/centraldogma/client/armeria/CentralDogmaRepositoryTest.java b/client/java-armeria/src/test/java/com/linecorp/centraldogma/client/armeria/CentralDogmaRepositoryTest.java new file mode 100644 index 0000000000..4d4e20c42b --- /dev/null +++ b/client/java-armeria/src/test/java/com/linecorp/centraldogma/client/armeria/CentralDogmaRepositoryTest.java @@ -0,0 +1,125 @@ +/* + * Copyright 2021 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.linecorp.centraldogma.client.armeria; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static net.javacrumbs.jsonunit.fluent.JsonFluentAssert.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Maps; + +import com.linecorp.centraldogma.client.CentralDogma; +import com.linecorp.centraldogma.client.CentralDogmaRepository; +import com.linecorp.centraldogma.common.Change; +import com.linecorp.centraldogma.common.Commit; +import com.linecorp.centraldogma.common.Entry; +import com.linecorp.centraldogma.common.MergeSource; +import com.linecorp.centraldogma.common.MergedEntry; +import com.linecorp.centraldogma.common.PathPattern; +import com.linecorp.centraldogma.common.Query; +import com.linecorp.centraldogma.common.Revision; +import com.linecorp.centraldogma.testing.junit.CentralDogmaExtension; + +class CentralDogmaRepositoryTest { + + @RegisterExtension + static final CentralDogmaExtension centralDogma = new CentralDogmaExtension() { + @Override + protected void scaffold(CentralDogma client) { + client.createProject("foo").join(); + client.createRepository("foo", "bar").join(); + client.forRepo("foo", "bar") + .commit("commit2", ImmutableList.of(Change.ofJsonUpsert("/foo.json", "{ \"a\": \"b\" }"))) + .push() + .join(); + + client.forRepo("foo", "bar") + .commit("commit3", ImmutableList.of(Change.ofJsonUpsert("/bar.json", "{ \"a\": \"c\" }"))) + .push() + .join(); + } + }; + + @Test + void files() throws JsonParseException { + final CentralDogmaRepository centralDogmaRepo = centralDogma.client().forRepo("foo", "bar"); + assertThat(centralDogmaRepo.normalize(Revision.HEAD).join()).isEqualTo(new Revision(3)); + + final Entry fooJson = Entry.ofJson(new Revision(3), "/foo.json", "{ \"a\": \"b\" }"); + assertThat(centralDogmaRepo.file("/foo.json") + .get() + .join()) + .isEqualTo(fooJson); + + assertThat(centralDogmaRepo.file(Query.ofJson("/foo.json")) + .get() + .join()) + .isEqualTo(fooJson); + + final Entry barJson = Entry.ofJson(new Revision(3), "/bar.json", "{ \"a\": \"c\" }"); + assertThat(centralDogmaRepo.file(PathPattern.all()) + .get() + .join()) + .containsOnly(Maps.immutableEntry("/foo.json", fooJson), + Maps.immutableEntry("/bar.json", barJson)); + + final MergedEntry merged = centralDogmaRepo.merge(MergeSource.ofRequired("/foo.json"), + MergeSource.ofRequired("/bar.json")) + .get().join(); + assertThat(merged.paths()).containsExactly("/foo.json", "/bar.json"); + assertThat(merged.revision()).isEqualTo(new Revision(3)); + assertThatJson(merged.content()).isEqualTo("{ \"a\": \"c\" }"); + } + + @Test + void historyAndDiff() { + final CentralDogmaRepository centralDogmaRepo = centralDogma.client().forRepo("foo", "bar"); + final List commits = centralDogmaRepo.history().get(new Revision(2), Revision.HEAD).join(); + assertThat(commits.stream() + .map(Commit::summary) + .collect(toImmutableList())).containsExactly("commit3", "commit2"); + assertThat(centralDogmaRepo.diff("/foo.json") + .get(Revision.INIT, Revision.HEAD) + .join()) + .isEqualTo(Change.ofJsonUpsert("/foo.json", "{ \"a\": \"b\" }")); + + assertThat(centralDogmaRepo.diff(Query.ofJson("/foo.json")) + .get(Revision.INIT, Revision.HEAD) + .join()) + .isEqualTo(Change.ofJsonUpsert("/foo.json", "{ \"a\": \"b\" }")); + + assertThat(centralDogmaRepo.diff(PathPattern.all()) + .get(Revision.INIT, Revision.HEAD) + .join()) + .containsExactlyInAnyOrder(Change.ofJsonUpsert("/foo.json", "{ \"a\": \"b\" }"), + Change.ofJsonUpsert("/bar.json", "{ \"a\": \"c\" }")); + + assertThat(centralDogmaRepo.diff(Change.ofJsonUpsert("/foo.json", "{ \"a\": \"d\" }")) + .get() + .join()) + .containsExactly(Change.ofJsonPatch( + "/foo.json", + "[{\"op\":\"safeReplace\",\"path\":\"/a\",\"oldValue\":\"b\",\"value\":\"d\"}]")); + } +} diff --git a/client/java-armeria/src/test/java/com/linecorp/centraldogma/client/armeria/WatcherTest.java b/client/java-armeria/src/test/java/com/linecorp/centraldogma/client/armeria/WatcherTest.java new file mode 100644 index 0000000000..65e9159bd4 --- /dev/null +++ b/client/java-armeria/src/test/java/com/linecorp/centraldogma/client/armeria/WatcherTest.java @@ -0,0 +1,123 @@ +/* + * Copyright 2021 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.linecorp.centraldogma.client.armeria; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.linecorp.centraldogma.client.CentralDogma; +import com.linecorp.centraldogma.client.Watcher; +import com.linecorp.centraldogma.common.Change; +import com.linecorp.centraldogma.common.Query; +import com.linecorp.centraldogma.testing.junit.CentralDogmaExtension; + +class WatcherTest { + + @RegisterExtension + static final CentralDogmaExtension dogma = new CentralDogmaExtension() { + @Override + protected void scaffold(CentralDogma client) { + client.createProject("foo").join(); + client.createRepository("foo", "bar").join(); + client.forRepo("foo", "bar") + .commit("Add baz.txt", Change.ofTextUpsert("/baz.txt", "")) + .push().join(); + } + }; + + @Test + void mapperIsCalledOnlyOnceWhenFileIsChanged() throws Exception { + final Watcher watcher = dogma.client() + .forRepo("foo", "bar") + .watcher(Query.ofText("/baz.txt")) + .start(); + assertThat(watcher.initialValueFuture().join().value()).isEmpty(); + final AtomicInteger mapperCounter = new AtomicInteger(); + final Watcher childWatcher = watcher.newChild(str -> mapperCounter.getAndIncrement()); + for (int i = 0; i < 10; i++) { + childWatcher.watch(value -> { + /* no-op */ + }); + } + + Thread.sleep(1000); + // mapperCount is called for the first latest. + assertThat(mapperCounter.get()).isOne(); + + dogma.client() + .forRepo("foo", "bar") + .commit("Modify baz.txt", Change.ofTextUpsert("/baz.txt", "1")) + .push() + .join(); + + // mapperCount is called only once when the value is updated. + await().until(() -> mapperCounter.get() == 2); + Thread.sleep(1000); + // It's still the same. + assertThat(mapperCounter.get()).isEqualTo(2); + watcher.close(); + } + + @Test + void multipleMap() throws Exception { + final Watcher watcher = dogma.client() + .forRepo("foo", "bar") + .watcher(Query.ofText("/baz.txt")) + .map(txt -> 1) + .map(intValue -> Integer.toString(intValue)) + .map(str -> "1".equals(str) ? true : false) + .start(); + + assertThat(watcher.initialValueFuture().join().value()).isTrue(); + watcher.close(); + } + + @Test + void multipleNewChild() throws Exception { + final Watcher originalWatcher = dogma.client() + .forRepo("foo", "bar") + .watcher(Query.ofText("/baz.txt")) + .start(); + final Watcher watcher = originalWatcher + .newChild(txt -> 1) + .newChild(intValue -> Integer.toString(intValue)) + .newChild(str -> "1".equals(str) ? true : false); + + assertThat(watcher.initialValueFuture().join().value()).isTrue(); + originalWatcher.close(); + } + + @Test + void mapperException() { + final Watcher originalWatcher = dogma.client() + .forRepo("foo", "bar") + .watcher(Query.ofText("/baz.txt")) + .start(); + originalWatcher.initialValueFuture().join(); + final Watcher watcher = originalWatcher.newChild(unused -> { + throw new RuntimeException(); + }).newChild(val -> "not called"); + await().untilAsserted(() -> assertThatThrownBy(() -> watcher.initialValueFuture().join()) + .hasCauseExactlyInstanceOf(RuntimeException.class)); + originalWatcher.close(); + } +} diff --git a/client/java/src/main/java/com/linecorp/centraldogma/client/AbstractCentralDogma.java b/client/java/src/main/java/com/linecorp/centraldogma/client/AbstractCentralDogma.java index 5030882973..26535856eb 100644 --- a/client/java/src/main/java/com/linecorp/centraldogma/client/AbstractCentralDogma.java +++ b/client/java/src/main/java/com/linecorp/centraldogma/client/AbstractCentralDogma.java @@ -16,6 +16,7 @@ package com.linecorp.centraldogma.client; +import static com.linecorp.centraldogma.internal.PathPatternUtil.toPathPattern; import static java.util.Objects.requireNonNull; import java.util.List; @@ -34,8 +35,6 @@ import com.linecorp.centraldogma.common.PushResult; import com.linecorp.centraldogma.common.Query; import com.linecorp.centraldogma.common.Revision; -import com.linecorp.centraldogma.internal.client.FileWatcher; -import com.linecorp.centraldogma.internal.client.RepositoryWatcher; /** * A skeletal {@link CentralDogma} implementation. @@ -63,6 +62,13 @@ protected final ScheduledExecutorService executor() { return blockingTaskExecutor; } + @Override + public CentralDogmaRepository forRepo(String projectName, String repositoryName) { + requireNonNull(projectName, "projectName"); + requireNonNull(repositoryName, "repositoryName"); + return new CentralDogmaRepository(this, projectName, repositoryName, blockingTaskExecutor); + } + @Override public final CompletableFuture> getFile( String projectName, String repositoryName, Revision revision, String path) { @@ -143,23 +149,6 @@ public final CompletableFuture push( author, summary, detail, markup, changes); } - @Override - public final CompletableFuture watchRepository( - String projectName, String repositoryName, Revision lastKnownRevision, String pathPattern) { - return CentralDogma.super.watchRepository(projectName, repositoryName, lastKnownRevision, pathPattern); - } - - @Override - public final CompletableFuture> watchFile( - String projectName, String repositoryName, Revision lastKnownRevision, Query query) { - return CentralDogma.super.watchFile(projectName, repositoryName, lastKnownRevision, query); - } - - @Override - public final Watcher fileWatcher(String projectName, String repositoryName, Query query) { - return CentralDogma.super.fileWatcher(projectName, repositoryName, query); - } - @Override public Watcher fileWatcher( String projectName, String repositoryName, Query query, @@ -170,17 +159,11 @@ public Watcher fileWatcher( @Override public Watcher fileWatcher(String projectName, String repositoryName, Query query, Function function, Executor executor) { - final FileWatcher watcher = - new FileWatcher<>(this, blockingTaskExecutor, executor, projectName, repositoryName, query, - function); - watcher.start(); - return watcher; - } - - @Override - public final Watcher repositoryWatcher( - String projectName, String repositoryName, String pathPattern) { - return CentralDogma.super.repositoryWatcher(projectName, repositoryName, pathPattern); + //noinspection unchecked + return (Watcher) forRepo(projectName, repositoryName).watcher(query) + .map(function) + .mapperExecutor(executor) + .start(); } @Override @@ -193,12 +176,11 @@ public Watcher repositoryWatcher( @Override public Watcher repositoryWatcher(String projectName, String repositoryName, String pathPattern, Function function, Executor executor) { - - final RepositoryWatcher watcher = - new RepositoryWatcher<>(this, blockingTaskExecutor, executor, - projectName, repositoryName, pathPattern, function); - watcher.start(); - return watcher; + //noinspection unchecked + return (Watcher) forRepo(projectName, repositoryName).watcher(toPathPattern(pathPattern)) + .map(function) + .mapperExecutor(executor) + .start(); } /** diff --git a/client/java/src/main/java/com/linecorp/centraldogma/internal/client/AbstractWatcher.java b/client/java/src/main/java/com/linecorp/centraldogma/client/AbstractWatcher.java similarity index 66% rename from client/java/src/main/java/com/linecorp/centraldogma/internal/client/AbstractWatcher.java rename to client/java/src/main/java/com/linecorp/centraldogma/client/AbstractWatcher.java index 915157d5fa..4d49f56cc7 100644 --- a/client/java/src/main/java/com/linecorp/centraldogma/internal/client/AbstractWatcher.java +++ b/client/java/src/main/java/com/linecorp/centraldogma/client/AbstractWatcher.java @@ -13,7 +13,7 @@ * License for the specific language governing permissions and limitations * under the License. */ -package com.linecorp.centraldogma.internal.client; +package com.linecorp.centraldogma.client; import static com.google.common.base.Preconditions.checkState; import static com.google.common.math.LongMath.saturatedAdd; @@ -28,7 +28,6 @@ import java.util.concurrent.CompletionException; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; -import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ThreadLocalRandom; @@ -36,14 +35,16 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; +import javax.annotation.Nullable; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.linecorp.centraldogma.client.CentralDogma; -import com.linecorp.centraldogma.client.Latest; -import com.linecorp.centraldogma.client.Watcher; +import com.google.common.base.MoreObjects; + import com.linecorp.centraldogma.common.CentralDogmaException; import com.linecorp.centraldogma.common.EntryNotFoundException; +import com.linecorp.centraldogma.common.PathPattern; import com.linecorp.centraldogma.common.Query; import com.linecorp.centraldogma.common.RepositoryNotFoundException; import com.linecorp.centraldogma.common.Revision; @@ -52,52 +53,6 @@ abstract class AbstractWatcher implements Watcher { private static final Logger logger = LoggerFactory.getLogger(AbstractWatcher.class); - private static final CompletableFuture COMPLETED_FUTURE = CompletableFuture.completedFuture(null); - - private static final long DELAY_ON_SUCCESS_MILLIS = TimeUnit.SECONDS.toMillis(1); - private static final long MIN_INTERVAL_MILLIS = DELAY_ON_SUCCESS_MILLIS * 2; - private static final long MAX_INTERVAL_MILLIS = TimeUnit.MINUTES.toMillis(1); - private static final double JITTER_RATE = 0.2; - - private static long nextDelayMillis(int numAttemptsSoFar) { - final long nextDelayMillis; - if (numAttemptsSoFar == 1) { - nextDelayMillis = MIN_INTERVAL_MILLIS; - } else { - nextDelayMillis = Math.min( - saturatedMultiply(MIN_INTERVAL_MILLIS, Math.pow(2.0, numAttemptsSoFar - 1)), - MAX_INTERVAL_MILLIS); - } - - final long minJitter = (long) (nextDelayMillis * (1 - JITTER_RATE)); - final long maxJitter = (long) (nextDelayMillis * (1 + JITTER_RATE)); - final long bound = maxJitter - minJitter + 1; - final long millis = random(bound); - return Math.max(0, saturatedAdd(minJitter, millis)); - } - - private static long saturatedMultiply(long left, double right) { - final double result = left * right; - return result >= Long.MAX_VALUE ? Long.MAX_VALUE : (long) result; - } - - private static long random(long bound) { - assert bound > 0; - final long mask = bound - 1; - final Random random = ThreadLocalRandom.current(); - long result = random.nextLong(); - - if ((bound & mask) == 0L) { - // power of two - result &= mask; - } else { // reject over-represented candidates - for (long u = result >>> 1; u + mask - (result = u % bound) < 0L; u = random.nextLong() >>> 1) { - continue; - } - } - - return result; - } private enum State { INIT, @@ -105,31 +60,47 @@ private enum State { STOPPED } - private final CentralDogma client; private final ScheduledExecutorService watchScheduler; private final String projectName; private final String repositoryName; private final String pathPattern; - - private final List, Executor>> updateListeners; - private final AtomicReference state; - private final CompletableFuture> initialValueFuture; - + private final boolean errorOnEntryNotFound; + private final long delayOnSuccessMillis; + private final long initialDelayMillis; + private final long maxDelayMillis; + private final double multiplier; + private final double jitterRate; + + private final List, Executor>> updateListeners = + new CopyOnWriteArrayList<>(); + private final AtomicReference state = new AtomicReference<>(State.INIT); + private final CompletableFuture> initialValueFuture = new CompletableFuture<>(); + + @Nullable private volatile Latest latest; + @Nullable private volatile ScheduledFuture currentScheduleFuture; + @Nullable private volatile CompletableFuture currentWatchFuture; - protected AbstractWatcher(CentralDogma client, ScheduledExecutorService watchScheduler, - String projectName, String repositoryName, String pathPattern) { - this.client = requireNonNull(client, "client"); - this.watchScheduler = requireNonNull(watchScheduler, "watchScheduler"); - this.projectName = requireNonNull(projectName, "projectName"); - this.repositoryName = requireNonNull(repositoryName, "repositoryName"); - this.pathPattern = requireNonNull(pathPattern, "pathPattern"); - - updateListeners = new CopyOnWriteArrayList<>(); - state = new AtomicReference<>(State.INIT); - initialValueFuture = new CompletableFuture<>(); + AbstractWatcher(ScheduledExecutorService watchScheduler, String projectName, String repositoryName, + String pathPattern, boolean errorOnEntryNotFound, long delayOnSuccessMillis, + long initialDelayMillis, long maxDelayMillis, double multiplier, double jitterRate) { + this.watchScheduler = watchScheduler; + this.projectName = projectName; + this.repositoryName = repositoryName; + this.pathPattern = pathPattern; + this.errorOnEntryNotFound = errorOnEntryNotFound; + this.delayOnSuccessMillis = delayOnSuccessMillis; + this.initialDelayMillis = initialDelayMillis; + this.maxDelayMillis = maxDelayMillis; + this.multiplier = multiplier; + this.jitterRate = jitterRate; + } + + @Override + public ScheduledExecutorService watchScheduler() { + return watchScheduler; } @Override @@ -147,10 +118,10 @@ public Latest latest() { } /** - * Starts to watch the file specified in the {@link Query} or the {@code pathPattern} + * Starts to watch the file specified in the {@link Query} or the {@link PathPattern} * given with the constructor. */ - public void start() { + void start() { if (state.compareAndSet(State.INIT, State.STARTED)) { scheduleWatch(0); } @@ -189,16 +160,16 @@ public void watch(BiConsumer listener, Executor exe checkState(!isStopped(), "watcher closed"); updateListeners.add(new SimpleImmutableEntry<>(listener, executor)); + final Latest latest = this.latest; if (latest != null) { - // Perform initial notification so that the listener always gets the initial value. - try { - executor.execute(() -> { - final Latest latest = this.latest; - listener.accept(latest.revision(), latest.value()); - }); - } catch (RejectedExecutionException e) { - handleExecutorShutdown(executor, e); - } + // There's a chance that listener.accept(...) is called twice for the same value + // if this watch method is called: + // - after " this.latest = newLatest;" is invoked. + // - and before notifyListener() is called. + // However, it's such a rare case and we usually call `watch` method right after creating a Watcher, + // which means latest is probably not set yet, so we don't use a lock to guarantee + // the atomicity. + executor.execute(() -> listener.accept(latest.revision(), latest.value())); } } @@ -209,19 +180,55 @@ private void scheduleWatch(int numAttemptsSoFar) { final long delay; if (numAttemptsSoFar == 0) { - delay = latest != null ? DELAY_ON_SUCCESS_MILLIS : 0; + delay = latest != null ? delayOnSuccessMillis : 0; } else { delay = nextDelayMillis(numAttemptsSoFar); } - try { - currentScheduleFuture = watchScheduler.schedule(() -> { - currentScheduleFuture = null; - doWatch(numAttemptsSoFar); - }, delay, TimeUnit.MILLISECONDS); - } catch (RejectedExecutionException e) { - handleExecutorShutdown(watchScheduler, e); + currentScheduleFuture = watchScheduler.schedule(() -> { + currentScheduleFuture = null; + doWatch(numAttemptsSoFar); + }, delay, TimeUnit.MILLISECONDS); + } + + private long nextDelayMillis(int numAttemptsSoFar) { + final long nextDelayMillis; + if (numAttemptsSoFar == 1) { + nextDelayMillis = initialDelayMillis; + } else { + nextDelayMillis = + Math.min(saturatedMultiply(initialDelayMillis, Math.pow(multiplier, numAttemptsSoFar - 1)), + maxDelayMillis); } + + final long minJitter = (long) (nextDelayMillis * (1 - jitterRate)); + final long maxJitter = (long) (nextDelayMillis * (1 + jitterRate)); + final long bound = maxJitter - minJitter + 1; + final long millis = random(bound); + return Math.max(0, saturatedAdd(minJitter, millis)); + } + + private static long saturatedMultiply(long left, double right) { + final double result = left * right; + return result >= Long.MAX_VALUE ? Long.MAX_VALUE : (long) result; + } + + private static long random(long bound) { + assert bound > 0; + final long mask = bound - 1; + final Random random = ThreadLocalRandom.current(); + long result = random.nextLong(); + + if ((bound & mask) == 0L) { + // power of two + result &= mask; + } else { // reject over-represented candidates + for (long u = result >>> 1; u + mask - (result = u % bound) < 0L; u = random.nextLong() >>> 1) { + continue; + } + } + + return result; } private void doWatch(int numAttemptsSoFar) { @@ -229,19 +236,19 @@ private void doWatch(int numAttemptsSoFar) { return; } + final Latest latest = this.latest; final Revision lastKnownRevision = latest != null ? latest.revision() : Revision.INIT; - final CompletableFuture> f = doWatch(client, projectName, repositoryName, lastKnownRevision); + final CompletableFuture> f = doWatch(lastKnownRevision); currentWatchFuture = f; - f.whenComplete((result, cause) -> currentWatchFuture = null) - .thenAccept(newLatest -> { + f.thenAccept(newLatest -> { + currentWatchFuture = null; if (newLatest != null) { - final Latest oldLatest = latest; - latest = newLatest; + this.latest = newLatest; logger.debug("watcher noticed updated file {}/{}{}: rev={}", projectName, repositoryName, pathPattern, newLatest.revision()); - notifyListeners(); - if (oldLatest == null) { + notifyListeners(newLatest); + if (!initialValueFuture.isDone()) { initialValueFuture.complete(newLatest); } } @@ -250,11 +257,17 @@ private void doWatch(int numAttemptsSoFar) { scheduleWatch(0); }) .exceptionally(thrown -> { + currentWatchFuture = null; try { final Throwable cause = thrown instanceof CompletionException ? thrown.getCause() : thrown; boolean logged = false; if (cause instanceof CentralDogmaException) { if (cause instanceof EntryNotFoundException) { + if (!initialValueFuture.isDone() && errorOnEntryNotFound) { + initialValueFuture.completeExceptionally(thrown); + close(); + return null; + } logger.info("{}/{}{} does not exist yet; trying again", projectName, repositoryName, pathPattern); logged = true; @@ -286,16 +299,14 @@ private void doWatch(int numAttemptsSoFar) { }); } - protected abstract CompletableFuture> doWatch( - CentralDogma client, String projectName, String repositoryName, Revision lastKnownRevision); + abstract CompletableFuture> doWatch(Revision lastKnownRevision); - private void notifyListeners() { + private void notifyListeners(Latest latest) { if (isStopped()) { // Do not notify after stopped. return; } - final Latest latest = this.latest; for (Map.Entry, Executor> entry : updateListeners) { final BiConsumer listener = entry.getKey(); final Executor executor = entry.getValue(); @@ -310,13 +321,20 @@ private void notifyListeners() { } } - private void handleExecutorShutdown(Executor executor, RejectedExecutionException e) { - if (logger.isTraceEnabled()) { - logger.trace("Stopping to watch since the executor is shut down. executor: {}", executor, e); - } else { - logger.debug("Stopping to watch since the executor is shut down. executor: {}", executor); - } - - close(); + @Override + public String toString() { + return MoreObjects.toStringHelper(this).omitNullValues() + .add("watchScheduler", watchScheduler) + .add("projectName", projectName) + .add("repositoryName", repositoryName) + .add("pathPattern", pathPattern) + .add("errorOnEntryNotFound", errorOnEntryNotFound) + .add("delayOnSuccessMillis", delayOnSuccessMillis) + .add("initialDelayMillis", initialDelayMillis) + .add("maxDelayMillis", maxDelayMillis) + .add("multiplier", multiplier) + .add("jitterRate", jitterRate) + .add("latest", latest) + .toString(); } } diff --git a/client/java/src/main/java/com/linecorp/centraldogma/client/CentralDogma.java b/client/java/src/main/java/com/linecorp/centraldogma/client/CentralDogma.java index 66bd8abffe..591abb66af 100644 --- a/client/java/src/main/java/com/linecorp/centraldogma/client/CentralDogma.java +++ b/client/java/src/main/java/com/linecorp/centraldogma/client/CentralDogma.java @@ -15,6 +15,7 @@ */ package com.linecorp.centraldogma.client; +import static com.linecorp.centraldogma.internal.PathPatternUtil.toPathPattern; import static java.util.Objects.requireNonNull; import java.util.List; @@ -31,11 +32,13 @@ import com.linecorp.centraldogma.common.Change; import com.linecorp.centraldogma.common.Commit; import com.linecorp.centraldogma.common.Entry; +import com.linecorp.centraldogma.common.EntryNotFoundException; import com.linecorp.centraldogma.common.EntryType; import com.linecorp.centraldogma.common.Markup; import com.linecorp.centraldogma.common.MergeQuery; import com.linecorp.centraldogma.common.MergeSource; import com.linecorp.centraldogma.common.MergedEntry; +import com.linecorp.centraldogma.common.PathPattern; import com.linecorp.centraldogma.common.PushResult; import com.linecorp.centraldogma.common.Query; import com.linecorp.centraldogma.common.QueryType; @@ -46,6 +49,13 @@ * Central Dogma client. */ public interface CentralDogma { + + /** + * Returns a new {@link CentralDogmaRepository} that is used to send a request to the specified + * {@code projectName} and {@code repositoryName}. + */ + CentralDogmaRepository forRepo(String projectName, String repositoryName); + /** * Creates a project. */ @@ -124,20 +134,32 @@ public interface CentralDogma { CompletableFuture normalizeRevision(String projectName, String repositoryName, Revision revision); /** - * Retrieves the list of the files matched by the given path pattern. A path pattern is a variant of glob: - *
    - *
  • {@code "/**"} - find all files recursively
  • - *
  • {@code "*.json"} - find all JSON files recursively
  • - *
  • {@code "/foo/*.json"} - find all JSON files under the directory {@code /foo}
  • - *
  • "/*/foo.txt" - find all files named {@code foo.txt} at the second depth level
  • - *
  • {@code "*.json,/bar/*.txt"} - use comma to specify more than one pattern. A file will be matched - * if any pattern matches.
  • - *
+ * Retrieves the list of the files matched by the given path pattern. + * + * @return a {@link Map} of file path and type pairs + * + * @deprecated Use {@link FilesRequest#list(Revision)} via {@link CentralDogmaRepository#file(PathPattern)}. + */ + @Deprecated + default CompletableFuture> listFiles(String projectName, String repositoryName, + Revision revision, String pathPattern) { + return listFiles(projectName, repositoryName, revision, toPathPattern(pathPattern)); + } + + /** + * Retrieves the list of the files matched by the given {@link PathPattern}. + * This method is equivalent to calling: + *
{@code
+     * CentralDogma dogma = ...
+     * dogma.forRepo(projectName, repositoryName)
+     *      .files(pathPattern)
+     *      .list(revision);
+     * }
* * @return a {@link Map} of file path and type pairs */ CompletableFuture> listFiles(String projectName, String repositoryName, - Revision revision, String pathPattern); + Revision revision, PathPattern pathPattern); /** * Retrieves the file at the specified revision and path. This method is a shortcut of @@ -146,7 +168,10 @@ CompletableFuture> listFiles(String projectName, String r * {@link Query#ofJson(String)} if you already know the file type. * * @return the {@link Entry} at the given {@code path} + * + * @deprecated Use {@link FileRequest#get(Revision)} via {@link CentralDogmaRepository#file(String)}. */ + @Deprecated default CompletableFuture> getFile(String projectName, String repositoryName, Revision revision, String path) { @SuppressWarnings("unchecked") @@ -157,6 +182,13 @@ default CompletableFuture> getFile(String projectName, String repositor /** * Queries a file at the specified revision and path with the specified {@link Query}. + * This method is equivalent to calling: + *
{@code
+     * CentralDogma dogma = ...
+     * dogma.forRepo(projectName, repositoryName)
+     *      .file(query)
+     *      .get(revision);
+     * }
* * @return the {@link Entry} that is matched by the given {@link Query} */ @@ -164,20 +196,32 @@ CompletableFuture> getFile(String projectName, String repositoryNam Revision revision, Query query); /** - * Retrieves the files matched by the path pattern. A path pattern is a variant of glob: - *
    - *
  • {@code "/**"} - find all files recursively
  • - *
  • {@code "*.json"} - find all JSON files recursively
  • - *
  • {@code "/foo/*.json"} - find all JSON files under the directory {@code /foo}
  • - *
  • "/*/foo.txt" - find all files named {@code foo.txt} at the second depth level
  • - *
  • {@code "*.json,/bar/*.txt"} - use comma to specify more than one pattern. A file will be matched - * if any pattern matches.
  • - *
+ * Retrieves the files matched by the path pattern. + * + * @return a {@link Map} of file path and {@link Entry} pairs + * + * @deprecated Use {@link FilesRequest#get(Revision)} via {@link CentralDogmaRepository#file(PathPattern)}. + */ + @Deprecated + default CompletableFuture>> getFiles(String projectName, String repositoryName, + Revision revision, String pathPattern) { + return getFiles(projectName, repositoryName, revision, toPathPattern(pathPattern)); + } + + /** + * Retrieves the files matched by the {@link PathPattern}. + * This method is equivalent to calling: + *
{@code
+     * CentralDogma dogma = ...
+     * dogma.forRepo(projectName, repositoryName)
+     *      .file(pathPattern)
+     *      .get(revision);
+     * }
* * @return a {@link Map} of file path and {@link Entry} pairs */ CompletableFuture>> getFiles(String projectName, String repositoryName, - Revision revision, String pathPattern); + Revision revision, PathPattern pathPattern); /** * Retrieves the merged entry of the specified {@link MergeSource}s at the specified revision. @@ -188,7 +232,11 @@ CompletableFuture>> getFiles(String projectName, String rep * simply replaced. * * @return the {@link MergedEntry} which contains the result of the merge + * + * @deprecated Use {@link MergeRequest#get(Revision)} via + * {@link CentralDogmaRepository#merge(MergeSource...)}. */ + @Deprecated default CompletableFuture> mergeFiles( String projectName, String repositoryName, Revision revision, MergeSource... mergeSources) { @@ -205,7 +253,11 @@ default CompletableFuture> mergeFiles( * simply replaced. * * @return the {@link MergedEntry} which contains the result of the merge + * + * @deprecated Use {@link MergeRequest#get(Revision)} via + * {@link CentralDogmaRepository#merge(Iterable)}. */ + @Deprecated default CompletableFuture> mergeFiles( String projectName, String repositoryName, Revision revision, Iterable mergeSources) { @@ -220,6 +272,13 @@ default CompletableFuture> mergeFiles( * Retrieves the merged entry of the specified {@link MergeQuery} at the specified revision. * Only JSON entry merge is currently supported. The JSON files are merged sequentially as specified in * the {@link MergeQuery}. + * This method is equivalent to calling: + *
{@code
+     * CentralDogma dogma = ...
+     * dogma.forRepo(projectName, repositoryName)
+     *      .merge(mergeQuery)
+     *      .get(revision);
+     * }
* *

Note that only {@link ObjectNode} is recursively merged traversing the children. Other node types are * simply replaced. @@ -234,10 +293,14 @@ CompletableFuture> mergeFiles(String projectName, String repo * {@code getHistory(projectName, repositoryName, from, to, "/**")}. Note that this method does not * retrieve the diffs but only metadata about the changes. * Use {@link #getDiff(String, String, Revision, Revision, Query)} or - * {@link #getDiffs(String, String, Revision, Revision, String)} to retrieve the diffs. + * {@link #getDiff(String, String, Revision, Revision, PathPattern)} to retrieve the diffs. * * @return a {@link List} that contains the {@link Commit}s of the specified repository + * + * @deprecated Use {@link HistoryRequest#get(Revision, Revision)} via + * {@link CentralDogmaRepository#history()}. */ + @Deprecated default CompletableFuture> getHistory( String projectName, String repositoryName, Revision from, Revision to) { return getHistory(projectName, repositoryName, from, to, "/**"); @@ -245,25 +308,42 @@ default CompletableFuture> getHistory( /** * Retrieves the history of the files matched by the given path pattern between two {@link Revision}s. - * A path pattern is a variant of glob: - *

    - *
  • {@code "/**"} - find all files recursively
  • - *
  • {@code "*.json"} - find all JSON files recursively
  • - *
  • {@code "/foo/*.json"} - find all JSON files under the directory {@code /foo}
  • - *
  • "/*/foo.txt" - find all files named {@code foo.txt} at the second depth level
  • - *
  • {@code "*.json,/bar/*.txt"} - use comma to specify more than one pattern. A file will be matched - * if any pattern matches.
  • - *
* *

Note that this method does not retrieve the diffs but only metadata about the changes. * Use {@link #getDiff(String, String, Revision, Revision, Query)} or - * {@link #getDiffs(String, String, Revision, Revision, String)} to retrieve the diffs. + * {@link #getDiff(String, String, Revision, Revision, PathPattern)} to retrieve the diffs. * * @return a {@link List} that contains the {@link Commit}s of the files matched by the given - * {@code pathPattern} in the specified repository + * {@link PathPattern} in the specified repository + * + * @deprecated Use {@link HistoryRequest#get(Revision, Revision)} via + * {@link CentralDogmaRepository#history(PathPattern)}. + */ + @Deprecated + default CompletableFuture> getHistory( + String projectName, String repositoryName, Revision from, Revision to, String pathPattern) { + return getHistory(projectName, repositoryName, from, to, toPathPattern(pathPattern)); + } + + /** + * Retrieves the history of the files matched by the given {@link PathPattern} between + * two {@link Revision}s. + * This method is equivalent to calling: + *

{@code
+     * CentralDogma dogma = ...
+     * dogma.forRepo(projectName, repositoryName)
+     *      .history(pathPattern)
+     *      .get(from, to);
+     * }
+ * + *

Note that this method does not retrieve the diffs but only metadata about the changes. + * Use {@link DiffRequest} to retrieve the diffs. + * + * @return a {@link List} that contains the {@link Commit}s of the files matched by the given + * {@link PathPattern} in the specified repository */ CompletableFuture> getHistory( - String projectName, String repositoryName, Revision from, Revision to, String pathPattern); + String projectName, String repositoryName, Revision from, Revision to, PathPattern pathPattern); /** * Returns the diff of a file between two {@link Revision}s. This method is a shortcut of @@ -273,7 +353,11 @@ CompletableFuture> getHistory( * * @return the {@link Change} that contains the diff of the given {@code path} between the specified * two revisions + * + * @deprecated Use {@link DiffRequest#get(Revision, Revision)} via + * {@link CentralDogmaRepository#diff(String)}. */ + @Deprecated default CompletableFuture> getDiff(String projectName, String repositoryName, Revision from, Revision to, String path) { @SuppressWarnings({ "unchecked", "rawtypes" }) @@ -284,6 +368,13 @@ default CompletableFuture> getDiff(String projectName, String reposito /** * Queries a file at two different revisions and returns the diff of the two {@link Query} results. + * This method is equivalent to calling: + *

{@code
+     * CentralDogma dogma = ...
+     * dogma.forRepo(projectName, repositoryName)
+     *      .diff(query)
+     *      .get(from, to);
+     * }
* * @return the {@link Change} that contains the diff of the file matched by the given {@code query} * between the specified two revisions @@ -291,23 +382,35 @@ default CompletableFuture> getDiff(String projectName, String reposito CompletableFuture> getDiff(String projectName, String repositoryName, Revision from, Revision to, Query query); + /** + * Retrieves the diffs of the files matched by the given {@link PathPattern} between two {@link Revision}s. + *
{@code
+     * CentralDogma dogma = ...
+     * dogma.forRepo(projectName, repositoryName)
+     *      .diff(pathPattern)
+     *      .get(from, to);
+     * }
+ * + * @return a {@link List} of the {@link Change}s that contain the diffs between the files matched by the + * given {@link PathPattern} between two revisions. + */ + CompletableFuture>> getDiff(String projectName, String repositoryName, + Revision from, Revision to, PathPattern pathPattern); + /** * Retrieves the diffs of the files matched by the given path pattern between two {@link Revision}s. - * A path pattern is a variant of glob: - *
    - *
  • {@code "/**"} - find all files recursively
  • - *
  • {@code "*.json"} - find all JSON files recursively
  • - *
  • {@code "/foo/*.json"} - find all JSON files under the directory {@code /foo}
  • - *
  • "/*/foo.txt" - find all files named {@code foo.txt} at the second depth level
  • - *
  • {@code "*.json,/bar/*.txt"} - use comma to specify more than one pattern. A file will be matched - * if any pattern matches.
  • - *
* * @return a {@link List} of the {@link Change}s that contain the diffs between the files matched by the - * given {@code pathPattern} between two revisions. + * given {@link PathPattern} between two revisions. + * + * @deprecated Use {@link DiffRequest#get(Revision, Revision)} via + * {@link CentralDogmaRepository#diff(PathPattern)}. */ - CompletableFuture>> getDiffs(String projectName, String repositoryName, - Revision from, Revision to, String pathPattern); + @Deprecated + default CompletableFuture>> getDiffs(String projectName, String repositoryName, + Revision from, Revision to, String pathPattern) { + return getDiff(projectName, repositoryName, from, to, toPathPattern(pathPattern)); + } /** * Retrieves the preview diffs, which are hypothetical diffs generated if the specified @@ -315,7 +418,11 @@ CompletableFuture>> getDiffs(String projectName, String repositor * pre-checking if the specified {@link Change}s will be applied as expected without any conflicts. * * @return the diffs which would be committed if the specified {@link Change}s were pushed successfully + * + * @deprecated Use {@link PreviewDiffRequest#get(Revision)} via + * {@link CentralDogmaRepository#diff(Change[])}. */ + @Deprecated default CompletableFuture>> getPreviewDiffs(String projectName, String repositoryName, Revision baseRevision, Change... changes) { return getPreviewDiffs(projectName, repositoryName, baseRevision, ImmutableList.copyOf(changes)); @@ -325,6 +432,13 @@ default CompletableFuture>> getPreviewDiffs(String projectName, S * Retrieves the preview diffs, which are hypothetical diffs generated if the specified * {@link Change}s were successfully pushed to the specified repository. This operation is useful for * pre-checking if the specified {@link Change}s will be applied as expected without any conflicts. + * This method is equivalent to calling: + *
{@code
+     * CentralDogma dogma = ...
+     * dogma.forRepo(projectName, repositoryName)
+     *      .diff(changes)
+     *      .get(baseRevision);
+     * }
* * @return the diffs which would be committed if the specified {@link Change}s were pushed successfully */ @@ -336,7 +450,11 @@ CompletableFuture>> getPreviewDiffs(String projectName, String re * Pushes the specified {@link Change}s to the repository. * * @return the {@link PushResult} which tells the {@link Revision} and timestamp of the new {@link Commit} + * + * @deprecated Use {@link CommitRequest#push(Revision)} via + * {@link CentralDogmaRepository#commit(String, Change...)}. */ + @Deprecated default CompletableFuture push(String projectName, String repositoryName, Revision baseRevision, String summary, Change... changes) { return push(projectName, repositoryName, baseRevision, summary, @@ -347,7 +465,11 @@ default CompletableFuture push(String projectName, String repository * Pushes the specified {@link Change}s to the repository. * * @return the {@link PushResult} which tells the {@link Revision} and timestamp of the new {@link Commit} + * + * @deprecated Use {@link CommitRequest#push(Revision)} via + * {@link CentralDogmaRepository#commit(String, Iterable)}. */ + @Deprecated default CompletableFuture push(String projectName, String repositoryName, Revision baseRevision, String summary, Iterable> changes) { return push(projectName, repositoryName, baseRevision, summary, "", Markup.PLAINTEXT, changes); @@ -357,7 +479,11 @@ default CompletableFuture push(String projectName, String repository * Pushes the specified {@link Change}s to the repository. * * @return the {@link PushResult} which tells the {@link Revision} and timestamp of the new {@link Commit} + * + * @deprecated Use {@link CommitRequest#push(Revision)} via + * {@link CentralDogmaRepository#commit(String, Change...)}. */ + @Deprecated default CompletableFuture push(String projectName, String repositoryName, Revision baseRevision, String summary, String detail, Markup markup, Change... changes) { @@ -367,6 +493,14 @@ default CompletableFuture push(String projectName, String repository /** * Pushes the specified {@link Change}s to the repository. + * This method is equivalent to calling: + *
{@code
+     * CentralDogma dogma = ...
+     * dogma.forRepo(projectName, repositoryName)
+     *      .commit(summary, changes)
+     *      .detail(detail, markup)
+     *      .push(baseRevision);
+     * }
* * @return the {@link PushResult} which tells the {@link Revision} and timestamp of the new {@link Commit} */ @@ -379,7 +513,8 @@ CompletableFuture push(String projectName, String repositoryName, Re * * @return the {@link PushResult} which tells the {@link Revision} and timestamp of the new {@link Commit} * - * @deprecated Use {@link #push(String, String, Revision, String, Change...)}. + * @deprecated Use {@link CommitRequest#push(Revision)} via + * {@link CentralDogmaRepository#commit(String, Change...)}. */ @Deprecated default CompletableFuture push(String projectName, String repositoryName, Revision baseRevision, @@ -392,7 +527,8 @@ default CompletableFuture push(String projectName, String repository * * @return the {@link PushResult} which tells the {@link Revision} and timestamp of the new {@link Commit} * - * @deprecated Use {@link #push(String, String, Revision, String, Iterable)}. + * @deprecated Use {@link CommitRequest#push(Revision)} via + * {@link CentralDogmaRepository#commit(String, Iterable)}. */ @Deprecated default CompletableFuture push(String projectName, String repositoryName, Revision baseRevision, @@ -406,7 +542,8 @@ default CompletableFuture push(String projectName, String repository * * @return the {@link PushResult} which tells the {@link Revision} and timestamp of the new {@link Commit} * - * @deprecated Use {@link #push(String, String, Revision, String, String, Markup, Change...)}. + * @deprecated Use {@link CommitRequest#push(Revision)} via + * {@link CentralDogmaRepository#commit(String, Change...)}. */ @Deprecated default CompletableFuture push(String projectName, String repositoryName, Revision baseRevision, @@ -421,7 +558,8 @@ default CompletableFuture push(String projectName, String repository * * @return the {@link PushResult} which tells the {@link Revision} and timestamp of the new {@link Commit} * - * @deprecated Use {@link #push(String, String, Revision, String, String, Markup, Iterable)}. + * @deprecated Use {@link CommitRequest#push(Revision)} via + * {@link CentralDogmaRepository#commit(String, Iterable)}. */ @Deprecated CompletableFuture push(String projectName, String repositoryName, Revision baseRevision, @@ -435,27 +573,62 @@ CompletableFuture push(String projectName, String repositoryName, Re * * @return the latest known {@link Revision} which contains the changes for the matched files. * {@code null} if the files were not changed for 1 minute since the invocation of this method. + * + * @deprecated Use {@link WatchFilesRequest#start(Revision)} via + * {@link CentralDogmaRepository#watch(PathPattern)}. */ + @Deprecated default CompletableFuture watchRepository(String projectName, String repositoryName, Revision lastKnownRevision, String pathPattern) { - return watchRepository(projectName, repositoryName, lastKnownRevision, pathPattern, - WatchConstants.DEFAULT_WATCH_TIMEOUT_MILLIS); + return watchRepository(projectName, repositoryName, lastKnownRevision, toPathPattern(pathPattern), + WatchConstants.DEFAULT_WATCH_TIMEOUT_MILLIS, + WatchConstants.DEFAULT_WATCH_ERROR_ON_ENTRY_NOT_FOUND); } /** * Waits for the files matched by the specified {@code pathPattern} to be changed since the specified * {@code lastKnownRevision}. If no changes were made within the specified {@code timeoutMillis}, the * returned {@link CompletableFuture} will be completed with {@code null}. It is recommended to specify - * the largest {@code timeoutMillis} allowed by the server. If unsure, use - * {@link #watchRepository(String, String, Revision, String)}. + * the largest {@code timeoutMillis} allowed by the server. * * @return the latest known {@link Revision} which contains the changes for the matched files. * {@code null} if the files were not changed for {@code timeoutMillis} milliseconds * since the invocation of this method. + * + * @deprecated Use {@link CentralDogmaRepository#watch(PathPattern)} and + * {@link WatchFilesRequest#start(Revision)}. + */ + @Deprecated + default CompletableFuture watchRepository(String projectName, String repositoryName, + Revision lastKnownRevision, String pathPattern, + long timeoutMillis) { + return watchRepository(projectName, repositoryName, lastKnownRevision, toPathPattern(pathPattern), + timeoutMillis, WatchConstants.DEFAULT_WATCH_ERROR_ON_ENTRY_NOT_FOUND); + } + + /** + * Waits for the files matched by the specified {@link PathPattern} to be changed since the specified + * {@code lastKnownRevision}. If no changes were made within the specified {@code timeoutMillis}, the + * returned {@link CompletableFuture} will be completed with {@code null}. It is recommended to specify + * the largest {@code timeoutMillis} allowed by the server. + * This method is equivalent to calling: + *
{@code
+     * CentralDogma dogma = ...
+     * dogma.forRepo(projectName, repositoryName)
+     *      .watch(pathPattern)
+     *      .timeoutMillis(timeoutMillis)
+     *      .errorOnEntryNotFound(errorOnEntryNotFound)
+     *      .start(lastKnownRevision);
+     * }
+ * + * @return the latest known {@link Revision} which contains the changes for the matched files. + * {@code null} if the files were not changed for {@code timeoutMillis} milliseconds + * since the invocation of this method. {@link EntryNotFoundException} is raised if the + * target does not exist. */ CompletableFuture watchRepository(String projectName, String repositoryName, - Revision lastKnownRevision, String pathPattern, - long timeoutMillis); + Revision lastKnownRevision, PathPattern pathPattern, + long timeoutMillis, boolean errorOnEntryNotFound); /** * Waits for the file matched by the specified {@link Query} to be changed since the specified @@ -464,27 +637,62 @@ CompletableFuture watchRepository(String projectName, String repositor * * @return the {@link Entry} which contains the latest known {@link Query} result. * {@code null} if the file was not changed for 1 minute since the invocation of this method. + * + * @deprecated Use {@link WatchRequest#start(Revision)} via {@link CentralDogmaRepository#watch(Query)}. */ + @Deprecated default CompletableFuture> watchFile(String projectName, String repositoryName, Revision lastKnownRevision, Query query) { return watchFile(projectName, repositoryName, lastKnownRevision, query, - WatchConstants.DEFAULT_WATCH_TIMEOUT_MILLIS); + WatchConstants.DEFAULT_WATCH_TIMEOUT_MILLIS, + WatchConstants.DEFAULT_WATCH_ERROR_ON_ENTRY_NOT_FOUND); } /** * Waits for the file matched by the specified {@link Query} to be changed since the specified * {@code lastKnownRevision}. If no changes were made within the specified {@code timeoutMillis}, the * returned {@link CompletableFuture} will be completed with {@code null}. It is recommended to specify - * the largest {@code timeoutMillis} allowed by the server. If unsure, use - * {@link #watchFile(String, String, Revision, Query)}. + * the largest {@code timeoutMillis} allowed by the server. * * @return the {@link Entry} which contains the latest known {@link Query} result. * {@code null} if the file was not changed for {@code timeoutMillis} milliseconds * since the invocation of this method. + * + * @deprecated Use {@link WatchRequest#start(Revision)} via {@link CentralDogmaRepository#watch(Query)}. + */ + @Deprecated + default CompletableFuture> watchFile(String projectName, String repositoryName, + Revision lastKnownRevision, Query query, + long timeoutMillis) { + return watchFile(projectName, repositoryName, lastKnownRevision, query, + timeoutMillis, WatchConstants.DEFAULT_WATCH_ERROR_ON_ENTRY_NOT_FOUND); + } + + /** + * Waits for the file matched by the specified {@link Query} to be changed since the specified + * {@code lastKnownRevision}. If the file does not exist and {@code errorOnEntryNotFound} is {@code true}, + * the returned {@link CompletableFuture} will be completed exceptionally with + * {@link EntryNotFoundException}. If no changes were made within the specified {@code timeoutMillis}, + * the returned {@link CompletableFuture} will be completed with {@code null}. + * It is recommended to specify the largest {@code timeoutMillis} allowed by the server. + * This method is equivalent to calling: + *
{@code
+     * CentralDogma dogma = ...
+     * dogma.forRepo(projectName, repositoryName)
+     *      .watch(query)
+     *      .timeoutMillis(timeoutMillis)
+     *      .errorOnEntryNotFound(errorOnEntryNotFound)
+     *      .start(lastKnownRevision);
+     * }
+ * + * @return the {@link Entry} which contains the latest known {@link Query} result. + * {@code null} if the file was not changed for {@code timeoutMillis} milliseconds + * since the invocation of this method. {@link EntryNotFoundException} is raised if the + * target does not exist. */ CompletableFuture> watchFile(String projectName, String repositoryName, Revision lastKnownRevision, Query query, - long timeoutMillis); + long timeoutMillis, boolean errorOnEntryNotFound); /** * Returns a {@link Watcher} which notifies its listeners when the result of the @@ -496,9 +704,12 @@ CompletableFuture> watchFile(String projectName, String repositoryN * assert content instanceof JsonNode; * ... * });} + * + * @deprecated Use {@link WatcherRequest#start()} via {@link CentralDogmaRepository#watcher(Query)}. */ + @Deprecated default Watcher fileWatcher(String projectName, String repositoryName, Query query) { - return fileWatcher(projectName, repositoryName, query, Function.identity()); + return forRepo(projectName, repositoryName).watcher(query).start(); } /** @@ -516,7 +727,10 @@ default Watcher fileWatcher(String projectName, String repositoryName, Qu * *

Note that {@link Function} by default is executed by a blocking task executor so that you can * safely call a blocking operation. + * + * @deprecated Use {@link WatcherRequest#start()} via {@link CentralDogmaRepository#watcher(Query)}. */ + @Deprecated Watcher fileWatcher(String projectName, String repositoryName, Query query, Function function); @@ -533,10 +747,11 @@ Watcher fileWatcher(String projectName, String repositoryName, * ... * });} * - * @param executor the {@link Executor} that executes the {@link Function} + * @deprecated Use {@link WatcherRequest#start()} via {@link CentralDogmaRepository#watcher(Query)}. */ - Watcher fileWatcher(String projectName, String repositoryName, - Query query, Function function, Executor executor); + @Deprecated + Watcher fileWatcher(String projectName, String repositoryName, Query query, + Function function, Executor executor); /** * Returns a {@link Watcher} which notifies its listeners when the specified repository has a new commit @@ -547,9 +762,12 @@ Watcher fileWatcher(String projectName, String repositoryName, * watcher.watch(revision -> { * ... * });} + * + * @deprecated Use {@link WatcherRequest#start()} via {@link CentralDogmaRepository#watcher(PathPattern)}. */ + @Deprecated default Watcher repositoryWatcher(String projectName, String repositoryName, String pathPattern) { - return repositoryWatcher(projectName, repositoryName, pathPattern, Function.identity()); + return forRepo(projectName, repositoryName).watcher(toPathPattern(pathPattern)).start(); } /** @@ -568,7 +786,10 @@ default Watcher repositoryWatcher(String projectName, String repositor *

Note that you may get {@link RevisionNotFoundException} during the {@code getFiles()} call and * may have to retry in the above example due to * a known issue. + * + * @deprecated Use {@link WatcherRequest#start()} via {@link CentralDogmaRepository#watcher(PathPattern)}. */ + @Deprecated Watcher repositoryWatcher(String projectName, String repositoryName, String pathPattern, Function function); @@ -588,7 +809,10 @@ Watcher repositoryWatcher(String projectName, String repositoryName, Stri * a known issue. * * @param executor the {@link Executor} that executes the {@link Function} + * + * @deprecated Use {@link WatcherRequest#start()} via {@link CentralDogmaRepository#watcher(PathPattern)}. */ + @Deprecated Watcher repositoryWatcher(String projectName, String repositoryName, String pathPattern, Function function, Executor executor); } diff --git a/client/java/src/main/java/com/linecorp/centraldogma/client/CentralDogmaRepository.java b/client/java/src/main/java/com/linecorp/centraldogma/client/CentralDogmaRepository.java new file mode 100644 index 0000000000..a7d9662832 --- /dev/null +++ b/client/java/src/main/java/com/linecorp/centraldogma/client/CentralDogmaRepository.java @@ -0,0 +1,284 @@ +/* + * Copyright 2021 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.linecorp.centraldogma.client; + +import static java.util.Objects.requireNonNull; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ScheduledExecutorService; + +import com.google.common.collect.ImmutableList; + +import com.linecorp.centraldogma.common.Change; +import com.linecorp.centraldogma.common.Markup; +import com.linecorp.centraldogma.common.MergeQuery; +import com.linecorp.centraldogma.common.MergeSource; +import com.linecorp.centraldogma.common.PathPattern; +import com.linecorp.centraldogma.common.Query; +import com.linecorp.centraldogma.common.QueryType; +import com.linecorp.centraldogma.common.Revision; + +/** + * Prepares to send requests to the Central Dogma repository. + */ +public final class CentralDogmaRepository { + + private final CentralDogma centralDogma; + private final String projectName; + private final String repositoryName; + private final ScheduledExecutorService blockingTaskExecutor; + + CentralDogmaRepository(CentralDogma centralDogma, String projectName, String repositoryName, + ScheduledExecutorService blockingTaskExecutor) { + this.centralDogma = centralDogma; + this.projectName = projectName; + this.repositoryName = repositoryName; + this.blockingTaskExecutor = blockingTaskExecutor; + } + + CentralDogma centralDogma() { + return centralDogma; + } + + String projectName() { + return projectName; + } + + String repositoryName() { + return repositoryName; + } + + /** + * Converts the relative revision number to the absolute revision number. e.g. {@code -1 -> 3} + * + * @return the absolute {@link Revision} + */ + public CompletableFuture normalize(Revision revision) { + requireNonNull(revision, "revision"); + return centralDogma.normalizeRevision(projectName, repositoryName, revision); + } + + /** + * Returns a new {@link FileRequest} that is used to retrieve the file in the Central Dogma repository. + * Call {@link FileRequest#get(Revision)} to perform the same operation as + * {@link CentralDogma#getFile(String, String, Revision, Query)}. + */ + public FileRequest file(String path) { + requireNonNull(path, "path"); + return file(Query.of(QueryType.IDENTITY, path)); + } + + /** + * Returns a new {@link FileRequest} that is used to retrieve the file in the Central Dogma repository. + * Call {@link FileRequest#get(Revision)} to perform the same operation as + * {@link CentralDogma#getFile(String, String, Revision, Query)}. + */ + public FileRequest file(Query query) { + requireNonNull(query, "query"); + return new FileRequest<>(this, query); + } + + /** + * Returns a new {@link FilesRequest} that is used to retrieve or list files in the + * Central Dogma repository. + * Call {@link FilesRequest#get(Revision)} or {@link FilesRequest#list(Revision)} for those operation. + */ + public FilesRequest file(PathPattern pathPattern) { + requireNonNull(pathPattern, "pathPattern"); + return new FilesRequest(this, pathPattern); + } + + /** + * Returns a new {@link MergeRequest} that is used to retrieve the merged file in the + * Central Dogma repository. + * Call {@link MergeRequest#get(Revision)} to perform the same operation as + * {@link CentralDogma#mergeFiles(String, String, Revision, MergeQuery)}. + */ + public MergeRequest merge(MergeSource... mergeSources) { + requireNonNull(mergeSources, "mergeSources"); + return merge(ImmutableList.copyOf(mergeSources)); + } + + /** + * Returns a new {@link MergeRequest} that is used to retrieve the merged file in the + * Central Dogma repository. + * Call {@link MergeRequest#get(Revision)} to perform the same operation as + * {@link CentralDogma#mergeFiles(String, String, Revision, MergeQuery)}. + */ + public MergeRequest merge(Iterable mergeSources) { + requireNonNull(mergeSources, "mergeSources"); + return merge(MergeQuery.ofJson(mergeSources)); + } + + /** + * Returns a new {@link MergeRequest} that is used to retrieve the merged file in the + * Central Dogma repository. + * Call {@link MergeRequest#get(Revision)} to perform the same operation as + * {@link CentralDogma#mergeFiles(String, String, Revision, MergeQuery)}. + */ + public MergeRequest merge(MergeQuery mergeQuery) { + requireNonNull(mergeQuery, "mergeQuery"); + return new MergeRequest<>(this, mergeQuery); + } + + /** + * Returns a new {@link HistoryRequest} that is used to retrieve the history of all files in the + * Central Dogma repository. + * Call {@link HistoryRequest#get(Revision, Revision)} to perform the same operation as + * {@link CentralDogma#getHistory(String, String, Revision, Revision, PathPattern)}. + */ + public HistoryRequest history() { + return history(PathPattern.all()); + } + + /** + * Returns a new {@link HistoryRequest} that is used to retrieve the history of files in the + * Central Dogma repository. + * Call {@link HistoryRequest#get(Revision, Revision)} to perform the same operation as + * {@link CentralDogma#getHistory(String, String, Revision, Revision, PathPattern)}. + */ + public HistoryRequest history(PathPattern pathPattern) { + requireNonNull(pathPattern, "pathPattern"); + return new HistoryRequest(this, pathPattern); + } + + /** + * Returns a new {@link DiffRequest} that is used to retrieve the diff of the file in the + * Central Dogma repository. + * Call {@link DiffRequest#get(Revision, Revision)} to perform the same operation as + * {@link CentralDogma#getDiff(String, String, Revision, Revision, Query)}. + */ + public DiffRequest diff(String path) { + requireNonNull(path, "path"); + return diff(Query.of(QueryType.IDENTITY, path)); + } + + /** + * Returns a new {@link DiffRequest} that is used to retrieve the diff of the file in the + * Central Dogma repository. + * Call {@link DiffRequest#get(Revision, Revision)} to perform the same operation as + * {@link CentralDogma#getDiff(String, String, Revision, Revision, Query)}. + */ + public DiffRequest diff(Query query) { + requireNonNull(query, "query"); + return new DiffRequest<>(this, query); + } + + /** + * Returns a new {@link DiffFilesRequest} that is used to retrieve the diff of files in the + * Central Dogma repository. + * Call {@link DiffFilesRequest#get(Revision, Revision)} to perform the same operation as + * {@link CentralDogma#getDiff(String, String, Revision, Revision, PathPattern)}. + */ + public DiffFilesRequest diff(PathPattern pathPattern) { + requireNonNull(pathPattern, "pathPattern"); + return new DiffFilesRequest(this, pathPattern); + } + + /** + * Returns a new {@link PreviewDiffRequest} that is used to retrieve the preview diff of files in the + * Central Dogma repository. + * Call {@link PreviewDiffRequest#get(Revision)} to perform the same operation as + * {@link CentralDogma#getPreviewDiffs(String, String, Revision, Iterable)}. + */ + public PreviewDiffRequest diff(Change... changes) { + requireNonNull(changes, "changes"); + return new PreviewDiffRequest(this, ImmutableList.copyOf(changes)); + } + + /** + * Returns a new {@link PreviewDiffRequest} that is used to retrieve the preview diff of files in the + * Central Dogma repository. + * Call {@link PreviewDiffRequest#get(Revision)} to perform the same operation as + * {@link CentralDogma#getPreviewDiffs(String, String, Revision, Iterable)}. + */ + public PreviewDiffRequest diff(Iterable> changes) { + requireNonNull(changes, "changes"); + return new PreviewDiffRequest(this, changes); + } + + /** + * Returns a new {@link CommitRequest} that is used to push the {@link Change}s to the + * Central Dogma repository. + * Call {@link CommitRequest#push(Revision)} to perform the same operation as + * {@link CentralDogma#push(String, String, Revision, String, String, Markup, Iterable)}. + */ + public CommitRequest commit(String summary, Change... changes) { + requireNonNull(changes, "changes"); + return commit(summary, ImmutableList.copyOf(changes)); + } + + /** + * Returns a new {@link CommitRequest} that is used to push the {@link Change}s to the + * Central Dogma repository. + * Call {@link CommitRequest#push(Revision)} to perform the same operation as + * {@link CentralDogma#push(String, String, Revision, String, String, Markup, Iterable)}. + */ + public CommitRequest commit(String summary, Iterable> changes) { + requireNonNull(summary, "summary"); + requireNonNull(changes, "changes"); + return new CommitRequest(this, summary, changes); + } + + /** + * Returns a new {@link WatchRequest} that is used to watch the file in the + * Central Dogma repository. + * Call {@link WatchRequest#start(Revision)} to perform the same operation as + * {@link CentralDogma#watchFile(String, String, Revision, Query, long, boolean)}. + */ + public WatchRequest watch(String path) { + requireNonNull(path, "path"); + return watch(Query.of(QueryType.IDENTITY, path)); + } + + /** + * Returns a new {@link WatchRequest} that is used to watch the file in the + * Central Dogma repository. + * Call {@link WatchRequest#start(Revision)} to perform the same operation as + * {@link CentralDogma#watchFile(String, String, Revision, Query, long, boolean)}. + */ + public WatchRequest watch(Query query) { + requireNonNull(query, "query"); + return new WatchRequest<>(this, query); + } + + /** + * Returns a new {@link WatchFilesRequest} that is used to watch the files in the + * Central Dogma repository. + * Call {@link WatchFilesRequest#start(Revision)} to perform the same operation as + * {@link CentralDogma#watchRepository(String, String, Revision, PathPattern, long, boolean)}. + */ + public WatchFilesRequest watch(PathPattern pathPattern) { + requireNonNull(pathPattern, "pathPattern"); + return new WatchFilesRequest(this, pathPattern); + } + + /** + * Returns a new {@link WatcherRequest} that is used to create a {@link Watcher}. + */ + public WatcherRequest watcher(Query query) { + requireNonNull(query, "query"); + return new WatcherRequest<>(this, query, blockingTaskExecutor); + } + + /** + * Returns a new {@link WatcherRequest} that is used to create a {@link Watcher}. + */ + public WatcherRequest watcher(PathPattern pathPattern) { + requireNonNull(pathPattern, "pathPattern"); + return new WatcherRequest<>(this, pathPattern, blockingTaskExecutor); + } +} diff --git a/client/java/src/main/java/com/linecorp/centraldogma/client/CommitRequest.java b/client/java/src/main/java/com/linecorp/centraldogma/client/CommitRequest.java new file mode 100644 index 0000000000..0776c4417d --- /dev/null +++ b/client/java/src/main/java/com/linecorp/centraldogma/client/CommitRequest.java @@ -0,0 +1,77 @@ +/* + * Copyright 2021 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.linecorp.centraldogma.client; + +import static java.util.Objects.requireNonNull; + +import java.util.concurrent.CompletableFuture; + +import com.linecorp.centraldogma.common.Change; +import com.linecorp.centraldogma.common.Commit; +import com.linecorp.centraldogma.common.Markup; +import com.linecorp.centraldogma.common.PushResult; +import com.linecorp.centraldogma.common.Revision; + +/** + * Prepares to send a {@link CentralDogma#push(String, String, Revision, String, String, Markup, Iterable)} + * request to the Central Dogma repository. + */ +public final class CommitRequest { + + private final CentralDogmaRepository centralDogmaRepo; + private final String summary; + private final Iterable> changes; + + private String detail = ""; + private Markup markup = Markup.PLAINTEXT; + + CommitRequest(CentralDogmaRepository centralDogmaRepo, + String summary, Iterable> changes) { + this.centralDogmaRepo = centralDogmaRepo; + this.summary = summary; + this.changes = changes; + } + + /** + * Sets the detail and {@link Markup} of a {@link Commit}. + */ + public CommitRequest detail(String detail, Markup markup) { + this.detail = requireNonNull(detail, "detail"); + this.markup = requireNonNull(markup, "markup"); + return this; + } + + /** + * Pushes the {@link Change}s to the repository with {@link Revision#HEAD}. + * + * @return the {@link PushResult} which tells the {@link Revision} and timestamp of the new {@link Commit} + */ + public CompletableFuture push() { + return push(Revision.HEAD); + } + + /** + * Pushes the {@link Change}s to the repository with the {@link Revision}. + * + * @return the {@link PushResult} which tells the {@link Revision} and timestamp of the new {@link Commit} + */ + public CompletableFuture push(Revision baseRevision) { + requireNonNull(baseRevision, "baseRevision"); + return centralDogmaRepo.centralDogma().push(centralDogmaRepo.projectName(), + centralDogmaRepo.repositoryName(), + baseRevision, summary, detail, markup, changes); + } +} diff --git a/client/java/src/main/java/com/linecorp/centraldogma/client/DiffFilesRequest.java b/client/java/src/main/java/com/linecorp/centraldogma/client/DiffFilesRequest.java new file mode 100644 index 0000000000..022b7426ff --- /dev/null +++ b/client/java/src/main/java/com/linecorp/centraldogma/client/DiffFilesRequest.java @@ -0,0 +1,50 @@ +/* + * Copyright 2021 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.linecorp.centraldogma.client; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import com.linecorp.centraldogma.common.Change; +import com.linecorp.centraldogma.common.PathPattern; +import com.linecorp.centraldogma.common.Revision; + +/** + * Prepares to send a {@link CentralDogma#getDiff(String, String, Revision, Revision, PathPattern)} request + * to the Central Dogma repository. + */ +public final class DiffFilesRequest { + + private final CentralDogmaRepository centralDogmaRepo; + private final PathPattern pathPattern; + + DiffFilesRequest(CentralDogmaRepository centralDogmaRepo, PathPattern pathPattern) { + this.centralDogmaRepo = centralDogmaRepo; + this.pathPattern = pathPattern; + } + + /** + * Retrieves the diffs of the files matched by the given path pattern between two {@link Revision}s. + * + * @return a {@link List} of the {@link Change}s that contain the diffs between the files matched by the + * given {@link PathPattern} between two revisions. + */ + public CompletableFuture>> get(Revision from, Revision to) { + return centralDogmaRepo.centralDogma().getDiff(centralDogmaRepo.projectName(), + centralDogmaRepo.repositoryName(), + from, to, pathPattern); + } +} diff --git a/client/java/src/main/java/com/linecorp/centraldogma/client/DiffRequest.java b/client/java/src/main/java/com/linecorp/centraldogma/client/DiffRequest.java new file mode 100644 index 0000000000..0234962077 --- /dev/null +++ b/client/java/src/main/java/com/linecorp/centraldogma/client/DiffRequest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2021 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.linecorp.centraldogma.client; + +import static java.util.Objects.requireNonNull; + +import java.util.concurrent.CompletableFuture; + +import com.linecorp.centraldogma.common.Change; +import com.linecorp.centraldogma.common.Query; +import com.linecorp.centraldogma.common.Revision; + +/** + * Prepares to send a {@link CentralDogma#getDiff(String, String, Revision, Revision, Query)} request to the + * Central Dogma repository. + */ +public final class DiffRequest { + + private final CentralDogmaRepository centralDogmaRepo; + private final Query query; + + DiffRequest(CentralDogmaRepository centralDogmaRepo, Query query) { + this.centralDogmaRepo = centralDogmaRepo; + this.query = query; + } + + /** + * Queries a file at two different revisions and returns the diff of the two {@link Query} results. + * + * @return the {@link Change} that contains the diff of the file matched by the given {@code query} + * between the specified two revisions + */ + public CompletableFuture> get(Revision from, Revision to) { + requireNonNull(from, "from"); + requireNonNull(to, "to"); + return centralDogmaRepo.centralDogma().getDiff(centralDogmaRepo.projectName(), + centralDogmaRepo.repositoryName(), + from, to, query); + } +} diff --git a/client/java/src/main/java/com/linecorp/centraldogma/client/FileRequest.java b/client/java/src/main/java/com/linecorp/centraldogma/client/FileRequest.java new file mode 100644 index 0000000000..a9a5b38ca8 --- /dev/null +++ b/client/java/src/main/java/com/linecorp/centraldogma/client/FileRequest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2021 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.linecorp.centraldogma.client; + +import static java.util.Objects.requireNonNull; + +import java.util.concurrent.CompletableFuture; + +import com.linecorp.centraldogma.common.Entry; +import com.linecorp.centraldogma.common.Query; +import com.linecorp.centraldogma.common.Revision; + +/** + * Prepares to send a {@link CentralDogma#getFile(String, String, Revision, Query)} request to the + * Central Dogma repository. + */ +public final class FileRequest { + + private final Query query; + private final CentralDogmaRepository centralDogmaRepo; + + FileRequest(CentralDogmaRepository centralDogmaRepo, Query query) { + this.query = query; + this.centralDogmaRepo = centralDogmaRepo; + } + + /** + * Retrieves a file located at {@link Query#path()} at the {@link Revision#HEAD}. + * + * @return the {@link Entry} that is matched by the given {@link Query} + */ + public CompletableFuture> get() { + return get(Revision.HEAD); + } + + /** + * Retrieves a file located at {@link Query#path()} at the {@link Revision}. + * + * @return the {@link Entry} that is matched by the given {@link Query} + */ + public CompletableFuture> get(Revision revision) { + requireNonNull(revision, "revision"); + return centralDogmaRepo.centralDogma().getFile(centralDogmaRepo.projectName(), + centralDogmaRepo.repositoryName(), + revision, query); + } +} diff --git a/client/java/src/main/java/com/linecorp/centraldogma/client/FileWatcher.java b/client/java/src/main/java/com/linecorp/centraldogma/client/FileWatcher.java new file mode 100644 index 0000000000..9cd8e3da17 --- /dev/null +++ b/client/java/src/main/java/com/linecorp/centraldogma/client/FileWatcher.java @@ -0,0 +1,79 @@ +/* + * Copyright 2021 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.linecorp.centraldogma.client; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Function; + +import javax.annotation.Nullable; + +import com.linecorp.centraldogma.common.Entry; +import com.linecorp.centraldogma.common.Query; +import com.linecorp.centraldogma.common.Revision; + +final class FileWatcher extends AbstractWatcher { + + private final CentralDogma centralDogma; + private final String projectName; + private final String repositoryName; + private final Query query; + private final long timeoutMillis; + private final boolean errorOnEntryNotFound; + @Nullable + private final Function mapper; + @Nullable + private final Executor mapperExecutor; + + FileWatcher(CentralDogma centralDogma, ScheduledExecutorService watchScheduler, + String projectName, String repositoryName, Query query, + long timeoutMillis, boolean errorOnEntryNotFound, + @Nullable Function mapper, Executor mapperExecutor, + long delayOnSuccessMillis, long initialDelayMillis, long maxDelayMillis, + double multiplier, double jitterRate) { + super(watchScheduler, projectName, repositoryName, query.path(), errorOnEntryNotFound, + delayOnSuccessMillis, initialDelayMillis, maxDelayMillis, multiplier, jitterRate); + this.centralDogma = centralDogma; + this.projectName = projectName; + this.repositoryName = repositoryName; + this.query = query; + this.timeoutMillis = timeoutMillis; + this.errorOnEntryNotFound = errorOnEntryNotFound; + this.mapper = mapper; + this.mapperExecutor = mapperExecutor; + } + + @Override + CompletableFuture> doWatch(Revision lastKnownRevision) { + final CompletableFuture> future = centralDogma.watchFile( + projectName, repositoryName, lastKnownRevision, query, timeoutMillis, errorOnEntryNotFound); + if (mapper == null) { + return future.thenApply(entry -> { + if (entry == null) { + return null; + } + return new Latest<>(entry.revision(), entry.content()); + }); + } + return future.thenApplyAsync(entry -> { + if (entry == null) { + return null; + } + return new Latest<>(entry.revision(), mapper.apply(entry.content())); + }, mapperExecutor); + } +} diff --git a/client/java/src/main/java/com/linecorp/centraldogma/client/FilesRequest.java b/client/java/src/main/java/com/linecorp/centraldogma/client/FilesRequest.java new file mode 100644 index 0000000000..9269757ef4 --- /dev/null +++ b/client/java/src/main/java/com/linecorp/centraldogma/client/FilesRequest.java @@ -0,0 +1,84 @@ +/* + * Copyright 2021 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.linecorp.centraldogma.client; + +import static java.util.Objects.requireNonNull; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import com.linecorp.centraldogma.common.Entry; +import com.linecorp.centraldogma.common.EntryType; +import com.linecorp.centraldogma.common.PathPattern; +import com.linecorp.centraldogma.common.Revision; + +/** + * Prepares to send a {@link CentralDogma#getFiles(String, String, Revision, PathPattern)} or + * {@link CentralDogma#listFiles(String, String, Revision, PathPattern)} request to the + * Central Dogma repository. + */ +public final class FilesRequest { + + private final CentralDogmaRepository centralDogmaRepo; + private final PathPattern pathPattern; + + FilesRequest(CentralDogmaRepository centralDogmaRepo, PathPattern pathPattern) { + this.centralDogmaRepo = centralDogmaRepo; + this.pathPattern = pathPattern; + } + + /** + * Retrieves the list of the files matched by the given path pattern at the {@link Revision#HEAD}. + * + * @return a {@link Map} of file path and type pairs + */ + public CompletableFuture> list() { + return list(Revision.HEAD); + } + + /** + * Retrieves the list of the files matched by the given path pattern at the {@link Revision}. + * + * @return a {@link Map} of file path and type pairs + */ + public CompletableFuture> list(Revision revision) { + requireNonNull(revision, "revision"); + return centralDogmaRepo.centralDogma().listFiles(centralDogmaRepo.projectName(), + centralDogmaRepo.repositoryName(), + revision, pathPattern); + } + + /** + * Retrieves the files matched by the path pattern at the {@link Revision#HEAD}. + * + * @return a {@link Map} of file path and {@link Entry} pairs + */ + public CompletableFuture>> get() { + return get(Revision.HEAD); + } + + /** + * Retrieves the files matched by the path pattern at the {@link Revision}. + * + * @return a {@link Map} of file path and {@link Entry} pairs + */ + public CompletableFuture>> get(Revision revision) { + requireNonNull(revision, "revision"); + return centralDogmaRepo.centralDogma().getFiles(centralDogmaRepo.projectName(), + centralDogmaRepo.repositoryName(), + revision, pathPattern); + } +} diff --git a/client/java/src/main/java/com/linecorp/centraldogma/client/FilesWatcher.java b/client/java/src/main/java/com/linecorp/centraldogma/client/FilesWatcher.java new file mode 100644 index 0000000000..202e2a3b33 --- /dev/null +++ b/client/java/src/main/java/com/linecorp/centraldogma/client/FilesWatcher.java @@ -0,0 +1,82 @@ +/* + * Copyright 2021 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.linecorp.centraldogma.client; + +import static com.linecorp.centraldogma.internal.Util.unsafeCast; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Function; + +import javax.annotation.Nullable; + +import com.linecorp.centraldogma.common.PathPattern; +import com.linecorp.centraldogma.common.Revision; + +final class FilesWatcher extends AbstractWatcher { + + private final CentralDogma centralDogma; + private final String projectName; + private final String repositoryName; + private final PathPattern pathPattern; + private final long timeoutMillis; + private final boolean errorOnEntryNotFound; + @Nullable + private final Function mapper; + @Nullable + private final Executor mapperExecutor; + + FilesWatcher(CentralDogma centralDogma, ScheduledExecutorService watchScheduler, + String projectName, String repositoryName, PathPattern pathPattern, + long timeoutMillis, boolean errorOnEntryNotFound, + @Nullable Function mapper, Executor mapperExecutor, + long delayOnSuccessMillis, long initialDelayMillis, long maxDelayMillis, + double multiplier, double jitterRate) { + super(watchScheduler, projectName, repositoryName, pathPattern.patternString(), errorOnEntryNotFound, + delayOnSuccessMillis, initialDelayMillis, maxDelayMillis, multiplier, jitterRate); + this.centralDogma = centralDogma; + this.projectName = projectName; + this.repositoryName = repositoryName; + this.pathPattern = pathPattern; + this.timeoutMillis = timeoutMillis; + this.errorOnEntryNotFound = errorOnEntryNotFound; + this.mapper = mapper != null ? unsafeCast(mapper) : null; + this.mapperExecutor = mapperExecutor; + } + + @Override + CompletableFuture> doWatch(Revision lastKnownRevision) { + final CompletableFuture future = centralDogma.watchRepository( + projectName, repositoryName, lastKnownRevision, + pathPattern, timeoutMillis, errorOnEntryNotFound); + if (mapper == null) { + return future.thenApply(revision -> { + if (revision == null) { + return null; + } + //noinspection unchecked + return new Latest<>(revision, (T) revision); + }); + } + return future.thenApplyAsync(revision -> { + if (revision == null) { + return null; + } + return new Latest<>(revision, mapper.apply(revision)); + }, mapperExecutor); + } +} diff --git a/client/java/src/main/java/com/linecorp/centraldogma/client/HistoryRequest.java b/client/java/src/main/java/com/linecorp/centraldogma/client/HistoryRequest.java new file mode 100644 index 0000000000..12c2336552 --- /dev/null +++ b/client/java/src/main/java/com/linecorp/centraldogma/client/HistoryRequest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2021 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.linecorp.centraldogma.client; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import com.linecorp.centraldogma.common.Commit; +import com.linecorp.centraldogma.common.PathPattern; +import com.linecorp.centraldogma.common.Revision; + +/** + * Prepares to send a {@link CentralDogma#getHistory(String, String, Revision, Revision, PathPattern)} request + * to the Central Dogma repository. + */ +public final class HistoryRequest { + + private final CentralDogmaRepository centralDogmaRepo; + private final PathPattern pathPattern; + + HistoryRequest(CentralDogmaRepository centralDogmaRepo, PathPattern pathPattern) { + this.centralDogmaRepo = centralDogmaRepo; + this.pathPattern = pathPattern; + } + + /** + * Retrieves the history of the files matched by the given path pattern between two {@link Revision}s. + * + *

Note that this method does not retrieve the diffs but only metadata about the changes. + * + * @return a {@link List} that contains the {@link Commit}s of the files matched by the given + * {@link PathPattern} in the specified repository + */ + public CompletableFuture> get(Revision from, Revision to) { + requireNonNull(from, "from"); + requireNonNull(to, "to"); + return centralDogmaRepo.centralDogma().getHistory(centralDogmaRepo.projectName(), + centralDogmaRepo.repositoryName(), + from, to, pathPattern); + } +} diff --git a/client/java/src/main/java/com/linecorp/centraldogma/client/Latest.java b/client/java/src/main/java/com/linecorp/centraldogma/client/Latest.java index 0ec8f8e87e..4eaa0bc6f2 100644 --- a/client/java/src/main/java/com/linecorp/centraldogma/client/Latest.java +++ b/client/java/src/main/java/com/linecorp/centraldogma/client/Latest.java @@ -27,7 +27,7 @@ import com.linecorp.centraldogma.common.Revision; /** - * An immutable holder of the latest known value and its {@link Revision} retrieved by {@link Watcher}. + * An immutable holder of the latest known value and its {@link Revision} retrieved by a {@link Watcher}. * * @param the value type */ diff --git a/client/java/src/main/java/com/linecorp/centraldogma/client/MappingWatcher.java b/client/java/src/main/java/com/linecorp/centraldogma/client/MappingWatcher.java new file mode 100644 index 0000000000..56047360cd --- /dev/null +++ b/client/java/src/main/java/com/linecorp/centraldogma/client/MappingWatcher.java @@ -0,0 +1,192 @@ +/* + * Copyright 2019 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.linecorp.centraldogma.client; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import javax.annotation.Nullable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.Maps; + +import com.linecorp.centraldogma.common.Revision; + +final class MappingWatcher implements Watcher { + + private static final Logger logger = LoggerFactory.getLogger(MappingWatcher.class); + + static MappingWatcher of(Watcher parent, Function mapper, + Executor executor, boolean closeParentWhenClosing) { + requireNonNull(parent, "parent"); + requireNonNull(mapper, "mapper"); + requireNonNull(executor, "executor"); + // TODO(minwoo): extract mapper function and combine it with the new mapper. + return new MappingWatcher<>(parent, mapper, executor, closeParentWhenClosing); + } + + private final Watcher parent; + private final Function mapper; + private final Executor mapperExecutor; + private final boolean closeParentWhenClosing; + private final CompletableFuture> initialValueFuture = new CompletableFuture<>(); + private final List, Executor>> updateListeners = + new CopyOnWriteArrayList<>(); + + @Nullable + private volatile Latest mappedLatest; + private volatile boolean closed; + + MappingWatcher(Watcher parent, Function mapper, Executor mapperExecutor, + boolean closeParentWhenClosing) { + this.parent = parent; + this.mapper = mapper; + this.mapperExecutor = mapperExecutor; + this.closeParentWhenClosing = closeParentWhenClosing; + parent.initialValueFuture().exceptionally(cause -> { + initialValueFuture.completeExceptionally(cause); + return null; + }); + parent.watch((revision, value) -> { + if (closed) { + return; + } + final U mappedValue; + try { + mappedValue = mapper.apply(value); + } catch (Exception e) { + logger.warn("Unexpected exception is raised from mapper.apply(). mapper: {}", mapper, e); + if (!initialValueFuture.isDone()) { + initialValueFuture.completeExceptionally(e); + } + close(); + return; + } + final Latest oldLatest = mappedLatest; + if (oldLatest != null && oldLatest.value() == mappedValue) { + return; + } + + // mappedValue can be nullable which is fine. + final Latest newLatest = new Latest<>(revision, mappedValue); + mappedLatest = newLatest; + notifyListeners(newLatest); + if (!initialValueFuture.isDone()) { + initialValueFuture.complete(newLatest); + } + }, mapperExecutor); + } + + private void notifyListeners(Latest latest) { + if (closed) { + return; + } + + for (Map.Entry, Executor> entry : updateListeners) { + final BiConsumer listener = entry.getKey(); + final Executor executor = entry.getValue(); + if (mapperExecutor == executor) { + notifyListener(latest, listener); + } else { + executor.execute(() -> notifyListener(latest, listener)); + } + } + } + + private void notifyListener(Latest latest, BiConsumer listener) { + try { + listener.accept(latest.revision(), latest.value()); + } catch (Exception e) { + logger.warn("Unexpected exception is raised from {}: rev={}", + listener, latest.revision(), e); + } + } + + @Override + public ScheduledExecutorService watchScheduler() { + return parent.watchScheduler(); + } + + @Override + public CompletableFuture> initialValueFuture() { + return initialValueFuture; + } + + @Override + public Latest latest() { + final Latest mappedLatest = this.mappedLatest; + if (mappedLatest == null) { + throw new IllegalStateException("value not available yet"); + } + return mappedLatest; + } + + @Override + public void close() { + closed = true; + if (!initialValueFuture.isDone()) { + initialValueFuture.cancel(false); + } + if (closeParentWhenClosing) { + parent.close(); + } + } + + @Override + public void watch(BiConsumer listener) { + watch(listener, parent.watchScheduler()); + } + + @Override + public void watch(BiConsumer listener, Executor executor) { + requireNonNull(listener, "listener"); + requireNonNull(executor, "executor"); + updateListeners.add(Maps.immutableEntry(listener, executor)); + + final Latest mappedLatest = this.mappedLatest; + if (mappedLatest != null) { + // There's a chance that listener.accept(...) is called twice for the same value + // if this watch method is called: + // - after "mappedLatest = newLatest;" is invoked. + // - and before notifyListener() is called. + // However, it's such a rare case and we usually call `watch` method after creating a Watcher, + // which means mappedLatest is probably not set yet, so we don't use a lock to guarantee + // the atomicity. + executor.execute(() -> listener.accept(mappedLatest.revision(), mappedLatest.value())); + } + } + + @Override + public String toString() { + return toStringHelper(this) + .add("parent", parent) + .add("mapper", mapper) + .add("closed", closed) + .toString(); + } +} diff --git a/client/java/src/main/java/com/linecorp/centraldogma/client/MergeRequest.java b/client/java/src/main/java/com/linecorp/centraldogma/client/MergeRequest.java new file mode 100644 index 0000000000..68fe8d23db --- /dev/null +++ b/client/java/src/main/java/com/linecorp/centraldogma/client/MergeRequest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2021 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.linecorp.centraldogma.client; + +import static java.util.Objects.requireNonNull; + +import java.util.concurrent.CompletableFuture; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +import com.linecorp.centraldogma.common.MergeQuery; +import com.linecorp.centraldogma.common.MergedEntry; +import com.linecorp.centraldogma.common.Revision; + +/** + * Prepares to send a {@link CentralDogma#mergeFiles(String, String, Revision, MergeQuery)} request to the + * Central Dogma repository. + */ +public final class MergeRequest { + + private final CentralDogmaRepository centralDogmaRepo; + private final MergeQuery mergeQuery; + + MergeRequest(CentralDogmaRepository centralDogmaRepo, + MergeQuery mergeQuery) { + this.centralDogmaRepo = centralDogmaRepo; + this.mergeQuery = mergeQuery; + } + + /** + * Retrieves the merged entry of the {@link MergeQuery} at the {@link Revision#HEAD}. + * Only JSON entry merge is currently supported. The JSON files are merged sequentially as specified in + * the {@link MergeQuery}. + * + *

Note that only {@link ObjectNode} is recursively merged traversing the children. Other node types are + * simply replaced. + * + * @return the {@link MergedEntry} which contains the result of the merge + */ + public CompletableFuture> get() { + return get(Revision.HEAD); + } + + /** + * Retrieves the merged entry of the {@link MergeQuery} at the {@link Revision}. + * Only JSON entry merge is currently supported. The JSON files are merged sequentially as specified in + * the {@link MergeQuery}. + * + *

Note that only {@link ObjectNode} is recursively merged traversing the children. Other node types are + * simply replaced. + * + * @return the {@link MergedEntry} which contains the result of the merge + */ + public CompletableFuture> get(Revision revision) { + requireNonNull(revision, "revision"); + return centralDogmaRepo.centralDogma().mergeFiles(centralDogmaRepo.projectName(), + centralDogmaRepo.repositoryName(), + revision, mergeQuery); + } +} diff --git a/client/java/src/main/java/com/linecorp/centraldogma/client/PreviewDiffRequest.java b/client/java/src/main/java/com/linecorp/centraldogma/client/PreviewDiffRequest.java new file mode 100644 index 0000000000..70bf657ecf --- /dev/null +++ b/client/java/src/main/java/com/linecorp/centraldogma/client/PreviewDiffRequest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2021 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.linecorp.centraldogma.client; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import com.linecorp.centraldogma.common.Change; +import com.linecorp.centraldogma.common.Revision; + +/** + * Prepares to send a {@link CentralDogma#getPreviewDiffs(String, String, Revision, Iterable)} request to the + * Central Dogma repository. + */ +public final class PreviewDiffRequest { + + private final CentralDogmaRepository centralDogmaRepo; + private final Iterable> changes; + + PreviewDiffRequest(CentralDogmaRepository centralDogmaRepo, + Iterable> changes) { + this.centralDogmaRepo = centralDogmaRepo; + this.changes = changes; + } + + /** + * Retrieves the preview diffs, which are hypothetical diffs generated from the + * {@link Revision#HEAD} and the latest {@link Revision} if the {@link Change}s were + * successfully pushed to the specified repository. This operation is useful for + * pre-checking if the {@link Change}s will be applied as expected without any conflicts. + * + * @return the diffs which would be committed if the {@link Change}s were pushed successfully + */ + public CompletableFuture>> get() { + return get(Revision.HEAD); + } + + /** + * Retrieves the preview diffs, which are hypothetical diffs generated from the specified + * {@link Revision} and the latest {@link Revision} if the {@link Change}s were + * successfully pushed to the specified repository. This operation is useful for + * pre-checking if the {@link Change}s will be applied as expected without any conflicts. + * + * @return the diffs which would be committed if the {@link Change}s were pushed successfully + */ + public CompletableFuture>> get(Revision baseRevision) { + requireNonNull(baseRevision, "baseRevision"); + return centralDogmaRepo.centralDogma().getPreviewDiffs(centralDogmaRepo.projectName(), + centralDogmaRepo.repositoryName(), + baseRevision, changes); + } +} diff --git a/client/java/src/main/java/com/linecorp/centraldogma/client/TransformingWatcher.java b/client/java/src/main/java/com/linecorp/centraldogma/client/TransformingWatcher.java deleted file mode 100644 index b9b89ff355..0000000000 --- a/client/java/src/main/java/com/linecorp/centraldogma/client/TransformingWatcher.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2019 LINE Corporation - * - * LINE Corporation licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package com.linecorp.centraldogma.client; - -import static com.google.common.base.MoreObjects.toStringHelper; -import static java.util.Objects.requireNonNull; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; -import java.util.function.BiConsumer; -import java.util.function.Function; - -import javax.annotation.Nullable; - -import com.linecorp.centraldogma.common.Revision; - -final class TransformingWatcher implements Watcher { - - private final Watcher parent; - private final Function transformer; - - @Nullable - private volatile Latest transformedLatest; - private volatile boolean closed; - private volatile boolean firstWatch = true; - - TransformingWatcher(Watcher parent, Function transformer) { - this.parent = parent; - this.transformer = transformer; - } - - @Override - public CompletableFuture> initialValueFuture() { - return parent.initialValueFuture().thenApply(ignored -> latest()); - } - - @Override - public Latest latest() { - if (!closed) { - final Latest latestParent = parent.latest(); - final U transformedValue = transformer.apply(latestParent.value()); - transformedLatest = new Latest<>(latestParent.revision(), transformedValue); - } - return transformedLatest; - } - - @Override - public void close() { - closed = true; - // do nothing else. We don't own the parent's lifecycle - } - - @Override - public void watch(BiConsumer listener) { - requireNonNull(listener, "listener"); - parent.watch(transform(listener)); - } - - @Override - public void watch(BiConsumer listener, Executor executor) { - requireNonNull(listener, "listener"); - requireNonNull(executor, "executor"); - parent.watch(transform(listener), executor); - } - - private BiConsumer transform(BiConsumer listener) { - return (revision, value) -> { - if (closed) { - return; - } - final U transformedValue = transformer.apply(value); - final boolean changed; - if (firstWatch) { - firstWatch = false; - changed = true; - } else { - changed = !transformedLatest.value().equals(transformedValue); - } - if (changed) { - transformedLatest = new Latest<>(revision, transformedValue); - listener.accept(revision, transformedValue); - } // else, transformed value has not changed - }; - } - - @Override - public String toString() { - return toStringHelper(this) - .add("parent", parent) - .add("transformer", transformer) - .add("closed", closed) - .add("firstWatch", firstWatch) - .toString(); - } -} diff --git a/client/java/src/main/java/com/linecorp/centraldogma/client/WatchConstants.java b/client/java/src/main/java/com/linecorp/centraldogma/client/WatchConstants.java index 2ecdd2ba43..b3aa6204a9 100644 --- a/client/java/src/main/java/com/linecorp/centraldogma/client/WatchConstants.java +++ b/client/java/src/main/java/com/linecorp/centraldogma/client/WatchConstants.java @@ -20,6 +20,7 @@ final class WatchConstants { static final long DEFAULT_WATCH_TIMEOUT_MILLIS = TimeUnit.MINUTES.toMillis(1); + static final boolean DEFAULT_WATCH_ERROR_ON_ENTRY_NOT_FOUND = false; static final int RECOMMENDED_AWAIT_TIMEOUT_SECONDS = 20; private WatchConstants() {} diff --git a/client/java/src/main/java/com/linecorp/centraldogma/client/WatchFilesRequest.java b/client/java/src/main/java/com/linecorp/centraldogma/client/WatchFilesRequest.java new file mode 100644 index 0000000000..da4b0826eb --- /dev/null +++ b/client/java/src/main/java/com/linecorp/centraldogma/client/WatchFilesRequest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2021 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.linecorp.centraldogma.client; + +import static java.util.Objects.requireNonNull; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; + +import com.linecorp.centraldogma.common.EntryNotFoundException; +import com.linecorp.centraldogma.common.PathPattern; +import com.linecorp.centraldogma.common.Revision; + +/** + * Prepares to send a {@link CentralDogma#watchRepository(String, String, Revision, PathPattern, long, boolean)} + * request to the Central Dogma repository. + */ +public final class WatchFilesRequest extends WatchOptions { + + private final CentralDogmaRepository centralDogmaRepo; + private final PathPattern pathPattern; + + WatchFilesRequest(CentralDogmaRepository centralDogmaRepo, PathPattern pathPattern) { + this.centralDogmaRepo = centralDogmaRepo; + this.pathPattern = pathPattern; + } + + @Override + public WatchFilesRequest timeout(Duration timeout) { + return (WatchFilesRequest) super.timeout(timeout); + } + + @Override + public WatchFilesRequest timeoutMillis(long timeoutMillis) { + return (WatchFilesRequest) super.timeoutMillis(timeoutMillis); + } + + @Override + public WatchFilesRequest errorOnEntryNotFound(boolean errorOnEntryNotFound) { + return (WatchFilesRequest) super.errorOnEntryNotFound(errorOnEntryNotFound); + } + + /** + * Waits for the files matched by the {@link PathPattern} to be changed since the {@link Revision#HEAD}. + * If no changes were made within the {@link #timeoutMillis(long)}, the + * returned {@link CompletableFuture} will be completed with {@code null}. + * + * @return the latest known {@link Revision} which contains the changes for the matched files. + * {@code null} if the files were not changed for {@code timeoutMillis} milliseconds + * since the invocation of this method. {@link EntryNotFoundException} is raised if the + * target does not exist. + */ + public CompletableFuture start() { + return start(Revision.HEAD); + } + + /** + * Waits for the files matched by the {@link PathPattern} to be changed since the {@code lastKnownRevision}. + * If no changes were made within the {@link #timeoutMillis(long)}, the + * returned {@link CompletableFuture} will be completed with {@code null}. + * + * @return the latest known {@link Revision} which contains the changes for the matched files. + * {@code null} if the files were not changed for {@code timeoutMillis} milliseconds + * since the invocation of this method. {@link EntryNotFoundException} is raised if the + * target does not exist. + */ + public CompletableFuture start(Revision lastKnownRevision) { + requireNonNull(lastKnownRevision, "lastKnownRevision"); + return centralDogmaRepo.centralDogma().watchRepository(centralDogmaRepo.projectName(), + centralDogmaRepo.repositoryName(), + lastKnownRevision, pathPattern, + timeoutMillis(), errorOnEntryNotFound()); + } +} diff --git a/client/java/src/main/java/com/linecorp/centraldogma/client/WatchOptions.java b/client/java/src/main/java/com/linecorp/centraldogma/client/WatchOptions.java new file mode 100644 index 0000000000..0fd78e5296 --- /dev/null +++ b/client/java/src/main/java/com/linecorp/centraldogma/client/WatchOptions.java @@ -0,0 +1,66 @@ +/* + * Copyright 2021 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.linecorp.centraldogma.client; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; + +import java.time.Duration; + +import com.linecorp.centraldogma.common.EntryNotFoundException; + +/** + * Options for a watch request. + */ +abstract class WatchOptions { + + private long timeoutMillis = WatchConstants.DEFAULT_WATCH_TIMEOUT_MILLIS; + private boolean errorOnEntryNotFound = WatchConstants.DEFAULT_WATCH_ERROR_ON_ENTRY_NOT_FOUND; + + /** + * Sets the timeout for a watch request. + */ + WatchOptions timeout(Duration timeout) { + requireNonNull(timeout, "timeout"); + checkArgument(!timeout.isZero() && !timeout.isNegative(), "timeout: %s (expected: > 0)", timeout); + return timeoutMillis(timeout.toMillis()); + } + + /** + * Sets the timeout for a watch request in milliseconds. + */ + WatchOptions timeoutMillis(long timeoutMillis) { + checkArgument(timeoutMillis > 0, "timeoutMillis: %s (expected: > 0)", timeoutMillis); + this.timeoutMillis = timeoutMillis; + return this; + } + + long timeoutMillis() { + return timeoutMillis; + } + + /** + * Sets whether to throw an {@link EntryNotFoundException} if the watch target does not exist. + */ + public WatchOptions errorOnEntryNotFound(boolean errorOnEntryNotFound) { + this.errorOnEntryNotFound = errorOnEntryNotFound; + return this; + } + + boolean errorOnEntryNotFound() { + return errorOnEntryNotFound; + } +} diff --git a/client/java/src/main/java/com/linecorp/centraldogma/client/WatchRequest.java b/client/java/src/main/java/com/linecorp/centraldogma/client/WatchRequest.java new file mode 100644 index 0000000000..a67062801c --- /dev/null +++ b/client/java/src/main/java/com/linecorp/centraldogma/client/WatchRequest.java @@ -0,0 +1,91 @@ +/* + * Copyright 2021 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.linecorp.centraldogma.client; + +import static java.util.Objects.requireNonNull; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; + +import com.linecorp.centraldogma.common.Entry; +import com.linecorp.centraldogma.common.EntryNotFoundException; +import com.linecorp.centraldogma.common.Query; +import com.linecorp.centraldogma.common.Revision; + +/** + * Prepares to send a {@link CentralDogma#watchFile(String, String, Revision, Query, long, boolean)} + * request to the Central Dogma repository or create a new {@link Watcher}. + */ +public final class WatchRequest extends WatchOptions { + + private final CentralDogmaRepository centralDogmaRepo; + private final Query query; + + WatchRequest(CentralDogmaRepository centralDogmaRepo, Query query) { + this.centralDogmaRepo = centralDogmaRepo; + this.query = query; + } + + @Override + public WatchRequest timeout(Duration timeout) { + //noinspection unchecked + return (WatchRequest) super.timeout(timeout); + } + + @Override + public WatchRequest timeoutMillis(long timeoutMillis) { + //noinspection unchecked + return (WatchRequest) super.timeoutMillis(timeoutMillis); + } + + @Override + public WatchRequest errorOnEntryNotFound(boolean errorOnEntryNotFound) { + //noinspection unchecked + return (WatchRequest) super.errorOnEntryNotFound(errorOnEntryNotFound); + } + + /** + * Waits for the file matched by the {@link Query} to be changed since the {@link Revision#HEAD}. + * If no changes were made within the {@link #timeoutMillis(long)}, the + * returned {@link CompletableFuture} will be completed with {@code null}. + * + * @return the {@link Entry} which contains the latest known {@link Query} result. + * {@code null} if the file was not changed for {@link #timeoutMillis(long)} milliseconds + * since the invocation of this method. {@link EntryNotFoundException} is raised if the + * target does not exist. + */ + public CompletableFuture> start() { + return start(Revision.HEAD); + } + + /** + * Waits for the file matched by the {@link Query} to be changed since the {@code lastKnownRevision}. + * If no changes were made within the {@link #timeoutMillis(long)}, the + * returned {@link CompletableFuture} will be completed with {@code null}. + * + * @return the {@link Entry} which contains the latest known {@link Query} result. + * {@code null} if the file was not changed for {@link #timeoutMillis(long)} milliseconds + * since the invocation of this method. {@link EntryNotFoundException} is raised if the + * target does not exist. + */ + public CompletableFuture> start(Revision lastKnownRevision) { + requireNonNull(lastKnownRevision, "lastKnownRevision"); + return centralDogmaRepo.centralDogma().watchFile(centralDogmaRepo.projectName(), + centralDogmaRepo.repositoryName(), + lastKnownRevision, query, + timeoutMillis(), errorOnEntryNotFound()); + } +} diff --git a/client/java/src/main/java/com/linecorp/centraldogma/client/Watcher.java b/client/java/src/main/java/com/linecorp/centraldogma/client/Watcher.java index f49435582b..c18ba0756d 100644 --- a/client/java/src/main/java/com/linecorp/centraldogma/client/Watcher.java +++ b/client/java/src/main/java/com/linecorp/centraldogma/client/Watcher.java @@ -21,6 +21,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.BiConsumer; @@ -32,11 +33,14 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.MissingNode; +import com.linecorp.centraldogma.common.EntryNotFoundException; +import com.linecorp.centraldogma.common.PathPattern; import com.linecorp.centraldogma.common.Query; import com.linecorp.centraldogma.common.Revision; /** - * Watches the changes of a repository or a file. + * Watches the changes of a repository or a file. The {@link Watcher} must be closed via + * {@link Watcher#close()} after use. * * @param the watch result type */ @@ -70,6 +74,11 @@ public String toString() { }); } + /** + * Returns the {@link ScheduledExecutorService} that is used to schedule watch. + */ + ScheduledExecutorService watchScheduler(); + /** * Returns the {@link CompletableFuture} which is completed when the initial value retrieval is done * successfully. @@ -83,12 +92,13 @@ public String toString() { * the initial value came from. * * @throws CancellationException if this watcher has been closed by {@link #close()} + * @throws EntryNotFoundException if {@code errorOnEntryNotFound} is {@code true} and entry isn't found on + * watching the initial value */ default Latest awaitInitialValue() throws InterruptedException { try { return initialValueFuture().get(); } catch (ExecutionException e) { - // Should never occur because we never complete this future exceptionally. throw new Error(e); } } @@ -114,6 +124,8 @@ default Latest awaitInitialValue() throws InterruptedException { * the initial value came from. * * @throws CancellationException if this watcher has been closed by {@link #close()} + * @throws EntryNotFoundException if {@code errorOnEntryNotFound} is {@code true} and entry isn't found on + * watching the initial value * @throws TimeoutException if failed to retrieve the initial value within the specified timeout */ default Latest awaitInitialValue(long timeout, TimeUnit unit) throws InterruptedException, @@ -122,7 +134,6 @@ default Latest awaitInitialValue(long timeout, TimeUnit unit) throws Interrup try { return initialValueFuture().get(timeout, unit); } catch (ExecutionException e) { - // Should never occur because we never complete this future exceptionally. throw new Error(e); } } @@ -144,6 +155,8 @@ default Latest awaitInitialValue(long timeout, TimeUnit unit) throws Interrup * @return the initial value, or the default value if timed out. * * @throws CancellationException if this watcher has been closed by {@link #close()} + * @throws EntryNotFoundException if {@code errorOnEntryNotFound} is {@code true} and entry isn't found on + * watching the initial value */ @Nullable default T awaitInitialValue(long timeout, TimeUnit unit, @Nullable T defaultValue) @@ -156,7 +169,7 @@ default T awaitInitialValue(long timeout, TimeUnit unit, @Nullable T defaultValu } /** - * Returns the latest {@link Revision} and value of {@code watchFile()} result. + * Returns the latest {@link Revision} and value of {@code watchFile()} or {@code watchRepository()} result. * * @throws IllegalStateException if the value is not available yet. * Use {@link #awaitInitialValue(long, TimeUnit)} first or @@ -165,7 +178,7 @@ default T awaitInitialValue(long timeout, TimeUnit unit, @Nullable T defaultValu Latest latest(); /** - * Returns the latest value of {@code watchFile()} result. + * Returns the latest value of {@code watchFile()} or {@code watchRepository()} result. * * @throws IllegalStateException if the value is not available yet. * Use {@link #awaitInitialValue(long, TimeUnit)} first or @@ -177,7 +190,7 @@ default T latestValue() { } /** - * Returns the latest value of {@code watchFile()} result. + * Returns the latest value of {@code watchFile()} or {@code watchRepository()} result. * * @param defaultValue the default value which is returned when the value is not available yet */ @@ -192,7 +205,7 @@ default T latestValue(@Nullable T defaultValue) { } /** - * Stops watching the file specified in the {@link Query} or the {@code pathPattern} in the repository. + * Stops watching the file specified in the {@link Query} or the {@link PathPattern} in the repository. */ @Override void close(); @@ -200,6 +213,11 @@ default T latestValue(@Nullable T defaultValue) { /** * Registers a {@link BiConsumer} that will be invoked when the value of the watched entry becomes * available or changes. + * + *

Note that the specified {@link BiConsumer} is not called when {@code errorOnEntryNotFound} is + * {@code true} and the target doesn't exist in the Central Dogma server when this {@link Watcher} sends + * the initial watch call. You should use {@link #initialValueFuture()} or {@link #awaitInitialValue()} to + * check the target exists or not. */ void watch(BiConsumer listener); @@ -207,6 +225,11 @@ default T latestValue(@Nullable T defaultValue) { * Registers a {@link BiConsumer} that will be invoked when the value of the watched entry becomes * available or changes. * + *

Note that the specified {@link BiConsumer} is not called when {@code errorOnEntryNotFound} is + * {@code true} and the target doesn't exist in the Central Dogma server when this {@link Watcher} sends + * the initial watch call. You should use {@link #initialValueFuture()} or {@link #awaitInitialValue()} to + * check the target exists or not. + * * @param executor the {@link Executor} that executes the {@link BiConsumer} */ void watch(BiConsumer listener, Executor executor); @@ -231,14 +254,18 @@ default void watch(Consumer listener, Executor executor) { } /** - * Forks into a new {@link Watcher}, that reuses the current watcher and applies a transformation. - * - * @return A {@link Watcher} that is effectively filtering in a sense that, - * its listeners are not notified when a change has no effect on the transformed value. - * Furthermore, it does not need to be closed after use. + * Returns a {@link Watcher} that applies the {@link Function} for the {@link Latest#value()}. */ - default Watcher newChild(Function transformer) { - requireNonNull(transformer, "transformer"); - return new TransformingWatcher<>(this, transformer); + default Watcher newChild(Function mapper) { + return newChild(mapper, watchScheduler()); + } + + /** + * Returns a {@link Watcher} that applies the {@link Function} for the {@link Latest#value()}. + */ + default Watcher newChild(Function mapper, Executor executor) { + requireNonNull(mapper, "mapper"); + requireNonNull(executor, "executor"); + return MappingWatcher.of(this, mapper, executor, false); } } diff --git a/client/java/src/main/java/com/linecorp/centraldogma/client/WatcherRequest.java b/client/java/src/main/java/com/linecorp/centraldogma/client/WatcherRequest.java new file mode 100644 index 0000000000..ad9d6887c4 --- /dev/null +++ b/client/java/src/main/java/com/linecorp/centraldogma/client/WatcherRequest.java @@ -0,0 +1,188 @@ +/* + * Copyright 2021 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.linecorp.centraldogma.client; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; + +import java.time.Duration; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +import javax.annotation.Nullable; + +import com.linecorp.centraldogma.common.PathPattern; +import com.linecorp.centraldogma.common.Query; + +/** + * Prepares to create a {@link Watcher}. + */ +public final class WatcherRequest extends WatchOptions { + + private static final long DEFAULT_DELAY_ON_SUCCESS_MILLIS = TimeUnit.SECONDS.toMillis(1); + private static final long DEFAULT_MAX_DELAY_MILLIS = TimeUnit.MINUTES.toMillis(1); + private static final double DEFAULT_MULTIPLIER = 2.0; + private static final double DEFAULT_JITTER_RATE = 0.2; + + private final CentralDogmaRepository centralDogmaRepo; + @Nullable + private final Query query; + @Nullable + private final PathPattern pathPattern; + private final ScheduledExecutorService blockingTaskExecutor; + + @Nullable + private Function mapper; + private Executor executor; + + private long delayOnSuccessMillis = DEFAULT_DELAY_ON_SUCCESS_MILLIS; + private long initialDelayMillis = DEFAULT_DELAY_ON_SUCCESS_MILLIS * 2; + private long maxDelayMillis = DEFAULT_MAX_DELAY_MILLIS; + private double multiplier = DEFAULT_MULTIPLIER; + private double jitterRate = DEFAULT_JITTER_RATE; + + WatcherRequest(CentralDogmaRepository centralDogmaRepo, Query query, + ScheduledExecutorService blockingTaskExecutor) { + this(centralDogmaRepo, query, null, blockingTaskExecutor); + } + + WatcherRequest(CentralDogmaRepository centralDogmaRepo, PathPattern pathPattern, + ScheduledExecutorService blockingTaskExecutor) { + this(centralDogmaRepo, null, pathPattern, blockingTaskExecutor); + } + + private WatcherRequest(CentralDogmaRepository centralDogmaRepo, + @Nullable Query query, @Nullable PathPattern pathPattern, + ScheduledExecutorService blockingTaskExecutor) { + this.centralDogmaRepo = centralDogmaRepo; + this.query = query; + this.pathPattern = pathPattern; + this.blockingTaskExecutor = blockingTaskExecutor; + executor = blockingTaskExecutor; + } + + /** + * Sets the {@link Function} to apply to the result of a watch request. + */ + @SuppressWarnings("unchecked") + public WatcherRequest map(Function mapper) { + if (this.mapper == null) { + this.mapper = (Function) mapper; + } else { + this.mapper = (Function) this.mapper.andThen(mapper); + } + return (WatcherRequest) this; + } + + /** + * Sets the {@link Executor} to execute the {@link #map(Function)}. + */ + public WatcherRequest mapperExecutor(Executor executor) { + this.executor = executor; + return this; + } + + /** + * Sets the delay for sending the next watch request when the previous request succeeds. + */ + public WatcherRequest delayOnSuccess(Duration delayOnSuccess) { + requireNonNull(delayOnSuccess, "delayOnSuccess"); + checkArgument(!delayOnSuccess.isNegative(), + "delayOnSuccess: %s (expected: >= 0)", delayOnSuccess); + return delayOnSuccessMillis(delayOnSuccess.toMillis()); + } + + /** + * Sets the delay in milliseconds for sending the next watch request when the previous request succeeds. + */ + public WatcherRequest delayOnSuccessMillis(long delayOnSuccessMillis) { + this.delayOnSuccessMillis = delayOnSuccessMillis; + checkArgument(delayOnSuccessMillis >= 0, + "delayOnSuccessMillis: %s (expected: >= 0)", delayOnSuccessMillis); + return this; + } + + /** + * Sets the delays and multiplier which is used to calculate the delay + * for sending the next watch request when the previous request fails. + * Currently, it uses exponential backoff. File a feature request if you need another algorithm. + */ + public WatcherRequest backoffOnFailure(long initialDelayMillis, long maxDelayMillis, + double multiplier) { + checkArgument(initialDelayMillis >= 0, "initialDelayMillis: %s (expected: >= 0)", initialDelayMillis); + checkArgument(initialDelayMillis <= maxDelayMillis, "maxDelayMillis: %s (expected: >= %s)", + maxDelayMillis, initialDelayMillis); + checkArgument(multiplier > 1.0, "multiplier: %s (expected: > 1.0)", multiplier); + this.initialDelayMillis = initialDelayMillis; + this.maxDelayMillis = maxDelayMillis; + this.multiplier = multiplier; + return this; + } + + /** + * Sets the jitter to apply the delay. + */ + public WatcherRequest jitterRate(double jitterRate) { + checkArgument(0.0 <= jitterRate && jitterRate <= 1.0, + "jitterRate: %s (expected: >= 0.0 and <= 1.0)", jitterRate); + this.jitterRate = jitterRate; + return this; + } + + @Override + public WatcherRequest timeout(Duration timeout) { + //noinspection unchecked + return (WatcherRequest) super.timeout(timeout); + } + + @Override + public WatcherRequest timeoutMillis(long timeoutMillis) { + //noinspection unchecked + return (WatcherRequest) super.timeoutMillis(timeoutMillis); + } + + @Override + public WatcherRequest errorOnEntryNotFound(boolean errorOnEntryNotFound) { + //noinspection unchecked + return (WatcherRequest) super.errorOnEntryNotFound(errorOnEntryNotFound); + } + + /** + * Creates a new {@link Watcher} and starts to watch the target. The {@link Watcher} must be closed via + * {@link Watcher#close()} after use. + */ + public Watcher start() { + final String proName = centralDogmaRepo.projectName(); + final String repoName = centralDogmaRepo.repositoryName(); + final AbstractWatcher watcher; + if (query != null) { + watcher = new FileWatcher<>( + centralDogmaRepo.centralDogma(), blockingTaskExecutor, proName, repoName, query, + timeoutMillis(), errorOnEntryNotFound(), mapper, executor, delayOnSuccessMillis, + initialDelayMillis, maxDelayMillis, multiplier, jitterRate); + } else { + assert pathPattern != null; + watcher = new FilesWatcher<>( + centralDogmaRepo.centralDogma(), blockingTaskExecutor, proName, repoName, pathPattern, + timeoutMillis(), errorOnEntryNotFound(), mapper, executor, delayOnSuccessMillis, + initialDelayMillis, maxDelayMillis, multiplier, jitterRate); + } + watcher.start(); + return watcher; + } +} diff --git a/client/java/src/main/java/com/linecorp/centraldogma/client/updater/CentralDogmaBeanFactory.java b/client/java/src/main/java/com/linecorp/centraldogma/client/updater/CentralDogmaBeanFactory.java index d97d5c61b1..0d62d1b3c7 100644 --- a/client/java/src/main/java/com/linecorp/centraldogma/client/updater/CentralDogmaBeanFactory.java +++ b/client/java/src/main/java/com/linecorp/centraldogma/client/updater/CentralDogmaBeanFactory.java @@ -288,20 +288,20 @@ public T get(T defaultValue, Class beanType, Consumer changeListener, checkState(!isNullOrEmpty(settings.repository().get()), "settings.repositoryName should non-null"); checkState(!isNullOrEmpty(settings.path().get()), "settings.fileName should non-null"); - final Watcher watcher = dogma.fileWatcher( - settings.project().get(), - settings.repository().get(), - buildQuery(settings), - jsonNode -> { - try { - final T value = objectMapper.treeToValue(jsonNode, beanType); - changeListener.accept(value); - return value; - } catch (JsonProcessingException e) { - throw new IllegalStateException( - "Failed to convert a JSON node into: " + beanType.getName(), e); - } - }); + final Watcher watcher = + dogma.forRepo(settings.project().get(), settings.repository().get()) + .watcher(buildQuery(settings)) + .map(jsonNode -> { + try { + final T value = objectMapper.treeToValue(jsonNode, beanType); + changeListener.accept(value); + return value; + } catch (JsonProcessingException e) { + throw new IllegalStateException( + "Failed to convert a JSON node into: " + beanType.getName(), e); + } + }) + .start(); if (initialValueTimeout > 0) { final long t0 = System.nanoTime(); diff --git a/client/java/src/main/java/com/linecorp/centraldogma/internal/client/FileWatcher.java b/client/java/src/main/java/com/linecorp/centraldogma/internal/client/FileWatcher.java deleted file mode 100644 index 97a34d22ca..0000000000 --- a/client/java/src/main/java/com/linecorp/centraldogma/internal/client/FileWatcher.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2018 LINE Corporation - * - * LINE Corporation licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package com.linecorp.centraldogma.internal.client; - -import static com.linecorp.centraldogma.internal.Util.unsafeCast; -import static java.util.Objects.requireNonNull; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; -import java.util.concurrent.ScheduledExecutorService; -import java.util.function.Function; - -import com.linecorp.centraldogma.client.CentralDogma; -import com.linecorp.centraldogma.client.Latest; -import com.linecorp.centraldogma.common.Query; -import com.linecorp.centraldogma.common.Revision; - -public final class FileWatcher extends AbstractWatcher { - private final Query query; - private final Function function; - private final Executor callbackExecutor; - - /** - * Creates a new instance. - */ - public FileWatcher(CentralDogma client, ScheduledExecutorService watchScheduler, - Executor callbackExecutor, - String projectName, String repositoryName, - Query query, Function function) { - - super(client, watchScheduler, projectName, repositoryName, requireNonNull(query, "query").path()); - this.query = query; - this.function = unsafeCast(requireNonNull(function, "function")); - this.callbackExecutor = requireNonNull(callbackExecutor, "callbackExecutor"); - } - - @Override - protected CompletableFuture> doWatch(CentralDogma client, String projectName, - String repositoryName, Revision lastKnownRevision) { - return client.watchFile(projectName, repositoryName, lastKnownRevision, query) - .thenApplyAsync(result -> { - if (result == null) { - return null; - } - return new Latest<>(result.revision(), function.apply(result.content())); - }, callbackExecutor); - } -} diff --git a/client/java/src/main/java/com/linecorp/centraldogma/internal/client/ReplicationLagTolerantCentralDogma.java b/client/java/src/main/java/com/linecorp/centraldogma/internal/client/ReplicationLagTolerantCentralDogma.java index d3d45ea25a..2a2ced3e0e 100644 --- a/client/java/src/main/java/com/linecorp/centraldogma/internal/client/ReplicationLagTolerantCentralDogma.java +++ b/client/java/src/main/java/com/linecorp/centraldogma/internal/client/ReplicationLagTolerantCentralDogma.java @@ -55,6 +55,7 @@ import com.linecorp.centraldogma.common.Markup; import com.linecorp.centraldogma.common.MergeQuery; import com.linecorp.centraldogma.common.MergedEntry; +import com.linecorp.centraldogma.common.PathPattern; import com.linecorp.centraldogma.common.PushResult; import com.linecorp.centraldogma.common.Query; import com.linecorp.centraldogma.common.Revision; @@ -214,7 +215,7 @@ public String toString() { @Override public CompletableFuture> listFiles( - String projectName, String repositoryName, Revision revision, String pathPattern) { + String projectName, String repositoryName, Revision revision, PathPattern pathPattern) { return normalizeRevisionAndExecuteWithRetries( projectName, repositoryName, revision, new Function>>() { @@ -252,7 +253,7 @@ public String toString() { @Override public CompletableFuture>> getFiles( - String projectName, String repositoryName, Revision revision, String pathPattern) { + String projectName, String repositoryName, Revision revision, PathPattern pathPattern) { return normalizeRevisionAndExecuteWithRetries( projectName, repositoryName, revision, new Function>>>() { @@ -292,7 +293,7 @@ public String toString() { @Override public CompletableFuture> getHistory( String projectName, String repositoryName, Revision from, - Revision to, String pathPattern) { + Revision to, PathPattern pathPattern) { return normalizeRevisionsAndExecuteWithRetries( projectName, repositoryName, from, to, new BiFunction>>() { @@ -331,15 +332,15 @@ public String toString() { } @Override - public CompletableFuture>> getDiffs( - String projectName, String repositoryName, Revision from, Revision to, String pathPattern) { + public CompletableFuture>> getDiff( + String projectName, String repositoryName, Revision from, Revision to, PathPattern pathPattern) { return normalizeRevisionsAndExecuteWithRetries( projectName, repositoryName, from, to, new BiFunction>>>() { @Override public CompletableFuture>> apply(Revision normFromRev, Revision normToRev) { - return delegate.getDiffs(projectName, repositoryName, - normFromRev, normToRev, pathPattern); + return delegate.getDiff(projectName, repositoryName, + normFromRev, normToRev, pathPattern); } @Override @@ -429,7 +430,7 @@ private BiPredicate pushRetryPredicate( @Override public CompletableFuture watchRepository( String projectName, String repositoryName, Revision lastKnownRevision, - String pathPattern, long timeoutMillis) { + PathPattern pathPattern, long timeoutMillis, boolean errorOnEntryNotFound) { return normalizeRevisionAndExecuteWithRetries( projectName, repositoryName, lastKnownRevision, @@ -437,7 +438,7 @@ public CompletableFuture watchRepository( @Override public CompletableFuture apply(Revision normLastKnownRevision) { return delegate.watchRepository(projectName, repositoryName, normLastKnownRevision, - pathPattern, timeoutMillis) + pathPattern, timeoutMillis, errorOnEntryNotFound) .thenApply(newLastKnownRevision -> { if (newLastKnownRevision != null) { updateLatestKnownRevision(projectName, repositoryName, @@ -450,7 +451,8 @@ public CompletableFuture apply(Revision normLastKnownRevision) { @Override public String toString() { return "watchRepository(" + projectName + ", " + repositoryName + ", " + - lastKnownRevision + ", " + pathPattern + ", " + timeoutMillis + ')'; + lastKnownRevision + ", " + pathPattern + ", " + timeoutMillis + ", " + + errorOnEntryNotFound + ')'; } }); } @@ -458,7 +460,7 @@ public String toString() { @Override public CompletableFuture> watchFile( String projectName, String repositoryName, Revision lastKnownRevision, - Query query, long timeoutMillis) { + Query query, long timeoutMillis, boolean errorOnEntryNotFound) { return normalizeRevisionAndExecuteWithRetries( projectName, repositoryName, lastKnownRevision, @@ -466,7 +468,7 @@ public CompletableFuture> watchFile( @Override public CompletableFuture> apply(Revision normLastKnownRevision) { return delegate.watchFile(projectName, repositoryName, normLastKnownRevision, - query, timeoutMillis) + query, timeoutMillis, errorOnEntryNotFound) .thenApply(entry -> { if (entry != null) { updateLatestKnownRevision(projectName, repositoryName, @@ -479,7 +481,8 @@ public CompletableFuture> apply(Revision normLastKnownRevision) { @Override public String toString() { return "watchFile(" + projectName + ", " + repositoryName + ", " + - lastKnownRevision + ", " + query + ", " + timeoutMillis + ')'; + lastKnownRevision + ", " + query + ", " + timeoutMillis + ", " + + errorOnEntryNotFound + ')'; } }); } diff --git a/client/java/src/main/java/com/linecorp/centraldogma/internal/client/RepositoryWatcher.java b/client/java/src/main/java/com/linecorp/centraldogma/internal/client/RepositoryWatcher.java deleted file mode 100644 index 6d354ab2e4..0000000000 --- a/client/java/src/main/java/com/linecorp/centraldogma/internal/client/RepositoryWatcher.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2018 LINE Corporation - * - * LINE Corporation licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package com.linecorp.centraldogma.internal.client; - -import static java.util.Objects.requireNonNull; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; -import java.util.concurrent.ScheduledExecutorService; -import java.util.function.Function; - -import com.linecorp.centraldogma.client.CentralDogma; -import com.linecorp.centraldogma.client.Latest; -import com.linecorp.centraldogma.common.Revision; - -public final class RepositoryWatcher extends AbstractWatcher { - private final String pathPattern; - private final Function function; - private final Executor callbackExecutor; - - /** - * Creates a new instance. - */ - public RepositoryWatcher(CentralDogma client, ScheduledExecutorService watchScheduler, - Executor callbackExecutor, - String projectName, String repositoryName, - String pathPattern, Function function) { - super(client, watchScheduler, projectName, repositoryName, pathPattern); - this.pathPattern = requireNonNull(pathPattern, "pathPattern"); - this.function = requireNonNull(function, "function"); - this.callbackExecutor = requireNonNull(callbackExecutor, "callbackExecutor"); - } - - @Override - protected CompletableFuture> doWatch(CentralDogma client, String projectName, - String repositoryName, Revision lastKnownRevision) { - return client.watchRepository(projectName, repositoryName, lastKnownRevision, pathPattern) - .thenApplyAsync(revision -> { - if (revision == null) { - return null; - } - return new Latest<>(revision, function.apply(revision)); - }, callbackExecutor); - } -} diff --git a/client/java/src/test/java/com/linecorp/centraldogma/internal/client/ReplicationLagTolerantCentralDogmaTest.java b/client/java/src/test/java/com/linecorp/centraldogma/internal/client/ReplicationLagTolerantCentralDogmaTest.java index 09feb04d3a..f98ade7c25 100644 --- a/client/java/src/test/java/com/linecorp/centraldogma/internal/client/ReplicationLagTolerantCentralDogmaTest.java +++ b/client/java/src/test/java/com/linecorp/centraldogma/internal/client/ReplicationLagTolerantCentralDogmaTest.java @@ -20,6 +20,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.reset; @@ -47,6 +48,7 @@ import com.linecorp.centraldogma.common.Change; import com.linecorp.centraldogma.common.Entry; import com.linecorp.centraldogma.common.Markup; +import com.linecorp.centraldogma.common.PathPattern; import com.linecorp.centraldogma.common.ProjectNotFoundException; import com.linecorp.centraldogma.common.PushResult; import com.linecorp.centraldogma.common.Query; @@ -323,16 +325,17 @@ void watchRepository() { final Revision latestRevision = new Revision(3); when(delegate.normalizeRevision(any(), any(), any())) .thenReturn(completedFuture(Revision.INIT)); - when(delegate.watchRepository(any(), any(), any(), any(), anyLong())) + when(delegate.watchRepository(any(), any(), any(), any(), anyLong(), anyBoolean())) .thenReturn(completedFuture(latestRevision)); - assertThat(dogma.watchRepository("foo", "bar", Revision.INIT, "/**", 10000L).join()) + assertThat(dogma.watchRepository("foo", "bar", Revision.INIT, PathPattern.all(), 10000L, false).join()) .isEqualTo(latestRevision); assertThat(dogma.latestKnownRevision("foo", "bar")).isEqualTo(latestRevision); verify(delegate, times(1)).normalizeRevision("foo", "bar", Revision.INIT); - verify(delegate, times(1)).watchRepository("foo", "bar", Revision.INIT, "/**", 10000L); + verify(delegate, times(1)).watchRepository("foo", "bar", Revision.INIT, PathPattern.all(), + 10000L, false); verifyNoMoreInteractions(delegate); reset(delegate); @@ -358,16 +361,17 @@ void watchFile() { final Entry latestEntry = Entry.ofText(latestRevision, "/a.txt", "a"); when(delegate.normalizeRevision(any(), any(), any())) .thenReturn(completedFuture(Revision.INIT)); - when(delegate.watchFile(any(), any(), any(), (Query) any(), anyLong())) + when(delegate.watchFile(any(), any(), any(), (Query) any(), anyLong(), anyBoolean())) .thenReturn(completedFuture(latestEntry)); - assertThat(dogma.watchFile("foo", "bar", Revision.INIT, Query.ofText("/a.txt"), 10000L).join()) + assertThat(dogma.watchFile("foo", "bar", Revision.INIT, Query.ofText("/a.txt"), 10000L, false).join()) .isEqualTo(latestEntry); assertThat(dogma.latestKnownRevision("foo", "bar")).isEqualTo(latestRevision); verify(delegate, times(1)).normalizeRevision("foo", "bar", Revision.INIT); - verify(delegate, times(1)).watchFile("foo", "bar", Revision.INIT, Query.ofText("/a.txt"), 10000L); + verify(delegate, times(1)).watchFile("foo", "bar", Revision.INIT, Query.ofText("/a.txt"), 10000L, + false); verifyNoMoreInteractions(delegate); } @@ -382,7 +386,7 @@ void getFile() { throw new RevisionNotFoundException(); })) .thenReturn(completedFuture(latestEntry)); - assertThat(dogma.getFile("foo", "bar", Revision.HEAD, "/a.txt").join()) + assertThat(dogma.forRepo("foo", "bar").file("/a.txt").get(Revision.HEAD).join()) .isEqualTo(latestEntry); verify(delegate).normalizeRevision("foo", "bar", Revision.HEAD); diff --git a/common/src/main/java/com/linecorp/centraldogma/common/DefaultPathPattern.java b/common/src/main/java/com/linecorp/centraldogma/common/DefaultPathPattern.java new file mode 100644 index 0000000000..c25ca2744e --- /dev/null +++ b/common/src/main/java/com/linecorp/centraldogma/common/DefaultPathPattern.java @@ -0,0 +1,110 @@ +/* + * Copyright 2021 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.linecorp.centraldogma.common; + +import static com.google.common.base.Preconditions.checkArgument; + +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; +import com.google.common.math.IntMath; + +final class DefaultPathPattern implements PathPattern { + + private static final Pattern PATH_PATTERN_PATTERN = Pattern.compile("^[- /*_.0-9a-zA-Z]+$"); + + static final String ALL = "/**"; + + static final DefaultPathPattern allPattern = new DefaultPathPattern(ALL, ALL); + + private final String patterns; + + @Nullable + private String encoded; + + DefaultPathPattern(Set patterns) { + this.patterns = patterns.stream() + .peek(DefaultPathPattern::validatePathPattern) + .filter(pattern -> !pattern.isEmpty()) + .map(pattern -> { + if (pattern.charAt(0) != '/') { + return "/**/" + pattern; + } + return pattern; + }).collect(Collectors.joining(",")); + } + + private DefaultPathPattern(String patterns, String encoded) { + this.patterns = patterns; + this.encoded = encoded; + } + + @Override + public String patternString() { + return patterns; + } + + @Override + public String encoded() { + if (encoded != null) { + return encoded; + } + return encoded = encodePathPattern(patterns); + } + + @VisibleForTesting + static String encodePathPattern(String pathPattern) { + // We do not need full escaping because we validated the path pattern already and thus contains only + // -, ' ', /, *, _, ., ',', a-z, A-Z, 0-9. + int spacePos = pathPattern.indexOf(' '); + if (spacePos < 0) { + return pathPattern; + } + + final StringBuilder buf = new StringBuilder(IntMath.saturatedMultiply(pathPattern.length(), 2)); + for (int pos = 0;;) { + buf.append(pathPattern, pos, spacePos); + buf.append("%20"); + pos = spacePos + 1; + spacePos = pathPattern.indexOf(' ', pos); + if (spacePos < 0) { + buf.append(pathPattern, pos, pathPattern.length()); + break; + } + } + + return buf.toString(); + } + + private static String validatePathPattern(String pattern) { + checkArgument(PATH_PATTERN_PATTERN.matcher(pattern).matches(), + "pattern: %s (expected: %s)", pattern, PATH_PATTERN_PATTERN); + return pattern; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("patterns", patterns) + .add("encoded", encoded) + .toString(); + } +} diff --git a/common/src/main/java/com/linecorp/centraldogma/common/PathPattern.java b/common/src/main/java/com/linecorp/centraldogma/common/PathPattern.java new file mode 100644 index 0000000000..5682c1d9b7 --- /dev/null +++ b/common/src/main/java/com/linecorp/centraldogma/common/PathPattern.java @@ -0,0 +1,73 @@ +/* + * Copyright 2021 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.linecorp.centraldogma.common; + +import static com.linecorp.centraldogma.common.DefaultPathPattern.ALL; +import static com.linecorp.centraldogma.common.DefaultPathPattern.allPattern; +import static java.util.Objects.requireNonNull; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Streams; + +/** + * A path pattern that represents a variant of glob. For example: + *

    + *
  • {@code "/**"} - all files
  • + *
  • {@code "*.json"} - all JSON files
  • + *
  • {@code "/foo/*.json"} - all JSON files under the directory {@code /foo}
  • + *
  • "/*/foo.txt" - all files named {@code foo.txt} at the second depth level
  • + *
  • {@code "*.json,/bar/*.txt"} - use comma to specify more than one pattern. A file will be matched + * if any pattern matches.
  • + *
+ */ +public interface PathPattern { + + /** + * Returns the path pattern that represents all files. + */ + static PathPattern all() { + return allPattern; + } + + /** + * Creates a path pattern with the {@code patterns}. + */ + static PathPattern of(String... patterns) { + return of(ImmutableSet.copyOf(requireNonNull(patterns, "patterns"))); + } + + /** + * Creates a path pattern with the {@code patterns}. + */ + static PathPattern of(Iterable patterns) { + requireNonNull(patterns, "patterns"); + if (Streams.stream(patterns).anyMatch(ALL::equals)) { + return allPattern; + } + + return new DefaultPathPattern(ImmutableSet.copyOf(patterns)); + } + + /** + * Returns the path pattern that concatenates the {@code patterns} using ','. + */ + String patternString(); + + /** + * Returns the encoded {@link #patternString()} which just encodes a space to '%20'. + */ + String encoded(); +} diff --git a/common/src/main/java/com/linecorp/centraldogma/internal/PathPatternUtil.java b/common/src/main/java/com/linecorp/centraldogma/internal/PathPatternUtil.java new file mode 100644 index 0000000000..3a0879e291 --- /dev/null +++ b/common/src/main/java/com/linecorp/centraldogma/internal/PathPatternUtil.java @@ -0,0 +1,33 @@ +/* + * Copyright 2021 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.linecorp.centraldogma.internal; + +import static java.util.Objects.requireNonNull; + +import com.google.common.base.Splitter; + +import com.linecorp.centraldogma.common.PathPattern; + +public final class PathPatternUtil { + + private static final Splitter splitter = Splitter.on(',').omitEmptyStrings().trimResults(); + + public static PathPattern toPathPattern(String pathPattern) { + return PathPattern.of(splitter.split(requireNonNull(pathPattern, "pathPattern"))); + } + + private PathPatternUtil() {} +} diff --git a/common/src/test/java/com/linecorp/centraldogma/common/DefaultPathPatternTest.java b/common/src/test/java/com/linecorp/centraldogma/common/DefaultPathPatternTest.java new file mode 100644 index 0000000000..13787c2c6c --- /dev/null +++ b/common/src/test/java/com/linecorp/centraldogma/common/DefaultPathPatternTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2021 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.linecorp.centraldogma.common; + +import static com.linecorp.centraldogma.common.DefaultPathPattern.encodePathPattern; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +import com.google.common.collect.ImmutableSet; + +class DefaultPathPatternTest { + + @Test + void pathPattern() { + PathPattern pathPattern = PathPattern.of( + ImmutableSet.of("/foo/*.json", + "/*/ foo.txt", + "*.json")); // /**/ is prepended when the path does not start with / + + assertThat(pathPattern.patternString()).isEqualTo("/foo/*.json,/*/ foo.txt,/**/*.json"); + assertThat(pathPattern.encoded()).isEqualTo("/foo/*.json,/*/%20foo.txt,/**/*.json"); + + pathPattern = PathPattern.of(ImmutableSet.of("/foo/*.json", "/*/foo.txt", "/**")); + assertThat(pathPattern.patternString()).isEqualTo("/**"); + assertThat(pathPattern.encoded()).isEqualTo("/**"); + } + + @Test + void invalidPathPattern() { + assertThatThrownBy(() -> new DefaultPathPattern(ImmutableSet.of("/,foo/*.json"))) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testEncodePathPattern() { + assertThat(encodePathPattern("/")).isEqualTo("/"); + assertThat(encodePathPattern(" ")).isEqualTo("%20"); + assertThat(encodePathPattern(" ")).isEqualTo("%20%20"); + assertThat(encodePathPattern("a b")).isEqualTo("a%20b"); + assertThat(encodePathPattern(" a ")).isEqualTo("%20a%20"); + + // No new string has to be created when escaping is not necessary. + final String pathPatternThatDoesNotNeedEscaping = "/*.zip,/**/*.jar"; + assertThat(encodePathPattern(pathPatternThatDoesNotNeedEscaping)) + .isSameAs(pathPatternThatDoesNotNeedEscaping); + } +} diff --git a/it/src/test/java/com/linecorp/centraldogma/it/CacheTest.java b/it/src/test/java/com/linecorp/centraldogma/it/CacheTest.java index 433366a072..dbe3da8456 100644 --- a/it/src/test/java/com/linecorp/centraldogma/it/CacheTest.java +++ b/it/src/test/java/com/linecorp/centraldogma/it/CacheTest.java @@ -34,6 +34,7 @@ import com.linecorp.centraldogma.common.Change; import com.linecorp.centraldogma.common.Commit; import com.linecorp.centraldogma.common.Entry; +import com.linecorp.centraldogma.common.PathPattern; import com.linecorp.centraldogma.common.PushResult; import com.linecorp.centraldogma.common.Query; import com.linecorp.centraldogma.common.Revision; @@ -58,8 +59,10 @@ void getFile(ClientType clientType, TestInfo testInfo) { client.createRepository(project, REPO_FOO).join(); final Map meters1 = metersSupplier.get(); - final PushResult res = client.push(project, REPO_FOO, HEAD, "Add a file", - Change.ofTextUpsert("/foo.txt", "bar")).join(); + final PushResult res = client.forRepo(project, REPO_FOO) + .commit("Add a file", Change.ofTextUpsert("/foo.txt", "bar")) + .push() + .join(); final Map meters2 = metersSupplier.get(); // Metadata needs to access to check a write quota (one cache miss). @@ -105,20 +108,23 @@ void history(ClientType clientType, TestInfo testInfo) { client.createProject(project).join(); client.createRepository(project, REPO_FOO).join(); - final PushResult res1 = client.push(project, REPO_FOO, HEAD, "Add a file", - Change.ofTextUpsert("/foo.txt", "bar")).join(); + final PushResult res1 = client.forRepo(project, REPO_FOO) + .commit("Add a file", Change.ofTextUpsert("/foo.txt", "bar")) + .push() + .join(); final Map meters1 = metersSupplier.get(); // Get the history in various combination of from/to revisions. final List history1 = - client.getHistory(project, REPO_FOO, HEAD, new Revision(-2), "/**").join(); + client.getHistory(project, REPO_FOO, HEAD, new Revision(-2), PathPattern.all()).join(); final List history2 = - client.getHistory(project, REPO_FOO, HEAD, INIT, "/**").join(); + client.getHistory(project, REPO_FOO, HEAD, INIT, PathPattern.all()).join(); final List history3 = - client.getHistory(project, REPO_FOO, res1.revision(), new Revision(-2), "/**").join(); + client.getHistory(project, REPO_FOO, res1.revision(), new Revision(-2), PathPattern.all()) + .join(); final List history4 = - client.getHistory(project, REPO_FOO, res1.revision(), INIT, "/**").join(); + client.getHistory(project, REPO_FOO, res1.revision(), INIT, PathPattern.all()).join(); // and they should all same. assertThat(history1).isEqualTo(history2); @@ -140,20 +146,22 @@ void getDiffs(ClientType clientType, TestInfo testInfo) { client.createProject(project).join(); client.createRepository(project, REPO_FOO).join(); - final PushResult res1 = client.push(project, REPO_FOO, HEAD, "Add a file", - Change.ofTextUpsert("/foo.txt", "bar")).join(); + final PushResult res1 = client.forRepo(project, REPO_FOO) + .commit("Add a file", Change.ofTextUpsert("/foo.txt", "bar")) + .push() + .join(); final Map meters1 = metersSupplier.get(); // Get the diffs in various combination of from/to revisions. final List> diff1 = - client.getDiffs(project, REPO_FOO, HEAD, new Revision(-2), "/**").join(); + client.getDiff(project, REPO_FOO, HEAD, new Revision(-2), PathPattern.all()).join(); final List> diff2 = - client.getDiffs(project, REPO_FOO, HEAD, INIT, "/**").join(); + client.getDiff(project, REPO_FOO, HEAD, INIT, PathPattern.all()).join(); final List> diff3 = - client.getDiffs(project, REPO_FOO, res1.revision(), new Revision(-2), "/**").join(); + client.getDiff(project, REPO_FOO, res1.revision(), new Revision(-2), PathPattern.all()).join(); final List> diff4 = - client.getDiffs(project, REPO_FOO, res1.revision(), INIT, "/**").join(); + client.getDiff(project, REPO_FOO, res1.revision(), INIT, PathPattern.all()).join(); // and they should all same. assertThat(diff1).isEqualTo(diff2); diff --git a/it/src/test/java/com/linecorp/centraldogma/it/CentralDogmaEndpointGroupTest.java b/it/src/test/java/com/linecorp/centraldogma/it/CentralDogmaEndpointGroupTest.java index e97e40b9f0..49fc379b12 100644 --- a/it/src/test/java/com/linecorp/centraldogma/it/CentralDogmaEndpointGroupTest.java +++ b/it/src/test/java/com/linecorp/centraldogma/it/CentralDogmaEndpointGroupTest.java @@ -40,7 +40,6 @@ import com.linecorp.centraldogma.client.armeria.EndpointListDecoder; import com.linecorp.centraldogma.common.Change; import com.linecorp.centraldogma.common.Query; -import com.linecorp.centraldogma.common.Revision; import com.linecorp.centraldogma.testing.junit.CentralDogmaExtension; class CentralDogmaEndpointGroupTest { @@ -69,14 +68,14 @@ class CentralDogmaEndpointGroupTest { protected void scaffold(CentralDogma client) { client.createProject("directory").join(); client.createRepository("directory", "my-service").join(); - client.push("directory", "my-service", - Revision.HEAD, "commit", - Change.ofJsonUpsert("/endpoint.json", HOST_AND_PORT_LIST_JSON)) + client.forRepo("directory", "my-service") + .commit("commit", Change.ofJsonUpsert("/endpoint.json", HOST_AND_PORT_LIST_JSON)) + .push() .join(); - client.push("directory", "my-service", - Revision.HEAD, "commit", - Change.ofTextUpsert("/endpoints.txt", - String.join("\n", HOST_AND_PORT_LIST))) + client.forRepo("directory", "my-service") + .commit("commit", Change.ofTextUpsert("/endpoints.txt", + String.join("\n", HOST_AND_PORT_LIST))) + .push() .join(); } @@ -88,8 +87,9 @@ protected boolean runForEachTest() { @Test void json() throws Exception { - try (Watcher watcher = dogma.client().fileWatcher("directory", "my-service", - Query.ofJson("/endpoint.json"))) { + try (Watcher watcher = dogma.client().forRepo("directory", "my-service") + .watcher(Query.ofJson("/endpoint.json")) + .start()) { final CentralDogmaEndpointGroup endpointGroup = CentralDogmaEndpointGroup.builder( watcher, EndpointListDecoder.JSON).build(); endpointGroup.whenReady().get(); @@ -100,20 +100,24 @@ void json() throws Exception { @Test void text() throws Exception { final AtomicInteger counter = new AtomicInteger(); - try (Watcher watcher = dogma.client().fileWatcher( - "directory", "my-service", Query.ofText("/endpoints.txt"), entry -> { - counter.incrementAndGet(); - return entry; - })) { + try (Watcher watcher = dogma.client() + .forRepo("directory", "my-service") + .watcher(Query.ofText("/endpoints.txt")) + .map(entry -> { + counter.incrementAndGet(); + return entry; + }) + .start()) { final CentralDogmaEndpointGroup endpointGroup = CentralDogmaEndpointGroup.ofWatcher( watcher, EndpointListDecoder.TEXT); endpointGroup.whenReady().get(); assertThat(endpointGroup.endpoints()).isEqualTo(ENDPOINT_LIST); assertThat(counter.get()).isOne(); - dogma.client().push("directory", "my-service", - Revision.HEAD, "commit", - Change.ofTextUpsert("/endpoints.txt", "foo.bar:1234")) + dogma.client() + .forRepo("directory", "my-service") + .commit("commit", Change.ofTextUpsert("/endpoints.txt", "foo.bar:1234")) + .push() .join(); await().untilAsserted(() -> assertThat(endpointGroup.endpoints()) @@ -125,11 +129,14 @@ void text() throws Exception { @Test void recoverFromNotFound() throws Exception { final AtomicInteger counter = new AtomicInteger(); - try (Watcher watcher = dogma.client().fileWatcher( - "directory", "new-service", Query.ofText("/endpoints.txt"), entry -> { - counter.incrementAndGet(); - return entry; - })) { + try (Watcher watcher = dogma.client() + .forRepo("directory", "new-service") + .watcher(Query.ofText("/endpoints.txt")) + .map(entry -> { + counter.incrementAndGet(); + return entry; + }) + .start()) { final CentralDogmaEndpointGroup endpointGroup = CentralDogmaEndpointGroup.ofWatcher( watcher, EndpointListDecoder.TEXT); @@ -140,9 +147,10 @@ void recoverFromNotFound() throws Exception { assertThat(counter.get()).isZero(); dogma.client().createRepository("directory", "new-service").join(); - dogma.client().push("directory", "new-service", - Revision.HEAD, "commit", - Change.ofTextUpsert("/endpoints.txt", "foo.bar:1234")) + dogma.client() + .forRepo("directory", "new-service") + .commit("commit", Change.ofTextUpsert("/endpoints.txt", "foo.bar:1234")) + .push() .join(); endpointGroup.whenReady().get(20, TimeUnit.SECONDS); diff --git a/it/src/test/java/com/linecorp/centraldogma/it/CentralDogmaExtensionWithScaffolding.java b/it/src/test/java/com/linecorp/centraldogma/it/CentralDogmaExtensionWithScaffolding.java index 8a8103d207..4d3a5ea7ee 100644 --- a/it/src/test/java/com/linecorp/centraldogma/it/CentralDogmaExtensionWithScaffolding.java +++ b/it/src/test/java/com/linecorp/centraldogma/it/CentralDogmaExtensionWithScaffolding.java @@ -26,7 +26,6 @@ import com.linecorp.centraldogma.client.CentralDogma; import com.linecorp.centraldogma.common.Change; -import com.linecorp.centraldogma.common.Revision; import com.linecorp.centraldogma.testing.junit.CentralDogmaExtension; public class CentralDogmaExtensionWithScaffolding extends CentralDogmaExtension { @@ -90,8 +89,10 @@ public void importDirectory(String resourceDir, String targetDir) { final List> changes = Change.fromDirectory(f.toPath(), targetDir); - client().push(testProject, testRepository1, Revision.HEAD, - "Import " + resourceDir + " into " + targetDir, changes).join(); + client().forRepo(testProject, testRepository1) + .commit("Import " + resourceDir + " into " + targetDir, changes) + .push() + .join(); } @Override diff --git a/it/src/test/java/com/linecorp/centraldogma/it/FileHistoryAndDiffTest.java b/it/src/test/java/com/linecorp/centraldogma/it/FileHistoryAndDiffTest.java index 68208d2a62..74da4f02cd 100644 --- a/it/src/test/java/com/linecorp/centraldogma/it/FileHistoryAndDiffTest.java +++ b/it/src/test/java/com/linecorp/centraldogma/it/FileHistoryAndDiffTest.java @@ -21,6 +21,7 @@ import org.junit.jupiter.params.provider.EnumSource; import com.linecorp.centraldogma.client.CentralDogma; +import com.linecorp.centraldogma.common.PathPattern; import com.linecorp.centraldogma.common.Revision; class FileHistoryAndDiffTest { @@ -37,6 +38,6 @@ void getHistory(ClientType clientType) { final CentralDogma client = clientType.client(dogma); System.err.println( client.getHistory(dogma.project(), dogma.repo1(), - new Revision(1), Revision.HEAD, "/**").join()); + new Revision(1), Revision.HEAD, PathPattern.all()).join()); } } diff --git a/it/src/test/java/com/linecorp/centraldogma/it/FileManagementTest.java b/it/src/test/java/com/linecorp/centraldogma/it/FileManagementTest.java index 68db1c486d..3948b8f87b 100644 --- a/it/src/test/java/com/linecorp/centraldogma/it/FileManagementTest.java +++ b/it/src/test/java/com/linecorp/centraldogma/it/FileManagementTest.java @@ -31,6 +31,7 @@ import com.linecorp.centraldogma.common.Entry; import com.linecorp.centraldogma.common.EntryNoContentException; import com.linecorp.centraldogma.common.EntryType; +import com.linecorp.centraldogma.common.PathPattern; import com.linecorp.centraldogma.common.Revision; class FileManagementTest { @@ -46,8 +47,10 @@ protected void scaffold(CentralDogma client) { super.scaffold(client); for (int i = 0; i < NUM_FILES; i++) { - client.push(project(), repo1(), Revision.HEAD, - "Put test files", Change.ofJsonUpsert(TEST_ROOT + i + ".json", "{}")).join(); + client.forRepo(project(), repo1()) + .commit("Put test files", Change.ofJsonUpsert(TEST_ROOT + i + ".json", "{}")) + .push() + .join(); } } }; @@ -59,7 +62,7 @@ void getFiles(ClientType clientType) { final Revision headRev = client.normalizeRevision( dogma.project(), dogma.repo1(), Revision.HEAD).join(); final Map> files = client.getFiles( - dogma.project(), dogma.repo1(), Revision.HEAD, TEST_ROOT + "*.json").join(); + dogma.project(), dogma.repo1(), Revision.HEAD, PathPattern.of(TEST_ROOT + "*.json")).join(); assertThat(files).hasSize(NUM_FILES); files.values().forEach(f -> { assertThat(f.revision()).isEqualTo(headRev); @@ -74,7 +77,7 @@ void getFilesWithDirectory(ClientType clientType) { final String testRootWithoutSlash = TEST_ROOT.substring(0, TEST_ROOT.length() - 1); final Map> files = client.getFiles( dogma.project(), dogma.repo1(), Revision.HEAD, - testRootWithoutSlash + ", " + TEST_ROOT + '*').join(); + PathPattern.of(testRootWithoutSlash, TEST_ROOT + '*')).join(); assertThat(files).hasSize(NUM_FILES + 1); @@ -96,7 +99,7 @@ void getFilesWithDirectory(ClientType clientType) { void listFiles(ClientType clientType) { final CentralDogma client = clientType.client(dogma); final Map files = client.listFiles( - dogma.project(), dogma.repo1(), Revision.HEAD, TEST_ROOT + "*.json").join(); + dogma.project(), dogma.repo1(), Revision.HEAD, PathPattern.of(TEST_ROOT + "*.json")).join(); assertThat(files).hasSize(NUM_FILES); files.values().forEach(t -> assertThat(t).isEqualTo(EntryType.JSON)); } @@ -106,7 +109,7 @@ void listFiles(ClientType clientType) { void listFilesEmpty(ClientType clientType) { final CentralDogma client = clientType.client(dogma); final Map files = client.listFiles( - dogma.project(), dogma.repo1(), Revision.HEAD, TEST_ROOT + "*.none").join(); + dogma.project(), dogma.repo1(), Revision.HEAD, PathPattern.of(TEST_ROOT + "*.none")).join(); assertThat(files).isEmpty(); } @@ -116,7 +119,7 @@ void listFilesSingle(ClientType clientType) { final CentralDogma client = clientType.client(dogma); final String path = TEST_ROOT + "0.json"; final Map files = client.listFiles( - dogma.project(), dogma.repo1(), Revision.HEAD, path).join(); + dogma.project(), dogma.repo1(), Revision.HEAD, PathPattern.of(path)).join(); assertThat(files).hasSize(1); assertThat(files.get(path)).isEqualTo(EntryType.JSON); } diff --git a/it/src/test/java/com/linecorp/centraldogma/it/GetDiffTest.java b/it/src/test/java/com/linecorp/centraldogma/it/GetDiffTest.java index 75d463d820..25dd68f7e3 100644 --- a/it/src/test/java/com/linecorp/centraldogma/it/GetDiffTest.java +++ b/it/src/test/java/com/linecorp/centraldogma/it/GetDiffTest.java @@ -16,7 +16,6 @@ package com.linecorp.centraldogma.it; -import static com.linecorp.centraldogma.common.Revision.HEAD; import static net.javacrumbs.jsonunit.fluent.JsonFluentAssert.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; @@ -44,9 +43,9 @@ void queryByRange(ClientType clientType) { final String path = "/test_json_file.json"; for (int i = 0; i < 5; i++) { final Change change = Change.ofJsonUpsert(path, String.format("{ \"key\" : \"%d\"}", i)); - client.push( - dogma.project(), dogma.repo1(), HEAD, - TestConstants.randomText(), change).join(); + client.forRepo(dogma.project(), dogma.repo1()) + .commit(TestConstants.randomText(), change) + .push().join(); } final Change res = client.getDiff( @@ -70,11 +69,17 @@ void queryByRange(ClientType clientType) { void diff_remove(ClientType clientType) { final CentralDogma client = clientType.client(dogma); - final Revision rev1 = client.push(dogma.project(), dogma.repo1(), HEAD, "summary1", - Change.ofTextUpsert("/foo.txt", "hello")).join().revision(); + final Revision rev1 = client.forRepo(dogma.project(), dogma.repo1()) + .commit("summary1", Change.ofTextUpsert("/foo.txt", "hello")) + .push() + .join() + .revision(); - final Revision rev2 = client.push(dogma.project(), dogma.repo1(), HEAD, "summary2", - Change.ofRemoval("/foo.txt")).join().revision(); + final Revision rev2 = client.forRepo(dogma.project(), dogma.repo1()) + .commit("summary2", Change.ofRemoval("/foo.txt")) + .push() + .join() + .revision(); assertThat(rev1.forward(1)).isEqualTo(rev2); @@ -93,11 +98,17 @@ void diff_rename(ClientType clientType) { final CentralDogma client = clientType.client(dogma); try { - final Revision rev1 = client.push(dogma.project(), dogma.repo1(), HEAD, "summary1", - Change.ofTextUpsert("/bar.txt", "hello")).join().revision(); - - final Revision rev2 = client.push(dogma.project(), dogma.repo1(), HEAD, "summary2", - Change.ofRename("/bar.txt", "/baz.txt")).join().revision(); + final Revision rev1 = client.forRepo(dogma.project(), dogma.repo1()) + .commit("summary1", Change.ofTextUpsert("/bar.txt", "hello")) + .push() + .join() + .revision(); + + final Revision rev2 = client.forRepo(dogma.project(), dogma.repo1()) + .commit("summary2", Change.ofRename("/bar.txt", "/baz.txt")) + .push() + .join() + .revision(); assertThat(rev1.forward(1)).isEqualTo(rev2); @@ -105,7 +116,10 @@ void diff_rename(ClientType clientType) { Query.ofText("/bar.txt")).join()) .isEqualTo(Change.ofRemoval("/bar.txt")); } finally { - client.push(dogma.project(), dogma.repo1(), HEAD, "summary3", Change.ofRemoval("/baz.txt")).join(); + client.forRepo(dogma.project(), dogma.repo1()) + .commit("summary3", Change.ofRemoval("/baz.txt")) + .push() + .join(); } } } diff --git a/it/src/test/java/com/linecorp/centraldogma/it/GetFileTest.java b/it/src/test/java/com/linecorp/centraldogma/it/GetFileTest.java index 102a05fb35..877f0d06df 100644 --- a/it/src/test/java/com/linecorp/centraldogma/it/GetFileTest.java +++ b/it/src/test/java/com/linecorp/centraldogma/it/GetFileTest.java @@ -48,8 +48,9 @@ class GetFileTest { @EnumSource(ClientType.class) void getJsonAsText(ClientType clientType) throws Exception { final CentralDogma client = clientType.client(dogma); - client.push(dogma.project(), dogma.repo1(), Revision.HEAD, "Add a file", - Change.ofJsonUpsert("/test/foo.json", "{ \"a\": \"b\" }")).join(); + client.forRepo(dogma.project(), dogma.repo1()) + .commit("Add a file", Change.ofJsonUpsert("/test/foo.json", "{ \"a\": \"b\" }")) + .push().join(); final Entry json = client.getFile(dogma.project(), dogma.repo1(), Revision.HEAD, Query.ofJson("/test/foo.json")).join(); assertThatJson(json.content()).isEqualTo("{\"a\":\"b\"}"); @@ -57,8 +58,9 @@ void getJsonAsText(ClientType clientType) throws Exception { final Entry text = client.getFile(dogma.project(), dogma.repo1(), Revision.HEAD, Query.ofText("/test/foo.json")).join(); assertThat(text.content()).isEqualTo("{\"a\":\"b\"}"); - client.push(dogma.project(), dogma.repo1(), Revision.HEAD, "Remove a file", - Change.ofRemoval("/test/foo.json")).join(); + client.forRepo(dogma.project(), dogma.repo1()) + .commit("Remove a file", Change.ofRemoval("/test/foo.json")) + .push().join(); } @ParameterizedTest diff --git a/it/src/test/java/com/linecorp/centraldogma/it/GracefulShutdownTest.java b/it/src/test/java/com/linecorp/centraldogma/it/GracefulShutdownTest.java index 68f2666219..c36566a9b6 100644 --- a/it/src/test/java/com/linecorp/centraldogma/it/GracefulShutdownTest.java +++ b/it/src/test/java/com/linecorp/centraldogma/it/GracefulShutdownTest.java @@ -27,6 +27,7 @@ import org.junit.jupiter.params.provider.EnumSource; import com.linecorp.centraldogma.client.CentralDogma; +import com.linecorp.centraldogma.common.PathPattern; import com.linecorp.centraldogma.common.Query; import com.linecorp.centraldogma.common.Revision; import com.linecorp.centraldogma.common.ShuttingDownException; @@ -54,7 +55,7 @@ void startServer() { void watchRepositoryGracefulShutdown(ClientType clientType) throws Exception { final CentralDogma client = clientType.client(dogma); testGracefulShutdown(client.watchRepository( - dogma.project(), dogma.repo1(), Revision.HEAD, "/**", 60000)); + dogma.project(), dogma.repo1(), Revision.HEAD, PathPattern.all(), 60000, false)); } @ParameterizedTest @@ -62,7 +63,7 @@ void watchRepositoryGracefulShutdown(ClientType clientType) throws Exception { void watchFileGracefulShutdown(ClientType clientType) throws Exception { final CentralDogma client = clientType.client(dogma); testGracefulShutdown(client.watchFile( - dogma.project(), dogma.repo1(), Revision.HEAD, Query.ofJson("/test.json"), 60000)); + dogma.project(), dogma.repo1(), Revision.HEAD, Query.ofJson("/test.json"), 60000, false)); } private static void testGracefulShutdown(CompletableFuture future) throws Exception { diff --git a/it/src/test/java/com/linecorp/centraldogma/it/MergeFileTest.java b/it/src/test/java/com/linecorp/centraldogma/it/MergeFileTest.java index d0124d1557..6064467405 100644 --- a/it/src/test/java/com/linecorp/centraldogma/it/MergeFileTest.java +++ b/it/src/test/java/com/linecorp/centraldogma/it/MergeFileTest.java @@ -39,7 +39,7 @@ import com.linecorp.centraldogma.common.Revision; import com.linecorp.centraldogma.testing.junit.CentralDogmaExtension; -class MergeFileTest { +class MergeFileTest { @RegisterExtension final CentralDogmaExtension dogma = new CentralDogmaExtension() { @@ -47,10 +47,12 @@ class MergeFileTest { protected void scaffold(CentralDogma client) { client.createProject("myPro").join(); client.createRepository("myPro", "myRepo").join(); - client.push("myPro", "myRepo", Revision.HEAD, "Initial files", - Change.ofJsonUpsert("/foo.json", "{ \"a\": \"bar\" }"), - Change.ofJsonUpsert("/foo1.json", "{ \"b\": \"baz\" }"), - Change.ofJsonUpsert("/foo2.json", "{ \"a\": \"new_bar\" }")).join(); + client.forRepo("myPro", "myRepo") + .commit("Initial files", + Change.ofJsonUpsert("/foo.json", "{ \"a\": \"bar\" }"), + Change.ofJsonUpsert("/foo1.json", "{ \"b\": \"baz\" }"), + Change.ofJsonUpsert("/foo2.json", "{ \"a\": \"new_bar\" }")) + .push().join(); } @Override @@ -63,13 +65,14 @@ protected boolean runForEachTest() { @EnumSource(ClientType.class) void mergeJsonFiles(ClientType clientType) { final CentralDogma client = clientType.client(dogma); - final MergedEntry merged = client.mergeFiles("myPro", "myRepo", Revision.HEAD, - MergeSource.ofRequired("/foo.json"), - MergeSource.ofRequired("/foo1.json"), - MergeSource.ofRequired("/foo2.json"), - MergeSource.ofOptional("/foo3.json")).join(); + final MergedEntry merged = client.forRepo("myPro", "myRepo") + .merge(MergeSource.ofRequired("/foo.json"), + MergeSource.ofRequired("/foo1.json"), + MergeSource.ofRequired("/foo2.json"), + MergeSource.ofOptional("/foo3.json")) // optional + .get().join(); - assertThat(merged.paths()).containsExactly("/foo.json", "/foo1.json","/foo2.json"); + assertThat(merged.paths()).containsExactly("/foo.json", "/foo1.json", "/foo2.json"); assertThat(merged.revision()).isEqualTo(new Revision(2)); assertThatJson(merged.content()).isEqualTo("{ \"a\": \"new_bar\", \"b\": \"baz\" }"); @@ -87,11 +90,12 @@ void mergeJsonFiles(ClientType clientType) { .content()) .isEqualTo("{ \"a\": \"new_bar\" }"); - assertThatThrownBy(() -> client.mergeFiles("myPro", "myRepo", Revision.HEAD, - MergeSource.ofRequired("/foo.json"), - MergeSource.ofRequired("/foo1.json"), - MergeSource.ofRequired("/foo2.json"), - MergeSource.ofRequired("/foo3.json")).join()) + assertThatThrownBy(() -> client.forRepo("myPro", "myRepo") + .merge(MergeSource.ofRequired("/foo.json"), + MergeSource.ofRequired("/foo1.json"), + MergeSource.ofRequired("/foo2.json"), + MergeSource.ofRequired("/foo3.json")) // required + .get().join()) .isInstanceOf(CompletionException.class) .hasCauseInstanceOf(EntryNotFoundException.class); } @@ -100,9 +104,10 @@ void mergeJsonFiles(ClientType clientType) { @EnumSource(ClientType.class) void exceptionWhenOnlyOptionalFilesAndDoNotExist(ClientType clientType) { final CentralDogma client = clientType.client(dogma); - assertThatThrownBy(() -> client.mergeFiles("myPro", "myRepo", Revision.HEAD, - MergeSource.ofOptional("/non_existent1.json"), - MergeSource.ofRequired("/non_existent2.json")).join()) + assertThatThrownBy(() -> client.forRepo("myPro", "myRepo") + .merge(MergeSource.ofOptional("/non_existent1.json"), + MergeSource.ofRequired("/non_existent2.json")) + .get().join()) .isInstanceOf(CompletionException.class) .hasCauseInstanceOf(EntryNotFoundException.class); } @@ -111,14 +116,16 @@ void exceptionWhenOnlyOptionalFilesAndDoNotExist(ClientType clientType) { @EnumSource(ClientType.class) void mismatchedValueWhileMerging(ClientType clientType) { final CentralDogma client = clientType.client(dogma); - client.push("myPro", "myRepo", Revision.HEAD, "Add /foo10.json", - Change.ofJsonUpsert("/foo10.json", "{ \"a\": 1 }")).join(); - - assertThatThrownBy(() -> client.mergeFiles("myPro", "myRepo", Revision.HEAD, - MergeSource.ofRequired("/foo.json"), - MergeSource.ofRequired("/foo1.json"), - MergeSource.ofRequired("/foo2.json"), - MergeSource.ofRequired("/foo10.json")).join()) + client.forRepo("myPro", "myRepo") + .commit("Add /foo10.json", Change.ofJsonUpsert("/foo10.json", "{ \"a\": 1 }")) + .push().join(); + + assertThatThrownBy(() -> client.forRepo("myPro", "myRepo") + .merge(MergeSource.ofRequired("/foo.json"), + MergeSource.ofRequired("/foo1.json"), + MergeSource.ofRequired("/foo2.json"), + MergeSource.ofRequired("/foo10.json")) + .get().join()) .isInstanceOf(CompletionException.class) .hasCauseInstanceOf(QueryExecutionException.class); } @@ -136,7 +143,7 @@ void mergeJsonPaths(ClientType clientType) { final MergedEntry merged = client.mergeFiles("myPro", "myRepo", Revision.HEAD, query).join(); - assertThat(merged.paths()).containsExactly("/foo.json", "/foo1.json","/foo2.json"); + assertThat(merged.paths()).containsExactly("/foo.json", "/foo1.json", "/foo2.json"); assertThat(merged.revision()).isEqualTo(new Revision(2)); assertThatJson(merged.content()).isStringEqualTo("baz"); diff --git a/it/src/test/java/com/linecorp/centraldogma/it/PrematureClientFactoryCloseTest.java b/it/src/test/java/com/linecorp/centraldogma/it/PrematureClientFactoryCloseTest.java index 1617e33bf7..9e42d0d77b 100644 --- a/it/src/test/java/com/linecorp/centraldogma/it/PrematureClientFactoryCloseTest.java +++ b/it/src/test/java/com/linecorp/centraldogma/it/PrematureClientFactoryCloseTest.java @@ -15,7 +15,6 @@ */ package com.linecorp.centraldogma.it; -import static com.linecorp.centraldogma.common.Revision.HEAD; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; @@ -30,8 +29,8 @@ import com.linecorp.centraldogma.client.CentralDogma; import com.linecorp.centraldogma.client.armeria.ArmeriaCentralDogmaBuilder; import com.linecorp.centraldogma.common.Change; +import com.linecorp.centraldogma.common.PathPattern; import com.linecorp.centraldogma.common.Query; -import com.linecorp.centraldogma.common.Revision; import com.linecorp.centraldogma.testing.junit.CentralDogmaExtension; /** @@ -45,19 +44,28 @@ class PrematureClientFactoryCloseTest { protected void scaffold(CentralDogma client) { client.createProject("foo").join(); client.createRepository("foo", "bar").join(); - client.push("foo", "bar", Revision.HEAD, - "Add baz.txt", Change.ofTextUpsert("/baz.txt", "")).join(); + client.forRepo("foo", "bar") + .commit("Add baz.txt", Change.ofTextUpsert("/baz.txt", "")) + .push().join(); } }; @Test void watchRepository() throws Exception { - test(client -> client.watchRepository("foo", "bar", HEAD, "/**", Long.MAX_VALUE)); + test(client -> client.forRepo("foo", "bar") + .watch(PathPattern.all()) + .timeoutMillis(Long.MAX_VALUE) + .errorOnEntryNotFound(false) + .start()); } @Test void watchFile() throws Exception { - test(client -> client.watchFile("foo", "bar", HEAD, Query.ofText("/baz.txt"), Long.MAX_VALUE)); + test(client -> client.forRepo("foo", "bar") + .watch(Query.ofText("/baz.txt")) + .timeoutMillis(Long.MAX_VALUE) + .errorOnEntryNotFound(false) + .start()); } private static void test(Function> watchAction) throws Exception { diff --git a/it/src/test/java/com/linecorp/centraldogma/it/PreviewDiffsTest.java b/it/src/test/java/com/linecorp/centraldogma/it/PreviewDiffsTest.java index c17d648d20..9b4dc525ce 100644 --- a/it/src/test/java/com/linecorp/centraldogma/it/PreviewDiffsTest.java +++ b/it/src/test/java/com/linecorp/centraldogma/it/PreviewDiffsTest.java @@ -49,7 +49,7 @@ void invalidPatch(ClientType clientType) throws Exception { final Change change = Change.ofJsonPatch("/test/new_json_file.json", "{ \"a\": \"apple\" }", "{ \"a\": \"angle\" }"); assertThatThrownByWithExpectedException(ChangeConflictException.class, "/test/new_json_file.json", () -> - client.getPreviewDiffs(dogma.project(), dogma.repo1(), Revision.HEAD, change).join()) + client.forRepo(dogma.project(), dogma.repo1()).diff(change).get().join()) .isInstanceOf(CompletionException.class).hasCauseInstanceOf(ChangeConflictException.class); } @@ -60,7 +60,10 @@ void invalidRemoval(ClientType clientType) throws Exception { // Apply a conflict removal final Change change = Change.ofRemoval("/non_existent_path.txt"); assertThatThrownByWithExpectedException(ChangeConflictException.class, "non_existent_path.txt", () -> - client.getPreviewDiffs(dogma.project(), dogma.repo1(), Revision.HEAD, change).join()) + client.forRepo(dogma.project(), dogma.repo1()) + .diff(change) + .get() + .join()) .isInstanceOf(CompletionException.class).hasCauseInstanceOf(ChangeConflictException.class); } @@ -70,8 +73,10 @@ void invalidRevision(ClientType clientType) throws Exception { final CentralDogma client = clientType.client(dogma); final Change change = Change.ofTextUpsert("/a_new_text_file.txt", "text"); assertThatThrownByWithExpectedException(RevisionNotFoundException.class, "2147483647", () -> - client.getPreviewDiffs( - dogma.project(), dogma.repo1(), new Revision(Integer.MAX_VALUE), change).join()) + client.forRepo(dogma.project(), dogma.repo1()) + .diff(change) + .get(new Revision(Integer.MAX_VALUE)) + .join()) .isInstanceOf(CompletionException.class).hasCauseInstanceOf(RevisionNotFoundException.class); } @@ -79,7 +84,10 @@ void invalidRevision(ClientType clientType) throws Exception { @EnumSource(ClientType.class) void emptyChange(ClientType clientType) { final CentralDogma client = clientType.client(dogma); - assertThat(client.getPreviewDiffs(dogma.project(), dogma.repo1(), Revision.HEAD).join()).isEmpty(); + assertThat(client.forRepo(dogma.project(), dogma.repo1()) + .diff() + .get() + .join()).isEmpty(); } @ParameterizedTest @@ -89,8 +97,10 @@ void applyUpsertOnExistingPath(ClientType clientType) { final String jsonPath = "/a_new_json_file.json"; try { - client.push(dogma.project(), dogma.repo1(), Revision.HEAD, - "Add a new JSON file", Change.ofJsonUpsert(jsonPath, "{ \"a\": \"apple\" }")).join(); + client.forRepo(dogma.project(), dogma.repo1()) + .commit("Add a new JSON file", Change.ofJsonUpsert(jsonPath, "{ \"a\": \"apple\" }")) + .push() + .join(); } catch (CompletionException e) { // Might have been added already in previous run. assertThat(e.getCause()).isInstanceOf(RedundantChangeException.class); @@ -100,8 +110,10 @@ void applyUpsertOnExistingPath(ClientType clientType) { Change.ofJsonPatch(jsonPath, "{ \"a\": \"apple\" }", "{ \"a\": \"angle\" }"); final List> returnedList = - client.getPreviewDiffs(dogma.project(), dogma.repo1(), - Revision.HEAD, change).join(); + client.forRepo(dogma.project(), dogma.repo1()) + .diff(change) + .get() + .join(); assertThat(returnedList).hasSize(1); assertThat(returnedList.get(0).type()).isEqualTo(ChangeType.APPLY_JSON_PATCH); diff --git a/it/src/test/java/com/linecorp/centraldogma/it/WatchTest.java b/it/src/test/java/com/linecorp/centraldogma/it/WatchTest.java index c1d706d650..48eb8fa2ad 100644 --- a/it/src/test/java/com/linecorp/centraldogma/it/WatchTest.java +++ b/it/src/test/java/com/linecorp/centraldogma/it/WatchTest.java @@ -13,7 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ - package com.linecorp.centraldogma.it; import static net.javacrumbs.jsonunit.fluent.JsonFluentAssert.assertThatJson; @@ -23,6 +22,7 @@ import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -37,6 +37,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.EnumSource.Mode; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.TextNode; @@ -49,6 +50,9 @@ import com.linecorp.centraldogma.client.armeria.legacy.LegacyCentralDogmaBuilder; import com.linecorp.centraldogma.common.Change; import com.linecorp.centraldogma.common.Entry; +import com.linecorp.centraldogma.common.EntryNotFoundException; +import com.linecorp.centraldogma.common.EntryType; +import com.linecorp.centraldogma.common.PathPattern; import com.linecorp.centraldogma.common.PushResult; import com.linecorp.centraldogma.common.Query; import com.linecorp.centraldogma.common.Revision; @@ -87,7 +91,7 @@ void watchRepository(ClientType clientType) throws Exception { final Revision rev1 = client.normalizeRevision(dogma.project(), dogma.repo1(), Revision.HEAD).join(); final CompletableFuture future = - client.watchRepository(dogma.project(), dogma.repo1(), rev1, "/**", 3000); + client.watchRepository(dogma.project(), dogma.repo1(), rev1, PathPattern.all(), 3000, false); assertThatThrownBy(() -> future.get(500, TimeUnit.MILLISECONDS)).isInstanceOf(TimeoutException.class); @@ -95,8 +99,10 @@ void watchRepository(ClientType clientType) throws Exception { "[" + System.currentTimeMillis() + ", " + System.nanoTime() + ']'); - final PushResult result = client.push( - dogma.project(), dogma.repo1(), rev1, "Add test3.json", change).join(); + final PushResult result = client.forRepo(dogma.project(), dogma.repo1()) + .commit("Add test3.json", change) + .push(rev1) + .join(); final Revision rev2 = result.revision(); @@ -115,15 +121,17 @@ void watchRepositoryImmediateWakeup(ClientType clientType) throws Exception { "[" + System.currentTimeMillis() + ", " + System.nanoTime() + ']'); - final PushResult result = client.push( - dogma.project(), dogma.repo1(), rev1, "Add test3.json", change).join(); + final PushResult result = client.forRepo(dogma.project(), dogma.repo1()) + .commit("Add test3.json", change) + .push(rev1) + .join(); final Revision rev2 = result.revision(); assertThat(rev2).isEqualTo(rev1.forward(1)); final CompletableFuture future = - client.watchRepository(dogma.project(), dogma.repo1(), rev1, "/**", 3000); + client.watchRepository(dogma.project(), dogma.repo1(), rev1, PathPattern.all(), 3000, false); assertThat(future.get(3, TimeUnit.SECONDS)).isEqualTo(rev2); } @@ -135,7 +143,8 @@ void watchRepositoryWithUnrelatedChange(ClientType clientType) throws Exception final CentralDogma client = clientType.client(dogma); final Revision rev0 = client.normalizeRevision(dogma.project(), dogma.repo1(), Revision.HEAD).join(); final CompletableFuture future = - client.watchRepository(dogma.project(), dogma.repo1(), rev0, "/test/test4.json", 3000); + client.watchRepository(dogma.project(), dogma.repo1(), rev0, + PathPattern.of("/test/test4.json"), 3000, false); final Change change1 = Change.ofJsonUpsert("/test/test3.json", "[" + System.currentTimeMillis() + ", " + @@ -144,16 +153,20 @@ void watchRepositoryWithUnrelatedChange(ClientType clientType) throws Exception "[" + System.currentTimeMillis() + ", " + System.nanoTime() + ']'); - final PushResult result1 = client.push( - dogma.project(), dogma.repo1(), rev0, "Add test3.json", change1).join(); + final PushResult result1 = client.forRepo(dogma.project(), dogma.repo1()) + .commit("Add test3.json", change1) + .push(rev0) + .join(); final Revision rev1 = result1.revision(); assertThat(rev1).isEqualTo(rev0.forward(1)); // Ensure that the watcher is not notified because the path pattern does not match test3.json. assertThatThrownBy(() -> future.get(500, TimeUnit.MILLISECONDS)).isInstanceOf(TimeoutException.class); - final PushResult result2 = client.push( - dogma.project(), dogma.repo1(), rev1, "Add test4.json", change2).join(); + final PushResult result2 = client.forRepo(dogma.project(), dogma.repo1()) + .commit("Add test4.json", change2) + .push(rev1) + .join(); final Revision rev2 = result2.revision(); assertThat(rev2).isEqualTo(rev1.forward(1)); @@ -168,10 +181,32 @@ void watchRepositoryTimeout(ClientType clientType) { final CentralDogma client = clientType.client(dogma); final Revision rev = client.watchRepository( - dogma.project(), dogma.repo1(), Revision.HEAD, "/**", 1000).join(); + dogma.project(), dogma.repo1(), Revision.HEAD, PathPattern.all(), 1000, false).join(); assertThat(rev).isNull(); } + @ParameterizedTest + @EnumSource(ClientType.class) + void watchRepositoryWithNotExist(ClientType clientType) throws Exception { + final CentralDogma client = clientType.client(dogma); + final CompletableFuture future1 = + client.watchRepository(dogma.project(), dogma.repo1(), + Revision.HEAD, PathPattern.of("/test_not_found/**"), + 1000, false); + assertThat(future1.join()).isNull(); + + // Legacy client doesn't support this feature. + if (clientType == ClientType.LEGACY) { + return; + } + + final CompletableFuture future2 = + client.watchRepository(dogma.project(), dogma.repo1(), + Revision.HEAD, PathPattern.of("/test_not_found/**"), + 1000, true); + assertThatThrownBy(future2::join).getCause().isInstanceOf(EntryNotFoundException.class); + } + @ParameterizedTest @EnumSource(ClientType.class) void watchFile(ClientType clientType) throws Exception { @@ -184,15 +219,17 @@ void watchFile(ClientType clientType) throws Exception { final CompletableFuture> future = client.watchFile(dogma.project(), dogma.repo1(), rev0, - Query.ofJsonPath("/test/test1.json", "$[0]"), 3000); + Query.ofJsonPath("/test/test1.json", "$[0]"), 3000, false); assertThatThrownBy(() -> future.get(500, TimeUnit.MILLISECONDS)).isInstanceOf(TimeoutException.class); // An irrelevant change should not trigger a notification. final Change change1 = Change.ofJsonUpsert("/test/test2.json", "[ 3, 2, 1 ]"); - final PushResult res1 = client.push( - dogma.project(), dogma.repo1(), rev0, "Add test2.json", change1).join(); + final PushResult res1 = client.forRepo(dogma.project(), dogma.repo1()) + .commit("Add test2.json", change1) + .push(rev0) + .join(); final Revision rev1 = res1.revision(); @@ -201,8 +238,10 @@ void watchFile(ClientType clientType) throws Exception { // Make a relevant change now. final Change change2 = Change.ofJsonUpsert("/test/test1.json", "[ -1, -2, -3 ]"); - final PushResult res2 = client.push( - dogma.project(), dogma.repo1(), rev1, "Add test1.json", change2).join(); + final PushResult res2 = client.forRepo(dogma.project(), dogma.repo1()) + .commit("Add test1.json", change2) + .push(rev1) + .join(); final Revision rev2 = res2.revision(); @@ -223,15 +262,17 @@ void watchFileWithIdentityQuery(ClientType clientType) throws Exception { final CompletableFuture> future = client.watchFile( dogma.project(), dogma.repo1(), rev0, - Query.ofJson("/test/test1.json"), 3000); + Query.ofJson("/test/test1.json"), 3000, false); assertThatThrownBy(() -> future.get(500, TimeUnit.MILLISECONDS)).isInstanceOf(TimeoutException.class); // An irrelevant change should not trigger a notification. final Change change1 = Change.ofJsonUpsert("/test/test2.json", "[ 3, 2, 1 ]"); - final PushResult res1 = client.push( - dogma.project(), dogma.repo1(), rev0, "Add test2.json", change1).join(); + final PushResult res1 = client.forRepo(dogma.project(), dogma.repo1()) + .commit("Add test2.json", change1) + .push(rev0) + .join(); final Revision rev1 = res1.revision(); @@ -240,8 +281,10 @@ void watchFileWithIdentityQuery(ClientType clientType) throws Exception { // Make a relevant change now. final Change change2 = Change.ofJsonUpsert("/test/test1.json", "[ -1, -2, -3 ]"); - final PushResult res2 = client.push( - dogma.project(), dogma.repo1(), rev1, "Update test1.json", change2).join(); + final PushResult res2 = client.forRepo(dogma.project(), dogma.repo1()) + .commit("Update test1.json", change2) + .push(rev1) + .join(); final Revision rev2 = res2.revision(); @@ -258,24 +301,49 @@ void watchFileWithTimeout(ClientType clientType) { final CentralDogma client = clientType.client(dogma); final Entry res = client.watchFile( dogma.project(), dogma.repo1(), Revision.HEAD, - Query.ofJsonPath("/test/test1.json", "$"), 1000).join(); + Query.ofJsonPath("/test/test1.json", "$"), 1000, false).join(); assertThat(res).isNull(); } + @ParameterizedTest + @EnumSource(ClientType.class) + void watchFileWithNotExistFile(ClientType clientType) throws Exception { + final CentralDogma client = clientType.client(dogma); + + final CompletableFuture> future1 = client.watchFile( + dogma.project(), dogma.repo1(), Revision.HEAD, Query.ofJson("/test_not_found/test.json"), + 1000, false); + assertThat(future1.get()).isNull(); + + // Legacy client doesn't support this feature. + if (clientType == ClientType.LEGACY) { + return; + } + + final CompletableFuture> future2 = client.watchFile( + dogma.project(), dogma.repo1(), Revision.HEAD, Query.ofJson("/test_not_found/test.json"), + 1000, true); + assertThatThrownBy(() -> future2.get()).getCause().isInstanceOf(EntryNotFoundException.class); + } + @ParameterizedTest @EnumSource(ClientType.class) void watchJsonAsText(ClientType clientType) throws InterruptedException { revertTestFiles(clientType); final CentralDogma client = clientType.client(dogma); - final Watcher jsonWatcher = client.fileWatcher(dogma.project(), dogma.repo1(), - Query.ofJson("/test/test2.json")); + final Watcher jsonWatcher = client.forRepo(dogma.project(), dogma.repo1()) + .watcher(Query.ofJson("/test/test2.json")) + .start(); assertThatJson(jsonWatcher.awaitInitialValue().value()).isEqualTo("{\"a\":\"apple\"}"); + jsonWatcher.close(); - final Watcher stringWatcher = client.fileWatcher(dogma.project(), dogma.repo1(), - Query.ofText("/test/test2.json")); + final Watcher stringWatcher = client.forRepo(dogma.project(), dogma.repo1()) + .watcher(Query.ofText("/test/test2.json")) + .start(); assertThat(stringWatcher.awaitInitialValue().value()).isEqualTo("{\"a\":\"apple\"}"); + stringWatcher.close(); } @ParameterizedTest @@ -285,8 +353,9 @@ void watcherThrowsException(ClientType clientType) throws InterruptedException { final CentralDogma client = clientType.client(dogma); final String filePath = "/test/test2.json"; - final Watcher jsonWatcher = client.fileWatcher(dogma.project(), dogma.repo1(), - Query.ofJson(filePath)); + final Watcher jsonWatcher = client.forRepo(dogma.project(), dogma.repo1()) + .watcher(Query.ofJson(filePath)) + .start(); // wait for initial value assertThatJson(jsonWatcher.awaitInitialValue().value()).isEqualTo("{\"a\":\"apple\"}"); @@ -306,11 +375,14 @@ void watcherThrowsException(ClientType clientType) throws InterruptedException { // update the json final Change update = Change.ofJsonUpsert( filePath, "{ \"a\": \"air\" }"); - client.push(dogma.project(), dogma.repo1(), rev0, "Modify /a", update) + client.forRepo(dogma.project(), dogma.repo1()) + .commit("Modify /a", update) + .push(rev0) .join(); // the updated json should be reflected in the second watcher await().untilTrue(atomicBoolean); + jsonWatcher.close(); } @ParameterizedTest @@ -320,8 +392,9 @@ void transformingWatcher(ClientType clientType) throws InterruptedException { final CentralDogma client = clientType.client(dogma); final String filePath = "/test/test2.json"; - final Watcher heavyWatcher = client.fileWatcher(dogma.project(), dogma.repo1(), - Query.ofJsonPath(filePath)); + final Watcher heavyWatcher = client.forRepo(dogma.project(), dogma.repo1()) + .watcher(Query.ofJsonPath(filePath)) + .start(); final Watcher forExisting = Watcher.atJsonPointer(heavyWatcher, "/a"); final AtomicReference> watchResult = new AtomicReference<>(); @@ -346,7 +419,9 @@ void transformingWatcher(ClientType clientType) throws InterruptedException { // An irrelevant change should not trigger a notification. final Change unrelatedChange = Change.ofJsonUpsert( filePath, "{ \"a\": \"apple\", \"b\": \"banana\" }"); - final Revision rev1 = client.push(dogma.project(), dogma.repo1(), rev0, "Add /b", unrelatedChange) + final Revision rev1 = client.forRepo(dogma.project(), dogma.repo1()) + .commit("Add /b", unrelatedChange) + .push(rev0) .join() .revision(); @@ -356,7 +431,9 @@ void transformingWatcher(ClientType clientType) throws InterruptedException { // An relevant change should trigger a notification. final Change relatedChange = Change.ofJsonUpsert( filePath, "{ \"a\": \"artichoke\", \"b\": \"banana\" }"); - final Revision rev2 = client.push(dogma.project(), dogma.repo1(), rev1, "Change /a", relatedChange) + final Revision rev2 = client.forRepo(dogma.project(), dogma.repo1()) + .commit("Change /a", relatedChange) + .push(rev1) .join() .revision(); @@ -370,8 +447,9 @@ void transformingWatcher(ClientType clientType) throws InterruptedException { final Change nextRelatedChange = Change.ofJsonUpsert( filePath, "{ \"a\": \"apricot\", \"b\": \"banana\" }"); - final Revision rev3 = client.push(dogma.project(), dogma.repo1(), rev2, "Change /a again", - nextRelatedChange) + final Revision rev3 = client.forRepo(dogma.project(), dogma.repo1()) + .commit("Change /a again", nextRelatedChange) + .push(rev2) .join() .revision(); @@ -381,6 +459,7 @@ void transformingWatcher(ClientType clientType) throws InterruptedException { assertThat(triggeredCount.get()).isEqualTo(2); assertThat(heavyWatcher.latestValue().at("/a")).isEqualTo(new TextNode("apricot")); assertThat(heavyWatcher.latest().revision()).isEqualTo(rev3); + heavyWatcher.close(); } @ParameterizedTest @@ -389,32 +468,37 @@ void transformingThread_withDefault(ClientType clientType) { final CentralDogma client = clientType.client(dogma); final String filePath = "/test/test.txt"; final Watcher watcher = - client.fileWatcher(dogma.project(), dogma.repo1(), - Query.ofText(filePath), - text -> { - assertThat(Thread.currentThread().getName()) - .startsWith(THREAD_NAME_PREFIX); - return text; - }); + client.forRepo(dogma.project(), dogma.repo1()) + .watcher(Query.ofText(filePath)) + .map(text -> { + assertThat(Thread.currentThread().getName()) + .startsWith(THREAD_NAME_PREFIX); + return text; + }) + .start(); final AtomicReference threadName = new AtomicReference<>(); watcher.watch(watched -> threadName.set(Thread.currentThread().getName())); - client.push(dogma.project(), dogma.repo1(), Revision.HEAD, "test", - Change.ofTextUpsert("/test/test.txt", "foo")); + client.forRepo(dogma.project(), dogma.repo1()) + .commit("test", Change.ofTextUpsert("/test/test.txt", "foo")) + .push(); await().untilAtomic(threadName, Matchers.startsWith(THREAD_NAME_PREFIX)); threadName.set(null); + watcher.close(); final Watcher watcher2 = - client.repositoryWatcher(dogma.project(), dogma.repo1(), - filePath, - revision -> { - assertThat(Thread.currentThread().getName()) - .startsWith(THREAD_NAME_PREFIX); - return revision; - }); + client.forRepo(dogma.project(), dogma.repo1()) + .watcher(PathPattern.of(filePath)) + .map(revision -> { + assertThat(Thread.currentThread().getName()) + .startsWith(THREAD_NAME_PREFIX); + return revision; + }) + .start(); watcher2.watch((revision1, revision2) -> threadName.set(Thread.currentThread().getName())); await().untilAtomic(threadName, Matchers.startsWith(THREAD_NAME_PREFIX)); + watcher2.close(); } @ParameterizedTest @@ -427,32 +511,247 @@ void transformingThread_withCustom(ClientType clientType) { final CentralDogma client = clientType.client(dogma); final String filePath = "/test/test.txt"; final Watcher watcher = - client.fileWatcher(dogma.project(), dogma.repo1(), - Query.ofText(filePath), - text -> { - assertThat(Thread.currentThread().getName()) - .startsWith(threadNamePrefix); - return text; - }, executor); + client.forRepo(dogma.project(), dogma.repo1()) + .watcher(Query.ofText(filePath)) + .map(text -> { + assertThat(Thread.currentThread().getName()) + .startsWith(threadNamePrefix); + return text; + }) + .mapperExecutor(executor) + .start(); final AtomicReference threadName = new AtomicReference<>(); watcher.watch(watched -> threadName.set(Thread.currentThread().getName()), executor); - client.push(dogma.project(), dogma.repo1(), Revision.HEAD, "test", - Change.ofTextUpsert("/test/test.txt", "foo")); + client.forRepo(dogma.project(), dogma.repo1()) + .commit("test", Change.ofTextUpsert("/test/test.txt", "foo")) + .push(); await().untilAtomic(threadName, Matchers.startsWith(threadNamePrefix)); threadName.set(null); + watcher.close(); final Watcher watcher2 = - client.repositoryWatcher(dogma.project(), dogma.repo1(), - filePath, - revision -> { - assertThat(Thread.currentThread().getName()) - .startsWith(threadNamePrefix); - return revision; - }, executor); + client.forRepo(dogma.project(), dogma.repo1()) + .watcher(PathPattern.of(filePath)) + .map(revision -> { + assertThat(Thread.currentThread().getName()) + .startsWith(threadNamePrefix); + return revision; + }) + .mapperExecutor(executor) + .start(); watcher2.watch((revision1, revision2) -> threadName.set(Thread.currentThread().getName()), executor); await().untilAtomic(threadName, Matchers.startsWith(threadNamePrefix)); + watcher2.close(); + } + + @ParameterizedTest + @EnumSource(value = ClientType.class, mode = Mode.EXCLUDE, names = "LEGACY") + void fileWatcher_errorOnEntryNotFound(ClientType clientType) { + // prepare test + revertTestFiles(clientType); + final CentralDogma client = clientType.client(dogma); + final String filePath = "/test_not_found/test.json"; + + // create watcher + final Watcher watcher = client.forRepo(dogma.project(), dogma.repo1()) + .watcher(Query.ofJson(filePath)) + .errorOnEntryNotFound(true) + .timeoutMillis(100) + .start(); + + // check entry does not exist when to get initial value + assertThatThrownBy(watcher::awaitInitialValue) + .getRootCause().isInstanceOf(EntryNotFoundException.class); + assertThatThrownBy(() -> watcher.awaitInitialValue(100, TimeUnit.MILLISECONDS)) + .getRootCause().isInstanceOf(EntryNotFoundException.class); + assertThatThrownBy(() -> watcher.awaitInitialValue(100, TimeUnit.MILLISECONDS, new TextNode("test"))) + .getRootCause().isInstanceOf(EntryNotFoundException.class); + + // when initialValueFuture throw 'EntryNotFoundException', you can't use 'watch' method. + assertThatThrownBy(() -> watcher.watch((rev, node) -> { + })).isInstanceOf(IllegalStateException.class); + watcher.close(); + } + + @ParameterizedTest + @EnumSource(value = ClientType.class, mode = Mode.EXCLUDE, names = "LEGACY") + void fileWatcher_errorOnEntryNotFound_watchIsNotWorking(ClientType clientType) throws Exception { + // prepare test + revertTestFiles(clientType); + final CentralDogma client = clientType.client(dogma); + final String filePath = "/test_not_found/test.json"; + + // create watcher + final Watcher watcher = client.forRepo(dogma.project(), dogma.repo1()) + .watcher(Query.ofJson(filePath)) + .timeoutMillis(100) + .errorOnEntryNotFound(true) + .start(); + + final AtomicReference> watchResult = new AtomicReference<>(); + final AtomicInteger triggeredCount = new AtomicInteger(); + watcher.watch((rev, node) -> { + watchResult.set(new Latest<>(rev, node)); + triggeredCount.incrementAndGet(); + }); + + // check entry does not exist when to get initial value + assertThatThrownBy(watcher::awaitInitialValue).getRootCause().isInstanceOf( + EntryNotFoundException.class); + + // add file + final Change change1 = Change.ofJsonUpsert( + filePath, "{ \"a\": \"apple\", \"b\": \"banana\" }"); + client.forRepo(dogma.project(), dogma.repo1()) + .commit("Add /a /b", change1) + .push() + .join(); + + // Wait over the timeoutMillis(100) + a + Thread.sleep(1000); + // check watch is not working + assertThat(triggeredCount.get()).isEqualTo(0); + assertThat(watchResult.get()).isEqualTo(null); + watcher.close(); + } + + @ParameterizedTest + @EnumSource(value = ClientType.class, mode = Mode.EXCLUDE, names = "LEGACY") + void fileWatcher_errorOnEntryNotFound_EntryIsRemovedOnWatching(ClientType clientType) throws Exception { + // prepare test + revertTestFiles(clientType); + final CentralDogma client = clientType.client(dogma); + final String filePath = "/test/test2.json"; + + // create watcher + final Watcher watcher = client.forRepo(dogma.project(), dogma.repo1()) + .watcher(Query.ofJson(filePath)) + .timeoutMillis(100) + .errorOnEntryNotFound(true) + .start(); + + final AtomicReference> watchResult = new AtomicReference<>(); + final AtomicInteger triggeredCount = new AtomicInteger(); + watcher.initialValueFuture().thenAccept(result -> watcher.watch((rev, node) -> { + watchResult.set(new Latest<>(rev, node)); + triggeredCount.incrementAndGet(); + })); + + // check initial value + assertThatJson(watcher.awaitInitialValue().value()).isEqualTo("{\"a\":\"apple\"}"); + await().untilAtomic(triggeredCount, Matchers.is(1)); + + final Revision rev0 = watcher.initialValueFuture().join().revision(); + + // change file + final Change change1 = Change.ofJsonUpsert( + filePath, "{ \"a\": \"artichoke\"}"); + final Revision rev1 = client.forRepo(dogma.project(), dogma.repo1()) + .commit("Change /a", change1) + .push(rev0) + .join() + .revision(); + await().untilAtomic(triggeredCount, Matchers.is(2)); + assertThat(watchResult.get()).isEqualTo(watcher.latest()); + + // remove file + final Change change2 = Change.ofRemoval(filePath); + final Revision rev2 = client.forRepo(dogma.project(), dogma.repo1()) + .commit("Removal", change2).push(rev1) + .join() + .revision(); + + // Wait over the timeoutMillis(100) + a + Thread.sleep(1000); + + // check utilize latest data before removal + assertThat(triggeredCount.get()).isEqualTo(2); + assertThat(watchResult.get()).isEqualTo(watcher.latest()); + + // add file + final Change change3 = Change.ofJsonUpsert( + filePath, "{ \"a\": \"apricot\", \"b\": \"banana\" }"); + client.forRepo(dogma.project(), dogma.repo1()) + .commit("Add /a /b", change3) + .push(rev2) + .join(); + await().untilAtomic(triggeredCount, Matchers.is(3)); + assertThat(watchResult.get()).isEqualTo(watcher.latest()); + watcher.close(); + } + + @ParameterizedTest + @EnumSource(value = ClientType.class, mode = Mode.EXCLUDE, names = "LEGACY") + void repositoryWatcher_errorOnEntryNotFound(ClientType clientType) { + // prepare test + revertTestFiles(clientType); + final CentralDogma client = clientType.client(dogma); + final String pathPattern = "/test_not_found/**"; + + // create watcher + final Watcher watcher = client.forRepo(dogma.project(), dogma.repo1()) + .watcher(PathPattern.of(pathPattern)) + .timeoutMillis(100) + .errorOnEntryNotFound(true) + .start(); + + // check entry does not exist when to get initial value + assertThatThrownBy(watcher::awaitInitialValue) + .getRootCause().isInstanceOf(EntryNotFoundException.class); + assertThatThrownBy(() -> watcher.awaitInitialValue(100, TimeUnit.MILLISECONDS)) + .getRootCause().isInstanceOf(EntryNotFoundException.class); + assertThatThrownBy(() -> watcher.awaitInitialValue(100, TimeUnit.MILLISECONDS, Revision.INIT)) + .getRootCause().isInstanceOf(EntryNotFoundException.class); + + // when initialValueFuture throw 'EntryNotFoundException', you can't use 'watch' method. + await().untilAsserted(() -> assertThatThrownBy( + () -> watcher.watch((rev, node) -> { + })) + .isInstanceOf(IllegalStateException.class)); + } + + @ParameterizedTest + @EnumSource(value = ClientType.class, mode = Mode.EXCLUDE, names = "LEGACY") + void repositoryWatcher_errorOnEntryNotFound_watchIsNotWorking(ClientType clientType) throws Exception { + // prepare test + revertTestFiles(clientType); + + final CentralDogma client = clientType.client(dogma); + final String pathPattern = "/test_not_found/**"; + final String filePath = "/test_not_found/test.json"; + + final Watcher watcher = client.forRepo(dogma.project(), dogma.repo1()) + .watcher(PathPattern.of(pathPattern)) + .timeoutMillis(100) + .errorOnEntryNotFound(true) + .start(); + + final AtomicReference watchResult = new AtomicReference<>(); + final AtomicInteger triggeredCount = new AtomicInteger(); + watcher.watch(rev -> { + watchResult.set(rev); + triggeredCount.incrementAndGet(); + }); + + // check entry does not exist when to get initial value + assertThatThrownBy(watcher::awaitInitialValue).getRootCause().isInstanceOf( + EntryNotFoundException.class); + + // add file + final Change change1 = Change.ofJsonUpsert( + filePath, "{ \"a\": \"apple\", \"b\": \"banana\" }"); + client.forRepo(dogma.project(), dogma.repo1()) + .commit("Add /a /b", change1) + .push() + .join(); + + // Wait over the timeoutMillis(100) + a + Thread.sleep(1000); + // check watch is not working + assertThat(triggeredCount.get()).isEqualTo(0); + assertThat(watchResult.get()).isEqualTo(null); } private static void revertTestFiles(ClientType clientType) { @@ -464,8 +763,20 @@ private static void revertTestFiles(ClientType clientType) { if (!client.getPreviewDiffs(dogma.project(), dogma.repo1(), Revision.HEAD, changes) .join().isEmpty()) { - client.push(dogma.project(), dogma.repo1(), Revision.HEAD, - "Revert test files", changes).join(); + client.forRepo(dogma.project(), dogma.repo1()) + .commit("Revert test files", changes) + .push() + .join(); + } + + final Change change3 = Change.ofRemoval("/test_not_found/test.json"); + final Map files = client.listFiles(dogma.project(), dogma.repo1(), Revision.HEAD, + PathPattern.of("/test_not_found/**")).join(); + if (files.containsKey(change3.path())) { + client.forRepo(dogma.project(), dogma.repo1()) + .commit("Remove test files", change3) + .push() + .join(); } } } diff --git a/it/src/test/java/com/linecorp/centraldogma/it/WriteQuotaTestBase.java b/it/src/test/java/com/linecorp/centraldogma/it/WriteQuotaTestBase.java index 586229072a..38c19b6608 100644 --- a/it/src/test/java/com/linecorp/centraldogma/it/WriteQuotaTestBase.java +++ b/it/src/test/java/com/linecorp/centraldogma/it/WriteQuotaTestBase.java @@ -40,7 +40,6 @@ import com.linecorp.centraldogma.common.CentralDogmaException; import com.linecorp.centraldogma.common.Change; import com.linecorp.centraldogma.common.PushResult; -import com.linecorp.centraldogma.common.Revision; import com.linecorp.centraldogma.internal.Jackson; import com.linecorp.centraldogma.server.QuotaConfig; import com.linecorp.centraldogma.server.metadata.ProjectMetadata; @@ -115,8 +114,10 @@ private static List> parallelPush(CentralDogma dog ImmutableList.builderWithExpectedSize(iteration); final int sleep = 1000 / concurrency + 50; for (int i = 0; i < iteration; i++) { - builder.add(dogmaClient.push(projectName, repoName, Revision.HEAD, i + ". test commit", - Change.ofTextUpsert("/foo.txt", "Hello CentralDogma! " + i))); + builder.add(dogmaClient.forRepo(projectName, repoName) + .commit(i + ". test commit", + Change.ofTextUpsert("/foo.txt", "Hello CentralDogma! " + i)) + .push()); Thread.sleep(sleep); } return builder.build(); diff --git a/it/src/test/java/com/linecorp/centraldogma/it/mirror/git/GitMirrorAuthTest.java b/it/src/test/java/com/linecorp/centraldogma/it/mirror/git/GitMirrorAuthTest.java index 68efa1c9ea..e1bf8c085b 100644 --- a/it/src/test/java/com/linecorp/centraldogma/it/mirror/git/GitMirrorAuthTest.java +++ b/it/src/test/java/com/linecorp/centraldogma/it/mirror/git/GitMirrorAuthTest.java @@ -44,7 +44,6 @@ import com.linecorp.centraldogma.client.CentralDogma; import com.linecorp.centraldogma.common.Change; -import com.linecorp.centraldogma.common.Revision; import com.linecorp.centraldogma.internal.Jackson; import com.linecorp.centraldogma.server.CentralDogmaBuilder; import com.linecorp.centraldogma.server.MirroringService; @@ -151,16 +150,18 @@ void auth(String projName, String gitUri, JsonNode credential) { // Add /credentials.json and /mirrors.json final ArrayNode credentials = JsonNodeFactory.instance.arrayNode().add(credential); - client.push(projName, Project.REPO_META, Revision.HEAD, "Add a mirror", - Change.ofJsonUpsert("/credentials.json", credentials), - Change.ofJsonUpsert("/mirrors.json", - "[{" + - " \"type\": \"single\"," + - " \"direction\": \"REMOTE_TO_LOCAL\"," + - " \"localRepo\": \"main\"," + - " \"localPath\": \"/\"," + - " \"remoteUri\": \"" + gitUri + '"' + - "}]")).join(); + client.forRepo(projName, Project.REPO_META) + .commit("Add a mirror", + Change.ofJsonUpsert("/credentials.json", credentials), + Change.ofJsonUpsert("/mirrors.json", + "[{" + + " \"type\": \"single\"," + + " \"direction\": \"REMOTE_TO_LOCAL\"," + + " \"localRepo\": \"main\"," + + " \"localPath\": \"/\"," + + " \"remoteUri\": \"" + gitUri + '"' + + "}]")) + .push().join(); // Try to perform mirroring to see if authentication works as expected. mirroringService.mirror().join(); diff --git a/it/src/test/java/com/linecorp/centraldogma/it/mirror/git/GitMirrorTest.java b/it/src/test/java/com/linecorp/centraldogma/it/mirror/git/GitMirrorTest.java index 04e589533f..00fda5c962 100644 --- a/it/src/test/java/com/linecorp/centraldogma/it/mirror/git/GitMirrorTest.java +++ b/it/src/test/java/com/linecorp/centraldogma/it/mirror/git/GitMirrorTest.java @@ -57,6 +57,7 @@ import com.linecorp.centraldogma.common.CentralDogmaException; import com.linecorp.centraldogma.common.Change; import com.linecorp.centraldogma.common.Entry; +import com.linecorp.centraldogma.common.PathPattern; import com.linecorp.centraldogma.common.Revision; import com.linecorp.centraldogma.server.CentralDogmaBuilder; import com.linecorp.centraldogma.server.MirrorException; @@ -161,7 +162,7 @@ void remoteToLocal() throws Exception { //// Make sure /mirror_state.json exists (and nothing else.) final Entry expectedInitialMirrorState = expectedMirrorState(rev1, "/"); - assertThat(client.getFiles(projName, REPO_FOO, rev1, "/**").join().values()) + assertThat(client.getFiles(projName, REPO_FOO, rev1, PathPattern.all()).join().values()) .containsExactly(expectedInitialMirrorState); // Try to mirror again with no changes in the git repository. @@ -186,7 +187,7 @@ void remoteToLocal() throws Exception { //// Make sure the two files are all there. final Entry expectedSecondMirrorState = expectedMirrorState(rev3, "/"); - assertThat(client.getFiles(projName, REPO_FOO, rev3, "/**").join().values()) + assertThat(client.getFiles(projName, REPO_FOO, rev3, PathPattern.all()).join().values()) .containsExactlyInAnyOrder(expectedSecondMirrorState, Entry.ofDirectory(rev3, "/first"), Entry.ofText(rev3, "/first/light.txt", "26-Aug-2014\n"), @@ -207,7 +208,7 @@ void remoteToLocal() throws Exception { //// Make sure the rewritten content is mirrored. final Entry expectedThirdMirrorState = expectedMirrorState(rev4, "/"); - assertThat(client.getFiles(projName, REPO_FOO, rev4, "/**").join().values()) + assertThat(client.getFiles(projName, REPO_FOO, rev4, PathPattern.all()).join().values()) .containsExactlyInAnyOrder(expectedThirdMirrorState, Entry.ofText(rev4, "/final_fantasy_xv.txt", "29-Nov-2016\n")); } @@ -228,8 +229,9 @@ void remoteToLocal_gitignore_with_array() throws Exception { void remoteToLocal_subdirectory() throws Exception { pushMirrorSettings("/target", "/source/main", null); - client.push(projName, REPO_FOO, Revision.HEAD, "Add a file that's not part of mirror", - Change.ofTextUpsert("/not_mirrored.txt", "")).join(); + client.forRepo(projName, REPO_FOO) + .commit("Add a file that's not part of mirror", Change.ofTextUpsert("/not_mirrored.txt", "")) + .push().join(); final Revision rev0 = client.normalizeRevision(projName, REPO_FOO, Revision.HEAD).join(); @@ -244,7 +246,7 @@ void remoteToLocal_subdirectory() throws Exception { //// Make sure /target/mirror_state.json exists (and nothing else.) final Entry expectedInitialMirrorState = expectedMirrorState(rev1, "/target/"); - assertThat(client.getFiles(projName, REPO_FOO, rev1, "/target/**").join().values()) + assertThat(client.getFiles(projName, REPO_FOO, rev1, PathPattern.of("/target/**")).join().values()) .containsExactly(expectedInitialMirrorState); // Now, add some files to the git repository and mirror. @@ -261,13 +263,15 @@ void remoteToLocal_subdirectory() throws Exception { //// Make sure 'target/first/light.txt' is mirrored. final Entry expectedSecondMirrorState = expectedMirrorState(rev2, "/target/"); - assertThat(client.getFiles(projName, REPO_FOO, rev2, "/target/**").join().values()) + assertThat(client.getFiles(projName, REPO_FOO, rev2, PathPattern.of("/target/**")).join().values()) .containsExactlyInAnyOrder(expectedSecondMirrorState, Entry.ofDirectory(rev2, "/target/first"), Entry.ofText(rev2, "/target/first/light.txt", "26-Aug-2014\n")); //// Make sure the files not under '/target' are not touched. (sample files) - assertThat(client.getFiles(projName, REPO_FOO, rev2, "/not_mirrored.txt").join().values()) + assertThat(client.getFiles(projName, REPO_FOO, rev2, PathPattern.of("/not_mirrored.txt")) + .join() + .values()) .isNotEmpty(); } @@ -319,7 +323,7 @@ void remoteToLocal_merge() throws Exception { "/alphabets.txt", "a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\nn\no\np\nq\nr\ns\nt\nu\nv\nw\nx\ny\nz\n"); - assertThat(client.getFiles(projName, REPO_FOO, Revision.HEAD, "/**").join().values()) + assertThat(client.getFiles(projName, REPO_FOO, Revision.HEAD, PathPattern.all()).join().values()) .containsExactlyInAnyOrder(expectedMirrorState, expectedAlphabets); } @@ -354,7 +358,7 @@ void remoteToLocal_submodule(TestInfo testInfo) throws Exception { mirroringService.mirror().join(); final Revision headRev = client.normalizeRevision(projName, REPO_FOO, Revision.HEAD).join(); final Entry expectedMirrorState = expectedMirrorState(headRev, "/"); - assertThat(client.getFiles(projName, REPO_FOO, Revision.HEAD, "/**").join().values()) + assertThat(client.getFiles(projName, REPO_FOO, Revision.HEAD, PathPattern.all()).join().values()) .containsExactly(expectedMirrorState); } @@ -422,16 +426,18 @@ private void pushMirrorSettings(@Nullable String localPath, @Nullable String rem private void pushMirrorSettings(String localRepo, @Nullable String localPath, @Nullable String remotePath, @Nullable String gitignore) { - client.push(projName, Project.REPO_META, Revision.HEAD, "Add /mirrors.json", - Change.ofJsonUpsert("/mirrors.json", - "[{" + - " \"type\": \"single\"," + - " \"direction\": \"REMOTE_TO_LOCAL\"," + - " \"localRepo\": \"" + localRepo + "\"," + - (localPath != null ? "\"localPath\": \"" + localPath + "\"," : "") + - " \"remoteUri\": \"" + gitUri + firstNonNull(remotePath, "") + '"' + - ",\"gitignore\": " + firstNonNull(gitignore, "\"\"") + - "}]")).join(); + client.forRepo(projName, Project.REPO_META) + .commit("Add /mirrors.json", + Change.ofJsonUpsert("/mirrors.json", + "[{" + + " \"type\": \"single\"," + + " \"direction\": \"REMOTE_TO_LOCAL\"," + + " \"localRepo\": \"" + localRepo + "\"," + + (localPath != null ? "\"localPath\": \"" + localPath + "\"," : "") + + " \"remoteUri\": \"" + gitUri + firstNonNull(remotePath, "") + '"' + + ",\"gitignore\": " + firstNonNull(gitignore, "\"\"") + + "}]")) + .push().join(); } private Entry expectedMirrorState(Revision revision, String localPath) throws IOException { diff --git a/it/src/test/java/com/linecorp/centraldogma/it/updater/CentralDogmaBeanTest.java b/it/src/test/java/com/linecorp/centraldogma/it/updater/CentralDogmaBeanTest.java index 95e54848cf..b12005ed18 100644 --- a/it/src/test/java/com/linecorp/centraldogma/it/updater/CentralDogmaBeanTest.java +++ b/it/src/test/java/com/linecorp/centraldogma/it/updater/CentralDogmaBeanTest.java @@ -88,13 +88,15 @@ void test() { final CentralDogma client = dogma.client(); final TestProperty property = factory.get(new TestProperty(), TestProperty.class, listener); - client.push("a", "b", Revision.HEAD, "Add c.json", - Change.ofJsonUpsert("/c.json", - '{' + - " \"foo\": 20," + - " \"bar\": \"Y\"," + - " \"qux\": [\"0\", \"1\"]" + - '}')).join(); + client.forRepo("a", "b") + .commit("Add c.json", + Change.ofJsonUpsert("/c.json", + '{' + + " \"foo\": 20," + + " \"bar\": \"Y\"," + + " \"qux\": [\"0\", \"1\"]" + + '}')) + .push().join(); // Wait until the changes are handled. await().atMost(5000, TimeUnit.SECONDS).until(() -> property.getFoo() == 20); @@ -106,13 +108,15 @@ void test() { // test that after close a watcher, it could not receive change anymore property.closeWatcher(); - client.push("a", "b", Revision.HEAD, "Modify c.json", - Change.ofJsonUpsert("/c.json", - '{' + - " \"foo\": 50," + - " \"bar\": \"Y2\"," + - " \"qux\": [\"M\", \"N\"]" + - '}')) + client.forRepo("a", "b") + .commit("Modify c.json", + Change.ofJsonUpsert("/c.json", + '{' + + " \"foo\": 50," + + " \"bar\": \"Y2\"," + + " \"qux\": [\"M\", \"N\"]" + + '}')) + .push() .join(); // TODO(huydx): this test may be flaky, is there any better way? final Throwable thrown = catchThrowable(() -> await().atMost(2, TimeUnit.SECONDS) @@ -125,14 +129,15 @@ void test() { throw new RuntimeException("test runtime exception"); }; final TestProperty failProp = factory.get(new TestProperty(), TestProperty.class, failListener); - client.push("a", "b", Revision.HEAD, "Add a.json", - Change.ofJsonUpsert("/c.json", - '{' + - " \"foo\": 211," + - " \"bar\": \"Y\"," + - " \"qux\": [\"11\", \"1\"]" + - '}')) - .join(); + client.forRepo("a", "b") + .commit("Add a.json", + Change.ofJsonUpsert("/c.json", + '{' + + " \"foo\": 211," + + " \"bar\": \"Y\"," + + " \"qux\": [\"11\", \"1\"]" + + '}')) + .push().join(); // await will fail due to exception is thrown before node get serialized // and revision will remain null final Throwable thrown2 = catchThrowable(() -> await().atMost(2, TimeUnit.SECONDS) @@ -145,14 +150,15 @@ void test() { void overrideSettings() { final CentralDogma client = dogma.client(); - client.push("alice", "bob", Revision.HEAD, "Add charlie.json", - Change.ofJsonUpsert("/charlie.json", - "[{" + - " \"foo\": 200," + - " \"bar\": \"YY\"," + - " \"qux\": [\"100\", \"200\"]" + - "}]")) - .join(); + client.forRepo("alice", "bob") + .commit("Add charlie.json", + Change.ofJsonUpsert("/charlie.json", + "[{" + + " \"foo\": 200," + + " \"bar\": \"YY\"," + + " \"qux\": [\"100\", \"200\"]" + + "}]")) + .push().join(); final TestProperty property = factory.get(new TestProperty(), TestProperty.class, (TestProperty x) -> {}, @@ -177,13 +183,16 @@ void updateListenerIgnoreDefault() { final CentralDogma client = dogma.client(); final AtomicReference update = new AtomicReference<>(); - client.push("a", "b", Revision.HEAD, "Add c.json", - Change.ofJsonUpsert("/c.json", - '{' + - " \"foo\": 21," + - " \"bar\": \"Y\"," + - " \"qux\": [\"0\", \"1\"]" + - '}')).join(); + client.forRepo("a", "b") + .commit("Add c.json", + Change.ofJsonUpsert("/c.json", + '{' + + " \"foo\": 21," + + " \"bar\": \"Y\"," + + " \"qux\": [\"0\", \"1\"]" + + '}')) + .push() + .join(); final TestProperty property = factory.get(new TestProperty(), TestProperty.class, update::set); await().untilAtomic(update, Matchers.notNullValue()); diff --git a/server/src/test/java/com/linecorp/centraldogma/server/ContentCompressionTest.java b/server/src/test/java/com/linecorp/centraldogma/server/ContentCompressionTest.java index 3d76159793..328a7c20f3 100644 --- a/server/src/test/java/com/linecorp/centraldogma/server/ContentCompressionTest.java +++ b/server/src/test/java/com/linecorp/centraldogma/server/ContentCompressionTest.java @@ -43,7 +43,6 @@ import com.linecorp.armeria.common.HttpStatus; import com.linecorp.centraldogma.client.CentralDogma; import com.linecorp.centraldogma.common.Change; -import com.linecorp.centraldogma.common.Revision; import com.linecorp.centraldogma.internal.CsrfToken; import com.linecorp.centraldogma.internal.api.v1.HttpApiV1Constants; import com.linecorp.centraldogma.internal.thrift.CentralDogmaService.Iface; @@ -68,8 +67,9 @@ class ContentCompressionTest { protected void scaffold(CentralDogma client) { client.createProject(PROJ).join(); client.createRepository(PROJ, REPO).join(); - client.push(PROJ, REPO, Revision.HEAD, "Create a large file.", - Change.ofTextUpsert(PATH, CONTENT)).join(); + client.forRepo(PROJ, REPO) + .commit("Create a large file.", Change.ofTextUpsert(PATH, CONTENT)) + .push().join(); } }; diff --git a/server/src/test/java/com/linecorp/centraldogma/server/MetricsTest.java b/server/src/test/java/com/linecorp/centraldogma/server/MetricsTest.java index efd303725b..a23ccb3aa1 100644 --- a/server/src/test/java/com/linecorp/centraldogma/server/MetricsTest.java +++ b/server/src/test/java/com/linecorp/centraldogma/server/MetricsTest.java @@ -19,8 +19,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import com.linecorp.armeria.common.AggregatedHttpResponse; import com.linecorp.armeria.common.HttpStatus; @@ -28,7 +26,6 @@ import com.linecorp.centraldogma.client.CentralDogma; import com.linecorp.centraldogma.common.Change; import com.linecorp.centraldogma.common.Query; -import com.linecorp.centraldogma.common.Revision; import com.linecorp.centraldogma.testing.junit.CentralDogmaExtension; import io.micrometer.prometheus.PrometheusMeterRegistry; @@ -36,16 +33,16 @@ class MetricsTest { - private static final Logger logger = LoggerFactory.getLogger(MetricsTest.class); - @RegisterExtension static CentralDogmaExtension dogma = new CentralDogmaExtension() { @Override protected void scaffold(CentralDogma client) { client.createProject("foo").join(); client.createRepository("foo", "bar").join(); - client.push("foo", "bar", Revision.HEAD, "Initial file", - Change.ofJsonUpsert("/foo.json", "{ \"a\": \"bar\" }")).join(); + client.forRepo("foo", "bar") + .commit("Initial file", Change.ofJsonUpsert("/foo.json", "{ \"a\": \"bar\" }")) + .push() + .join(); } }; @@ -61,7 +58,13 @@ void metrics() { assertThat(content).doesNotContain( "com.linecorp.centraldogma.server.internal.api.WatchContentServiceV1"); - dogma.client().watchFile("foo", "bar", Revision.HEAD, Query.ofJson("/foo.json"), 100).join(); + dogma.client() + .forRepo("foo", "bar") + .watch(Query.ofJson("/foo.json")) + .timeoutMillis(100) + .errorOnEntryNotFound(false) + .start() + .join(); res = dogma.httpClient().get("/monitor/metrics").aggregate().join(); content = res.contentUtf8(); assertThat(content).contains("com.linecorp.centraldogma.server.internal.api.WatchContentServiceV1");