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 874ccacd6a0..2b4a0b96273 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 @@ -97,7 +97,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 c93b0f8b74a..71864fc124c 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 @@ -44,6 +44,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; @@ -64,6 +66,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 6de2d8a734f..0bc4cb0acbd 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 @@ -63,6 +63,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; @@ -226,6 +228,8 @@ public EnvHolder(Extension versionStoreExtension) throws Exception { @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/tests/AbstractRestEntries.java b/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/tests/AbstractRestEntries.java index 33861e84428..cb427854d5f 100644 --- a/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/tests/AbstractRestEntries.java +++ b/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/tests/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/tests/AbstractRestInvalid.java b/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/tests/AbstractRestInvalid.java index a90b9f62b84..b183d02c7ef 100644 --- a/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/tests/AbstractRestInvalid.java +++ b/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/tests/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/tests/AbstractRestMergeTransplant.java b/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/tests/AbstractRestMergeTransplant.java index 61f0e630f6f..fefa1f02cd7 100644 --- a/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/tests/AbstractRestMergeTransplant.java +++ b/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/tests/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/tests/AbstractRestMisc.java b/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/tests/AbstractRestMisc.java index dd0d2884cbd..7f896dd1780 100644 --- a/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/tests/AbstractRestMisc.java +++ b/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/tests/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/tests/AbstractRestNamespace.java b/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/tests/AbstractRestNamespace.java index cc3b78f7a6f..ec125b9a796 100644 --- a/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/tests/AbstractRestNamespace.java +++ b/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/tests/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/tests/AbstractRestRefLog.java b/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/tests/AbstractRestRefLog.java index 6da2eca9f46..0e5335c151a 100644 --- a/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/tests/AbstractRestRefLog.java +++ b/servers/jax-rs-tests/src/main/java/org/projectnessie/jaxrs/tests/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/tests/AbstractRestSecurityContext.java b/servers/jax-rs-tests/src/test/java/org/projectnessie/jaxrs/tests/AbstractRestSecurityContext.java index eb0c814e8eb..709b598a745 100644 --- a/servers/jax-rs-tests/src/test/java/org/projectnessie/jaxrs/tests/AbstractRestSecurityContext.java +++ b/servers/jax-rs-tests/src/test/java/org/projectnessie/jaxrs/tests/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/tests/AbstractTestDatabaseAdapterResteasy.java b/servers/jax-rs-tests/src/test/java/org/projectnessie/jaxrs/tests/AbstractTestDatabaseAdapterResteasy.java index 4375505c819..e21147c6e27 100644 --- a/servers/jax-rs-tests/src/test/java/org/projectnessie/jaxrs/tests/AbstractTestDatabaseAdapterResteasy.java +++ b/servers/jax-rs-tests/src/test/java/org/projectnessie/jaxrs/tests/AbstractTestDatabaseAdapterResteasy.java @@ -41,12 +41,12 @@ abstract class AbstractTestDatabaseAdapterResteasy 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/tests/TestJerseyRestNaiveClientInMemory.java b/servers/jax-rs-tests/src/test/java/org/projectnessie/jaxrs/tests/TestJerseyRestNaiveClientInMemory.java index 9760ae372cd..1372721e009 100644 --- a/servers/jax-rs-tests/src/test/java/org/projectnessie/jaxrs/tests/TestJerseyRestNaiveClientInMemory.java +++ b/servers/jax-rs-tests/src/test/java/org/projectnessie/jaxrs/tests/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 AbstractTestDatabaseAdapterRest 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.