summaryrefslogtreecommitdiffstats
path: root/vespaclient-container-plugin/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'vespaclient-container-plugin/src/main')
-rw-r--r--vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/OperationHandler.java9
-rw-r--r--vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/OperationHandlerImpl.java85
-rw-r--r--vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/RestUri.java25
-rw-r--r--vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/resource/RestApi.java19
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));