From c0330f43c946e1cb401b23992b7c3c318ceaf52c Mon Sep 17 00:00:00 2001 From: Dmitri Bourlatchkov Date: Thu, 3 Nov 2022 17:35:01 -0400 Subject: [PATCH] Add API v2 Add v2 REST endpoints. Add java client implementation for v2 REST API. Since namespaces are treated as ordinary content objects in API v2 the old java client API for dealing with namespaces is implemented on the client side on top of v2 "get entries" and "commit" APIs. Run in-memory REST server tests for both v1 and v2 APIs. REST test using persistent backend only use v2 API. Resteasy test remain on v1 API since they construct REST requests according to Nessie API v1 specs. OpenAPI YAML is built and published only for v2. OpenAPI for v1 should be obtained from older Nessie publications. This is the main contribution towards #5112 --- .../client/ext/MultiVersionApiTest.java | 2 +- .../client/ext/NessieApiVersion.java | 2 + .../client/ext/NessieApiVersions.java | 2 +- .../projectnessie/client/api/NessieApiV2.java | 26 + .../client/http/ApiHttpRequest.java | 78 +++ .../client/http/HttpClientBuilder.java | 8 +- .../client/http/HttpRequest.java | 9 + .../client/http/NessieHttpClient.java | 2 +- .../v2api/BaseHttpOnReferenceRequest.java | 41 ++ .../client/http/v2api/BaseHttpRequest.java | 27 + .../client/http/v2api/HttpApiV2.java | 178 +++++ .../client/http/v2api/HttpAssignBranch.java | 41 ++ .../client/http/v2api/HttpAssignTag.java | 41 ++ .../v2api/HttpCommitMultipleOperations.java | 44 ++ .../http/v2api/HttpCreateReference.java | 58 ++ .../client/http/v2api/HttpDeleteBranch.java | 43 ++ .../client/http/v2api/HttpDeleteTag.java | 42 ++ .../http/v2api/HttpGetAllReferences.java | 54 ++ .../client/http/v2api/HttpGetCommitLog.java | 71 ++ .../client/http/v2api/HttpGetContent.java | 49 ++ .../client/http/v2api/HttpGetDiff.java | 42 ++ .../client/http/v2api/HttpGetEntries.java | 55 ++ .../client/http/v2api/HttpGetReference.java | 45 ++ .../client/http/v2api/HttpMergeReference.java | 60 ++ .../http/v2api/HttpTransplantCommits.java | 61 ++ .../util/v2api/ClientSideCreateNamespace.java | 98 +++ .../util/v2api/ClientSideDeleteNamespace.java | 99 +++ .../ClientSideGetMultipleNamespaces.java | 94 +++ .../util/v2api/ClientSideGetNamespace.java | 62 ++ .../util/v2api/ClientSideUpdateNamespace.java | 70 ++ .../iceberg/nessie/BaseIcebergTest.java | 3 +- .../org/projectnessie/gc/tool/TestCLI.java | 3 + model/build.gradle.kts | 2 +- .../org/projectnessie/api/v2/ConfigApi.java | 28 + .../org/projectnessie/api/v2/TreeApi.java | 283 ++++++++ .../api/v2/http/HttpConfigApi.java | 53 ++ .../api/v2/http/HttpTreeApi.java | 637 ++++++++++++++++++ .../api/v2/params/AbstractParams.java | 75 +++ .../api/v2/params/BaseMergeTransplant.java | 66 ++ .../api/v2/params/CommitLogParams.java | 147 ++++ .../api/v2/params/DiffParams.java | 77 +++ .../api/v2/params/EntriesParams.java | 100 +++ .../api/v2/params/GetReferenceParams.java | 98 +++ .../projectnessie/api/v2/params/Merge.java | 81 +++ .../api/v2/params/ReferencesParams.java | 135 ++++ .../api/v2/params/Transplant.java | 78 +++ .../projectnessie/model/CommitResponse.java | 42 ++ .../projectnessie/model/ContentResponse.java | 34 + .../projectnessie/model/RefLogResponse.java | 5 +- .../org/projectnessie/model/Reference.java | 8 + .../model/SingleReferenceResponse.java | 34 + .../java/org/projectnessie/model/Util.java | 46 ++ .../org/projectnessie/model/Validation.java | 10 +- .../src/main/resources/META-INF/openapi.yaml | 195 +++++- .../params/TestParamObjectsSerialization.java | 124 ++++ .../jaxrs/ext/NessieJaxRsExtension.java | 4 + .../jaxrs/AbstractRestEntries.java | 3 + .../jaxrs/AbstractRestInvalid.java | 24 +- .../jaxrs/AbstractRestMergeTransplant.java | 41 +- .../projectnessie/jaxrs/AbstractRestMisc.java | 2 +- .../jaxrs/AbstractRestNamespace.java | 30 +- .../jaxrs/AbstractRestRefLog.java | 4 + .../jaxrs/AbstractRestSecurityContext.java | 3 + .../jaxrs/AbstractTestJerseyResteasy.java | 2 +- .../TestJerseyRestNaiveClientInMemory.java | 2 + .../src/main/resources/application.properties | 9 + .../AbstractQuarkusRestWithMetrics.java | 53 +- .../server/TestAuthorizationRules.java | 4 + .../server/error/ITNessieError.java | 73 +- .../services/rest/RestV2ConfigResource.java | 37 + .../services/rest/RestV2TreeResource.java | 238 +++++++ .../services/impl/ConfigApiImpl.java | 2 +- ...g.assertj.core.configuration.Configuration | 2 +- 73 files changed, 4320 insertions(+), 81 deletions(-) create mode 100644 clients/client/src/main/java/org/projectnessie/client/api/NessieApiV2.java create mode 100644 clients/client/src/main/java/org/projectnessie/client/http/ApiHttpRequest.java create mode 100644 clients/client/src/main/java/org/projectnessie/client/http/v2api/BaseHttpOnReferenceRequest.java create mode 100644 clients/client/src/main/java/org/projectnessie/client/http/v2api/BaseHttpRequest.java create mode 100644 clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpApiV2.java create mode 100644 clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpAssignBranch.java create mode 100644 clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpAssignTag.java create mode 100644 clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpCommitMultipleOperations.java create mode 100644 clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpCreateReference.java create mode 100644 clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpDeleteBranch.java create mode 100644 clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpDeleteTag.java create mode 100644 clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpGetAllReferences.java create mode 100644 clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpGetCommitLog.java create mode 100644 clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpGetContent.java create mode 100644 clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpGetDiff.java create mode 100644 clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpGetEntries.java create mode 100644 clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpGetReference.java create mode 100644 clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpMergeReference.java create mode 100644 clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpTransplantCommits.java create mode 100644 clients/client/src/main/java/org/projectnessie/client/util/v2api/ClientSideCreateNamespace.java create mode 100644 clients/client/src/main/java/org/projectnessie/client/util/v2api/ClientSideDeleteNamespace.java create mode 100644 clients/client/src/main/java/org/projectnessie/client/util/v2api/ClientSideGetMultipleNamespaces.java create mode 100644 clients/client/src/main/java/org/projectnessie/client/util/v2api/ClientSideGetNamespace.java create mode 100644 clients/client/src/main/java/org/projectnessie/client/util/v2api/ClientSideUpdateNamespace.java create mode 100644 model/src/main/java/org/projectnessie/api/v2/ConfigApi.java create mode 100644 model/src/main/java/org/projectnessie/api/v2/TreeApi.java create mode 100644 model/src/main/java/org/projectnessie/api/v2/http/HttpConfigApi.java create mode 100644 model/src/main/java/org/projectnessie/api/v2/http/HttpTreeApi.java create mode 100644 model/src/main/java/org/projectnessie/api/v2/params/AbstractParams.java create mode 100644 model/src/main/java/org/projectnessie/api/v2/params/BaseMergeTransplant.java create mode 100644 model/src/main/java/org/projectnessie/api/v2/params/CommitLogParams.java create mode 100644 model/src/main/java/org/projectnessie/api/v2/params/DiffParams.java create mode 100644 model/src/main/java/org/projectnessie/api/v2/params/EntriesParams.java create mode 100644 model/src/main/java/org/projectnessie/api/v2/params/GetReferenceParams.java create mode 100644 model/src/main/java/org/projectnessie/api/v2/params/Merge.java create mode 100644 model/src/main/java/org/projectnessie/api/v2/params/ReferencesParams.java create mode 100644 model/src/main/java/org/projectnessie/api/v2/params/Transplant.java create mode 100644 model/src/main/java/org/projectnessie/model/CommitResponse.java create mode 100644 model/src/main/java/org/projectnessie/model/ContentResponse.java create mode 100644 model/src/main/java/org/projectnessie/model/SingleReferenceResponse.java create mode 100644 model/src/test/java/org/projectnessie/api/v2/params/TestParamObjectsSerialization.java create mode 100644 servers/rest-services/src/main/java/org/projectnessie/services/rest/RestV2ConfigResource.java create mode 100644 servers/rest-services/src/main/java/org/projectnessie/services/rest/RestV2TreeResource.java diff --git a/clients/client-testextension/src/main/java/org/projectnessie/client/ext/MultiVersionApiTest.java b/clients/client-testextension/src/main/java/org/projectnessie/client/ext/MultiVersionApiTest.java index 1fa828dcf4e..5bbd8b010a0 100644 --- a/clients/client-testextension/src/main/java/org/projectnessie/client/ext/MultiVersionApiTest.java +++ b/clients/client-testextension/src/main/java/org/projectnessie/client/ext/MultiVersionApiTest.java @@ -34,7 +34,7 @@ public class MultiVersionApiTest implements MultiEnvTestExtension, ExecutionCond public static final String API_VERSION_SEGMENT_TYPE = "nessie-api"; // API version to be used for tests not annotated with `@ForNessieApiVersions` - private static final NessieApiVersion DEFAULT_API_VERSION = NessieApiVersion.V1; + private static final NessieApiVersion DEFAULT_API_VERSION = NessieApiVersion.V2; @Override public String segmentType() { diff --git a/clients/client-testextension/src/main/java/org/projectnessie/client/ext/NessieApiVersion.java b/clients/client-testextension/src/main/java/org/projectnessie/client/ext/NessieApiVersion.java index b20c18960f5..23099432e06 100644 --- a/clients/client-testextension/src/main/java/org/projectnessie/client/ext/NessieApiVersion.java +++ b/clients/client-testextension/src/main/java/org/projectnessie/client/ext/NessieApiVersion.java @@ -18,9 +18,11 @@ import java.net.URI; import org.projectnessie.client.NessieClientBuilder; import org.projectnessie.client.api.NessieApiV1; +import org.projectnessie.client.api.NessieApiV2; public enum NessieApiVersion { V1("v1", NessieApiV1.class), + V2("v2", NessieApiV2.class), ; private final String uriPathElement; diff --git a/clients/client-testextension/src/main/java/org/projectnessie/client/ext/NessieApiVersions.java b/clients/client-testextension/src/main/java/org/projectnessie/client/ext/NessieApiVersions.java index 75542636592..793647adf7c 100644 --- a/clients/client-testextension/src/main/java/org/projectnessie/client/ext/NessieApiVersions.java +++ b/clients/client-testextension/src/main/java/org/projectnessie/client/ext/NessieApiVersions.java @@ -36,5 +36,5 @@ @ExtendWith(MultiVersionApiTest.class) @Inherited public @interface NessieApiVersions { - NessieApiVersion[] versions() default {NessieApiVersion.V1}; + NessieApiVersion[] versions() default {NessieApiVersion.V1, NessieApiVersion.V2}; } diff --git a/clients/client/src/main/java/org/projectnessie/client/api/NessieApiV2.java b/clients/client/src/main/java/org/projectnessie/client/api/NessieApiV2.java new file mode 100644 index 00000000000..596a9dfda98 --- /dev/null +++ b/clients/client/src/main/java/org/projectnessie/client/api/NessieApiV2.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.client.api; + +/** + * Interface for the Nessie V2 API implementation. + * + *

At the java client level this API uses the same builder classes and model types as API v1, + * however the behaviour of some API methods is different. + * + *

