diff options
Diffstat (limited to 'vespaclient-container-plugin/src/main/java/com')
4 files changed, 110 insertions, 28 deletions
diff --git a/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/OperationHandler.java b/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/OperationHandler.java index 35999df5a6b..775207d8629 100644 --- a/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/OperationHandler.java +++ b/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/OperationHandler.java @@ -29,6 +29,7 @@ public interface OperationHandler { public final Optional<Integer> wantedDocumentCount; public final Optional<String> fieldSet; public final Optional<Integer> concurrency; + public final Optional<String> bucketSpace; /** @deprecated Use a VisitOptions.Builder instead */ @Deprecated @@ -38,6 +39,7 @@ public interface OperationHandler { this.wantedDocumentCount = wantedDocumentCount; this.fieldSet = Optional.empty(); this.concurrency = Optional.empty(); + this.bucketSpace = Optional.empty(); } private VisitOptions(Builder builder) { @@ -46,6 +48,7 @@ public interface OperationHandler { this.wantedDocumentCount = Optional.ofNullable(builder.wantedDocumentCount); this.fieldSet = Optional.ofNullable(builder.fieldSet); this.concurrency = Optional.ofNullable(builder.concurrency); + this.bucketSpace = Optional.ofNullable(builder.bucketSpace); } public static class Builder { @@ -54,6 +57,7 @@ public interface OperationHandler { Integer wantedDocumentCount; String fieldSet; Integer concurrency; + String bucketSpace; public Builder cluster(String cluster) { this.cluster = cluster; @@ -80,6 +84,11 @@ public interface OperationHandler { return this; } + public Builder bucketSpace(String bucketSpace) { + this.bucketSpace = bucketSpace; + return this; + } + public VisitOptions build() { return new VisitOptions(this); } diff --git a/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/OperationHandlerImpl.java b/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/OperationHandlerImpl.java index c0ce3e84232..c3844e398fe 100644 --- a/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/OperationHandlerImpl.java +++ b/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/OperationHandlerImpl.java @@ -4,10 +4,10 @@ package com.yahoo.document.restapi; import com.yahoo.document.Document; import com.yahoo.document.DocumentId; import com.yahoo.document.DocumentRemove; +import com.yahoo.document.FixedBucketSpaces; import com.yahoo.document.TestAndSetCondition; import com.yahoo.document.json.JsonWriter; import com.yahoo.document.DocumentPut; -import com.yahoo.document.restapi.resource.RestApi; import com.yahoo.documentapi.DocumentAccess; import com.yahoo.documentapi.DocumentAccessException; import com.yahoo.documentapi.SyncParameters; @@ -142,7 +142,7 @@ public class OperationHandlerImpl implements OperationHandler { restUri, RestUri.apiErrorCodes.DOCUMENT_CONDITION_NOT_MET); } return Response.createErrorResponse(getHTTPStatusCode(documentException.getErrorCodes()), documentException.getMessage(), restUri, - RestUri.apiErrorCodes.DOCUMENT_EXCPETION); + RestUri.apiErrorCodes.DOCUMENT_EXCEPTION); } @Override @@ -282,7 +282,7 @@ public class OperationHandlerImpl implements OperationHandler { response = Response.createErrorResponse(412, "Condition not met: " + documentException.getMessage(), restUri, RestUri.apiErrorCodes.DOCUMENT_CONDITION_NOT_MET); } else { - response = Response.createErrorResponse(400, documentException.getMessage(), restUri, RestUri.apiErrorCodes.DOCUMENT_EXCPETION); + response = Response.createErrorResponse(400, documentException.getMessage(), restUri, RestUri.apiErrorCodes.DOCUMENT_EXCEPTION); } } catch (Exception e) { response = Response.createErrorResponse(500, ExceptionUtils.getStackTrace(e), restUri, RestUri.apiErrorCodes.UNSPECIFIED); @@ -321,16 +321,40 @@ public class OperationHandlerImpl implements OperationHandler { return get(restUri, Optional.empty()); } - protected BucketSpaceRoute resolveBucketSpaceRoute(Optional<String> wantedCluster, String docType) throws RestApiException { + private static boolean isValidBucketSpace(String spaceName) { + // TODO need bucket space repo in Java as well + return (FixedBucketSpaces.defaultSpace().equals(spaceName) + || FixedBucketSpaces.globalSpace().equals(spaceName)); + } + + protected BucketSpaceRoute resolveBucketSpaceRoute(Optional<String> wantedCluster, + Optional<String> wantedBucketSpace, + RestUri restUri) throws RestApiException { final List<ClusterDef> clusters = clusterEnumerator.enumerateClusters(); ClusterDef clusterDef = resolveClusterDef(wantedCluster, clusters); - Optional<String> targetBucketSpace = bucketSpaceResolver.clusterBucketSpaceFromDocumentType(clusterDef.getConfigId(), docType); - if (!targetBucketSpace.isPresent()) { - throw new RestApiException(Response.createErrorResponse(400, String.format( - "Document type '%s' in cluster '%s' is not mapped to a known bucket space", docType, clusterDef.getName()), - RestUri.apiErrorCodes.UNKNOWN_BUCKET_SPACE)); + + String targetBucketSpace; + if (!restUri.isRootOnly()) { + String docType = restUri.getDocumentType(); + Optional<String> resolvedSpace = bucketSpaceResolver.clusterBucketSpaceFromDocumentType(clusterDef.getConfigId(), docType); + if (!resolvedSpace.isPresent()) { + throw new RestApiException(Response.createErrorResponse(400, String.format( + "Document type '%s' in cluster '%s' is not mapped to a known bucket space", docType, clusterDef.getName()), + RestUri.apiErrorCodes.UNKNOWN_BUCKET_SPACE)); + } + targetBucketSpace = resolvedSpace.get(); + } else { + if (wantedBucketSpace.isPresent() && !isValidBucketSpace(wantedBucketSpace.get())) { + // TODO enumerate known bucket spaces from a repo instead of having a fixed set + throw new RestApiException(Response.createErrorResponse(400, String.format( + "Bucket space '%s' is not a known bucket space (expected '%s' or '%s')", + wantedBucketSpace.get(), FixedBucketSpaces.defaultSpace(), FixedBucketSpaces.globalSpace()), + RestUri.apiErrorCodes.UNKNOWN_BUCKET_SPACE)); + } + targetBucketSpace = wantedBucketSpace.orElse(FixedBucketSpaces.defaultSpace()); } - return new BucketSpaceRoute(clusterDefToRoute(clusterDef), targetBucketSpace.get()); + + return new BucketSpaceRoute(clusterDefToRoute(clusterDef), targetBucketSpace); } protected static ClusterDef resolveClusterDef(Optional<String> wantedCluster, List<ClusterDef> clusters) throws RestApiException { @@ -365,26 +389,41 @@ public class OperationHandlerImpl implements OperationHandler { return clusterListString.toString(); } - private VisitorParameters createVisitorParameters( - RestUri restUri, - String documentSelection, - VisitOptions options) - throws RestApiException { - + private static String buildAugmentedDocumentSelection(RestUri restUri, String documentSelection) { + if (restUri.isRootOnly()) { + return documentSelection; // May be empty, that's fine. + } StringBuilder selection = new StringBuilder(); - if (! documentSelection.isEmpty()) { - // TODO shouldn't selection be wrapped in () itself ? - selection.append("(").append(documentSelection).append(" and "); + selection.append("((").append(documentSelection).append(") and "); } selection.append(restUri.getDocumentType()).append(" and (id.namespace=='").append(restUri.getNamespace()).append("')"); if (! documentSelection.isEmpty()) { selection.append(")"); } + return selection.toString(); + } + + private VisitorParameters createVisitorParameters( + RestUri restUri, + String documentSelection, + VisitOptions options) + throws RestApiException { + + if (restUri.isRootOnly() && !options.cluster.isPresent()) { + throw new RestApiException(Response.createErrorResponse(400, + "Must set 'cluster' parameter to a valid content cluster id when visiting at a root /document/v1/ level", + RestUri.apiErrorCodes.MISSING_CLUSTER)); + } + + String augmentedSelection = buildAugmentedDocumentSelection(restUri, documentSelection); - VisitorParameters params = new VisitorParameters(selection.toString()); - // Only return fieldset that is part of the document. - params.fieldSet(options.fieldSet.orElse(restUri.getDocumentType() + ":[document]")); + VisitorParameters params = new VisitorParameters(augmentedSelection); + // Only return fieldset that is part of the document, unless we're visiting across all + // document types in which case we can't explicitly state a single document type. + // This matches legacy /visit API and vespa-visit tool behavior. + params.fieldSet(options.fieldSet.orElse( + restUri.isRootOnly() ? "[all]" : restUri.getDocumentType() + ":[document]")); params.setMaxBucketsPerVisitor(1); params.setMaxPending(32); params.setMaxFirstPassHits(1); @@ -399,7 +438,7 @@ public class OperationHandlerImpl implements OperationHandler { params.visitInconsistentBuckets(true); // TODO document this as part of consistency doc params.setVisitorOrdering(VisitorOrdering.ASCENDING); - BucketSpaceRoute bucketSpaceRoute = resolveBucketSpaceRoute(options.cluster, restUri.getDocumentType()); + BucketSpaceRoute bucketSpaceRoute = resolveBucketSpaceRoute(options.cluster, options.bucketSpace, restUri); params.setRoute(bucketSpaceRoute.getClusterRoute()); params.setBucketSpace(bucketSpaceRoute.getBucketSpace()); diff --git a/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/RestUri.java b/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/RestUri.java index e3423eec2c8..975075fd2fa 100644 --- a/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/RestUri.java +++ b/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/RestUri.java @@ -36,7 +36,7 @@ public class RestUri { TOO_MANY_PARALLEL_REQUESTS(-8), MISSING_CLUSTER(-9), INTERNAL_EXCEPTION(-9), DOCUMENT_CONDITION_NOT_MET(-10), - DOCUMENT_EXCPETION(-11), + DOCUMENT_EXCEPTION(-11), PARSER_ERROR(-11), GROUP_AND_EXPRESSION_ERROR(-12), TIME_OUT(-13), @@ -67,6 +67,10 @@ public class RestUri { private Optional<Group> group = Optional.empty(); private final String rawPath; + public boolean isRootOnly() { + return namespace == null; + } + public String getRawPath() { return rawPath; } @@ -89,8 +93,8 @@ public class RestUri { public String generateFullId() { return ID + namespace + ":" + documentType + ":" - + (getGroup().isPresent() ? group.get().name + "=" + group.get().value : "") - + ":" + docId; + + group.map(g -> String.format("%s=%s", g.name, g.value)).orElse("") + + ":" + docId; } static class PathParser { @@ -102,6 +106,11 @@ public class RestUri { this.originalPath = path; this.rawParts = Splitter.on('/').splitToList(path); } + + boolean hasNextToken() { + return readPos < rawParts.size(); + } + String nextTokenOrException() throws RestApiException { if (readPos >= rawParts.size()) { throwUsage(originalPath); @@ -132,7 +141,15 @@ public class RestUri { ! pathParser.nextTokenOrException().equals(V_1)) { throwUsage(uri.getRawPath()); } - namespace = pathParser.nextTokenOrException(); + // If /document/v1 root request, there's an empty token at the end. + String maybeNamespace = pathParser.nextTokenOrException(); + if (maybeNamespace.isEmpty()) { + namespace = null; + documentType = null; + docId = null; + return; + } + namespace = maybeNamespace; documentType = pathParser.nextTokenOrException(); switch (pathParser.nextTokenOrException()) { case "number": diff --git a/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/resource/RestApi.java b/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/resource/RestApi.java index 49b27eba613..1bd4cf535c9 100644 --- a/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/resource/RestApi.java +++ b/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/resource/RestApi.java @@ -38,6 +38,8 @@ import java.util.Optional; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; +import static com.yahoo.jdisc.Response.Status.BAD_REQUEST; + /** * API for handling single operation on a document and visiting. * @@ -58,6 +60,7 @@ public class RestApi extends LoggingRequestHandler { private static final String WANTED_DOCUMENT_COUNT = "wantedDocumentCount"; private static final String FIELD_SET = "fieldSet"; private static final String CONCURRENCY = "concurrency"; + private static final String BUCKET_SPACE = "bucketSpace"; private static final String APPLICATION_JSON = "application/json"; private final OperationHandler operationHandler; private SingleDocumentParser singleDocumentParser; @@ -148,11 +151,24 @@ public class RestApi extends LoggingRequestHandler { } } + private static void validateUriStructureForRequestMethod(RestUri uri, com.yahoo.jdisc.http.HttpRequest.Method method) throws RestApiException { + if ((method != com.yahoo.jdisc.http.HttpRequest.Method.GET) && uri.isRootOnly()) { + throw new RestApiException(Response.createErrorResponse(BAD_REQUEST, + "Root /document/v1/ requests only supported for HTTP GET", + RestUri.apiErrorCodes.ERROR_ID_BASIC_USAGE)); + } + } + + private static boolean isVisitRequestUri(RestUri uri) { + return (uri.isRootOnly() || uri.getDocId().isEmpty()); + } + // protected for testing protected HttpResponse handleInternal(HttpRequest request) { final RestUri restUri; try { restUri = new RestUri(request.getUri()); + validateUriStructureForRequestMethod(restUri, request.getMethod()); } catch (RestApiException e) { return e.getResponse(); } catch (Exception e2) { @@ -173,7 +189,7 @@ public class RestApi extends LoggingRequestHandler { try { switch (request.getMethod()) { case GET: // Vespa Visit/Get - return restUri.getDocId().isEmpty() ? handleVisit(restUri, request) : handleGet(restUri, request); + return isVisitRequestUri(restUri) ? handleVisit(restUri, request) : handleGet(restUri, request); case POST: // Vespa Put operationHandler.put(restUri, createPutOperation(request, restUri.generateFullId(), condition), route); break; @@ -276,6 +292,7 @@ public class RestApi extends LoggingRequestHandler { Optional.ofNullable(request.getProperty(CLUSTER)).ifPresent(c -> optionsBuilder.cluster(c)); Optional.ofNullable(request.getProperty(CONTINUATION)).ifPresent(c -> optionsBuilder.continuation(c)); Optional.ofNullable(request.getProperty(FIELD_SET)).ifPresent(fs -> optionsBuilder.fieldSet(fs)); + Optional.ofNullable(request.getProperty(BUCKET_SPACE)).ifPresent(s -> optionsBuilder.bucketSpace(s)); parsePositiveIntegerRequestParameter(WANTED_DOCUMENT_COUNT, request).ifPresent(c -> optionsBuilder.wantedDocumentCount(c)); parsePositiveIntegerRequestParameter(CONCURRENCY, request).ifPresent(c -> optionsBuilder.concurrency(c)); |