Most changes between v1 and v2 exist at the REST level (HTTP). + */ +public interface NessieApiV2 extends NessieApiV1 {} diff --git a/clients/client/src/main/java/org/projectnessie/client/http/ApiHttpRequest.java b/clients/client/src/main/java/org/projectnessie/client/http/ApiHttpRequest.java new file mode 100644 index 00000000000..db5f1dbfff6 --- /dev/null +++ b/clients/client/src/main/java/org/projectnessie/client/http/ApiHttpRequest.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.client.http; + +import java.util.function.Supplier; + +/** + * This is a helper class for use with an {@link HttpRequest} that will unwrap the specified + * API-level exceptions from {@link HttpClientException} thrown during the execution of the {@link + * HttpRequest}. + * + *

Currently this class supports up to two distinct API-level exception types, but it can easily + * be extended to support more if required. + * + *

The exception types to be unwrapped are specifies as arguments to {@link + * HttpRequest#unwrap(Class)} and {@link HttpRequest#unwrap(Class, Class)} calls. + * + * @param the first API-level exception that should be unwrapped + * @param the second API-level exception that should be unwrapped + */ +public class ApiHttpRequest { + private final HttpRequest request; + private final Class ex1; + private final Class ex2; + + ApiHttpRequest(HttpRequest request, Class ex1, Class ex2) { + this.request = request; + this.ex1 = ex1; + this.ex2 = ex2; + } + + public HttpResponse get() throws E1, E2 { + return unwrap(request::get); + } + + public HttpResponse delete() throws E1, E2 { + return unwrap(request::delete); + } + + public HttpResponse post(Object obj) throws E1, E2 { + return unwrap(() -> request.post(obj)); + } + + public HttpResponse put(Object obj) throws E1, E2 { + return unwrap(() -> request.put(obj)); + } + + private HttpResponse unwrap(Supplier action) throws E1, E2 { + try { + return action.get(); + } catch (HttpClientException e) { + Throwable cause = e.getCause(); + + if (ex1.isInstance(cause)) { + throw ex1.cast(cause); + } + + if (ex2.isInstance(cause)) { + throw ex2.cast(cause); + } + + throw e; + } + } +} diff --git a/clients/client/src/main/java/org/projectnessie/client/http/HttpClientBuilder.java b/clients/client/src/main/java/org/projectnessie/client/http/HttpClientBuilder.java index 5f4f0f6208d..35341152bf6 100644 --- a/clients/client/src/main/java/org/projectnessie/client/http/HttpClientBuilder.java +++ b/clients/client/src/main/java/org/projectnessie/client/http/HttpClientBuilder.java @@ -43,6 +43,7 @@ import org.projectnessie.client.auth.NessieAuthentication; import org.projectnessie.client.auth.NessieAuthenticationProvider; import org.projectnessie.client.http.v1api.HttpApiV1; +import org.projectnessie.client.http.v2api.HttpApiV2; /** * A builder class that creates a {@link NessieHttpClient} via {@link HttpClientBuilder#builder()}. @@ -282,12 +283,17 @@ public HttpClientBuilder withForceUrlConnectionClient(boolean forceUrlConnection @Override public API build(Class apiVersion) { Objects.requireNonNull(apiVersion, "API version class must be non-null"); - NessieHttpClient client = new NessieHttpClient(authentication, tracing, builder); if (apiVersion.isAssignableFrom(HttpApiV1.class)) { + NessieHttpClient client = new NessieHttpClient(authentication, tracing, builder); return (API) new HttpApiV1(client); } + if (apiVersion.isAssignableFrom(HttpApiV2.class)) { + HttpClient httpClient = NessieHttpClient.buildClient(authentication, tracing, builder); + return (API) new HttpApiV2(httpClient); + } + throw new IllegalArgumentException( String.format("API version %s is not supported.", apiVersion.getName())); } diff --git a/clients/client/src/main/java/org/projectnessie/client/http/HttpRequest.java b/clients/client/src/main/java/org/projectnessie/client/http/HttpRequest.java index 2e75ab91955..ec2ccb43696 100644 --- a/clients/client/src/main/java/org/projectnessie/client/http/HttpRequest.java +++ b/clients/client/src/main/java/org/projectnessie/client/http/HttpRequest.java @@ -89,4 +89,13 @@ public HttpRequest resolveTemplate(String name, String value) { uriBuilder.resolveTemplate(name, value); return this; } + + public ApiHttpRequest unwrap(Class ex) { + return new ApiHttpRequest<>(this, ex, RuntimeException.class); + } + + public ApiHttpRequest unwrap( + Class ex1, Class ex2) { + return new ApiHttpRequest<>(this, ex1, ex2); + } } diff --git a/clients/client/src/main/java/org/projectnessie/client/http/NessieHttpClient.java b/clients/client/src/main/java/org/projectnessie/client/http/NessieHttpClient.java index 518b195301c..fee37455bfd 100644 --- a/clients/client/src/main/java/org/projectnessie/client/http/NessieHttpClient.java +++ b/clients/client/src/main/java/org/projectnessie/client/http/NessieHttpClient.java @@ -65,7 +65,7 @@ public class NessieHttpClient extends NessieApiClient { this(buildClient(authentication, enableTracing, clientBuilder)); } - private static HttpClient buildClient( + static HttpClient buildClient( HttpAuthentication authentication, boolean enableTracing, HttpClient.Builder clientBuilder) { clientBuilder.setObjectMapper(MAPPER); if (enableTracing) { diff --git a/clients/client/src/main/java/org/projectnessie/client/http/v2api/BaseHttpOnReferenceRequest.java b/clients/client/src/main/java/org/projectnessie/client/http/v2api/BaseHttpOnReferenceRequest.java new file mode 100644 index 00000000000..e50f425872c --- /dev/null +++ b/clients/client/src/main/java/org/projectnessie/client/http/v2api/BaseHttpOnReferenceRequest.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.client.http.v2api; + +import org.projectnessie.client.api.OnReferenceBuilder; +import org.projectnessie.client.http.HttpClient; + +abstract class BaseHttpOnReferenceRequest> extends BaseHttpRequest + implements OnReferenceBuilder { + protected String refName; + protected String hashOnRef; + + protected BaseHttpOnReferenceRequest(HttpClient client) { + super(client); + } + + @Override + public R refName(String refName) { + this.refName = refName; + return (R) this; + } + + @Override + public R hashOnRef(String hashOnRef) { + this.hashOnRef = hashOnRef; + return (R) this; + } +} diff --git a/clients/client/src/main/java/org/projectnessie/client/http/v2api/BaseHttpRequest.java b/clients/client/src/main/java/org/projectnessie/client/http/v2api/BaseHttpRequest.java new file mode 100644 index 00000000000..c9f619df2d4 --- /dev/null +++ b/clients/client/src/main/java/org/projectnessie/client/http/v2api/BaseHttpRequest.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.client.http.v2api; + +import org.projectnessie.client.http.HttpClient; + +public abstract class BaseHttpRequest { + + protected final HttpClient client; + + protected BaseHttpRequest(HttpClient client) { + this.client = client; + } +} diff --git a/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpApiV2.java b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpApiV2.java new file mode 100644 index 00000000000..d034da1f037 --- /dev/null +++ b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpApiV2.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.client.http.v2api; + +import org.projectnessie.client.api.AssignBranchBuilder; +import org.projectnessie.client.api.AssignTagBuilder; +import org.projectnessie.client.api.CommitMultipleOperationsBuilder; +import org.projectnessie.client.api.CreateNamespaceBuilder; +import org.projectnessie.client.api.CreateReferenceBuilder; +import org.projectnessie.client.api.DeleteBranchBuilder; +import org.projectnessie.client.api.DeleteNamespaceBuilder; +import org.projectnessie.client.api.DeleteTagBuilder; +import org.projectnessie.client.api.GetAllReferencesBuilder; +import org.projectnessie.client.api.GetCommitLogBuilder; +import org.projectnessie.client.api.GetContentBuilder; +import org.projectnessie.client.api.GetDiffBuilder; +import org.projectnessie.client.api.GetEntriesBuilder; +import org.projectnessie.client.api.GetMultipleNamespacesBuilder; +import org.projectnessie.client.api.GetNamespaceBuilder; +import org.projectnessie.client.api.GetRefLogBuilder; +import org.projectnessie.client.api.GetReferenceBuilder; +import org.projectnessie.client.api.MergeReferenceBuilder; +import org.projectnessie.client.api.NessieApiV2; +import org.projectnessie.client.api.TransplantCommitsBuilder; +import org.projectnessie.client.api.UpdateNamespaceBuilder; +import org.projectnessie.client.http.HttpClient; +import org.projectnessie.client.util.v2api.ClientSideCreateNamespace; +import org.projectnessie.client.util.v2api.ClientSideDeleteNamespace; +import org.projectnessie.client.util.v2api.ClientSideGetMultipleNamespaces; +import org.projectnessie.client.util.v2api.ClientSideGetNamespace; +import org.projectnessie.client.util.v2api.ClientSideUpdateNamespace; +import org.projectnessie.error.NessieNotFoundException; +import org.projectnessie.model.Branch; +import org.projectnessie.model.NessieConfiguration; +import org.projectnessie.model.SingleReferenceResponse; + +public class HttpApiV2 implements NessieApiV2 { + private final HttpClient client; + + public HttpApiV2(HttpClient client) { + this.client = client; + } + + @Override + public void close() { + // nop + } + + @Override + public NessieConfiguration getConfig() { + return client.newRequest().path("config").get().readEntity(NessieConfiguration.class); + } + + @Override + public Branch getDefaultBranch() throws NessieNotFoundException { + return (Branch) + client + .newRequest() + .path("trees/main") // TODO: use trees/- + .unwrap(NessieNotFoundException.class) + .get() + .readEntity(SingleReferenceResponse.class) + .getReference(); + } + + @Override + public GetContentBuilder getContent() { + return new HttpGetContent(client); + } + + @Override + public GetAllReferencesBuilder getAllReferences() { + return new HttpGetAllReferences(client); + } + + @Override + public CreateReferenceBuilder createReference() { + return new HttpCreateReference(client); + } + + @Override + public GetReferenceBuilder getReference() { + return new HttpGetReference(client); + } + + @Override + public GetEntriesBuilder getEntries() { + return new HttpGetEntries(client); + } + + @Override + public GetCommitLogBuilder getCommitLog() { + return new HttpGetCommitLog(client); + } + + @Override + public AssignTagBuilder assignTag() { + return new HttpAssignTag(client); + } + + @Override + public DeleteTagBuilder deleteTag() { + return new HttpDeleteTag(client); + } + + @Override + public AssignBranchBuilder assignBranch() { + return new HttpAssignBranch(client); + } + + @Override + public DeleteBranchBuilder deleteBranch() { + return new HttpDeleteBranch(client); + } + + @Override + public TransplantCommitsBuilder transplantCommitsIntoBranch() { + return new HttpTransplantCommits(client); + } + + @Override + public MergeReferenceBuilder mergeRefIntoBranch() { + return new HttpMergeReference(client); + } + + @Override + public CommitMultipleOperationsBuilder commitMultipleOperations() { + return new HttpCommitMultipleOperations(client); + } + + @Override + public GetDiffBuilder getDiff() { + return new HttpGetDiff(client); + } + + @Override + public GetRefLogBuilder getRefLog() { + throw new UnsupportedOperationException("Reflog is not supported in API v2"); + } + + @Override + public GetNamespaceBuilder getNamespace() { + return new ClientSideGetNamespace(this); + } + + @Override + public GetMultipleNamespacesBuilder getMultipleNamespaces() { + return new ClientSideGetMultipleNamespaces(this); + } + + @Override + public CreateNamespaceBuilder createNamespace() { + return new ClientSideCreateNamespace(this); + } + + @Override + public DeleteNamespaceBuilder deleteNamespace() { + return new ClientSideDeleteNamespace(this); + } + + @Override + public UpdateNamespaceBuilder updateProperties() { + return new ClientSideUpdateNamespace(this); + } +} diff --git a/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpAssignBranch.java b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpAssignBranch.java new file mode 100644 index 00000000000..2bba5b16749 --- /dev/null +++ b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpAssignBranch.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.client.http.v2api; + +import org.projectnessie.client.builder.BaseAssignBranchBuilder; +import org.projectnessie.client.http.HttpClient; +import org.projectnessie.error.NessieConflictException; +import org.projectnessie.error.NessieNotFoundException; +import org.projectnessie.model.Reference; + +final class HttpAssignBranch extends BaseAssignBranchBuilder { + private final HttpClient client; + + HttpAssignBranch(HttpClient client) { + this.client = client; + } + + @Override + public void assign() throws NessieNotFoundException, NessieConflictException { + client + .newRequest() + .path("trees/{ref}") + .resolveTemplate("ref", Reference.toPathString(branchName, hash)) + .queryParam("type", Reference.ReferenceType.BRANCH.name()) + .unwrap(NessieNotFoundException.class, NessieConflictException.class) + .put(assignTo); + } +} diff --git a/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpAssignTag.java b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpAssignTag.java new file mode 100644 index 00000000000..4c7651e069e --- /dev/null +++ b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpAssignTag.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.client.http.v2api; + +import org.projectnessie.client.builder.BaseAssignTagBuilder; +import org.projectnessie.client.http.HttpClient; +import org.projectnessie.error.NessieConflictException; +import org.projectnessie.error.NessieNotFoundException; +import org.projectnessie.model.Reference; + +final class HttpAssignTag extends BaseAssignTagBuilder { + private final HttpClient client; + + HttpAssignTag(HttpClient client) { + this.client = client; + } + + @Override + public void assign() throws NessieNotFoundException, NessieConflictException { + client + .newRequest() + .path("trees/{ref}") + .resolveTemplate("ref", Reference.toPathString(tagName, hash)) + .queryParam("type", Reference.ReferenceType.TAG.name()) + .unwrap(NessieNotFoundException.class, NessieConflictException.class) + .put(assignTo); + } +} diff --git a/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpCommitMultipleOperations.java b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpCommitMultipleOperations.java new file mode 100644 index 00000000000..76ce27dddc4 --- /dev/null +++ b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpCommitMultipleOperations.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.client.http.v2api; + +import org.projectnessie.client.builder.BaseCommitMultipleOperationsBuilder; +import org.projectnessie.client.http.HttpClient; +import org.projectnessie.error.NessieConflictException; +import org.projectnessie.error.NessieNotFoundException; +import org.projectnessie.model.Branch; +import org.projectnessie.model.CommitResponse; +import org.projectnessie.model.Reference; + +final class HttpCommitMultipleOperations extends BaseCommitMultipleOperationsBuilder { + private final HttpClient client; + + HttpCommitMultipleOperations(HttpClient client) { + this.client = client; + } + + @Override + public Branch commit() throws NessieNotFoundException, NessieConflictException { + return client + .newRequest() + .path("trees/{ref}/history/commit") + .resolveTemplate("ref", Reference.toPathString(branchName, hash)) + .unwrap(NessieNotFoundException.class, NessieConflictException.class) + .post(operations.build()) + .readEntity(CommitResponse.class) + .getTargetBranch(); + } +} diff --git a/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpCreateReference.java b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpCreateReference.java new file mode 100644 index 00000000000..616d33ee326 --- /dev/null +++ b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpCreateReference.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.client.http.v2api; + +import org.projectnessie.client.builder.BaseCreateReferenceBuilder; +import org.projectnessie.client.http.HttpClient; +import org.projectnessie.error.NessieConflictException; +import org.projectnessie.error.NessieNotFoundException; +import org.projectnessie.model.Branch; +import org.projectnessie.model.Detached; +import org.projectnessie.model.Reference; +import org.projectnessie.model.SingleReferenceResponse; + +public class HttpCreateReference extends BaseCreateReferenceBuilder { + + private final HttpClient client; + + protected HttpCreateReference(HttpClient client) { + this.client = client; + } + + @Override + public Reference create() throws NessieNotFoundException, NessieConflictException { + Reference source; + if (sourceRefName == null) { + if (reference.getHash() == null) { + source = null; + } else { + source = Detached.of(reference.getHash()); + } + } else { + source = Branch.of(sourceRefName, reference.getHash()); + } + + return client + .newRequest() + .path("trees") + .queryParam("name", reference.getName()) + .queryParam("type", reference.getType().name()) + .unwrap(NessieNotFoundException.class, NessieConflictException.class) + .post(source) // TODO: support all types + .readEntity(SingleReferenceResponse.class) + .getReference(); + } +} diff --git a/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpDeleteBranch.java b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpDeleteBranch.java new file mode 100644 index 00000000000..44f3e4c13e5 --- /dev/null +++ b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpDeleteBranch.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.client.http.v2api; + +import org.projectnessie.client.api.DeleteBranchBuilder; +import org.projectnessie.client.builder.BaseOnBranchBuilder; +import org.projectnessie.client.http.HttpClient; +import org.projectnessie.error.NessieConflictException; +import org.projectnessie.error.NessieNotFoundException; +import org.projectnessie.model.Reference; + +final class HttpDeleteBranch extends BaseOnBranchBuilder + implements DeleteBranchBuilder { + private final HttpClient client; + + HttpDeleteBranch(HttpClient client) { + this.client = client; + } + + @Override + public void delete() throws NessieConflictException, NessieNotFoundException { + client + .newRequest() + .path("trees/{ref}") + .resolveTemplate("ref", Reference.toPathString(branchName, hash)) + .queryParam("type", Reference.ReferenceType.BRANCH.name()) + .unwrap(NessieConflictException.class, NessieNotFoundException.class) + .delete(); + } +} diff --git a/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpDeleteTag.java b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpDeleteTag.java new file mode 100644 index 00000000000..abe20ef3bcc --- /dev/null +++ b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpDeleteTag.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.client.http.v2api; + +import org.projectnessie.client.api.DeleteTagBuilder; +import org.projectnessie.client.builder.BaseOnTagBuilder; +import org.projectnessie.client.http.HttpClient; +import org.projectnessie.error.NessieConflictException; +import org.projectnessie.error.NessieNotFoundException; +import org.projectnessie.model.Reference; + +final class HttpDeleteTag extends BaseOnTagBuilder implements DeleteTagBuilder { + private final HttpClient client; + + HttpDeleteTag(HttpClient client) { + this.client = client; + } + + @Override + public void delete() throws NessieConflictException, NessieNotFoundException { + client + .newRequest() + .path("trees/{ref}") + .resolveTemplate("ref", Reference.toPathString(tagName, hash)) + .queryParam("type", Reference.ReferenceType.TAG.name()) + .unwrap(NessieConflictException.class, NessieNotFoundException.class) + .delete(); + } +} diff --git a/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpGetAllReferences.java b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpGetAllReferences.java new file mode 100644 index 00000000000..a4663d85d78 --- /dev/null +++ b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpGetAllReferences.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.client.http.v2api; + +import org.projectnessie.api.params.FetchOption; +import org.projectnessie.api.v2.params.ReferencesParams; +import org.projectnessie.client.builder.BaseGetAllReferencesBuilder; +import org.projectnessie.client.http.HttpClient; +import org.projectnessie.model.ReferencesResponse; + +final class HttpGetAllReferences extends BaseGetAllReferencesBuilder { + + private final HttpClient client; + + HttpGetAllReferences(HttpClient client) { + super(ReferencesParams::forNextPage); + this.client = client; + } + + @Override + protected ReferencesParams params() { + return ReferencesParams.builder() + .fetchOption(fetchOption) + .filter(filter) + .maxRecords(maxRecords) + .build(); + } + + @Override + protected ReferencesResponse get(ReferencesParams p) { + return client + .newRequest() + .path("trees") + .queryParam("fetch", FetchOption.getFetchOptionName(p.fetchOption())) + .queryParam("max-records", p.maxRecords()) + .queryParam("page-token", p.pageToken()) + .queryParam("filter", p.filter()) + .get() + .readEntity(ReferencesResponse.class); + } +} diff --git a/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpGetCommitLog.java b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpGetCommitLog.java new file mode 100644 index 00000000000..f6e563b9b36 --- /dev/null +++ b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpGetCommitLog.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.client.http.v2api; + +import java.util.stream.Stream; +import org.projectnessie.api.params.FetchOption; +import org.projectnessie.api.v2.params.CommitLogParams; +import org.projectnessie.client.StreamingUtil; +import org.projectnessie.client.builder.BaseGetCommitLogBuilder; +import org.projectnessie.client.http.HttpClient; +import org.projectnessie.error.NessieNotFoundException; +import org.projectnessie.model.LogResponse; +import org.projectnessie.model.Reference; + +final class HttpGetCommitLog extends BaseGetCommitLogBuilder { + + private final HttpClient client; + + HttpGetCommitLog(HttpClient client) { + super(CommitLogParams::forNextPage); + this.client = client; + } + + @Override + protected CommitLogParams params() { + return CommitLogParams.builder() + .fetchOption(fetchOption) + .startHash(untilHash) + .maxRecords(maxRecords) + .filter(filter) + .build(); + } + + @Override + protected LogResponse get(CommitLogParams p) throws NessieNotFoundException { + return client + .newRequest() + .path("trees/{ref}/history") + .resolveTemplate( + "ref", + Reference.toPathString(refName, hashOnRef)) // TODO: move refName, hashOnRef to params + .queryParam("max-records", p.maxRecords()) + .queryParam("page-token", p.pageToken()) + .queryParam("filter", p.filter()) + .queryParam("limit-hash", p.startHash()) + .queryParam("fetch", FetchOption.getFetchOptionName(p.fetchOption())) + .unwrap(NessieNotFoundException.class) + .get() + .readEntity(LogResponse.class); + } + + @Override + public Stream stream() throws NessieNotFoundException { + CommitLogParams p = params(); + return StreamingUtil.generateStream( + LogResponse::getLogEntries, pageToken -> get(p.forNextPage(pageToken))); + } +} diff --git a/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpGetContent.java b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpGetContent.java new file mode 100644 index 00000000000..8c8ec2ac395 --- /dev/null +++ b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpGetContent.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.client.http.v2api; + +import java.util.Map; +import java.util.stream.Collectors; +import org.projectnessie.client.builder.BaseGetContentBuilder; +import org.projectnessie.client.http.HttpClient; +import org.projectnessie.error.NessieNotFoundException; +import org.projectnessie.model.Content; +import org.projectnessie.model.ContentKey; +import org.projectnessie.model.GetMultipleContentsResponse; +import org.projectnessie.model.GetMultipleContentsResponse.ContentWithKey; +import org.projectnessie.model.Reference; + +final class HttpGetContent extends BaseGetContentBuilder { + private final HttpClient client; + + HttpGetContent(HttpClient client) { + this.client = client; + } + + @Override + public Map get() throws NessieNotFoundException { + GetMultipleContentsResponse response = + client + .newRequest() + .path("trees/{ref}/contents") + .resolveTemplate("ref", Reference.toPathString(refName, hashOnRef)) + .unwrap(NessieNotFoundException.class) + .post(request.build()) + .readEntity(GetMultipleContentsResponse.class); + return response.getContents().stream() + .collect(Collectors.toMap(ContentWithKey::getKey, ContentWithKey::getContent)); + } +} diff --git a/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpGetDiff.java b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpGetDiff.java new file mode 100644 index 00000000000..38d1f8e70d2 --- /dev/null +++ b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpGetDiff.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.client.http.v2api; + +import org.projectnessie.client.builder.BaseGetDiffBuilder; +import org.projectnessie.client.http.HttpClient; +import org.projectnessie.error.NessieNotFoundException; +import org.projectnessie.model.DiffResponse; +import org.projectnessie.model.Reference; + +final class HttpGetDiff extends BaseGetDiffBuilder { + private final HttpClient client; + + HttpGetDiff(HttpClient client) { + this.client = client; + } + + @Override + public DiffResponse get() throws NessieNotFoundException { + return client + .newRequest() + .path("trees/{from}/diff/{to}") + .resolveTemplate("from", Reference.toPathString(fromRefName, fromHashOnRef)) + .resolveTemplate("to", Reference.toPathString(toRefName, toHashOnRef)) + .unwrap(NessieNotFoundException.class) + .get() + .readEntity(DiffResponse.class); + } +} diff --git a/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpGetEntries.java b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpGetEntries.java new file mode 100644 index 00000000000..3aa9c61afb0 --- /dev/null +++ b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpGetEntries.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.client.http.v2api; + +import org.projectnessie.api.v2.params.EntriesParams; +import org.projectnessie.client.builder.BaseGetEntriesBuilder; +import org.projectnessie.client.http.HttpClient; +import org.projectnessie.error.NessieNotFoundException; +import org.projectnessie.model.EntriesResponse; +import org.projectnessie.model.Reference; + +final class HttpGetEntries extends BaseGetEntriesBuilder { + + private final HttpClient client; + + HttpGetEntries(HttpClient client) { + super(EntriesParams::forNextPage); + this.client = client; + } + + @Override + protected EntriesParams params() { + return EntriesParams.builder() // TODO: namespace, derive prefix + .filter(filter) + .maxRecords(maxRecords) + .build(); + } + + @Override + protected EntriesResponse get(EntriesParams p) throws NessieNotFoundException { + return client + .newRequest() + .path("trees/{ref}/entries") + .resolveTemplate("ref", Reference.toPathString(refName, hashOnRef)) + .queryParam("filter", p.filter()) + .queryParam("page-token", p.pageToken()) + .queryParam("max-records", p.maxRecords()) + .unwrap(NessieNotFoundException.class) + .get() + .readEntity(EntriesResponse.class); + } +} diff --git a/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpGetReference.java b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpGetReference.java new file mode 100644 index 00000000000..e6b76b505ae --- /dev/null +++ b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpGetReference.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2020 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.client.http.v2api; + +import org.projectnessie.api.params.FetchOption; +import org.projectnessie.client.builder.BaseGetReferenceBuilder; +import org.projectnessie.client.http.HttpClient; +import org.projectnessie.error.NessieNotFoundException; +import org.projectnessie.model.Reference; +import org.projectnessie.model.SingleReferenceResponse; + +final class HttpGetReference extends BaseGetReferenceBuilder { + + private final HttpClient client; + + HttpGetReference(HttpClient client) { + this.client = client; + } + + @Override + public Reference get() throws NessieNotFoundException { + return client + .newRequest() + .path("trees/{ref}") + .queryParam("fetch", FetchOption.getFetchOptionName(fetchOption)) + .resolveTemplate("ref", refName) + .unwrap(NessieNotFoundException.class) + .get() + .readEntity(SingleReferenceResponse.class) + .getReference(); + } +} diff --git a/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpMergeReference.java b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpMergeReference.java new file mode 100644 index 00000000000..73f6a0714e5 --- /dev/null +++ b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpMergeReference.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.client.http.v2api; + +import org.projectnessie.api.v2.params.ImmutableMerge; +import org.projectnessie.client.api.MergeReferenceBuilder; +import org.projectnessie.client.builder.BaseMergeReferenceBuilder; +import org.projectnessie.client.http.HttpClient; +import org.projectnessie.error.NessieConflictException; +import org.projectnessie.error.NessieNotFoundException; +import org.projectnessie.model.MergeResponse; +import org.projectnessie.model.Reference; + +final class HttpMergeReference extends BaseMergeReferenceBuilder { + private final HttpClient client; + + public HttpMergeReference(HttpClient client) { + this.client = client; + } + + @Override + public MergeReferenceBuilder keepIndividualCommits(boolean keepIndividualCommits) { + if (keepIndividualCommits) { + throw new IllegalArgumentException("Commits are always squashed during merge operations."); + } + return this; + } + + @Override + public MergeResponse merge() throws NessieNotFoundException, NessieConflictException { + ImmutableMerge merge = + ImmutableMerge.builder() + .fromHash(fromHash) + .fromRefName(fromRefName) + .isDryRun(dryRun) + .isFetchAdditionalInfo(fetchAdditionalInfo) + .isReturnConflictAsResult(returnConflictAsResult) + .build(); // TODO: message + return client + .newRequest() + .path("trees/{ref}/history/merge") + .resolveTemplate("ref", Reference.toPathString(branchName, hash)) + .unwrap(NessieNotFoundException.class, NessieConflictException.class) + .post(merge) + .readEntity(MergeResponse.class); + } +} diff --git a/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpTransplantCommits.java b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpTransplantCommits.java new file mode 100644 index 00000000000..888deac7a6d --- /dev/null +++ b/clients/client/src/main/java/org/projectnessie/client/http/v2api/HttpTransplantCommits.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.client.http.v2api; + +import org.projectnessie.api.v2.params.ImmutableTransplant; +import org.projectnessie.client.api.TransplantCommitsBuilder; +import org.projectnessie.client.builder.BaseTransplantCommitsBuilder; +import org.projectnessie.client.http.HttpClient; +import org.projectnessie.error.NessieConflictException; +import org.projectnessie.error.NessieNotFoundException; +import org.projectnessie.model.MergeResponse; +import org.projectnessie.model.Reference; + +final class HttpTransplantCommits extends BaseTransplantCommitsBuilder { + private final HttpClient client; + + public HttpTransplantCommits(HttpClient client) { + this.client = client; + } + + @Override + public TransplantCommitsBuilder keepIndividualCommits(boolean keepIndividualCommits) { + if (!keepIndividualCommits) { + throw new IllegalArgumentException( + "Individual commits are always kept during transplant operations."); + } + return this; + } + + @Override + public MergeResponse transplant() throws NessieNotFoundException, NessieConflictException { + ImmutableTransplant.Builder transplant = + ImmutableTransplant.builder() + .message(message) + .fromRefName(fromRefName) + .hashesToTransplant(hashesToTransplant) + .isDryRun(dryRun) + .isReturnConflictAsResult(returnConflictAsResult) + .isFetchAdditionalInfo(fetchAdditionalInfo); + return client + .newRequest() + .path("trees/{ref}/history/transplant") + .resolveTemplate("ref", Reference.toPathString(branchName, hash)) + .unwrap(NessieNotFoundException.class, NessieConflictException.class) + .post(transplant.build()) + .readEntity(MergeResponse.class); + } +} diff --git a/clients/client/src/main/java/org/projectnessie/client/util/v2api/ClientSideCreateNamespace.java b/clients/client/src/main/java/org/projectnessie/client/util/v2api/ClientSideCreateNamespace.java new file mode 100644 index 00000000000..b8e3f2cd79b --- /dev/null +++ b/clients/client/src/main/java/org/projectnessie/client/util/v2api/ClientSideCreateNamespace.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.client.util.v2api; + +import java.util.Map; +import java.util.Optional; +import org.projectnessie.client.api.NessieApiV2; +import org.projectnessie.client.builder.BaseCreateNamespaceBuilder; +import org.projectnessie.error.NessieConflictException; +import org.projectnessie.error.NessieNamespaceAlreadyExistsException; +import org.projectnessie.error.NessieNotFoundException; +import org.projectnessie.error.NessieReferenceNotFoundException; +import org.projectnessie.model.Branch; +import org.projectnessie.model.CommitMeta; +import org.projectnessie.model.Content; +import org.projectnessie.model.ContentKey; +import org.projectnessie.model.ImmutableNamespace; +import org.projectnessie.model.Namespace; +import org.projectnessie.model.Operation; + +/** + * Supports previous "create namespace" functionality of the java client over Nessie API v2. + * + *

API v2 does not have methods dedicated to manging namespaces. Namespaces are expected to be + * managed as ordinary content objects. + */ +public final class ClientSideCreateNamespace extends BaseCreateNamespaceBuilder { + private final NessieApiV2 api; + + public ClientSideCreateNamespace(NessieApiV2 api) { + this.api = api; + } + + @Override + public Namespace create() + throws NessieReferenceNotFoundException, NessieNamespaceAlreadyExistsException { + if (namespace.isEmpty()) { + throw new IllegalArgumentException("Creating empty namespaces is not supported"); + } + + ImmutableNamespace content = + ImmutableNamespace.builder().from(namespace).properties(properties).build(); + ContentKey key = ContentKey.of(namespace.getElements()); + + Map contentMap; + try { + contentMap = api.getContent().refName(refName).hashOnRef(hashOnRef).key(key).get(); + } catch (NessieNotFoundException e) { + throw new NessieReferenceNotFoundException(e.getMessage(), e); + } + + if (contentMap.containsKey(key)) { + if (contentMap.get(key) instanceof Namespace) { + throw new NessieNamespaceAlreadyExistsException( + String.format("Namespace '%s' already exists", key.toPathString())); + } else { + throw new NessieNamespaceAlreadyExistsException( + String.format( + "Another content object with name '%s' already exists", key.toPathString())); + } + } + + try { + Branch branch = + api.commitMultipleOperations() + .commitMeta(CommitMeta.fromMessage("create namespace " + namespace.name())) + .branchName(refName) + .hash(hashOnRef) + .operation(Operation.Put.of(key, content)) + .commit(); + + contentMap = api.getContent().reference(branch).key(key).get(); + } catch (NessieNotFoundException | NessieConflictException e) { + throw new NessieReferenceNotFoundException(e.getMessage(), e); + } + + Optional result = Optional.ofNullable(contentMap.get(key)); + return result + .flatMap(r -> r.unwrap(Namespace.class)) + .orElseThrow( + () -> + new NessieReferenceNotFoundException( + String.format("Namespace '%s' not found", key))); + } +} diff --git a/clients/client/src/main/java/org/projectnessie/client/util/v2api/ClientSideDeleteNamespace.java b/clients/client/src/main/java/org/projectnessie/client/util/v2api/ClientSideDeleteNamespace.java new file mode 100644 index 00000000000..8653bb03c3b --- /dev/null +++ b/clients/client/src/main/java/org/projectnessie/client/util/v2api/ClientSideDeleteNamespace.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.client.util.v2api; + +import java.util.Map; +import java.util.Optional; +import org.projectnessie.client.api.NessieApiV2; +import org.projectnessie.client.builder.BaseDeleteNamespaceBuilder; +import org.projectnessie.error.NessieConflictException; +import org.projectnessie.error.NessieNamespaceNotEmptyException; +import org.projectnessie.error.NessieNamespaceNotFoundException; +import org.projectnessie.error.NessieNotFoundException; +import org.projectnessie.error.NessieReferenceNotFoundException; +import org.projectnessie.model.CommitMeta; +import org.projectnessie.model.Content; +import org.projectnessie.model.ContentKey; +import org.projectnessie.model.EntriesResponse; +import org.projectnessie.model.Namespace; +import org.projectnessie.model.Operation; + +/** + * Supports previous "delete namespace" functionality of the java client over Nessie API v2. + * + *

API v2 does not have methods dedicated to manging namespaces. Namespaces are expected to be + * managed as ordinary content objects. + */ +public final class ClientSideDeleteNamespace extends BaseDeleteNamespaceBuilder { + private final NessieApiV2 api; + + public ClientSideDeleteNamespace(NessieApiV2 api) { + this.api = api; + } + + @Override + public void delete() + throws NessieNamespaceNotFoundException, NessieReferenceNotFoundException, + NessieNamespaceNotEmptyException { + ContentKey key = ContentKey.of(namespace.getElements()); + Map contentMap; + try { + contentMap = api.getContent().refName(refName).hashOnRef(hashOnRef).key(key).get(); + } catch (NessieNotFoundException e) { + throw new NessieReferenceNotFoundException(e.getMessage(), e); + } + + Optional existingNamespace = + Optional.ofNullable(contentMap.get(key)).flatMap(c -> c.unwrap(Namespace.class)); + + if (!existingNamespace.isPresent()) { + throw new NessieNamespaceNotFoundException( + String.format("Namespace '%s' does not exist", key.toPathString())); + } + + Optional entry; + try { + entry = + api + .getEntries() + .refName(refName) + .hashOnRef(hashOnRef) + .filter(String.format("entry.namespace.startsWith('%s')", key)) + .stream() + .findAny(); + } catch (NessieNotFoundException e) { + throw new NessieReferenceNotFoundException(e.getMessage(), e); + } + + if (entry.isPresent()) { + throw new NessieNamespaceNotEmptyException( + String.format("Namespace '%s' is not empty", key.toPathString())); + } + + try { + api.commitMultipleOperations() + .branchName(refName) + .hash(hashOnRef) + .commitMeta(CommitMeta.fromMessage("delete namespace " + key)) + .operation(Operation.Delete.of(key)) + .commit(); + } catch (NessieNotFoundException e) { + throw new NessieReferenceNotFoundException(e.getMessage(), e); + } catch (NessieConflictException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/clients/client/src/main/java/org/projectnessie/client/util/v2api/ClientSideGetMultipleNamespaces.java b/clients/client/src/main/java/org/projectnessie/client/util/v2api/ClientSideGetMultipleNamespaces.java new file mode 100644 index 00000000000..e1608175ce8 --- /dev/null +++ b/clients/client/src/main/java/org/projectnessie/client/util/v2api/ClientSideGetMultipleNamespaces.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.client.util.v2api; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import org.projectnessie.client.api.GetContentBuilder; +import org.projectnessie.client.api.GetEntriesBuilder; +import org.projectnessie.client.api.NessieApiV2; +import org.projectnessie.client.builder.BaseGetMultipleNamespacesBuilder; +import org.projectnessie.error.NessieNotFoundException; +import org.projectnessie.error.NessieReferenceNotFoundException; +import org.projectnessie.model.Content; +import org.projectnessie.model.ContentKey; +import org.projectnessie.model.EntriesResponse; +import org.projectnessie.model.GetNamespacesResponse; +import org.projectnessie.model.ImmutableGetNamespacesResponse; +import org.projectnessie.model.Namespace; + +/** + * Supports previous "get multiple namespaces" functionality of the java client over Nessie API v2. + * + *

API v2 does not have methods dedicated to manging namespaces. Namespaces are expected to be + * managed as ordinary content objects. + */ +public final class ClientSideGetMultipleNamespaces extends BaseGetMultipleNamespacesBuilder { + private final NessieApiV2 api; + + public ClientSideGetMultipleNamespaces(NessieApiV2 api) { + this.api = api; + } + + @Override + public GetNamespacesResponse get() throws NessieReferenceNotFoundException { + List entries; + try { + GetEntriesBuilder getEntries = api.getEntries().refName(refName).hashOnRef(hashOnRef); + + if (namespace != null) { + getEntries.filter( + String.format("entry.key.startsWith('%s')", ContentKey.of(namespace.getElements()))); + } + + entries = + getEntries.stream() + .filter(e -> Content.Type.NAMESPACE.equals(e.getType())) + .map(EntriesResponse.Entry::getName) + .filter( + name -> + namespace == null + || Namespace.of(name.getElements()).isSameOrSubElementOf(namespace)) + .collect(Collectors.toList()); + } catch (NessieNotFoundException e) { + throw new NessieReferenceNotFoundException(e.getMessage(), e); + } + + if (entries.isEmpty()) { + return ImmutableGetNamespacesResponse.builder().build(); + } + + Map contentMap; + try { + GetContentBuilder getContent = api.getContent().refName(refName).hashOnRef(hashOnRef); + entries.forEach(getContent::key); + contentMap = getContent.get(); + } catch (NessieNotFoundException e) { + throw new NessieReferenceNotFoundException(e.getMessage(), e); + } + + ImmutableGetNamespacesResponse.Builder builder = ImmutableGetNamespacesResponse.builder(); + contentMap.values().stream() + .map(v -> v.unwrap(Namespace.class)) + .filter(Optional::isPresent) + .map(Optional::get) + .forEach(builder::addNamespaces); + + return builder.build(); + } +} diff --git a/clients/client/src/main/java/org/projectnessie/client/util/v2api/ClientSideGetNamespace.java b/clients/client/src/main/java/org/projectnessie/client/util/v2api/ClientSideGetNamespace.java new file mode 100644 index 00000000000..086420c55c0 --- /dev/null +++ b/clients/client/src/main/java/org/projectnessie/client/util/v2api/ClientSideGetNamespace.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.client.util.v2api; + +import java.util.Map; +import java.util.Optional; +import org.projectnessie.client.api.NessieApiV2; +import org.projectnessie.client.builder.BaseGetNamespaceBuilder; +import org.projectnessie.error.NessieNamespaceNotFoundException; +import org.projectnessie.error.NessieNotFoundException; +import org.projectnessie.error.NessieReferenceNotFoundException; +import org.projectnessie.model.Content; +import org.projectnessie.model.ContentKey; +import org.projectnessie.model.Namespace; + +/** + * Supports previous "get namespace" functionality of the java client over Nessie API v2. + * + *

API v2 does not have methods dedicated to manging namespaces. Namespaces are expected to be + * managed as ordinary content objects. + */ +public final class ClientSideGetNamespace extends BaseGetNamespaceBuilder { + private final NessieApiV2 api; + + public ClientSideGetNamespace(NessieApiV2 api) { + this.api = api; + } + + @Override + public Namespace get() throws NessieNamespaceNotFoundException, NessieReferenceNotFoundException { + ContentKey key = ContentKey.of(namespace.getElements()); + Map contentMap; + try { + contentMap = api.getContent().refName(refName).hashOnRef(hashOnRef).key(key).get(); + + } catch (NessieNotFoundException e) { + throw new NessieReferenceNotFoundException(e.getMessage(), e); + } + + Optional result = + Optional.ofNullable(contentMap.get(key)).flatMap(c -> c.unwrap(Namespace.class)); + if (!result.isPresent()) { + throw new NessieNamespaceNotFoundException( + String.format("Namespace '%s' does not exist", key.toPathString())); + } + + return result.get(); + } +} diff --git a/clients/client/src/main/java/org/projectnessie/client/util/v2api/ClientSideUpdateNamespace.java b/clients/client/src/main/java/org/projectnessie/client/util/v2api/ClientSideUpdateNamespace.java new file mode 100644 index 00000000000..93b8cda8842 --- /dev/null +++ b/clients/client/src/main/java/org/projectnessie/client/util/v2api/ClientSideUpdateNamespace.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.client.util.v2api; + +import java.util.HashMap; +import org.projectnessie.client.api.NessieApiV2; +import org.projectnessie.client.builder.BaseUpdateNamespaceBuilder; +import org.projectnessie.error.NessieConflictException; +import org.projectnessie.error.NessieNamespaceNotFoundException; +import org.projectnessie.error.NessieNotFoundException; +import org.projectnessie.error.NessieReferenceNotFoundException; +import org.projectnessie.model.CommitMeta; +import org.projectnessie.model.ContentKey; +import org.projectnessie.model.ImmutableNamespace; +import org.projectnessie.model.Namespace; +import org.projectnessie.model.Operation; + +/** + * Supports previous "update namespace" functionality of the java client over Nessie API v2. + * + *

API v2 does not have methods dedicated to manging namespaces. Namespaces are expected to be + * managed as ordinary content objects. + */ +public final class ClientSideUpdateNamespace extends BaseUpdateNamespaceBuilder { + private final NessieApiV2 api; + + public ClientSideUpdateNamespace(NessieApiV2 api) { + this.api = api; + } + + @Override + public void update() throws NessieNamespaceNotFoundException, NessieReferenceNotFoundException { + ContentKey key = ContentKey.of(namespace.getElements()); + Namespace oldNamespace = + api.getNamespace().refName(refName).hashOnRef(hashOnRef).namespace(namespace).get(); + + HashMap newProperties = new HashMap<>(oldNamespace.getProperties()); + propertyRemovals.forEach(newProperties::remove); + newProperties.putAll(propertyUpdates); + + ImmutableNamespace.Builder builder = + ImmutableNamespace.builder().from(oldNamespace).properties(newProperties); + + try { + api.commitMultipleOperations() + .branchName(refName) + .hash(hashOnRef) + .commitMeta(CommitMeta.fromMessage("update namespace " + key)) + .operation(Operation.Put.of(key, builder.build())) + .commit(); + } catch (NessieNotFoundException e) { + throw new NessieReferenceNotFoundException(e.getMessage(), e); + } catch (NessieConflictException e) { + throw new IllegalStateException(e.getMessage(), e); + } + } +} diff --git a/clients/iceberg-views/src/test/java/org/apache/iceberg/nessie/BaseIcebergTest.java b/clients/iceberg-views/src/test/java/org/apache/iceberg/nessie/BaseIcebergTest.java index a5b86ed3d73..08928aca785 100644 --- a/clients/iceberg-views/src/test/java/org/apache/iceberg/nessie/BaseIcebergTest.java +++ b/clients/iceberg-views/src/test/java/org/apache/iceberg/nessie/BaseIcebergTest.java @@ -96,7 +96,8 @@ private void resetData() throws NessieConflictException, NessieNotFoundException @BeforeEach public void beforeEach(NessieClientFactory clientFactory, @NessieClientUri URI uri) throws IOException { - this.uri = uri.toASCIIString(); + // The underlying NessieCatalog from Iceberg assumes v1 API + this.uri = uri.resolve("v1").toASCIIString(); this.api = clientFactory.make(); resetData(); diff --git a/gc/gc-tool/src/test/java/org/projectnessie/gc/tool/TestCLI.java b/gc/gc-tool/src/test/java/org/projectnessie/gc/tool/TestCLI.java index f2223ec4ef2..c586a981f85 100644 --- a/gc/gc-tool/src/test/java/org/projectnessie/gc/tool/TestCLI.java +++ b/gc/gc-tool/src/test/java/org/projectnessie/gc/tool/TestCLI.java @@ -43,6 +43,8 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.projectnessie.client.ext.NessieApiVersion; +import org.projectnessie.client.ext.NessieApiVersions; import org.projectnessie.client.ext.NessieClientUri; import org.projectnessie.gc.contents.ContentReference; import org.projectnessie.gc.contents.jdbc.AgroalJdbcDataSourceProvider; @@ -63,6 +65,7 @@ @NessieDbAdapterName(InmemoryDatabaseAdapterFactory.NAME) @NessieExternalDatabase(InmemoryTestConnectionProviderSource.class) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@NessieApiVersions(versions = NessieApiVersion.V1) public class TestCLI { public static final String JDBC_URL = "jdbc:h2:mem:nessie_gc;DB_CLOSE_DELAY=-1"; diff --git a/model/build.gradle.kts b/model/build.gradle.kts index f29a0789988..c2ed81bdc91 100644 --- a/model/build.gradle.kts +++ b/model/build.gradle.kts @@ -51,7 +51,7 @@ smallryeOpenApi { schemaFilename.set("META-INF/openapi/openapi") operationIdStrategy.set("METHOD") scanPackages.set( - listOf("org.projectnessie.api", "org.projectnessie.api.http", "org.projectnessie.model") + listOf("org.projectnessie.api.v2", "org.projectnessie.api.v2.http", "org.projectnessie.model") ) } diff --git a/model/src/main/java/org/projectnessie/api/v2/ConfigApi.java b/model/src/main/java/org/projectnessie/api/v2/ConfigApi.java new file mode 100644 index 00000000000..c5b979f1483 --- /dev/null +++ b/model/src/main/java/org/projectnessie/api/v2/ConfigApi.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.api.v2; + +import org.projectnessie.model.NessieConfiguration; + +public interface ConfigApi { + + // Note: When substantial changes in Nessie API (this and related interfaces) are made + // the API version number reported by NessieConfiguration.getMaxSupportedApiVersion() + // should be increased as well. + + /** Get the server configuration. */ + NessieConfiguration getConfig(); +} diff --git a/model/src/main/java/org/projectnessie/api/v2/TreeApi.java b/model/src/main/java/org/projectnessie/api/v2/TreeApi.java new file mode 100644 index 00000000000..4af5f18667b --- /dev/null +++ b/model/src/main/java/org/projectnessie/api/v2/TreeApi.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.api.v2; + +import javax.annotation.Nullable; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import org.projectnessie.api.v2.params.CommitLogParams; +import org.projectnessie.api.v2.params.DiffParams; +import org.projectnessie.api.v2.params.EntriesParams; +import org.projectnessie.api.v2.params.GetReferenceParams; +import org.projectnessie.api.v2.params.Merge; +import org.projectnessie.api.v2.params.ReferencesParams; +import org.projectnessie.api.v2.params.Transplant; +import org.projectnessie.error.NessieConflictException; +import org.projectnessie.error.NessieNotFoundException; +import org.projectnessie.model.Branch; +import org.projectnessie.model.CommitResponse; +import org.projectnessie.model.Content; +import org.projectnessie.model.ContentKey; +import org.projectnessie.model.ContentResponse; +import org.projectnessie.model.DiffResponse; +import org.projectnessie.model.EntriesResponse; +import org.projectnessie.model.GetMultipleContentsRequest; +import org.projectnessie.model.GetMultipleContentsResponse; +import org.projectnessie.model.LogResponse; +import org.projectnessie.model.MergeResponse; +import org.projectnessie.model.Operations; +import org.projectnessie.model.Reference; +import org.projectnessie.model.ReferencesResponse; +import org.projectnessie.model.SingleReferenceResponse; +import org.projectnessie.model.Validation; + +/** + * Interface for working with "trees", that is with collections of contents at a particular point in + * the change history, organized in trees by their respective namespaces. + * + *

A tree is identified by a reference (branch, tag or a "detached" commit hash). + * + *

Only branches and tags can be created / deleted / assigned via respective "*Reference" + * methods. Commits cannot be deleted and get created via commit / merge / transplant operations. + */ +public interface TreeApi { + + // Note: When substantial changes in Nessie API (this and related interfaces) are made + // the API version number reported by NessieConfiguration.getMaxSupportedApiVersion() + // should be increased as well. + + /** + * Get all references. + * + * @return A {@link ReferencesResponse} instance containing all references. + */ + ReferencesResponse getAllReferences(ReferencesParams params); + + /** + * Create a new reference. + * + *

The type of {@code reference}, which can be either a {@link Branch} or {@link + * org.projectnessie.model.Tag}, determines the type of the reference to be created. + * + *

{@link Reference#getName()} defines the name of the reference to be created, {@link + * Reference#getHash()} is the hash of the created reference, the HEAD of the created reference. + * {@code sourceRefName} is the name of the reference which contains {@link Reference#getHash()}, + * and must be present if {@link Reference#getHash()} is present. + * + *

Specifying no {@link Reference#getHash()} means that the new reference will be created "at + * the beginning of time". + */ + SingleReferenceResponse createReference( + @Valid + @NotNull + @Pattern( + regexp = Validation.REF_NAME_PATH_REGEX, + message = Validation.REF_NAME_PATH_MESSAGE) + String name, + @Valid @NotNull Reference.ReferenceType type, + @Valid @Nullable Reference sourceRef) + throws NessieNotFoundException, NessieConflictException; + + /** Get details of a particular ref, if it exists. */ + SingleReferenceResponse getReferenceByName(@Valid @NotNull GetReferenceParams params) + throws NessieNotFoundException; + + /** + * Retrieve objects for a ref, potentially truncated by the backend. + * + *

Retrieves up to {@code maxRecords} objects for the given named reference (tag or branch). + * The backend may respect the given {@code max} records hint, but return less or more + * entries. Backends may also cap the returned entries at a hard-coded limit, the default REST + * server implementation has such a hard-coded limit. + * + *

Invoking {@code getEntries()} does not guarantee to return all commit log entries + * of a given reference, because the result can be truncated by the backend. + * + *

To implement paging, check {@link EntriesResponse#isHasMore() EntriesResponse.isHasMore()} + * and, if {@code true}, pass the value of {@link EntriesResponse#getToken() + * EntriesResponse.getToken()} in the next invocation of {@code getEntries()} as the {@code + * pageToken} parameter. + * + *

See {@code org.projectnessie.client.StreamingUtil} in {@code nessie-client}. + */ + EntriesResponse getEntries( + @Valid + @NotNull + @Pattern( + regexp = Validation.REF_NAME_PATH_REGEX, + message = Validation.REF_NAME_PATH_MESSAGE) + String refName, + @Valid @NotNull EntriesParams params) + throws NessieNotFoundException; + + /** + * Retrieve the commit log for a ref, potentially truncated by the backend. + * + *

Retrieves up to {@code maxRecords} commit-log-entries starting at the HEAD of the given + * named reference (tag or branch). The backend may respect the given {@code max} records + * hint, but return less or more entries. Backends may also cap the returned entries at a + * hard-coded limit, the default REST server implementation has such a hard-coded limit. + * + *

Invoking {@code getCommitLog()} does not guarantee to return all commit log entries + * of a given reference, because the result can be truncated by the backend. + * + *

To implement paging, check {@link LogResponse#isHasMore() LogResponse.isHasMore()} and, if + * {@code true}, pass the value of {@link LogResponse#getToken() LogResponse.getToken()} in the + * next invocation of {@code getCommitLog()} as the {@code pageToken} parameter. + * + *

See {@code org.projectnessie.client.StreamingUtil} in {@code nessie-client}. + */ + LogResponse getCommitLog( + @Valid + @NotNull + @Pattern( + regexp = Validation.REF_NAME_PATH_REGEX, + message = Validation.REF_NAME_PATH_MESSAGE) + String ref, + @Valid @NotNull CommitLogParams params) + throws NessieNotFoundException; + + /** + * Returns a set of content differences between two given references. + * + * @param params The {@link DiffParams} that includes the parameters for this API call. + * @return A set of diff values that show the difference between two given references. + */ + DiffResponse getDiff(@Valid @NotNull DiffParams params) throws NessieNotFoundException; + + /** + * Update a reference's HEAD to point to a different commit. + * + * @param type Optional expected type of reference being assigned. Will be validated if present. + */ + SingleReferenceResponse assignReference( + @Valid Reference.ReferenceType type, + @Valid + @NotNull + @Pattern( + regexp = Validation.REF_NAME_PATH_REGEX, + message = Validation.REF_NAME_PATH_MESSAGE) + String reference, + @Valid @NotNull Reference assignTo) + throws NessieNotFoundException, NessieConflictException; + + /** + * Delete a named reference. + * + * @param type Optional expected type of reference being deleted. Will be validated if present. + */ + SingleReferenceResponse deleteReference( + @Valid Reference.ReferenceType type, + @Valid + @NotNull + @Pattern( + regexp = Validation.REF_NAME_PATH_REGEX, + message = Validation.REF_NAME_PATH_MESSAGE) + String reference) + throws NessieConflictException, NessieNotFoundException; + + /** Cherry-pick a set of commits into a branch. */ + MergeResponse transplantCommitsIntoBranch( + @Valid + @NotNull + @Pattern( + regexp = Validation.REF_NAME_PATH_REGEX, + message = Validation.REF_NAME_PATH_MESSAGE) + String branch, + @Valid Transplant transplant) + throws NessieNotFoundException, NessieConflictException; + + /** merge mergeRef onto ref. */ + MergeResponse mergeRefIntoBranch( + @Valid + @NotNull + @Pattern( + regexp = Validation.REF_NAME_PATH_REGEX, + message = Validation.REF_NAME_PATH_MESSAGE) + String branch, + @Valid @NotNull Merge merge) + throws NessieNotFoundException, NessieConflictException; + + /** + * Commit multiple operations against the given branch expecting that branch to have the given + * hash as its latest commit. The hash in the successful response contains the hash of the commit + * that contains the operations of the invocation. + * + * @param branch Branch to change, defaults to default branch. + * @param operations {@link Operations} to apply + * @return updated {@link Branch} objects with the hash of the new HEAD + * @throws NessieNotFoundException if {@code branchName} could not be found + * @throws NessieConflictException if the operations could not be applied to some conflict, which + * is either caused by a conflicting commit or concurrent commits. + */ + CommitResponse commitMultipleOperations( + @Valid + @NotNull + @Pattern( + regexp = Validation.REF_NAME_PATH_REGEX, + message = Validation.REF_NAME_PATH_MESSAGE) + String branch, + @Valid @NotNull Operations operations) + throws NessieNotFoundException, NessieConflictException; + + /** + * This operation returns the {@link Content} for a {@link ContentKey} in a named-reference (a + * {@link org.projectnessie.model.Branch} or {@link org.projectnessie.model.Tag}). + * + *

If the table-metadata is tracked globally (Iceberg), Nessie returns a {@link Content} + * object, that contains the most up-to-date part for the globally tracked part (Iceberg: + * table-metadata) plus the per-Nessie-reference/hash specific part (Iceberg: snapshot-ID, + * schema-ID, partition-spec-ID, default-sort-order-ID). + * + * @param key the {@link ContentKey}s to retrieve + * @param ref named-reference to retrieve the content for + * @return list of {@link GetMultipleContentsResponse.ContentWithKey}s + * @throws NessieNotFoundException if {@code ref} or {@code hashOnRef} does not exist + */ + ContentResponse getContent( + @Valid ContentKey key, + @Valid + @Pattern( + regexp = Validation.REF_NAME_PATH_REGEX, + message = Validation.REF_NAME_PATH_MESSAGE) + String ref) + throws NessieNotFoundException; + + /** + * Similar to {@link #getContent(ContentKey, String)}, but takes multiple {@link ContentKey}s and + * returns the {@link Content} for the one or more {@link ContentKey}s in a named-reference (a + * {@link org.projectnessie.model.Branch} or {@link org.projectnessie.model.Tag}). + * + *

If the table-metadata is tracked globally (Iceberg), Nessie returns a {@link Content} + * object, that contains the most up-to-date part for the globally tracked part (Iceberg: + * table-metadata) plus the per-Nessie-reference/hash specific part (Iceberg: snapshot-id, + * schema-id, partition-spec-id, default-sort-order-id). + * + * @param ref named-reference to retrieve the content for + * @param request the {@link ContentKey}s to retrieve + * @return list of {@link GetMultipleContentsResponse.ContentWithKey}s + * @throws NessieNotFoundException if {@code ref} or {@code hashOnRef} does not exist + */ + GetMultipleContentsResponse getMultipleContents( + @Valid + @Pattern( + regexp = Validation.REF_NAME_PATH_REGEX, + message = Validation.REF_NAME_PATH_MESSAGE) + String ref, + @Valid @NotNull GetMultipleContentsRequest request) + throws NessieNotFoundException; +} diff --git a/model/src/main/java/org/projectnessie/api/v2/http/HttpConfigApi.java b/model/src/main/java/org/projectnessie/api/v2/http/HttpConfigApi.java new file mode 100644 index 00000000000..53a89e59f3a --- /dev/null +++ b/model/src/main/java/org/projectnessie/api/v2/http/HttpConfigApi.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.api.v2.http; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.ExampleObject; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.projectnessie.api.v2.ConfigApi; +import org.projectnessie.model.NessieConfiguration; + +@Path("config") +@Tag(name = "v1") +@Tag(name = "v2") +public interface HttpConfigApi extends ConfigApi { + + @Override + @GET + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Returns repository and server settings relevant to clients.") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Configuration settings", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = NessieConfiguration.class), + examples = {@ExampleObject(ref = "nessieConfig")})), + @APIResponse(responseCode = "401", description = "Invalid credentials provided") + }) + NessieConfiguration getConfig(); +} diff --git a/model/src/main/java/org/projectnessie/api/v2/http/HttpTreeApi.java b/model/src/main/java/org/projectnessie/api/v2/http/HttpTreeApi.java new file mode 100644 index 00000000000..103ea382e24 --- /dev/null +++ b/model/src/main/java/org/projectnessie/api/v2/http/HttpTreeApi.java @@ -0,0 +1,637 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.api.v2.http; + +import java.util.List; +import javax.ws.rs.BeanParam; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.ExampleObject; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.projectnessie.api.v2.TreeApi; +import org.projectnessie.api.v2.params.CommitLogParams; +import org.projectnessie.api.v2.params.DiffParams; +import org.projectnessie.api.v2.params.EntriesParams; +import org.projectnessie.api.v2.params.GetReferenceParams; +import org.projectnessie.api.v2.params.Merge; +import org.projectnessie.api.v2.params.ReferencesParams; +import org.projectnessie.api.v2.params.Transplant; +import org.projectnessie.error.NessieConflictException; +import org.projectnessie.error.NessieNotFoundException; +import org.projectnessie.model.CommitResponse; +import org.projectnessie.model.ContentKey; +import org.projectnessie.model.ContentResponse; +import org.projectnessie.model.DiffResponse; +import org.projectnessie.model.EntriesResponse; +import org.projectnessie.model.GetMultipleContentsRequest; +import org.projectnessie.model.GetMultipleContentsResponse; +import org.projectnessie.model.ImmutableGetMultipleContentsRequest; +import org.projectnessie.model.LogResponse; +import org.projectnessie.model.MergeResponse; +import org.projectnessie.model.Operations; +import org.projectnessie.model.Reference; +import org.projectnessie.model.ReferencesResponse; +import org.projectnessie.model.SingleReferenceResponse; + +@Consumes(value = MediaType.APPLICATION_JSON) +@Path("trees") +@Tag(name = "v2") +public interface HttpTreeApi extends TreeApi { + + String REF_NAME_PATH_ELEMENT_REGEX = "([^/]+|[^@]+@[^@/]*?)"; + + @Override + @GET + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Get information about all branches and tags") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Returned references.", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + examples = { + @ExampleObject(ref = "referencesResponse"), + @ExampleObject(ref = "referencesResponseWithMetadata") + }, + schema = @Schema(implementation = ReferencesResponse.class))), + @APIResponse(responseCode = "401", description = "Invalid credentials provided"), + }) + ReferencesResponse getAllReferences(@BeanParam ReferencesParams params); + + @Override + @POST + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Create a new branch or tag", + description = + "The name and type query parameters define the kind of reference to be created. " + + "The payload object defines the new reference's origin in the commit history. " + + "\n" + + "Only branches and tags can be created by this method, but the payload object may be any" + + " valid reference, including a detached commit." + + "\n" + + "If the payload reference object does not define a commit hash, the HEAD of that reference " + + "will be used.") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Created successfully.", + content = { + @Content( + mediaType = MediaType.APPLICATION_JSON, + examples = {@ExampleObject(ref = "refObjNew")}, + schema = @Schema(implementation = Reference.class)) + }), + @APIResponse(responseCode = "401", description = "Invalid credentials provided"), + @APIResponse(responseCode = "403", description = "Not allowed to create reference"), + @APIResponse( + responseCode = "409", + description = "Another reference with the same name already exists"), + }) + SingleReferenceResponse createReference( + @Parameter(required = true, description = "New reference name") @QueryParam("name") + String name, + @Parameter( + required = true, + description = "Type of the reference to be created", + examples = {@ExampleObject(ref = "referenceType")}) + @QueryParam("type") + Reference.ReferenceType type, + @RequestBody( + required = true, + description = "Source reference data from which the new reference is to be created.", + content = { + @Content( + mediaType = MediaType.APPLICATION_JSON, + examples = {@ExampleObject(ref = "refObjNew")}) + }) + Reference reference) + throws NessieNotFoundException, NessieConflictException; + + @Override + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("{ref:" + REF_NAME_PATH_ELEMENT_REGEX + "}") + @Operation(summary = "Fetch details of a reference") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Found and returned reference.", + content = { + @Content( + mediaType = MediaType.APPLICATION_JSON, + examples = {@ExampleObject(ref = "refObj")}, + schema = @Schema(implementation = Reference.class)) + }), + @APIResponse(responseCode = "400", description = "Invalid input, ref name not valid"), + @APIResponse(responseCode = "401", description = "Invalid credentials provided"), + @APIResponse(responseCode = "403", description = "Not allowed to view the given reference"), + @APIResponse(responseCode = "404", description = "Ref not found") + }) + SingleReferenceResponse getReferenceByName(@BeanParam GetReferenceParams params) + throws NessieNotFoundException; + + @Override + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("{ref:([^/]+|[^~]+~)}/entries") + @Operation( + summary = "Fetch all entries for a given reference", + description = + "Retrieves objects for a ref, potentially truncated by the backend.\n" + + "\n" + + "Retrieves up to 'maxRecords' entries for the " + + "given named reference (tag or branch) or the given hash. " + + "The backend may respect the given 'max' records hint, but return less or more entries. " + + "Backends may also cap the returned entries at a hard-coded limit, the default " + + "REST server implementation has such a hard-coded limit.\n" + + "\n" + + "To implement paging, check 'hasMore' in the response and, if 'true', pass the value " + + "returned as 'token' in the next invocation as the 'pageToken' parameter.\n" + + "\n" + + "The content and meaning of the returned 'token' is \"private\" to the implementation," + + "treat is as an opaque value.\n" + + "\n" + + "It is wrong to assume that invoking this method with a very high 'maxRecords' value " + + "will return all commit log entries.\n" + + "\n" + + "The 'filter' parameter allows for advanced filtering capabilities using the Common Expression Language (CEL).\n" + + "An intro to CEL can be found at https://github.com/google/cel-spec/blob/master/doc/intro.md.\n" + + "\n" + + "The 'namespace' and 'derive-prefixes' parameters may be used to determine the first level of child " + + "namespaces relative to the 'namespace' parameters while fetching ordinary entries in the specified " + + "'namespace'. Note that when these parameters are set the returned prefixes are computed in the " + + "context the subset of entries used to fill the current results page. Subsequent pages may uncover " + + "new prefixes and they may contain previously reported prefixes too.") + @APIResponses({ + @APIResponse( + description = "List names and object types in a contents tree", + content = { + @Content( + mediaType = MediaType.APPLICATION_JSON, + examples = {@ExampleObject(ref = "entriesResponse")}, + schema = @Schema(implementation = EntriesResponse.class)) + }), + @APIResponse(responseCode = "200", description = "Returned successfully."), + @APIResponse(responseCode = "400", description = "Invalid input, ref name not valid"), + @APIResponse(responseCode = "401", description = "Invalid credentials provided"), + @APIResponse( + responseCode = "403", + description = "Not allowed to view the given reference or fetch entries for it"), + @APIResponse(responseCode = "404", description = "Ref not found") + }) + EntriesResponse getEntries( + @Parameter(ref = "refPathParameter") @PathParam("ref") String ref, + @BeanParam EntriesParams params) + throws NessieNotFoundException; + + @Override + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("{ref}/history") + @Operation( + summary = "Get commit log for a particular reference", + description = + "Retrieve the commit log for a reference, potentially truncated by the backend.\n" + + "\n" + + "The backend may respect the given 'max-entries' records hint, or may return more or less entries. " + + "Backends may also cap the returned entries at a hard-coded limit\n" + + "\n" + + "To implement paging, check the 'hasMore' property in the response and, if 'true', pass the value " + + "of its 'token' property in the next invocation as the 'page-token' parameter.\n" + + "\n" + + "The content and meaning of the returned 'token' is \"internal\" to the implementation," + + "treat is as an opaque value.\n" + + "\n" + + "Different pages may have different numbers of log records in them even if they come from another " + + "log API call with the same parameters.\n" + + "\n" + + "It is wrong to assume that invoking this method with a very high 'max-records' value " + + "will return all commit log entries.\n" + + "\n" + + "The 'filter' parameter allows for advanced filtering capabilities using the Common Expression Language (CEL).\n" + + "An intro to CEL can be found at https://github.com/google/cel-spec/blob/master/doc/intro.md.\n" + + "\n" + + "The fetching of the log starts from the HEAD of the given ref (or a more specific commit, if provided " + + "as part of the ref spec) and proceeds until the 'root' commit or the 'limit-hash' commit are " + + "encountered.") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Returned commits.", + content = { + @Content( + mediaType = MediaType.APPLICATION_JSON, + examples = { + @ExampleObject(ref = "logResponseAdditionalInfo"), + @ExampleObject(ref = "logResponseSimple") + }, + schema = @Schema(implementation = LogResponse.class)) + }), + @APIResponse(responseCode = "400", description = "Invalid input, ref name not valid"), + @APIResponse(responseCode = "401", description = "Invalid credentials provided"), + @APIResponse( + responseCode = "403", + description = "Not allowed to view the given reference or get commit log for it"), + @APIResponse(responseCode = "404", description = "Ref doesn't exists") + }) + LogResponse getCommitLog( + @Parameter(ref = "refPathParameter") @PathParam("ref") String ref, + @BeanParam CommitLogParams params) + throws NessieNotFoundException; + + @Override + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("{from-ref:([^/]+|[^~]+~)}/diff/{to-ref:([^/]+|[^~]+~)}") + @Operation( + summary = "Get contents that differ in the trees specified by the two given references", + description = + "The URL pattern is basically 'from' and 'to' reference specs separated by '/diff/'\n" + + "\n" + + "Examples: \n" + + "- ref/main/diff/myBranch\n" + + "- ref/main@1234567890123456/diff/myBranch\n" + + "- ref/main@1234567890123456/diff/myBranch@23445678\n" + + "- ref/main/diff/myBranch@23445678\n" + + "- ref/main/diff/myBranch@23445678\n" + + "- ref/myBranch/diff/-\n") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Returned diff for the given references.", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + examples = { + @ExampleObject(ref = "diffResponse"), + }, + schema = @Schema(implementation = DiffResponse.class))), + @APIResponse(responseCode = "400", description = "Invalid input, fromRef/toRef name not valid"), + @APIResponse(responseCode = "401", description = "Invalid credentials provided"), + @APIResponse(responseCode = "403", description = "Not allowed to view the given fromRef/toRef"), + @APIResponse(responseCode = "404", description = "fromRef/toRef not found"), + }) + DiffResponse getDiff(@BeanParam DiffParams params) throws NessieNotFoundException; + + @Override + @PUT + @Produces(MediaType.APPLICATION_JSON) + @Path("{ref:" + REF_NAME_PATH_ELEMENT_REGEX + "}") + @Operation( + summary = "Set a named reference to a specific hash via another reference.", + description = + "The 'ref' parameter identifies the branch or tag to be reassigned. The 'ref' spec may contain a hash " + + "qualifier. That hash as well as the optional 'type' parameter may be used to ensure the operation is " + + "performed on the same object that the user expects. If the 'hash' is present it will be validated to be " + + "equal to the current HEAD of the reference.\n" + + "\n" + + "Only branches and tags may be reassigned." + + "\n" + + "The payload object identifies any reference visible to the current user whose 'hash' will be used to " + + "define the new HEAD of the reference being reassigned. Detached hashes may be used in the payload.") + @APIResponses({ + @APIResponse(responseCode = "204", description = "Assigned successfully"), + @APIResponse(responseCode = "400", description = "Invalid input, ref specification not valid"), + @APIResponse(responseCode = "401", description = "Invalid credentials provided"), + @APIResponse(responseCode = "403", description = "Not allowed to view or assign reference"), + @APIResponse(responseCode = "404", description = "One or more references don't exist"), + @APIResponse( + responseCode = "409", + description = "Update conflict or expected hash / type mismatch") + }) + SingleReferenceResponse assignReference( + @Parameter( + description = "Optional expected type of the reference being reassigned", + examples = {@ExampleObject(ref = "referenceType")}) + @QueryParam("type") + Reference.ReferenceType type, + @Parameter(ref = "checkedRefParameter") @PathParam("ref") String ref, + @RequestBody( + description = + "Reference to which the 'ref' (from the path parameter) shall be assigned. This must be either a " + + "'Detached' commit, 'Branch' or 'Tag' via which the hash is visible to the caller.", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + examples = {@ExampleObject(ref = "refObj"), @ExampleObject(ref = "tagObj")})) + Reference assignTo) + throws NessieNotFoundException, NessieConflictException; + + @Override + @DELETE + @Produces(MediaType.APPLICATION_JSON) + @Path("{ref:" + REF_NAME_PATH_ELEMENT_REGEX + "}") + @Operation( + summary = "Delete a reference", + description = + "The 'ref' parameter identifies the branch or tag to be deleted. The 'ref' spec may contain a hash " + + "qualifier. That hash as well as the optional 'type' parameter may be used to ensure the operation is " + + "performed on the same object that the user expects. If the 'hash' is present it will be validated to be " + + "equal to the current HEAD of the reference.\n" + + "\n" + + "Only branches and tags can be deleted. However, deleting the default branch may be restricted.") + @APIResponses({ + @APIResponse(responseCode = "204", description = "Deleted successfully."), + @APIResponse(responseCode = "400", description = "Invalid input, ref/hash name not valid"), + @APIResponse(responseCode = "401", description = "Invalid credentials provided"), + @APIResponse(responseCode = "403", description = "Not allowed to view or delete reference"), + @APIResponse(responseCode = "404", description = "Ref doesn't exists"), + @APIResponse(responseCode = "409", description = "update conflict"), + }) + SingleReferenceResponse deleteReference( + @Parameter( + description = "Optional expected type of the reference being deleted", + examples = {@ExampleObject(ref = "referenceType")}) + @QueryParam("type") + Reference.ReferenceType type, + @Parameter(ref = "checkedRefParameter") @PathParam("ref") String ref) + throws NessieConflictException, NessieNotFoundException; + + @Override + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("{ref:" + REF_NAME_PATH_ELEMENT_REGEX + "}/contents/{key}") + @Operation( + summary = "Get the content object associated with a key.", + description = + "This operation returns the content value for a content key at a particular point in history as defined " + + "by the 'ref' parameter.") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Information for a table, view or another content object for the given key", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + examples = {@ExampleObject(ref = "iceberg")}, + schema = @Schema(implementation = org.projectnessie.model.Content.class))), + @APIResponse(responseCode = "400", description = "Invalid input, ref name not valid"), + @APIResponse(responseCode = "401", description = "Invalid credentials provided"), + @APIResponse( + responseCode = "403", + description = "Not allowed to view the given reference or read object content for a key"), + @APIResponse( + responseCode = "404", + description = "Table not found on 'ref' or non-existent reference") + }) + ContentResponse getContent( + @Parameter(ref = "keyPathParameter") @PathParam("key") ContentKey key, + @Parameter(ref = "refPathParameter") @PathParam("ref") String ref) + throws NessieNotFoundException; + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("{ref:" + REF_NAME_PATH_ELEMENT_REGEX + "}/contents") + @Operation( + summary = "Get multiple content objects.", + description = + "Similar to 'GET /trees/{ref}/content/{key}', but takes multiple 'key' query parameters and returns zero " + + "or more content values in the same JSON structure as the 'POST /trees/{ref}/content' endpoint.\n" + + "\n" + + "This is a convenience method for fetching a small number of content objects. It is mostly intended " + + "for human use. For automated use cases or when the number of keys is large the " + + "'POST /trees/{ref}/content' method is preferred.") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Retrieved successfully.", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + examples = @ExampleObject(ref = "multiGetResponse"), + schema = @Schema(implementation = GetMultipleContentsResponse.class))), + @APIResponse(responseCode = "400", description = "Invalid input, ref name not valid"), + @APIResponse(responseCode = "401", description = "Invalid credentials provided"), + @APIResponse( + responseCode = "403", + description = "Not allowed to view the given reference or read object content for a key"), + @APIResponse(responseCode = "404", description = "Provided ref doesn't exists") + }) + default GetMultipleContentsResponse getSeveralContents( + @Parameter( + description = "Reference to use.", + examples = {@ExampleObject(ref = "ref")}) + @PathParam("ref") + String ref, + @Parameter(ref = "keyQueryParameter") @QueryParam("key") List keys) + throws NessieNotFoundException { + ImmutableGetMultipleContentsRequest.Builder request = GetMultipleContentsRequest.builder(); + keys.forEach(k -> request.addRequestedKeys(ContentKey.fromPathString(k))); + return getMultipleContents(ref, request.build()); + } + + @Override + @POST + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @Path("{ref:" + REF_NAME_PATH_ELEMENT_REGEX + "}/contents") + @Operation( + summary = "Get multiple content objects.", + description = + "Similar to 'GET /trees/{ref}/content/{key}', but takes multiple 'ContentKey's (in the JSON payload) and " + + "returns zero or more content objects.\n" + + "\n" + + "Note that if some of the keys from the request do not have an associated content object at the " + + "point in history defined by the 'ref' parameter, the response will be successful, but no data will " + + "be returned for the missing keys.") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Retrieved successfully.", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + examples = @ExampleObject(ref = "multiGetResponse"), + schema = @Schema(implementation = GetMultipleContentsResponse.class))), + @APIResponse(responseCode = "400", description = "Invalid input, ref name not valid"), + @APIResponse(responseCode = "401", description = "Invalid credentials provided"), + @APIResponse( + responseCode = "403", + description = "Not allowed to view the given reference or read object content for a key"), + @APIResponse(responseCode = "404", description = "Provided ref doesn't exists") + }) + GetMultipleContentsResponse getMultipleContents( + @Parameter(ref = "refPathParameter") @PathParam("ref") String ref, + @RequestBody( + description = "Keys to retrieve.", + content = @Content(examples = @ExampleObject(ref = "multiGetRequest"))) + GetMultipleContentsRequest request) + throws NessieNotFoundException; + + @Override + @POST + @Produces(MediaType.APPLICATION_JSON) + @Path("{branch}/history/transplant") + @Operation( + summary = + "Transplant commits specified by the 'Transplant' payload object onto the given 'branch'", + description = + "This is done as an atomic operation such that only the last of the sequence is ever " + + "visible to concurrent readers/writers. The sequence to transplant must be " + + "contiguous and in order.\n" + + "\n" + + "The state of contents specified by the 'branch' reference will be used for detecting conflicts with " + + "the commits being transplanted.") + @APIResponses({ + @APIResponse( + responseCode = "200", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + examples = { + @ExampleObject(ref = "mergeResponseSuccess"), + @ExampleObject(ref = "mergeResponseFail") + }, + schema = @Schema(implementation = MergeResponse.class)), + description = + "Transplant operation completed. " + + "The actual transplant might have failed and reported as successful=false, " + + "if the client asked to return a conflict as a result instead of returning an error. " + + "Note: the 'commonAncestor' field in a response will always be null for a transplant."), + @APIResponse(responseCode = "400", description = "Invalid input, ref/hash name not valid"), + @APIResponse(responseCode = "401", description = "Invalid credentials provided"), + @APIResponse( + responseCode = "403", + description = "Not allowed to view the given reference or transplant commits"), + @APIResponse(responseCode = "404", description = "Ref doesn't exists"), + @APIResponse(responseCode = "409", description = "update conflict") + }) + MergeResponse transplantCommitsIntoBranch( + @Parameter(ref = "branchPathParameter") @PathParam("branch") String branch, + @RequestBody( + required = true, + description = "Commits to transplant", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + examples = {@ExampleObject(ref = "transplant")})) + Transplant transplant) + throws NessieNotFoundException, NessieConflictException; + + @Override + @POST + @Produces(MediaType.APPLICATION_JSON) + @Path("{branch}/history/merge") + @Operation( + summary = "Merge commits from another reference onto 'branch'.", + description = + "Merge commits referenced by the 'mergeRefName' and 'fromHash' parameters of the payload object into the " + + "requested 'branch'.\n" + + "\n" + + "The state of contents specified by the 'branch' reference will be used for detecting conflicts with " + + "the commits being transplanted.\n" + + "\n" + + "The merge is committed if it is free from conflicts. The set of commits merged into the target branch " + + "will be all of those starting at 'fromHash' on 'mergeRefName' until we arrive at the common ancestor. " + + "Depending on the underlying implementation, the number of commits allowed as part of this operation " + + "may be limited.") + @APIResponses({ + @APIResponse( + responseCode = "204", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + examples = { + @ExampleObject(ref = "mergeResponseSuccess"), + @ExampleObject(ref = "mergeResponseFail") + }, + schema = @Schema(implementation = MergeResponse.class)), + description = + "Merge operation completed. " + + "The actual merge might have failed and reported as successful=false, " + + "if the client asked to return a conflict as a result instead of returning an error."), + @APIResponse(responseCode = "400", description = "Invalid input, ref/hash name not valid"), + @APIResponse(responseCode = "401", description = "Invalid credentials provided"), + @APIResponse( + responseCode = "403", + description = "Not allowed to view the given reference or merge commits"), + @APIResponse(responseCode = "404", description = "Ref doesn't exists"), + @APIResponse(responseCode = "409", description = "update conflict") + }) + MergeResponse mergeRefIntoBranch( + @Parameter(ref = "branchPathParameter") @PathParam("branch") String branch, + @RequestBody( + required = true, + description = + "Merge operation that defines the source reference name and an optional hash. " + + "If 'fromHash' is not present, the current 'sourceRef's HEAD will be used.", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + examples = {@ExampleObject(ref = "merge")})) + Merge merge) + throws NessieNotFoundException, NessieConflictException; + + @Override + @POST + @Path("{branch}/history/commit") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @Operation( + summary = "Commit one or more operations against the given 'branch'.", + description = + "The state of contents specified by the 'branch' reference will be used for detecting conflicts with " + + "the operation being committed.\n" + + "\n" + + "The hash in the successful response will be the hash of the commit that contains the requested " + + "operations, whose immediate parent commit will be the current HEAD of the specified branch.") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Updated successfully.", + content = { + @Content( + mediaType = MediaType.APPLICATION_JSON, + examples = {@ExampleObject(ref = "refObj")}, + schema = @Schema(implementation = CommitResponse.class)) + }), + @APIResponse(responseCode = "400", description = "Invalid input, ref/hash name not valid"), + @APIResponse(responseCode = "401", description = "Invalid credentials provided"), + @APIResponse( + responseCode = "403", + description = "Not allowed to view the given reference or perform commits"), + @APIResponse(responseCode = "404", description = "Provided ref doesn't exists"), + @APIResponse(responseCode = "409", description = "Update conflict") + }) + CommitResponse commitMultipleOperations( + @Parameter(ref = "branchPathParameter") @PathParam("branch") String branch, + @RequestBody( + description = "Operations to commit", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + examples = {@ExampleObject(ref = "operations")})) + Operations operations) + throws NessieNotFoundException, NessieConflictException; +} diff --git a/model/src/main/java/org/projectnessie/api/v2/params/AbstractParams.java b/model/src/main/java/org/projectnessie/api/v2/params/AbstractParams.java new file mode 100644 index 00000000000..163504d7d10 --- /dev/null +++ b/model/src/main/java/org/projectnessie/api/v2/params/AbstractParams.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.api.v2.params; + +import javax.annotation.Nullable; +import javax.ws.rs.QueryParam; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; + +public abstract class AbstractParams> { + + @Parameter(description = "maximum number of entries to return, just a hint for the server") + @QueryParam("max-records") + @Nullable + private Integer maxRecords; + + @Parameter( + description = + "paging continuation token, as returned in the previous value of the field 'token' in " + + "the corresponding 'EntriesResponse' or 'LogResponse' or 'ReferencesResponse' or 'RefLogResponse'.") + @QueryParam("page-token") + @Nullable + private String pageToken; + + protected AbstractParams() {} + + protected AbstractParams(Integer maxRecords, String pageToken) { + this.maxRecords = maxRecords; + this.pageToken = pageToken; + } + + @Nullable + public Integer maxRecords() { + return maxRecords; + } + + @Nullable + public String pageToken() { + return pageToken; + } + + public abstract IMPL forNextPage(String pageToken); + + public abstract static class Builder> { + + protected Integer maxRecords; + protected String pageToken; + + protected Builder() {} + + @SuppressWarnings("unchecked") + public T maxRecords(Integer maxRecords) { + this.maxRecords = maxRecords; + return (T) this; + } + + @SuppressWarnings("unchecked") + public T pageToken(String pageToken) { + this.pageToken = pageToken; + return (T) this; + } + } +} diff --git a/model/src/main/java/org/projectnessie/api/v2/params/BaseMergeTransplant.java b/model/src/main/java/org/projectnessie/api/v2/params/BaseMergeTransplant.java new file mode 100644 index 00000000000..02364b42c88 --- /dev/null +++ b/model/src/main/java/org/projectnessie/api/v2/params/BaseMergeTransplant.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2020 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.api.v2.params; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import java.util.List; +import javax.annotation.Nullable; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; +import org.projectnessie.model.MergeBehavior; +import org.projectnessie.model.MergeKeyBehavior; +import org.projectnessie.model.MergeResponse; +import org.projectnessie.model.Validation; + +public interface BaseMergeTransplant { + + @Size(min = 1) + @JsonInclude(NON_NULL) + String getMessage(); + + @NotBlank + @Pattern(regexp = Validation.REF_NAME_REGEX, message = Validation.REF_NAME_MESSAGE) + String getFromRefName(); + + @Nullable + @JsonInclude(Include.NON_NULL) + List getKeyMergeModes(); + + @Nullable + @JsonInclude(Include.NON_NULL) + MergeBehavior getDefaultKeyMergeMode(); + + @Nullable + @JsonInclude(Include.NON_NULL) + Boolean isDryRun(); + + @Nullable + @JsonInclude(Include.NON_NULL) + Boolean isFetchAdditionalInfo(); + + /** + * When set to {@code true}, the {@code merge} and {@code transplant} operations will return + * {@link MergeResponse} objects when a content based conflict cannot be resolved, instead of + * throwing a {@link org.projectnessie.error.NessieReferenceConflictException}. + */ + @Nullable + @JsonInclude(Include.NON_NULL) + Boolean isReturnConflictAsResult(); +} diff --git a/model/src/main/java/org/projectnessie/api/v2/params/CommitLogParams.java b/model/src/main/java/org/projectnessie/api/v2/params/CommitLogParams.java new file mode 100644 index 00000000000..f5a17ea018d --- /dev/null +++ b/model/src/main/java/org/projectnessie/api/v2/params/CommitLogParams.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.api.v2.params; + +import java.util.Objects; +import java.util.StringJoiner; +import javax.annotation.Nullable; +import javax.validation.constraints.Pattern; +import javax.ws.rs.QueryParam; +import org.eclipse.microprofile.openapi.annotations.media.ExampleObject; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.projectnessie.api.params.FetchOption; +import org.projectnessie.model.Validation; + +/** + * The purpose of this class is to include optional parameters that can be passed to {@code + * HttpTreeApi#getCommitLog(String, CommitLogParams)}. + * + *

For easier usage of this class, there is {@link CommitLogParams#builder()}, which allows + * configuring/setting the different parameters. + */ +public class CommitLogParams extends AbstractParams { + + @Nullable + @Pattern(regexp = Validation.HASH_REGEX, message = Validation.HASH_MESSAGE) + @Parameter( + description = + "Hash on the given ref to identify the commit where the operation of fetching the log " + + "should stop, i.e. the 'far' end of the commit log, returned late in the result.", + examples = {@ExampleObject(ref = "nullHash"), @ExampleObject(ref = "hash")}) + @QueryParam("limit-hash") + private String startHash; + + @Nullable + @Parameter( + description = + "A Common Expression Language (CEL) expression. An intro to CEL can be found at https://github.com/google/cel-spec/blob/master/doc/intro.md.\n\n" + + "Usable variables within the expression are:\n\n" + + "- 'commit' with fields 'author' (string), 'committer' (string), 'commitTime' (timestamp), 'hash' (string), ',message' (string), 'properties' (map)\n\n" + + "- 'operations' (list), each operation has the fields 'type' (string, either 'PUT' or 'DELETE'), 'key' (string, namespace + table name), 'keyElements' (list of strings), 'namespace' (string), 'namespaceElements' (list of strings) and 'name' (string, the \"simple\" table name)\n\n" + + "Note that the expression can only test against 'operations', if 'fetch' is set to 'ALL'.\n\n" + + "Hint: when filtering commits, you can determine whether commits are \"missing\" (filtered) by checking whether 'LogEntry.parentCommitHash' is different from the hash of the previous commit in the log response.", + examples = { + @ExampleObject(ref = "expr_by_commit_author"), + @ExampleObject(ref = "expr_by_commit_committer"), + @ExampleObject(ref = "expr_by_commitTime"), + @ExampleObject(ref = "expr_by_commit_operations_table_name"), + @ExampleObject(ref = "expr_by_commit_operations_type") + }) + @QueryParam("filter") + private String filter; + + @Parameter( + description = + "Specify how much information to be returned. Will fetch additional metadata such as parent commit hash and operations in a commit, for each commit if set to 'ALL'.") + @QueryParam("fetch") + @Nullable + private FetchOption fetchOption; + + public CommitLogParams() {} + + @org.immutables.builder.Builder.Constructor + CommitLogParams( + @Nullable String startHash, + @Nullable Integer maxRecords, + @Nullable String pageToken, + @Nullable String filter, + @Nullable FetchOption fetchOption) { + super(maxRecords, pageToken); + this.startHash = startHash; + this.filter = filter; + this.fetchOption = fetchOption; + } + + @Nullable + public String startHash() { + return startHash; + } + + @Nullable + public String filter() { + return filter; + } + + @Nullable + public FetchOption fetchOption() { + return fetchOption; + } + + public static CommitLogParamsBuilder builder() { + return new CommitLogParamsBuilder(); + } + + public static CommitLogParams empty() { + return builder().build(); + } + + @Override + public CommitLogParams forNextPage(String pageToken) { + return new CommitLogParams(startHash, maxRecords(), pageToken, filter, fetchOption); + } + + @Override + public String toString() { + return new StringJoiner(", ", CommitLogParams.class.getSimpleName() + "[", "]") + .add("startHash='" + startHash + "'") + .add("maxRecords=" + maxRecords()) + .add("pageToken='" + pageToken() + "'") + .add("filter='" + filter + "'") + .add("fetchOption='" + fetchOption + "'") + .toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof CommitLogParams)) { + return false; + } + CommitLogParams that = (CommitLogParams) o; + return Objects.equals(startHash, that.startHash) + && Objects.equals(maxRecords(), that.maxRecords()) + && Objects.equals(pageToken(), that.pageToken()) + && Objects.equals(filter, that.filter) + && fetchOption == that.fetchOption; + } + + @Override + public int hashCode() { + return Objects.hash(startHash, maxRecords(), pageToken(), filter, fetchOption); + } +} diff --git a/model/src/main/java/org/projectnessie/api/v2/params/DiffParams.java b/model/src/main/java/org/projectnessie/api/v2/params/DiffParams.java new file mode 100644 index 00000000000..9f0d1dbbc0a --- /dev/null +++ b/model/src/main/java/org/projectnessie/api/v2/params/DiffParams.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.api.v2.params; + +import java.util.Objects; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import javax.ws.rs.PathParam; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.projectnessie.model.Validation; + +public class DiffParams { + + public static final String HASH_OPTIONAL_REGEX = "(" + Validation.HASH_REGEX + ")?"; + + @NotNull + @Pattern(regexp = Validation.REF_NAME_PATH_REGEX, message = Validation.REF_NAME_PATH_MESSAGE) + @Parameter(ref = "refPathFromParameter") + @PathParam("from-ref") + private String fromRef; + + @NotNull + @Pattern(regexp = Validation.REF_NAME_PATH_REGEX, message = Validation.REF_NAME_PATH_MESSAGE) + @Parameter(ref = "refPathToParameter") + @PathParam("to-ref") + private String toRef; + + public DiffParams() {} + + @org.immutables.builder.Builder.Constructor + DiffParams(@NotNull String fromRef, @NotNull String toRef) { + this.fromRef = fromRef; + this.toRef = toRef; + } + + public String getFromRef() { + return fromRef; + } + + public String getToRef() { + return toRef; + } + + public static DiffParamsBuilder builder() { + return new DiffParamsBuilder(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof DiffParams)) { + return false; + } + DiffParams that = (DiffParams) o; + return Objects.equals(fromRef, that.fromRef) && Objects.equals(toRef, that.toRef); + } + + @Override + public int hashCode() { + return Objects.hash(fromRef, toRef); + } +} diff --git a/model/src/main/java/org/projectnessie/api/v2/params/EntriesParams.java b/model/src/main/java/org/projectnessie/api/v2/params/EntriesParams.java new file mode 100644 index 00000000000..c80f45b3a43 --- /dev/null +++ b/model/src/main/java/org/projectnessie/api/v2/params/EntriesParams.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.api.v2.params; + +import java.util.Objects; +import java.util.StringJoiner; +import javax.annotation.Nullable; +import javax.ws.rs.QueryParam; +import org.eclipse.microprofile.openapi.annotations.media.ExampleObject; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; + +/** + * The purpose of this class is to include optional parameters that can be passed to {@code + * HttpTreeApi#getEntries(String, EntriesParams)}. + * + *

For easier usage of this class, there is {@link EntriesParams#builder()}, which allows + * configuring/setting the different parameters. + */ +public class EntriesParams extends AbstractParams { + + @Nullable + @Parameter( + description = + "A Common Expression Language (CEL) expression. An intro to CEL can be found at https://github.com/google/cel-spec/blob/master/doc/intro.md.\n" + + "Usable variables within the expression are 'entry.namespace' (string) & 'entry.contentType' (string)", + examples = { + @ExampleObject(ref = "expr_by_namespace"), + @ExampleObject(ref = "expr_by_contentType"), + @ExampleObject(ref = "expr_by_namespace_and_contentType") + }) + @QueryParam("filter") + private String filter; + + public EntriesParams() {} + + @org.immutables.builder.Builder.Constructor + EntriesParams(@Nullable Integer maxRecords, @Nullable String pageToken, @Nullable String filter) { + super(maxRecords, pageToken); + this.filter = filter; + } + + public static EntriesParamsBuilder builder() { + return new EntriesParamsBuilder(); + } + + public static EntriesParams empty() { + return builder().build(); + } + + @Nullable + public String filter() { + return filter; + } + + @Override + public EntriesParams forNextPage(String pageToken) { + return new EntriesParams(maxRecords(), pageToken, filter); + } + + @Override + public String toString() { + return new StringJoiner(", ", EntriesParams.class.getSimpleName() + "[", "]") + .add("maxRecords=" + maxRecords()) + .add("pageToken='" + pageToken() + "'") + .add("filter='" + filter + "'") + .toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof EntriesParams)) { + return false; + } + EntriesParams that = (EntriesParams) o; + return Objects.equals(maxRecords(), that.maxRecords()) + && Objects.equals(pageToken(), that.pageToken()) + && Objects.equals(filter, that.filter); + } + + @Override + public int hashCode() { + return Objects.hash(maxRecords(), pageToken(), filter); + } +} diff --git a/model/src/main/java/org/projectnessie/api/v2/params/GetReferenceParams.java b/model/src/main/java/org/projectnessie/api/v2/params/GetReferenceParams.java new file mode 100644 index 00000000000..a17629b2db7 --- /dev/null +++ b/model/src/main/java/org/projectnessie/api/v2/params/GetReferenceParams.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.api.v2.params; + +import java.util.Objects; +import java.util.StringJoiner; +import javax.annotation.Nullable; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import javax.ws.rs.PathParam; +import javax.ws.rs.QueryParam; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.immutables.builder.Builder; +import org.projectnessie.api.params.FetchOption; +import org.projectnessie.model.Validation; + +public class GetReferenceParams { + + @Parameter(ref = "refPathParameter") + @PathParam("ref") + @NotNull + @Pattern(regexp = Validation.REF_NAME_REGEX, message = Validation.REF_NAME_MESSAGE) + private String refName; + + @Parameter( + description = + "Specify how much information to be returned. Will fetch additional metadata for references if set to 'ALL'.\n\n" + + "A returned Branch instance will have the following information:\n\n" + + "- numCommitsAhead (number of commits ahead of the default branch)\n\n" + + "- numCommitsBehind (number of commits behind the default branch)\n\n" + + "- commitMetaOfHEAD (the commit metadata of the HEAD commit)\n\n" + + "- commonAncestorHash (the hash of the common ancestor in relation to the default branch).\n\n" + + "- numTotalCommits (the total number of commits in this reference).\n\n" + + "A returned Tag instance will only contain the 'commitMetaOfHEAD' and 'numTotalCommits' fields.\n\n" + + "Note that computing & fetching additional metadata might be computationally expensive on the server-side, so this flag should be used with care.") + @QueryParam("fetch") + @Nullable + private FetchOption fetchOption; + + public GetReferenceParams() {} + + @Builder.Constructor + GetReferenceParams(@NotNull String refName, @Nullable FetchOption fetchOption) { + this.refName = refName; + this.fetchOption = fetchOption; + } + + @Nullable + public FetchOption fetchOption() { + return fetchOption; + } + + public String getRefName() { + return refName; + } + + public static GetReferenceParamsBuilder builder() { + return new GetReferenceParamsBuilder(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof GetReferenceParams)) { + return false; + } + GetReferenceParams that = (GetReferenceParams) o; + return fetchOption == that.fetchOption && Objects.equals(refName, that.refName); + } + + @Override + public int hashCode() { + return Objects.hash(refName, fetchOption); + } + + @Override + public String toString() { + return new StringJoiner(", ", GetReferenceParams.class.getSimpleName() + "[", "]") + .add("refName='" + refName + "'") + .add("fetchOption=" + fetchOption) + .toString(); + } +} diff --git a/model/src/main/java/org/projectnessie/api/v2/params/Merge.java b/model/src/main/java/org/projectnessie/api/v2/params/Merge.java new file mode 100644 index 00000000000..3951976c68e --- /dev/null +++ b/model/src/main/java/org/projectnessie/api/v2/params/Merge.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2020 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.api.v2.params; + +import static org.projectnessie.model.Validation.validateHash; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import javax.annotation.Nullable; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.media.SchemaProperty; +import org.immutables.value.Value; +import org.projectnessie.model.Validation; + +@Schema( + title = "Merge Operation", + properties = { + @SchemaProperty( + name = "message", + description = + "Optional commit message for this merge request\n" + + "\n" + + "If not set, the server will generate a commit message automatically using metadata from the \n" + + "merged commits."), + @SchemaProperty( + name = "fromHash", + pattern = Validation.HASH_REGEX, + description = + "The hash of the last commit to merge.\n" + + "\n" + + "This commit must be present in the history on 'fromRefName' before the first common parent with respect " + + "to the target branch."), + @SchemaProperty(name = "fromRefName", ref = "fromRefName"), + @SchemaProperty(name = "keyMergeModes", ref = "keyMergeModes"), + @SchemaProperty(name = "defaultKeyMergeMode", ref = "defaultKeyMergeMode"), + @SchemaProperty(name = "dryRun", ref = "dryRun"), + @SchemaProperty(name = "fetchAdditionalInfo", ref = "fetchAdditionalInfo"), + @SchemaProperty(name = "returnConflictAsResult", ref = "returnConflictAsResult"), + }) +@Value.Immutable +@JsonSerialize(as = ImmutableMerge.class) +@JsonDeserialize(as = ImmutableMerge.class) +public interface Merge extends BaseMergeTransplant { + + @Override + @Nullable + @Size(min = 1) + String getMessage(); + + @NotBlank + @Pattern(regexp = Validation.HASH_REGEX, message = Validation.HASH_MESSAGE) + String getFromHash(); + + /** + * Validation rule using {@link org.projectnessie.model.Validation#validateHash(String)} + * (String)}. + */ + @Value.Check + default void checkHash() { + String hash = getFromHash(); + if (hash != null) { + validateHash(hash); + } + } +} diff --git a/model/src/main/java/org/projectnessie/api/v2/params/ReferencesParams.java b/model/src/main/java/org/projectnessie/api/v2/params/ReferencesParams.java new file mode 100644 index 00000000000..2b2e24606ec --- /dev/null +++ b/model/src/main/java/org/projectnessie/api/v2/params/ReferencesParams.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.api.v2.params; + +import java.util.Objects; +import java.util.StringJoiner; +import javax.annotation.Nullable; +import javax.ws.rs.QueryParam; +import org.eclipse.microprofile.openapi.annotations.media.ExampleObject; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.projectnessie.api.params.FetchOption; + +/** + * The purpose of this class is to include optional parameters that can be passed to API methods + * dealing with reference retrieval. + * + *

For easier usage of this class, there is {@link ReferencesParams#builder()}, which allows + * configuring/setting the different parameters. + */ +public class ReferencesParams extends AbstractParams { + + @Parameter( + description = + "Specifies how much extra information is to be retrived from the server.\n\n" + + "If the fetch option is set to 'ALL' the following addition information will be returned for each " + + "Branch object in the output:\n\n" + + "- numCommitsAhead (number of commits ahead of the default branch)\n\n" + + "- numCommitsBehind (number of commits behind the default branch)\n\n" + + "- commitMetaOfHEAD (the commit metadata of the HEAD commit)\n\n" + + "- commonAncestorHash (the hash of the common ancestor in relation to the default branch).\n\n" + + "- numTotalCommits (the total number of commits from the root to the HEAD of the branch).\n\n" + + "The returned Tag instances will only contain the 'commitMetaOfHEAD' and 'numTotalCommits' fields.\n\n" + + "Note that computing & fetching additional metadata might be computationally expensive on the " + + "server-side, so this flag should be used with care.") + @QueryParam("fetch") + @Nullable + private FetchOption fetchOption; + + @Parameter( + description = + "A Common Expression Language (CEL) expression. An intro to CEL can be found at https://github.com/google/cel-spec/blob/master/doc/intro.md.\n" + + "Usable variables within the expression are:\n\n" + + "- ref (Reference) describes the reference, with fields name (String), hash (String), metadata (ReferenceMetadata)\n\n" + + "- metadata (ReferenceMetadata) shortcut to ref.metadata, never null, but possibly empty\n\n" + + "- commit (CommitMeta) - shortcut to ref.metadata.commitMetaOfHEAD, never null, but possibly empty\n\n" + + "- refType (String) - the reference type, either BRANCH or TAG\n\n" + + "Note that the expression can only test attributes metadata and commit, if 'fetchOption' is set to 'ALL'.", + examples = { + @ExampleObject(ref = "expr_by_refType"), + @ExampleObject(ref = "expr_by_ref_name"), + @ExampleObject(ref = "expr_by_ref_commit") + }) + @QueryParam("filter") + @Nullable + private String filter; + + public ReferencesParams() {} + + @org.immutables.builder.Builder.Constructor + ReferencesParams( + @Nullable Integer maxRecords, + @Nullable String pageToken, + @Nullable FetchOption fetchOption, + @Nullable String filter) { + super(maxRecords, pageToken); + this.fetchOption = fetchOption; + this.filter = filter; + } + + @Nullable + public FetchOption fetchOption() { + return fetchOption; + } + + @Nullable + public String filter() { + return filter; + } + + public static ReferencesParamsBuilder builder() { + return new ReferencesParamsBuilder(); + } + + public static ReferencesParams empty() { + return builder().build(); + } + + @Override + public ReferencesParams forNextPage(String pageToken) { + return new ReferencesParams(maxRecords(), pageToken, fetchOption, filter); + } + + @Override + public String toString() { + return new StringJoiner(", ", ReferencesParams.class.getSimpleName() + "[", "]") + .add("maxRecords=" + maxRecords()) + .add("pageToken='" + pageToken() + "'") + .add("fetchOption=" + fetchOption) + .add("filter=" + filter) + .toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ReferencesParams)) { + return false; + } + ReferencesParams that = (ReferencesParams) o; + return Objects.equals(maxRecords(), that.maxRecords()) + && Objects.equals(pageToken(), that.pageToken()) + && fetchOption == that.fetchOption + && Objects.equals(filter, that.filter); + } + + @Override + public int hashCode() { + return Objects.hash(maxRecords(), pageToken(), fetchOption, filter); + } +} diff --git a/model/src/main/java/org/projectnessie/api/v2/params/Transplant.java b/model/src/main/java/org/projectnessie/api/v2/params/Transplant.java new file mode 100644 index 00000000000..afa9e01f969 --- /dev/null +++ b/model/src/main/java/org/projectnessie/api/v2/params/Transplant.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2020 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.api.v2.params; + +import static org.projectnessie.model.Validation.validateHash; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import java.util.List; +import javax.annotation.Nullable; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.media.SchemaProperty; +import org.immutables.value.Value; + +@Schema( + type = SchemaType.OBJECT, + title = "Transplant", + // Smallrye does neither support JsonFormat nor javax.validation.constraints.Pattern :( + properties = { + @SchemaProperty( + name = "message", + description = "Commit message for this transplant request."), + @SchemaProperty( + name = "hashesToTransplant", + uniqueItems = true, + description = + "Lists the hashes of commits that should be transplanted into the target branch."), + @SchemaProperty(name = "fromRefName", ref = "fromRefName"), + @SchemaProperty(name = "keyMergeModes", ref = "keyMergeModes"), + @SchemaProperty(name = "defaultKeyMergeMode", ref = "defaultKeyMergeMode"), + @SchemaProperty(name = "dryRun", ref = "dryRun"), + @SchemaProperty(name = "fetchAdditionalInfo", ref = "fetchAdditionalInfo"), + @SchemaProperty(name = "returnConflictAsResult", ref = "returnConflictAsResult"), + }) +@Value.Immutable +@JsonSerialize(as = ImmutableTransplant.class) +@JsonDeserialize(as = ImmutableTransplant.class) +public interface Transplant extends BaseMergeTransplant { + + @Override + @Nullable + @Size(min = 1) + String getMessage(); + + @NotNull + @Size(min = 1) + List getHashesToTransplant(); + + /** + * Validation rule using {@link org.projectnessie.model.Validation#validateHash(String)} + * (String)}. + */ + @Value.Check + default void checkHashes() { + List hashes = getHashesToTransplant(); + if (hashes != null) { + for (String hash : hashes) { + validateHash(hash); + } + } + } +} diff --git a/model/src/main/java/org/projectnessie/model/CommitResponse.java b/model/src/main/java/org/projectnessie/model/CommitResponse.java new file mode 100644 index 00000000000..199f8ed69c0 --- /dev/null +++ b/model/src/main/java/org/projectnessie/model/CommitResponse.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.model; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import javax.validation.constraints.NotNull; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.immutables.value.Value; + +@Schema(type = SchemaType.OBJECT, title = "Commit Response") +@Value.Immutable +@JsonSerialize(as = ImmutableCommitResponse.class) +@JsonDeserialize(as = ImmutableCommitResponse.class) +public interface CommitResponse { + + static ImmutableCommitResponse.Builder builder() { + return ImmutableCommitResponse.builder(); + } + + /** + * Returns updated information about the branch where the commit was applied. + * + *

Specifically, the hash of the {@link Branch} will be the hash of the applied commit. + */ + @NotNull + Branch getTargetBranch(); +} diff --git a/model/src/main/java/org/projectnessie/model/ContentResponse.java b/model/src/main/java/org/projectnessie/model/ContentResponse.java new file mode 100644 index 00000000000..0f418c69896 --- /dev/null +++ b/model/src/main/java/org/projectnessie/model/ContentResponse.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2020 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.model; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import javax.validation.constraints.NotNull; +import org.immutables.value.Value; + +@Value.Immutable +@JsonSerialize(as = ImmutableContentResponse.class) +@JsonDeserialize(as = ImmutableContentResponse.class) +public interface ContentResponse { + + static ImmutableContentResponse.Builder builder() { + return ImmutableContentResponse.builder(); + } + + @NotNull + Content getContent(); +} diff --git a/model/src/main/java/org/projectnessie/model/RefLogResponse.java b/model/src/main/java/org/projectnessie/model/RefLogResponse.java index e6b4ec01beb..1cc8ee3c619 100644 --- a/model/src/main/java/org/projectnessie/model/RefLogResponse.java +++ b/model/src/main/java/org/projectnessie/model/RefLogResponse.java @@ -24,16 +24,17 @@ import org.immutables.value.Value; @Value.Immutable -@Schema(type = SchemaType.OBJECT, title = "RefLogResponse") +@Schema(type = SchemaType.OBJECT, title = "RefLogResponse", deprecated = true, hidden = true) @JsonSerialize(as = ImmutableRefLogResponse.class) @JsonDeserialize(as = ImmutableRefLogResponse.class) +@Deprecated // Not supported since API v2 public interface RefLogResponse extends PaginatedResponse { @NotNull List getLogEntries(); @Value.Immutable - @Schema(type = SchemaType.OBJECT, title = "RefLogResponseEntry") + @Schema(type = SchemaType.OBJECT, title = "RefLogResponseEntry", deprecated = true, hidden = true) @JsonSerialize(as = ImmutableRefLogResponseEntry.class) @JsonDeserialize(as = ImmutableRefLogResponseEntry.class) interface RefLogResponseEntry { diff --git a/model/src/main/java/org/projectnessie/model/Reference.java b/model/src/main/java/org/projectnessie/model/Reference.java index 11530c6cab5..8952d62b6c2 100644 --- a/model/src/main/java/org/projectnessie/model/Reference.java +++ b/model/src/main/java/org/projectnessie/model/Reference.java @@ -89,6 +89,14 @@ default void checkHash() { @Value.Redacted ReferenceType getType(); + static String toPathString(String name, String hash) { + return Util.toPathStringRef(name, hash); + } + + static Reference fromPathString(String value, ReferenceType parseAsType) { + return Util.fromPathStringRef(value, parseAsType); + } + /** The reference type as an enum. */ @Schema(enumeration = {"branch", "tag"}) // Required to have lower-case values in OpenAPI enum ReferenceType { diff --git a/model/src/main/java/org/projectnessie/model/SingleReferenceResponse.java b/model/src/main/java/org/projectnessie/model/SingleReferenceResponse.java new file mode 100644 index 00000000000..c86ab07ed94 --- /dev/null +++ b/model/src/main/java/org/projectnessie/model/SingleReferenceResponse.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2020 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.model; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import javax.validation.constraints.NotNull; +import org.immutables.value.Value; + +@Value.Immutable +@JsonSerialize(as = ImmutableSingleReferenceResponse.class) +@JsonDeserialize(as = ImmutableSingleReferenceResponse.class) +public interface SingleReferenceResponse { + + static ImmutableSingleReferenceResponse.Builder builder() { + return ImmutableSingleReferenceResponse.builder(); + } + + @NotNull + Reference getReference(); +} diff --git a/model/src/main/java/org/projectnessie/model/Util.java b/model/src/main/java/org/projectnessie/model/Util.java index 5c27247095f..160623e3717 100644 --- a/model/src/main/java/org/projectnessie/model/Util.java +++ b/model/src/main/java/org/projectnessie/model/Util.java @@ -25,6 +25,7 @@ import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; +import javax.annotation.Nonnull; import org.projectnessie.model.types.ContentTypes; final class Util { @@ -61,6 +62,51 @@ public static String toPathString(List elements) { .collect(Collectors.joining(".")); } + public static String toPathStringRef(String name, String hash) { + StringBuilder builder = new StringBuilder(); + if (name != null) { + builder.append(name); + } + + if (hash != null) { + builder.append("@"); + builder.append(hash); + } + return builder.toString(); + } + + public static Reference fromPathStringRef( + @Nonnull String value, @Nonnull Reference.ReferenceType namedRefType) { + String name = null; + String hash = null; + int hashIdx = value.indexOf("@"); + + if (hashIdx > 0) { + name = value.substring(0, hashIdx); + } + + if (hashIdx < 0) { + name = value; + } + + if (hashIdx >= 0) { + hash = value.substring(hashIdx + 1); + } + + if (name == null) { + return Detached.of(hash); + } else { + switch (namedRefType) { + case TAG: + return Tag.of(name, hash); + case BRANCH: + return Branch.of(name, hash); + default: + throw new IllegalArgumentException("Unsupported reference type: " + namedRefType); + } + } + } + static final class ContentTypeDeserializer extends JsonDeserializer { @Override public Content.Type deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { diff --git a/model/src/main/java/org/projectnessie/model/Validation.java b/model/src/main/java/org/projectnessie/model/Validation.java index 6a3b2010f1a..27dbf9cff32 100644 --- a/model/src/main/java/org/projectnessie/model/Validation.java +++ b/model/src/main/java/org/projectnessie/model/Validation.java @@ -35,17 +35,25 @@ public final class Validation { public static final String REF_NAME_REGEX = "^" + REF_NAME_RAW_REGEX + "$"; public static final String REF_NAME_OR_HASH_REGEX = "^((" + HASH_RAW_REGEX + ")|(" + REF_NAME_RAW_REGEX + "))$"; + public static final String REF_NAME_PATH_REGEX = + "^" + REF_NAME_RAW_REGEX + "(@" + HASH_RAW_REGEX + ")?$"; public static final Pattern HASH_PATTERN = Pattern.compile(HASH_REGEX); public static final Pattern REF_NAME_PATTERN = Pattern.compile(REF_NAME_REGEX); public static final Pattern REF_NAME_OR_HASH_PATTERN = Pattern.compile(REF_NAME_OR_HASH_REGEX); - private static final String HASH_RULE = "consist of the hex representation of 4-32 bytes"; + public static final String HASH_RULE = "consist of the hex representation of 4-32 bytes"; private static final String REF_RULE = "start with a letter, followed by letters, digits, one of the ./_- characters, " + "not end with a slash or dot, not contain '..'"; public static final String HASH_MESSAGE = "Hash must " + HASH_RULE; + public static final String REF_NAME_PATH_MESSAGE = + "Reference name must " + + REF_RULE + + ", optionally followed " + + "by ~ and a commit hash, which must " + + HASH_RULE; public static final String REF_NAME_MESSAGE = "Reference name must " + REF_RULE; public static final String REF_NAME_OR_HASH_MESSAGE = "Reference must be either a reference name or hash, " + REF_RULE + " or " + HASH_RULE; diff --git a/model/src/main/resources/META-INF/openapi.yaml b/model/src/main/resources/META-INF/openapi.yaml index 578c1657b6d..e382c86d953 100644 --- a/model/src/main/resources/META-INF/openapi.yaml +++ b/model/src/main/resources/META-INF/openapi.yaml @@ -1,5 +1,5 @@ # -# Copyright (C) 2020 Dremio +# Copyright (C) 2022 Dremio # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -28,10 +28,181 @@ info: name: Apache 2.0 url: http://www.apache.org/licenses/LICENSE-2.0.html +tags: + - name: v1 + description: End points from the legacy v1 API that are still supported in v2 API + - name: v2 + description: End point from the Nessie v2 API + paths: {} # inferred from java annotations components: + schemas: + keyMergeModes: + type: array + items: + $ref: '#/components/schemas/MergeKeyBehavior' + description: | + Specific merge behaviour requests by content key. + + The default is set by the `defaultKeyMergeMode` parameter. + defaultKeyMergeMode: + type: + $ref: '#/components/schemas/MergeBehavior' + description: |- + The default merge mode. If not set `NORMAL` is assumed. + + This settings applies to key that are not explicitly mentioned in the `keyMergeModes` property. + dryRun: + type: boolean + description: When set to 'true' instructs the server to validate the request + but to avoid committing any changes. + fetchAdditionalInfo: + type: boolean + description: Whether to provide optional response data. + returnConflictAsResult: + type: boolean + description: When set to 'true' instructs the server to produce normal (non-error) + responses in case a conflict is detected and report conflict details in + the response payload. + fromRefName: + description: The name of the reference that contains the 'source' commits + for the requested merge or transplant operation. + pattern: "^[A-Za-z](((?![.][.])[A-Za-z0-9./_-])*[A-Za-z0-9_-])?$" + type: string + + parameters: + refPathParameter: &refPathParam + name: "ref" + in: "path" + required: true + description: | + A reference to a particular version of the contents tree (a point in history). + A reference can be specification in these forms: + - \- (literal minus character) - identifies the HEAD of the default branch + - name - identifies the HEAD commit of a branch or tag + - name@hash - identifies the 'hash' commit on a branch or tag + - @hash - identifies the 'hash' commit in an unspecified branch or tag + + If both 'name' and 'hash' are given, 'hash' must be reachable from the current HEAD of the branch or tag. + If 'name' is omitted, the reference will be of type 'DETACHED' (referencing a specific commit hash without + claiming its reachability from any live HEAD). Using references of the last form may have authorization + implications when compared to an equivalent reference of the former forms. + + The 'name@hash' form always refers to the exact commit on a specific branch or tag. This is the most complete + form of a reference. Other forms omit some of the details and require those gaps to be filled by the server at + runtime. Although these forms may be convenient to a human being, they may resolve differently at different + times depending on the state of the system. Using the full 'name@hash' form is recommended to avoid ambiguity. + schema: + pattern: "([^/]+|[^@]+@[^@/]*?)" + type: string + examples: + ref: + $ref: '#/components/examples/ref' + refWithHash: + $ref: '#/components/examples/refWithHash' + refDefault: + $ref: '#/components/examples/refDefault' + refDetached: + $ref: '#/components/examples/refDetached' + + checkedRefParameter: + name: "ref" + in: "path" + required: true + description: | + Specifies a named branch or tag reference. + A named reference can be specification in these forms: + - \- (literal minus character) - identifies the default branch + - name - identifies a branch or tag without a concrete HEAD 'hash' value + - name@hash - identifies the 'hash' commit on a branch or tag + + If both 'name' and 'hash' are given, 'hash' must be the current HEAD of the branch or tag. It will be used to + validate that at execution time the reference points to the same hash that the caller expected when the + operation arguments were constructed. + + Not specifying the 'hash' value relaxes server-side checks and can lead to unexpected side effects if multiple + changes to the same reference are executed concurrently. It is recommended to always specify the 'hash' value + when assigning or deleting a reference. + + Note that deleting the default branch may not be allowed. + schema: + pattern: "([^/]+|[^@]+@[^@/]*?)" + type: string + examples: + ref: + $ref: '#/components/examples/ref' + refWithHash: + $ref: '#/components/examples/refWithHash' + refDefault: + $ref: '#/components/examples/refDefault' + + branchPathParameter: + name: "branch" + in: "path" + required: true + description: | + A reference to a particular version of the contents tree (a point in history) on a branch. + A reference can be specification in these forms: + - \- (literal minus character) - identifies the HEAD of the default branch + - name - identifies the HEAD commit of a branch or tag + - name@hash - identifies the 'hash' commit on a branch or tag + + If both 'name' and 'hash' are given, 'hash' must be reachable from the current HEAD of the branch or tag. + In this case 'hash' indicates the state of contents that should be used for validating incoming changes + (commits / merges / transplants). + + Note that using the simple 'name' form will effectively disable content conflict checks and is + generally discouraged. + + The 'name@hash' form always refers to the exact commit on a specific branch. This is the most complete + form of a reference. Other forms omit some of the details and require those gaps to be filled by the server at + runtime. Although these forms may be convenient to a human-being, they may resolve differently at different + times depending on the state of the system. Using the full 'name@hash' form is recommended to avoid ambiguity. + schema: + pattern: "([^/]+|[^@]+@[^@/]*?)" + type: string + examples: + ref: + $ref: '#/components/examples/ref' + refWithHash: + $ref: '#/components/examples/refWithHash' + refDefault: + $ref: '#/components/examples/refDefault' + + refPathFromParameter: + <<: *refPathParam + name: from-ref + + refPathToParameter: + <<: *refPathParam + name: to-ref + description: Same reference spec as in the 'from-ref' parameter but identifying the other tree for comparison. + + keyPathParameter: &keyPathParam + name: "key" + in: "path" + required: true + description: | + The key to a content object. Key components (namespaces) are separated by the dot ('.') character. + Dot ('.') characters that are not Nessie namespace separators must be encoded as the 'group separator' + ASCII character (0x1D). + schema: + type: string + examples: + ContentKeyGet: + $ref: '#/components/examples/ContentKeyGet' + + keyQueryParameter: + <<: *keyPathParam + in: "query" + + examples: + nessieConfig: + value: + defaultBranch: main + maxSupportedApiVersion: 2 namespace: value: "a.b.c" @@ -39,6 +210,15 @@ components: ref: value: "main" + refWithHash: + value: "main@2e1cfa82b035c26cbbbdae632cea070514eb8b773f616aaeaf668e2f0be8f10d" + + refDefault: + value: "-" + + refDetached: + value: "@2e1cfa82b035c26cbbbdae632cea070514eb8b773f616aaeaf668e2f0be8f10d" + referenceType: value: "branch" @@ -174,6 +354,13 @@ components: - example - key type: ICEBERG_TABLE + prefixes: + - elements: + - example + - a + - elements: + - example + - b types: value: @@ -183,7 +370,6 @@ components: value: fromHash: "abcdef4242424242424242424242beef00dead42112233445566778899001122" fromRefName: "source-ref-name" - keepIndividualCommits: false defaultKeyMergeMode: "NORMAL" keyMergeModes: - key: @@ -200,7 +386,6 @@ components: hashesToTransplant: - "abcdef4242424242424242424242beef00dead42112233445566778899001122" fromRefName: "source-ref-name" - keepIndividualCommits: false defaultKeyMergeMode: "NORMAL" keyMergeModes: - key: @@ -514,8 +699,8 @@ components: diffs: - key: elements: - - example - - key + - example + - key from: type: ICEBERG_TABLE id: "f350b391-f492-41eb-9959-730a8c49f01e" diff --git a/model/src/test/java/org/projectnessie/api/v2/params/TestParamObjectsSerialization.java b/model/src/test/java/org/projectnessie/api/v2/params/TestParamObjectsSerialization.java new file mode 100644 index 00000000000..e65ac34c4a1 --- /dev/null +++ b/model/src/test/java/org/projectnessie/api/v2/params/TestParamObjectsSerialization.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.api.v2.params; + +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.projectnessie.model.ContentKey; +import org.projectnessie.model.MergeBehavior; +import org.projectnessie.model.MergeKeyBehavior; +import org.projectnessie.model.TestModelObjectsSerialization; + +/** + * This test merely checks the JSON serialization/deserialization of API parameter classes, with an + * intention to identify breaking cases whenever jackson version varies. + */ +@Execution(ExecutionMode.CONCURRENT) +public class TestParamObjectsSerialization extends TestModelObjectsSerialization { + + @SuppressWarnings("unused") // called by JUnit framework based on annotations in superclass + static List goodCases() { + final String branchName = "testBranch"; + + return Arrays.asList( + new Case( + ImmutableTransplant.builder() + .addHashesToTransplant(HASH) + .fromRefName(branchName) + .build(), + Transplant.class, + Json.from("fromRefName", "testBranch").addArr("hashesToTransplant", HASH)), + new Case( + ImmutableTransplant.builder() + .message("test-msg") + .addHashesToTransplant(HASH) + .fromRefName(branchName) + .build(), + Transplant.class, + Json.from("fromRefName", "testBranch") + .add("message", "test-msg") + .addArr("hashesToTransplant", HASH)), + new Case( + ImmutableMerge.builder().fromHash(HASH).fromRefName(branchName).build(), + Merge.class, + Json.from("fromRefName", "testBranch").add("fromHash", HASH)), + new Case( + ImmutableMerge.builder() + .message("test-msg") + .fromHash(HASH) + .fromRefName(branchName) + .build(), + Merge.class, + Json.from("fromRefName", "testBranch") + .add("message", "test-msg") + .add("fromHash", HASH)), + new Case( + ImmutableMerge.builder() + .fromHash(HASH) + .fromRefName(branchName) + .defaultKeyMergeMode(MergeBehavior.FORCE) + .isFetchAdditionalInfo(true) + .addKeyMergeModes( + MergeKeyBehavior.of(ContentKey.of("merge", "me"), MergeBehavior.NORMAL), + MergeKeyBehavior.of(ContentKey.of("ignore", "this"), MergeBehavior.DROP)) + .build(), + Merge.class, + Json.from("fromRefName", "testBranch") + .addArrNoQuotes( + "keyMergeModes", + Json.noQuotes("key", Json.arr("elements", "merge", "me")) + .add("mergeBehavior", "NORMAL"), + Json.noQuotes("key", Json.arr("elements", "ignore", "this")) + .add("mergeBehavior", "DROP")) + .add("defaultKeyMergeMode", "FORCE") + .add("fromHash", HASH) + .addNoQuotes("isFetchAdditionalInfo", "true")), + new Case( + ImmutableMerge.builder() + .fromHash(HASH) + .fromRefName(branchName) + .isDryRun(false) + .addKeyMergeModes( + MergeKeyBehavior.of(ContentKey.of("merge", "me"), MergeBehavior.NORMAL), + MergeKeyBehavior.of(ContentKey.of("ignore", "this"), MergeBehavior.DROP)) + .build(), + Merge.class, + Json.from("fromRefName", "testBranch") + .addArrNoQuotes( + "keyMergeModes", + Json.noQuotes("key", Json.arr("elements", "merge", "me")) + .add("mergeBehavior", "NORMAL"), + Json.noQuotes("key", Json.arr("elements", "ignore", "this")) + .add("mergeBehavior", "DROP")) + .add("fromHash", HASH) + .addNoQuotes("isDryRun", "false"))); + } + + @SuppressWarnings("unused") // called by JUnit framework based on annotations in superclass + static List negativeCases() { + return Arrays.asList( + new Case( + Transplant.class, + Json.arr("hashesToTransplant", "invalidhash").addNoQuotes("fromRefName", "null")), + + // Invalid hash + new Case( + Transplant.class, + Json.arr("hashesToTransplant", "invalidhash").add("fromRefName", "testBranch"))); + } +} diff --git a/servers/jax-rs-testextension/src/main/java/org/projectnessie/jaxrs/ext/NessieJaxRsExtension.java b/servers/jax-rs-testextension/src/main/java/org/projectnessie/jaxrs/ext/NessieJaxRsExtension.java index 652491484de..00b3f11ed4f 100644 --- a/servers/jax-rs-testextension/src/main/java/org/projectnessie/jaxrs/ext/NessieJaxRsExtension.java +++ b/servers/jax-rs-testextension/src/main/java/org/projectnessie/jaxrs/ext/NessieJaxRsExtension.java @@ -62,6 +62,8 @@ import org.projectnessie.services.rest.RestNamespaceResource; import org.projectnessie.services.rest.RestRefLogResource; import org.projectnessie.services.rest.RestTreeResource; +import org.projectnessie.services.rest.RestV2ConfigResource; +import org.projectnessie.services.rest.RestV2TreeResource; import org.projectnessie.services.rest.ValidationExceptionMapper; import org.projectnessie.versioned.PersistVersionStoreExtension; import org.projectnessie.versioned.persist.adapter.DatabaseAdapter; @@ -217,6 +219,8 @@ public EnvHolder(Supplier databaseAdapterSupplier) throws Excep @Override protected Application configure() { ResourceConfig config = new ResourceConfig(); + config.register(RestV2ConfigResource.class); + config.register(RestV2TreeResource.class); config.register(RestConfigResource.class); config.register(RestTreeResource.class); config.register(RestContentResource.class); diff --git a/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/AbstractRestEntries.java b/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/AbstractRestEntries.java index b5158de616b..62ad70d824a 100644 --- a/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/AbstractRestEntries.java +++ b/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/AbstractRestEntries.java @@ -27,6 +27,8 @@ import java.util.stream.Collectors; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; +import org.projectnessie.client.ext.NessieApiVersion; +import org.projectnessie.client.ext.NessieApiVersions; import org.projectnessie.error.BaseNessieClientServerException; import org.projectnessie.error.NessieNamespaceAlreadyExistsException; import org.projectnessie.error.NessieNamespaceNotEmptyException; @@ -273,6 +275,7 @@ public void filterEntriesByNamespace(ReferenceMode refMode) @ParameterizedTest @EnumSource(ReferenceMode.class) + @NessieApiVersions(versions = NessieApiVersion.V1) public void filterEntriesByNamespaceAndPrefixDepth(ReferenceMode refMode) throws BaseNessieClientServerException { Branch branch = createBranch("filterEntriesByNamespaceAndPrefixDepth"); diff --git a/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/AbstractRestInvalid.java b/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/AbstractRestInvalid.java index ffe109070a5..5deea48d8cf 100644 --- a/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/AbstractRestInvalid.java +++ b/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/AbstractRestInvalid.java @@ -18,7 +18,7 @@ import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; -import static org.projectnessie.model.Validation.HASH_MESSAGE; +import static org.projectnessie.model.Validation.HASH_RULE; import static org.projectnessie.model.Validation.REF_NAME_MESSAGE; import org.junit.jupiter.params.ParameterizedTest; @@ -192,7 +192,7 @@ public void invalidHashes(String invalidHashIn, String validHash) { .commit()) .isInstanceOf(NessieBadRequestException.class) .hasMessageContaining("Bad Request (HTTP/400):") - .hasMessageContaining(".expectedHash: " + HASH_MESSAGE) + .hasMessageContaining(HASH_RULE) .hasMessageContaining(opsCountMsg), () -> assertThatThrownBy( @@ -204,7 +204,7 @@ public void invalidHashes(String invalidHashIn, String validHash) { .delete()) .isInstanceOf(NessieBadRequestException.class) .hasMessageContaining("Bad Request (HTTP/400):") - .hasMessageContaining(".expectedHash: " + HASH_MESSAGE), + .hasMessageContaining(HASH_RULE), () -> assertThatThrownBy( () -> @@ -216,7 +216,7 @@ public void invalidHashes(String invalidHashIn, String validHash) { .assign()) .isInstanceOf(NessieBadRequestException.class) .hasMessageContaining("Bad Request (HTTP/400):") - .hasMessageContaining(".expectedHash: " + HASH_MESSAGE), + .hasMessageContaining(HASH_RULE), () -> assertThatThrownBy( () -> @@ -228,13 +228,13 @@ public void invalidHashes(String invalidHashIn, String validHash) { .merge()) .isInstanceOf(NessieBadRequestException.class) .hasMessageContaining("Bad Request (HTTP/400):") - .hasMessageContaining(".expectedHash: " + HASH_MESSAGE), + .hasMessageContaining(HASH_RULE), () -> assertThatThrownBy( () -> getApi().deleteTag().tagName(validBranchName).hash(invalidHash).delete()) .isInstanceOf(NessieBadRequestException.class) .hasMessageContaining("Bad Request (HTTP/400):") - .hasMessageContaining(".expectedHash: " + HASH_MESSAGE), + .hasMessageContaining(HASH_RULE), () -> assertThatThrownBy( () -> @@ -249,7 +249,7 @@ public void invalidHashes(String invalidHashIn, String validHash) { .transplant()) .isInstanceOf(NessieBadRequestException.class) .hasMessageContaining("Bad Request (HTTP/400):") - .hasMessageContaining(".expectedHash: " + HASH_MESSAGE), + .hasMessageContaining(HASH_RULE), () -> assertThatThrownBy(() -> getApi().getContent().refName(invalidHash).get()) .isInstanceOf(NessieBadRequestException.class) @@ -265,7 +265,7 @@ public void invalidHashes(String invalidHashIn, String validHash) { .hasMessageContaining("Bad Request (HTTP/400):") .hasMessageContaining( ".request.requestedKeys: size must be between 1 and 2147483647") - .hasMessageContaining(".hashOnRef: " + HASH_MESSAGE), + .hasMessageContaining(HASH_RULE), () -> assertThatThrownBy( () -> @@ -277,7 +277,7 @@ public void invalidHashes(String invalidHashIn, String validHash) { .get()) .isInstanceOf(NessieBadRequestException.class) .hasMessageContaining("Bad Request (HTTP/400):") - .hasMessageContaining(".hashOnRef: " + HASH_MESSAGE), + .hasMessageContaining(HASH_RULE), () -> assertThatThrownBy( () -> @@ -288,7 +288,7 @@ public void invalidHashes(String invalidHashIn, String validHash) { .get()) .isInstanceOf(NessieBadRequestException.class) .hasMessageContaining("Bad Request (HTTP/400):") - .hasMessageContaining(".params.startHash: " + HASH_MESSAGE), + .hasMessageContaining(HASH_RULE), () -> assertThatThrownBy( () -> @@ -299,14 +299,14 @@ public void invalidHashes(String invalidHashIn, String validHash) { .get()) .isInstanceOf(NessieBadRequestException.class) .hasMessageContaining("Bad Request (HTTP/400):") - .hasMessageContaining(".params.endHash: " + HASH_MESSAGE), + .hasMessageContaining(HASH_RULE), () -> assertThatThrownBy( () -> getApi().getEntries().refName(validBranchName).hashOnRef(invalidHash).get()) .isInstanceOf(NessieBadRequestException.class) .hasMessageContaining("Bad Request (HTTP/400):") - .hasMessageContaining(".params.hashOnRef: " + HASH_MESSAGE)); + .hasMessageContaining(HASH_RULE)); } @ParameterizedTest diff --git a/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/AbstractRestMergeTransplant.java b/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/AbstractRestMergeTransplant.java index bb4a29dad28..ca71b2b922f 100644 --- a/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/AbstractRestMergeTransplant.java +++ b/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/AbstractRestMergeTransplant.java @@ -24,10 +24,16 @@ import com.google.common.collect.ImmutableList; import org.assertj.core.api.InstanceOfAssertFactories; +import org.assertj.core.configuration.ConfigurationProvider; +import org.assertj.core.configuration.PreferredAssumptionException; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.EnumSource.Mode; import org.junit.jupiter.params.provider.ValueSource; +import org.projectnessie.client.api.NessieApiV2; +import org.projectnessie.client.ext.NessieApiVersion; +import org.projectnessie.client.ext.NessieApiVersions; import org.projectnessie.error.BaseNessieClientServerException; import org.projectnessie.error.NessieConflictException; import org.projectnessie.error.NessieNotFoundException; @@ -50,6 +56,12 @@ /** See {@link AbstractTestRest} for details about and reason for the inheritance model. */ public abstract class AbstractRestMergeTransplant extends AbstractRestInvalid { + static { + ConfigurationProvider.CONFIGURATION_PROVIDER + .configuration() + .setPreferredAssumptionException(PreferredAssumptionException.JUNIT5); + } + @ParameterizedTest @ValueSource(booleans = {true, false}) public void transplantKeepCommits(boolean withDetachedCommit) @@ -59,6 +71,7 @@ public void transplantKeepCommits(boolean withDetachedCommit) @ParameterizedTest @ValueSource(booleans = {true, false}) + @NessieApiVersions(versions = NessieApiVersion.V1) // API V2 does not allow squashed transplants public void transplantSquashed(boolean withDetachedCommit) throws BaseNessieClientServerException { testTransplant(withDetachedCommit, false); @@ -66,6 +79,11 @@ public void transplantSquashed(boolean withDetachedCommit) private void testTransplant(boolean withDetachedCommit, boolean keepIndividualCommits) throws BaseNessieClientServerException { + // API v2 does not allow squashing transplants + if (getApi() instanceof NessieApiV2) { + Assumptions.assumeTrue(keepIndividualCommits); + } + mergeTransplant( false, keepIndividualCommits, @@ -82,6 +100,7 @@ private void testTransplant(boolean withDetachedCommit, boolean keepIndividualCo @ParameterizedTest @EnumSource(names = {"UNCHANGED", "DETACHED"}) // hash is required + @NessieApiVersions(versions = NessieApiVersion.V1) // API V2 does not allow unsquashed merges public void mergeKeepCommits(ReferenceMode refMode) throws BaseNessieClientServerException { testMerge(refMode, true); } @@ -94,6 +113,11 @@ public void mergeSquashed(ReferenceMode refMode) throws BaseNessieClientServerEx private void testMerge(ReferenceMode refMode, boolean keepIndividualCommits) throws BaseNessieClientServerException { + // API v2 always squashed merges + if (getApi() instanceof NessieApiV2) { + Assumptions.assumeFalse(keepIndividualCommits); + } + mergeTransplant( !keepIndividualCommits, keepIndividualCommits, @@ -198,7 +222,6 @@ private void mergeTransplant( if (keepIndividualCommits) { assertThat( log.getLogEntries().stream().map(LogEntry::getCommitMeta).map(CommitMeta::getMessage)) - .hasSize(3) .containsExactly("test-branch2", "test-branch1", "test-main"); } else { assertThat( @@ -452,19 +475,19 @@ public void mergeWithNamespaces(ReferenceMode refMode) throws BaseNessieClientSe .mergeRefIntoBranch() .branch(base) .fromRef(refMode.transform(committed2)) - .keepIndividualCommits(true) + .keepIndividualCommits(false) .merge(); LogResponse log = getApi().getCommitLog().refName(base.getName()).untilHash(base.getHash()).get(); assertThat( - log.getLogEntries().stream().map(LogEntry::getCommitMeta).map(CommitMeta::getMessage)) - .containsExactly( - "test-branch2", - "test-branch1", - "create namespace a.b.c", - "test-main", - "create namespace a.b.c"); + log.getLogEntries().stream() + .map(LogEntry::getCommitMeta) + .map(CommitMeta::getMessage) + .findFirst()) + .isPresent() + .hasValueSatisfying(v -> assertThat(v).contains("test-branch1")) + .hasValueSatisfying(v -> assertThat(v).contains("test-branch2")); assertThat( getApi().getEntries().refName(base.getName()).get().getEntries().stream() diff --git a/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/AbstractRestMisc.java b/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/AbstractRestMisc.java index bec0c528d39..a8674e47851 100644 --- a/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/AbstractRestMisc.java +++ b/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/AbstractRestMisc.java @@ -34,7 +34,7 @@ public abstract class AbstractRestMisc extends AbstractRestMergeTransplant { @Test public void testSupportedApiVersions() { - assertThat(getApi().getConfig().getMaxSupportedApiVersion()).isEqualTo(1); + assertThat(getApi().getConfig().getMaxSupportedApiVersion()).isEqualTo(2); } @Test diff --git a/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/AbstractRestNamespace.java b/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/AbstractRestNamespace.java index 8497153b3e8..d6a779b3cf2 100644 --- a/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/AbstractRestNamespace.java +++ b/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/AbstractRestNamespace.java @@ -201,10 +201,30 @@ public void testNamespaceDeletion() throws BaseNessieClientServerException { .map(c -> Entry.builder().type(c.type).name(c.operation.getKey()).build()) .collect(Collectors.toList()); + CommitMultipleOperationsBuilder commit2 = + getApi() + .commitMultipleOperations() + .branch(branch) + .commitMeta(CommitMeta.fromMessage("create namespaces")); + entries.stream() + .map(e -> e.getName().getNamespace()) + .distinct() + .forEach( + ns -> { + commit2.operation(Put.of(ContentKey.of(ns.getElements()), ns)); + }); + commit2.commit(); + for (Entry e : entries) { Namespace namespace = e.getName().getNamespace(); - assertThat(getApi().getNamespace().refName(branch.getName()).namespace(namespace).get()) - .isEqualTo(namespace); + assertThat( + getApi() + .getNamespace() + .refName(branch.getName()) + .namespace(namespace) + .get() + .getElements()) + .isEqualTo(namespace.getElements()); assertThatThrownBy( () -> @@ -315,10 +335,6 @@ public void testNamespaceConflictWithOtherContent() throws BaseNessieClientServe () -> getApi().deleteNamespace().refName(branch.getName()).namespace(ns).delete()) .isInstanceOf(NessieNamespaceNotFoundException.class) .hasMessage("Namespace 'a.b.c' does not exist"); - - // it should only contain the parent namespace of the "a.b.c" table - assertThat(getApi().getMultipleNamespaces().refName(branch.getName()).get().getNamespaces()) - .containsExactly(Namespace.parse("a.b")); } @Test @@ -519,7 +535,7 @@ public void testNamespaceWithProperties() throws BaseNessieClientServerException getApi() .updateProperties() - .reference(branch) + .refName(branch.getName()) .namespace(namespace) .updateProperties(properties) .update(); diff --git a/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/AbstractRestRefLog.java b/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/AbstractRestRefLog.java index 3082374737f..d84ebf543fd 100644 --- a/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/AbstractRestRefLog.java +++ b/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/AbstractRestRefLog.java @@ -30,6 +30,8 @@ import org.assertj.core.groups.Tuple; import org.junit.jupiter.api.Test; import org.projectnessie.client.StreamingUtil; +import org.projectnessie.client.ext.NessieApiVersion; +import org.projectnessie.client.ext.NessieApiVersions; import org.projectnessie.error.BaseNessieClientServerException; import org.projectnessie.error.NessieRefLogNotFoundException; import org.projectnessie.model.Branch; @@ -44,7 +46,9 @@ /** See {@link AbstractTestRest} for details about and reason for the inheritance model. */ public abstract class AbstractRestRefLog extends AbstractRestReferences { @Test + @NessieApiVersions(versions = NessieApiVersion.V1) public void testReflog() throws BaseNessieClientServerException { + String tagName = "tag1_test_reflog_" + ThreadLocalRandom.current().nextInt(); String branch1 = "branch1_test_reflog"; String branch2 = "branch2_test_reflog"; diff --git a/servers/jax-rs-tests/src/test/java/org/projectnessie/jaxrs/AbstractRestSecurityContext.java b/servers/jax-rs-tests/src/test/java/org/projectnessie/jaxrs/AbstractRestSecurityContext.java index 045247988d9..16aba29ec1f 100644 --- a/servers/jax-rs-tests/src/test/java/org/projectnessie/jaxrs/AbstractRestSecurityContext.java +++ b/servers/jax-rs-tests/src/test/java/org/projectnessie/jaxrs/AbstractRestSecurityContext.java @@ -24,6 +24,8 @@ import java.util.stream.Collectors; import javax.ws.rs.core.SecurityContext; import org.junit.jupiter.api.Test; +import org.projectnessie.client.ext.NessieApiVersion; +import org.projectnessie.client.ext.NessieApiVersions; import org.projectnessie.error.NessieConflictException; import org.projectnessie.error.NessieNotFoundException; import org.projectnessie.jaxrs.ext.NessieSecurityContext; @@ -101,6 +103,7 @@ public void committerAndAuthorMerge(@NessieSecurityContext Consumer secContext) throws Exception { Branch main = makeCommits(createBranch("committerAndAuthorMergeUnsquashed_main"), secContext); diff --git a/servers/jax-rs-tests/src/test/java/org/projectnessie/jaxrs/AbstractTestJerseyResteasy.java b/servers/jax-rs-tests/src/test/java/org/projectnessie/jaxrs/AbstractTestJerseyResteasy.java index 482dc3ea382..197e85b1dcf 100644 --- a/servers/jax-rs-tests/src/test/java/org/projectnessie/jaxrs/AbstractTestJerseyResteasy.java +++ b/servers/jax-rs-tests/src/test/java/org/projectnessie/jaxrs/AbstractTestJerseyResteasy.java @@ -39,12 +39,12 @@ abstract class AbstractTestJerseyResteasy extends AbstractResteasyTest { @BeforeAll static void setup(@NessieClientUri URI uri) { RestAssured.baseURI = uri.toString(); + basePath = ""; // baseURI has the full base path RestAssured.port = uri.getPort(); RestAssured.requestSpecification = new RequestSpecBuilder() .setContentType(ContentType.JSON) .setAccept(ContentType.JSON) .build(); - basePath = "/"; } } diff --git a/servers/jax-rs-tests/src/test/java/org/projectnessie/jaxrs/TestJerseyRestNaiveClientInMemory.java b/servers/jax-rs-tests/src/test/java/org/projectnessie/jaxrs/TestJerseyRestNaiveClientInMemory.java index 522407cd2b4..9896869e613 100644 --- a/servers/jax-rs-tests/src/test/java/org/projectnessie/jaxrs/TestJerseyRestNaiveClientInMemory.java +++ b/servers/jax-rs-tests/src/test/java/org/projectnessie/jaxrs/TestJerseyRestNaiveClientInMemory.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.AfterEach; import org.projectnessie.client.NessieClientBuilder; +import org.projectnessie.client.ext.NessieApiVersions; import org.projectnessie.client.ext.NessieClientCustomizer; import org.projectnessie.client.http.HttpAuthentication; import org.projectnessie.client.http.RequestFilter; @@ -44,6 +45,7 @@ */ @NessieDbAdapterName(InmemoryDatabaseAdapterFactory.NAME) @NessieExternalDatabase(InmemoryTestConnectionProviderSource.class) +@NessieApiVersions class TestJerseyRestNaiveClientInMemory extends AbstractTestJerseyRest implements NessieClientCustomizer { diff --git a/servers/quarkus-server/src/main/resources/application.properties b/servers/quarkus-server/src/main/resources/application.properties index de2ba654153..e31ccd2c7e7 100644 --- a/servers/quarkus-server/src/main/resources/application.properties +++ b/servers/quarkus-server/src/main/resources/application.properties @@ -189,6 +189,15 @@ mp.openapi.extensions.smallrye.operationIdStrategy=METHOD # order matters below, since the first matching pattern will be used quarkus.micrometer.binder.http-server.match-patterns=\ + /api/v2/trees/.*/contents/.*=/api/v2/trees/{ref}/contents/{key},\ + /api/v2/trees/.*/contents=/api/v2/trees/{ref}/contents,\ + /api/v2/trees/.*/entries=/api/v2/trees/{ref}/entries,\ + /api/v2/trees/.*/diff/.*=/api/v2/trees/{from-ref}/diff/{to-ref},\ + /api/v2/trees/.*/history=/api/v2/trees/{ref}/history,\ + /api/v2/trees/.*/history/commit=/api/v2/trees/{ref}/history/commit,\ + /api/v2/trees/.*/history/merge=/api/v2/trees/{ref}/history/merge,\ + /api/v2/trees/.*/history/transplant=/api/v2/trees/{ref}/history/transplant,\ + /api/v2/trees/.*=/api/v2/trees/{ref},\ /api/v1/diffs/.*=/api/v1/diffs/{diff_params},\ /api/v1/trees/branch/.*/commit=/api/v1/trees/branch/{branchName}/commit,\ /api/v1/trees/branch/.*/transplant=/api/v1/trees/branch/{branchName}/transplant, \ diff --git a/servers/quarkus-server/src/test/java/org/projectnessie/server/AbstractQuarkusRestWithMetrics.java b/servers/quarkus-server/src/test/java/org/projectnessie/server/AbstractQuarkusRestWithMetrics.java index 8e1c79a30ea..0d60de6338a 100644 --- a/servers/quarkus-server/src/test/java/org/projectnessie/server/AbstractQuarkusRestWithMetrics.java +++ b/servers/quarkus-server/src/test/java/org/projectnessie/server/AbstractQuarkusRestWithMetrics.java @@ -19,31 +19,42 @@ import io.restassured.RestAssured; import org.junit.jupiter.api.Test; +import org.projectnessie.client.ext.NessieApiVersion; +import org.projectnessie.client.ext.NessieApiVersions; public abstract class AbstractQuarkusRestWithMetrics extends AbstractTestQuarkusRest { // We need to extend the base class because all Nessie metrics are created lazily. // They will appear in the `/q/metrics` endpoint only when some REST actions are executed. // this test is executed after all tests from the base class + + private String getMetrics() { + return RestAssured.given() + .when() + .basePath("/q/metrics") + .get() + .then() + .statusCode(200) + .extract() + .asString(); + } + @Test void smokeTestMetrics() { - // when - String body = - RestAssured.given() - .when() - .basePath("/q/metrics") - .get() - .then() - .statusCode(200) - .extract() - .asString(); - - // then + String body = getMetrics(); assertThat(body).contains("jvm_threads_live_threads"); assertThat(body).contains("nessie_versionstore_request_seconds_max"); assertThat(body).contains("nessie_versionstore_request_seconds_bucket"); assertThat(body).contains("nessie_versionstore_request_seconds_count"); assertThat(body).contains("nessie_versionstore_request_seconds_sum"); + assertThat(body).contains("http_server_connections_seconds_max"); + assertThat(body).contains("http_server_connections_seconds_active_count"); + } + + @Test + @NessieApiVersions(versions = NessieApiVersion.V1) + void smokeHttpApiV1Metrics() { + String body = getMetrics(); assertThat(body).contains("/api/v1/diffs/{diff_params}"); assertThat(body).contains("/api/v1/trees/{referenceType}/{ref}"); assertThat(body).contains("/api/v1/trees/branch/{branchName}/commit"); @@ -53,7 +64,21 @@ void smokeTestMetrics() { assertThat(body).contains("/api/v1/trees/tree/{ref}/entries"); assertThat(body).contains("/api/v1/namespaces/{ref}"); assertThat(body).contains("/api/v1/namespaces/namespace/{ref}/{name}"); - assertThat(body).contains("http_server_connections_seconds_max"); - assertThat(body).contains("http_server_connections_seconds_active_count"); + } + + @Test + @NessieApiVersions(versions = NessieApiVersion.V2) + void smokeHttpApiV2Metrics() { + String body = getMetrics(); + assertThat(body).contains("/api/v2/config"); + assertThat(body).contains("/api/v2/trees/{"); + assertThat(body).contains("/api/v2/trees/{ref}"); + assertThat(body).contains("/api/v2/trees/{ref}/contents"); + assertThat(body).contains("/api/v2/trees/{ref}/entries"); + assertThat(body).contains("/api/v2/trees/{ref}/history"); + assertThat(body).contains("/api/v2/trees/{ref}/history/commit"); + assertThat(body).contains("/api/v2/trees/{ref}/history/merge"); + assertThat(body).contains("/api/v2/trees/{ref}/history/transplant"); + assertThat(body).contains("/api/v2/trees/{from-ref}/diff/{to-ref}"); } } diff --git a/servers/quarkus-server/src/test/java/org/projectnessie/server/TestAuthorizationRules.java b/servers/quarkus-server/src/test/java/org/projectnessie/server/TestAuthorizationRules.java index 52429a250bf..0ac79fb8871 100644 --- a/servers/quarkus-server/src/test/java/org/projectnessie/server/TestAuthorizationRules.java +++ b/servers/quarkus-server/src/test/java/org/projectnessie/server/TestAuthorizationRules.java @@ -26,6 +26,8 @@ import java.util.stream.Collectors; import org.junit.jupiter.api.Test; import org.projectnessie.client.api.CommitMultipleOperationsBuilder; +import org.projectnessie.client.ext.NessieApiVersion; +import org.projectnessie.client.ext.NessieApiVersions; import org.projectnessie.error.BaseNessieClientServerException; import org.projectnessie.error.NessieForbiddenException; import org.projectnessie.error.NessieNotFoundException; @@ -202,6 +204,7 @@ void testReadEntityAllowed() throws Exception { @Test @TestSecurity(user = "admin_user") + @NessieApiVersions(versions = NessieApiVersion.V1) // Reflog is not supported in API v2 void testRefLogAllowed() throws Exception { //noinspection deprecation assertThat(api().getRefLog().stream()).isNotNull(); @@ -209,6 +212,7 @@ void testRefLogAllowed() throws Exception { @Test @TestSecurity(user = "disallowed_user") + @NessieApiVersions(versions = NessieApiVersion.V1) // Reflog is not supported in API v2 void testRefLogDisallowed() { //noinspection deprecation assertThatThrownBy(() -> api().getRefLog().stream()) diff --git a/servers/quarkus-server/src/test/java/org/projectnessie/server/error/ITNessieError.java b/servers/quarkus-server/src/test/java/org/projectnessie/server/error/ITNessieError.java index bd03f498069..ab9ad96245c 100644 --- a/servers/quarkus-server/src/test/java/org/projectnessie/server/error/ITNessieError.java +++ b/servers/quarkus-server/src/test/java/org/projectnessie/server/error/ITNessieError.java @@ -15,21 +15,30 @@ */ package org.projectnessie.server.error; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import io.quarkus.test.junit.QuarkusIntegrationTest; import io.quarkus.test.junit.TestProfile; -import org.junit.jupiter.api.BeforeEach; +import java.net.URI; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.projectnessie.client.api.NessieApiV1; +import org.projectnessie.client.ext.NessieApiVersion; +import org.projectnessie.client.ext.NessieApiVersions; import org.projectnessie.client.ext.NessieClientFactory; +import org.projectnessie.client.ext.NessieClientUri; +import org.projectnessie.client.http.HttpClient; +import org.projectnessie.client.rest.NessieHttpResponseFilter; import org.projectnessie.error.NessieBadRequestException; import org.projectnessie.model.CommitMeta; import org.projectnessie.model.ContentKey; import org.projectnessie.model.IcebergTable; -import org.projectnessie.model.Operation.Put; +import org.projectnessie.model.Operation; +import org.projectnessie.model.Reference; import org.projectnessie.quarkus.tests.profiles.QuarkusTestProfileInmemory; import org.projectnessie.server.QuarkusNessieClientResolver; @@ -42,29 +51,51 @@ @TestProfile( QuarkusTestProfileInmemory.class) // use the QuarkusTestProfileInmemory, as it can be reused @ExtendWith(QuarkusNessieClientResolver.class) +@NessieApiVersions public class ITNessieError { - private NessieApiV1 api; - - @BeforeEach - void init(NessieClientFactory clientFactory) { - api = clientFactory.make(); + @Test + @NessieApiVersions(versions = NessieApiVersion.V1) + void testNullParamViolationV1(NessieClientFactory client) { + try (NessieApiV1 api = client.make()) { + ContentKey k = ContentKey.of("a"); + IcebergTable t = IcebergTable.of("path1", 42, 42, 42, 42); + assertEquals( + "Bad Request (HTTP/400): commitMultipleOperations.expectedHash: must not be null", + assertThrows( + NessieBadRequestException.class, + () -> + api.commitMultipleOperations() + .branchName("branchName") + .operation(Operation.Put.of(k, t)) + .commitMeta(CommitMeta.fromMessage("message")) + .commit()) + .getMessage()); + } } @Test - void testNullParamViolation() { - ContentKey k = ContentKey.of("a"); - IcebergTable t = IcebergTable.of("path1", 42, 42, 42, 42); - assertEquals( - "Bad Request (HTTP/400): commitMultipleOperations.expectedHash: must not be null", - assertThrows( - NessieBadRequestException.class, - () -> - api.commitMultipleOperations() - .branchName("branchName") - .operation(Put.of(k, t)) - .commitMeta(CommitMeta.fromMessage("message")) - .commit()) - .getMessage()); + @NessieApiVersions(versions = NessieApiVersion.V2) + void testNullParamViolationV2(@NessieClientUri URI uri) { + ObjectMapper mapper = + new ObjectMapper() + .enable(SerializationFeature.INDENT_OUTPUT) + .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); + HttpClient client = + HttpClient.builder() + .setBaseUri(uri) + .setObjectMapper(mapper) + .addResponseFilter(new NessieHttpResponseFilter(mapper)) + .build(); + + assertThatThrownBy( + () -> + client + .newRequest() + .path("trees") + .queryParam("type", Reference.ReferenceType.BRANCH.name()) + .post(null)) + .isInstanceOf(NessieBadRequestException.class) + .hasMessage("Bad Request (HTTP/400): createReference.name: must not be null"); } } diff --git a/servers/rest-services/src/main/java/org/projectnessie/services/rest/RestV2ConfigResource.java b/servers/rest-services/src/main/java/org/projectnessie/services/rest/RestV2ConfigResource.java new file mode 100644 index 00000000000..2803b40d2e7 --- /dev/null +++ b/servers/rest-services/src/main/java/org/projectnessie/services/rest/RestV2ConfigResource.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2020 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.services.rest; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.ws.rs.Path; +import org.projectnessie.services.config.ServerConfig; + +/** REST endpoint to retrieve server settings. */ +@RequestScoped +@Path("v2/config") +public class RestV2ConfigResource extends RestConfigResource { + + // Mandated by CDI 2.0 + public RestV2ConfigResource() { + this(null); + } + + @Inject + public RestV2ConfigResource(ServerConfig config) { + super(config); + } +} diff --git a/servers/rest-services/src/main/java/org/projectnessie/services/rest/RestV2TreeResource.java b/servers/rest-services/src/main/java/org/projectnessie/services/rest/RestV2TreeResource.java new file mode 100644 index 00000000000..c896e6daa83 --- /dev/null +++ b/servers/rest-services/src/main/java/org/projectnessie/services/rest/RestV2TreeResource.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed 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 + * + * http://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 org.projectnessie.services.rest; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.SecurityContext; +import org.projectnessie.api.v2.http.HttpTreeApi; +import org.projectnessie.api.v2.params.CommitLogParams; +import org.projectnessie.api.v2.params.DiffParams; +import org.projectnessie.api.v2.params.EntriesParams; +import org.projectnessie.api.v2.params.GetReferenceParams; +import org.projectnessie.api.v2.params.Merge; +import org.projectnessie.api.v2.params.ReferencesParams; +import org.projectnessie.api.v2.params.Transplant; +import org.projectnessie.error.NessieConflictException; +import org.projectnessie.error.NessieNotFoundException; +import org.projectnessie.model.Branch; +import org.projectnessie.model.CommitResponse; +import org.projectnessie.model.Content; +import org.projectnessie.model.ContentKey; +import org.projectnessie.model.ContentResponse; +import org.projectnessie.model.DiffResponse; +import org.projectnessie.model.EntriesResponse; +import org.projectnessie.model.GetMultipleContentsRequest; +import org.projectnessie.model.GetMultipleContentsResponse; +import org.projectnessie.model.LogResponse; +import org.projectnessie.model.MergeResponse; +import org.projectnessie.model.Operations; +import org.projectnessie.model.Reference; +import org.projectnessie.model.ReferencesResponse; +import org.projectnessie.model.SingleReferenceResponse; +import org.projectnessie.services.authz.Authorizer; +import org.projectnessie.services.config.ServerConfig; +import org.projectnessie.services.impl.ContentApiImplWithAuthorization; +import org.projectnessie.services.impl.DiffApiImplWithAuthorization; +import org.projectnessie.services.impl.TreeApiImplWithAuthorization; +import org.projectnessie.services.spi.ContentService; +import org.projectnessie.services.spi.DiffService; +import org.projectnessie.services.spi.TreeService; +import org.projectnessie.versioned.VersionStore; + +/** REST endpoint for the tree-API. */ +@RequestScoped +@Path("v2/trees") +public class RestV2TreeResource implements HttpTreeApi { + + private final ServerConfig config; + private final VersionStore store; + private final Authorizer authorizer; + + @Context SecurityContext securityContext; + + // Mandated by CDI 2.0 + public RestV2TreeResource() { + this(null, null, null); + } + + @Inject + public RestV2TreeResource(ServerConfig config, VersionStore store, Authorizer authorizer) { + this.config = config; + this.store = store; + this.authorizer = authorizer; + } + + private TreeService tree() { + return new TreeApiImplWithAuthorization( + config, + store, + authorizer, + securityContext == null ? null : securityContext.getUserPrincipal()); + } + + private DiffService diff() { + return new DiffApiImplWithAuthorization( + config, + store, + authorizer, + securityContext == null ? null : securityContext.getUserPrincipal()); + } + + private ContentService content() { + return new ContentApiImplWithAuthorization( + config, + store, + authorizer, + securityContext == null ? null : securityContext.getUserPrincipal()); + } + + @Override + public ReferencesResponse getAllReferences(ReferencesParams params) { + return tree().getAllReferences(params.fetchOption(), params.filter()); + } + + @Override + public SingleReferenceResponse createReference( + String name, Reference.ReferenceType type, Reference reference) + throws NessieNotFoundException, NessieConflictException { + String fromRefName = null; + String fromHash = null; + if (reference != null) { + fromRefName = reference.getName(); + fromHash = reference.getHash(); + } + + Reference created = tree().createReference(name, type, fromHash, fromRefName); + return SingleReferenceResponse.builder().reference(created).build(); + } + + @Override + public SingleReferenceResponse getReferenceByName(GetReferenceParams params) + throws NessieNotFoundException { + return SingleReferenceResponse.builder() + .reference(tree().getReferenceByName(params.getRefName(), params.fetchOption())) + .build(); + } + + @Override + public EntriesResponse getEntries(String ref, EntriesParams params) + throws NessieNotFoundException { + Reference reference = Reference.fromPathString(ref, Reference.ReferenceType.BRANCH); + return tree().getEntries(reference.getName(), reference.getHash(), null, params.filter()); + } + + @Override + public LogResponse getCommitLog(String ref, CommitLogParams params) + throws NessieNotFoundException { + Reference reference = Reference.fromPathString(ref, Reference.ReferenceType.BRANCH); + return tree() + .getCommitLog( + reference.getName(), + params.fetchOption(), + params.startHash(), + reference.getHash(), + params.filter(), + params.maxRecords(), + params.pageToken()); + } + + @Override + public DiffResponse getDiff(DiffParams params) throws NessieNotFoundException { + Reference from = Reference.fromPathString(params.getFromRef(), Reference.ReferenceType.BRANCH); + Reference to = Reference.fromPathString(params.getToRef(), Reference.ReferenceType.BRANCH); + return diff().getDiff(from.getName(), from.getHash(), to.getName(), to.getHash()); + } + + @Override + public SingleReferenceResponse assignReference( + Reference.ReferenceType type, String ref, Reference assignTo) + throws NessieNotFoundException, NessieConflictException { + Reference reference = Reference.fromPathString(ref, type); + tree().assignReference(type, reference.getName(), reference.getHash(), assignTo); + return SingleReferenceResponse.builder().reference(reference).build(); + } + + @Override + public SingleReferenceResponse deleteReference(Reference.ReferenceType type, String ref) + throws NessieConflictException, NessieNotFoundException { + Reference reference = Reference.fromPathString(ref, type); + tree().deleteReference(type, reference.getName(), reference.getHash()); + return SingleReferenceResponse.builder().reference(reference).build(); + } + + @Override + public ContentResponse getContent(ContentKey key, String ref) throws NessieNotFoundException { + Reference reference = Reference.fromPathString(ref, Reference.ReferenceType.BRANCH); + Content content = content().getContent(key, reference.getName(), reference.getHash()); + return ContentResponse.builder().content(content).build(); + } + + @Override + public GetMultipleContentsResponse getMultipleContents( + String ref, GetMultipleContentsRequest request) throws NessieNotFoundException { + Reference reference = Reference.fromPathString(ref, Reference.ReferenceType.BRANCH); + return content() + .getMultipleContents(reference.getName(), reference.getHash(), request.getRequestedKeys()); + } + + @Override + public MergeResponse transplantCommitsIntoBranch(String branch, Transplant transplant) + throws NessieNotFoundException, NessieConflictException { + Reference ref = Reference.fromPathString(branch, Reference.ReferenceType.BRANCH); + return tree() + .transplantCommitsIntoBranch( + ref.getName(), + ref.getHash(), + transplant.getMessage(), + transplant.getHashesToTransplant(), + transplant.getFromRefName(), + true, + transplant.getKeyMergeModes(), + transplant.getDefaultKeyMergeMode(), + transplant.isDryRun(), + transplant.isFetchAdditionalInfo(), + transplant.isReturnConflictAsResult()); + } + + @Override + public MergeResponse mergeRefIntoBranch(String branch, Merge merge) + throws NessieNotFoundException, NessieConflictException { + Reference ref = Reference.fromPathString(branch, Reference.ReferenceType.BRANCH); + return tree() + .mergeRefIntoBranch( + ref.getName(), + ref.getHash(), + merge.getFromRefName(), + merge.getFromHash(), + false, + merge.getKeyMergeModes(), + merge.getDefaultKeyMergeMode(), + merge.isDryRun(), + merge.isFetchAdditionalInfo(), + merge.isReturnConflictAsResult()); + } + + @Override + public CommitResponse commitMultipleOperations(String branch, Operations operations) + throws NessieNotFoundException, NessieConflictException { + Reference ref = Reference.fromPathString(branch, Reference.ReferenceType.BRANCH); + Branch head = tree().commitMultipleOperations(ref.getName(), ref.getHash(), operations); + return CommitResponse.builder().targetBranch(head).build(); + } +} diff --git a/servers/services/src/main/java/org/projectnessie/services/impl/ConfigApiImpl.java b/servers/services/src/main/java/org/projectnessie/services/impl/ConfigApiImpl.java index b85562f00ad..03b01e95419 100644 --- a/servers/services/src/main/java/org/projectnessie/services/impl/ConfigApiImpl.java +++ b/servers/services/src/main/java/org/projectnessie/services/impl/ConfigApiImpl.java @@ -32,7 +32,7 @@ public ConfigApiImpl(ServerConfig config) { public NessieConfiguration getConfig() { return ImmutableNessieConfiguration.builder() .defaultBranch(this.config.getDefaultBranch()) - .maxSupportedApiVersion(1) + .maxSupportedApiVersion(2) .build(); } } diff --git a/versioned/tests/src/main/resources/META-INF/services/org.assertj.core.configuration.Configuration b/versioned/tests/src/main/resources/META-INF/services/org.assertj.core.configuration.Configuration index e8dbc0c018b..9c502b089d0 100644 --- a/versioned/tests/src/main/resources/META-INF/services/org.assertj.core.configuration.Configuration +++ b/versioned/tests/src/main/resources/META-INF/services/org.assertj.core.configuration.Configuration @@ -1,5 +1,5 @@ # -# Copyright (C) $YEAR Dremio +# Copyright (C) 2022 Dremio # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License.