diff options
author | Jon Marius Venstad <venstad@gmail.com> | 2020-11-03 17:14:28 +0100 |
---|---|---|
committer | Jon Marius Venstad <venstad@gmail.com> | 2020-11-03 17:14:28 +0100 |
commit | 7a17516b491c1bb4a248b4cf7d3488fe93eabe34 (patch) | |
tree | fc82a314db40d93aa63ef5c46a5443193a43aaef /vespaclient-container-plugin/src/main | |
parent | c3c0c67621ec2ad422e69159fe46698e45e2cac1 (diff) |
Remove old /document/v1 handler
Diffstat (limited to 'vespaclient-container-plugin/src/main')
8 files changed, 10 insertions, 1314 deletions
diff --git a/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/LocalDataVisitorHandler.java b/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/LocalDataVisitorHandler.java deleted file mode 100644 index 325c5492776..00000000000 --- a/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/LocalDataVisitorHandler.java +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.document.restapi; - -import com.yahoo.document.Document; -import com.yahoo.document.DocumentId; -import com.yahoo.document.json.JsonWriter; -import com.yahoo.documentapi.DumpVisitorDataHandler; -import com.yahoo.exception.ExceptionUtils; - -import java.nio.charset.StandardCharsets; - -/** - * Handling data from visit. - * - * @author dybis - */ -class LocalDataVisitorHandler extends DumpVisitorDataHandler { - - StringBuilder commaSeparatedJsonDocuments = new StringBuilder(); - final StringBuilder errors = new StringBuilder(); - - private boolean isFirst = true; - private final Object monitor = new Object(); - - String getErrors() { - return errors.toString(); - } - - String getCommaSeparatedJsonDocuments() { - return commaSeparatedJsonDocuments.toString(); - } - - @Override - public void onDocument(Document document, long l) { - try { - final String docJson = new String(JsonWriter.toByteArray(document), StandardCharsets.UTF_8.name()); - synchronized (monitor) { - if (!isFirst) { - commaSeparatedJsonDocuments.append(","); - } - isFirst = false; - commaSeparatedJsonDocuments.append(docJson); - } - } catch (Exception e) { - synchronized (monitor) { - errors.append(ExceptionUtils.getStackTraceAsString(e)).append("\n"); - } - } - } - - // TODO: Not sure if we should support removal or not. Do nothing here maybe? - @Override - public void onRemove(DocumentId documentId) { - try { - final String removeJson = new String(JsonWriter.documentRemove(documentId), StandardCharsets.UTF_8.name()); - synchronized (monitor) { - if (!isFirst) { - commaSeparatedJsonDocuments.append(","); - } - isFirst = false; - commaSeparatedJsonDocuments.append(removeJson); - } - } catch (Exception e) { - synchronized (monitor) { - errors.append(ExceptionUtils.getStackTraceAsString(e)).append("\n"); - } - } - } - -} 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 deleted file mode 100644 index 848fe4b5726..00000000000 --- a/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/OperationHandler.java +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.document.restapi; - -import com.yahoo.vespaxmlparser.FeedOperation; - -import java.util.Optional; - -/** - * Abstract the backend stuff for the REST API, such as retrieving or updating documents. - * - * @author Haakon Dybdahl - */ -public interface OperationHandler { - - class VisitResult { - - public final Optional<String> token; - public final String documentsAsJsonList; - - public VisitResult(Optional<String> token, String documentsAsJsonList) { - this.token = token; - this.documentsAsJsonList = documentsAsJsonList; - } - } - - class VisitOptions { - public final Optional<String> cluster; - public final Optional<String> continuation; - public final Optional<Integer> wantedDocumentCount; - public final Optional<String> fieldSet; - public final Optional<Integer> concurrency; - public final Optional<String> bucketSpace; - - private VisitOptions(Builder builder) { - this.cluster = Optional.ofNullable(builder.cluster); - this.continuation = Optional.ofNullable(builder.continuation); - 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 { - String cluster; - String continuation; - Integer wantedDocumentCount; - String fieldSet; - Integer concurrency; - String bucketSpace; - - public Builder cluster(String cluster) { - this.cluster = cluster; - return this; - } - - public Builder continuation(String continuation) { - this.continuation = continuation; - return this; - } - - public Builder wantedDocumentCount(Integer count) { - this.wantedDocumentCount = count; - return this; - } - - public Builder fieldSet(String fieldSet) { - this.fieldSet = fieldSet; - return this; - } - - public Builder concurrency(Integer concurrency) { - this.concurrency = concurrency; - return this; - } - - public Builder bucketSpace(String bucketSpace) { - this.bucketSpace = bucketSpace; - return this; - } - - public VisitOptions build() { - return new VisitOptions(this); - } - } - - public static Builder builder() { - return new Builder(); - } - } - - VisitResult visit(RestUri restUri, String documentSelection, VisitOptions options) throws RestApiException; - - void put(RestUri restUri, FeedOperation data, Optional<String> route) throws RestApiException; - - void update(RestUri restUri, FeedOperation data, Optional<String> route) throws RestApiException; - - void delete(RestUri restUri, String condition, Optional<String> route) throws RestApiException; - - Optional<String> get(RestUri restUri) throws RestApiException; - - default Optional<String> get(RestUri restUri, Optional<String> fieldSet) throws RestApiException { - return get(restUri); - } - - default Optional<String> get(RestUri restUri, Optional<String> fieldSet, Optional<String> cluster) throws RestApiException { - return get(restUri, fieldSet); - } - - /** Called just before this is disposed of */ - default void shutdown() {} - -} 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 deleted file mode 100644 index 3d3a8fc52ad..00000000000 --- a/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/OperationHandlerImpl.java +++ /dev/null @@ -1,460 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -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.fieldset.AllFields; -import com.yahoo.document.json.JsonWriter; -import com.yahoo.document.DocumentPut; -import com.yahoo.documentapi.DocumentAccess; -import com.yahoo.documentapi.DocumentAccessException; -import com.yahoo.documentapi.ProgressToken; -import com.yahoo.documentapi.SyncParameters; -import com.yahoo.documentapi.SyncSession; -import com.yahoo.documentapi.VisitorControlHandler; -import com.yahoo.documentapi.VisitorParameters; -import com.yahoo.documentapi.VisitorSession; -import com.yahoo.documentapi.messagebus.MessageBusSyncSession; -import com.yahoo.documentapi.messagebus.protocol.DocumentProtocol; -import com.yahoo.documentapi.metrics.DocumentApiMetrics; -import com.yahoo.documentapi.metrics.DocumentOperationStatus; -import com.yahoo.documentapi.metrics.DocumentOperationType; -import com.yahoo.exception.ExceptionUtils; -import com.yahoo.messagebus.StaticThrottlePolicy; -import com.yahoo.metrics.simple.MetricReceiver; -import com.yahoo.vespaclient.ClusterDef; -import com.yahoo.vespaxmlparser.FeedOperation; -import com.yahoo.yolean.concurrent.ConcurrentResourcePool; -import com.yahoo.yolean.concurrent.ResourceFactory; - -import java.io.ByteArrayOutputStream; -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * Sends operations to messagebus via document api. - * - * @author dybis - */ -public class OperationHandlerImpl implements OperationHandler { - - public interface ClusterEnumerator { - List<ClusterDef> enumerateClusters(); - } - - public interface BucketSpaceResolver { - Optional<String> clusterBucketSpaceFromDocumentType(String clusterId, String docType); - } - - public static class BucketSpaceRoute { - private final String clusterRoute; - private final String bucketSpace; - - public BucketSpaceRoute(String clusterRoute, String bucketSpace) { - this.clusterRoute = clusterRoute; - this.bucketSpace = bucketSpace; - } - - public String getClusterRoute() { - return clusterRoute; - } - - public String getBucketSpace() { - return bucketSpace; - } - } - - public static final int VISIT_TIMEOUT_MS = 120000; - public static final int WANTED_DOCUMENT_COUNT_UPPER_BOUND = 1000; // Approximates the max default size of a bucket - public static final int CONCURRENCY_UPPER_BOUND = 100; - private final DocumentAccess documentAccess; - private final DocumentApiMetrics metricsHelper; - private final ClusterEnumerator clusterEnumerator; - private final BucketSpaceResolver bucketSpaceResolver; - - private static final class SyncSessionFactory extends ResourceFactory<SyncSession> { - private final DocumentAccess documentAccess; - SyncSessionFactory(DocumentAccess documentAccess) { - this.documentAccess = documentAccess; - } - @Override - public SyncSession create() { - return documentAccess.createSyncSession(new SyncParameters.Builder().build()); - } - } - - private final ConcurrentResourcePool<SyncSession> syncSessions; - - public OperationHandlerImpl(DocumentAccess documentAccess, ClusterEnumerator clusterEnumerator, - BucketSpaceResolver bucketSpaceResolver, MetricReceiver metricReceiver) { - this.documentAccess = documentAccess; - this.clusterEnumerator = clusterEnumerator; - this.bucketSpaceResolver = bucketSpaceResolver; - syncSessions = new ConcurrentResourcePool<>(new SyncSessionFactory(documentAccess)); - metricsHelper = new DocumentApiMetrics(metricReceiver, "documentV1"); - } - - @Override - public void shutdown() { - for (SyncSession session : syncSessions) { - session.destroy(); - } - documentAccess.shutdown(); - } - - private static final int HTTP_STATUS_BAD_REQUEST = 400; - private static final int HTTP_STATUS_INSUFFICIENT_STORAGE = 507; - private static final int HTTP_PRECONDITION_FAILED = 412; - - public static int getHTTPStatusCode(Set<Integer> errorCodes) { - if (errorCodes.size() == 1 && errorCodes.contains(DocumentProtocol.ERROR_NO_SPACE)) { - return HTTP_STATUS_INSUFFICIENT_STORAGE; - } - if (errorCodes.contains(DocumentProtocol.ERROR_TEST_AND_SET_CONDITION_FAILED)) { - return HTTP_PRECONDITION_FAILED; - } - return HTTP_STATUS_BAD_REQUEST; - } - - private static Response createErrorResponse(DocumentAccessException documentException, RestUri restUri) { - if (documentException.hasConditionNotMetError()) { - return Response.createErrorResponse(getHTTPStatusCode(documentException.getErrorCodes()), "Condition did not match document.", - restUri, RestUri.apiErrorCodes.DOCUMENT_CONDITION_NOT_MET); - } - return Response.createErrorResponse(getHTTPStatusCode(documentException.getErrorCodes()), documentException.getMessage(), restUri, - RestUri.apiErrorCodes.DOCUMENT_EXCEPTION); - } - - @Override - public VisitResult visit(RestUri restUri, String documentSelection, VisitOptions options) throws RestApiException { - VisitorParameters visitorParameters = createVisitorParameters(restUri, documentSelection, options); - - VisitorControlHandler visitorControlHandler = new VisitorControlHandler(); - visitorParameters.setControlHandler(visitorControlHandler); - LocalDataVisitorHandler localDataVisitorHandler = new LocalDataVisitorHandler(); - visitorParameters.setLocalDataHandler(localDataVisitorHandler); - - final VisitorSession visitorSession; - try { - visitorSession = documentAccess.createVisitorSession(visitorParameters); - // Not sure if this line is required - visitorControlHandler.setSession(visitorSession); - } catch (Exception e) { - throw new RestApiException(Response.createErrorResponse( - 500, - "Failed during parsing of arguments for visiting: " + ExceptionUtils.getStackTraceAsString(e), - restUri, - RestUri.apiErrorCodes.VISITOR_ERROR)); - } - try { - return doVisit(visitorControlHandler, localDataVisitorHandler, restUri); - } finally { - visitorSession.destroy(); - } - } - - private static void throwIfFatalVisitingError(VisitorControlHandler handler, RestUri restUri) throws RestApiException { - final VisitorControlHandler.Result result = handler.getResult(); - if (result.getCode() == VisitorControlHandler.CompletionCode.TIMEOUT) { - if (! handler.hasVisitedAnyBuckets()) { - throw new RestApiException(Response.createErrorResponse(500, "Timed out", restUri, RestUri.apiErrorCodes.TIME_OUT)); - } // else: some progress has been made, let client continue with new token. - } else if (result.getCode() != VisitorControlHandler.CompletionCode.SUCCESS) { - throw new RestApiException(Response.createErrorResponse(400, result.toString(), RestUri.apiErrorCodes.VISITOR_ERROR)); - } - } - - private VisitResult doVisit(VisitorControlHandler visitorControlHandler, - LocalDataVisitorHandler localDataVisitorHandler, - RestUri restUri) throws RestApiException { - try { - visitorControlHandler.waitUntilDone(); // VisitorParameters' session timeout implicitly triggers timeout failures. - throwIfFatalVisitingError(visitorControlHandler, restUri); - } catch (InterruptedException e) { - throw new RestApiException(Response.createErrorResponse(500, ExceptionUtils.getStackTraceAsString(e), restUri, RestUri.apiErrorCodes.INTERRUPTED)); - } - if (localDataVisitorHandler.getErrors().isEmpty()) { - Optional<String> continuationToken; - if (! visitorControlHandler.getProgress().isFinished()) { - continuationToken = Optional.of(visitorControlHandler.getProgress().serializeToString()); - } else { - continuationToken = Optional.empty(); - } - return new VisitResult(continuationToken, localDataVisitorHandler.getCommaSeparatedJsonDocuments()); - } - throw new RestApiException(Response.createErrorResponse(500, localDataVisitorHandler.getErrors(), restUri, RestUri.apiErrorCodes.UNSPECIFIED)); - } - - private void setRoute(SyncSession session, Optional<String> route) throws RestApiException { - if (! (session instanceof MessageBusSyncSession)) { - // Not sure if this ever could happen but better be safe. - throw new RestApiException(Response.createErrorResponse( - 400, "Can not set route since the API is not using message bus.", - RestUri.apiErrorCodes.NO_ROUTE_WHEN_NOT_PART_OF_MESSAGEBUS)); - } - ((MessageBusSyncSession) session).setRoute(route.orElse("default")); - } - - @Override - public void put(RestUri restUri, FeedOperation data, Optional<String> route) throws RestApiException { - SyncSession syncSession = syncSessions.alloc(); - Response response; - try { - Instant startTime = Instant.now(); - DocumentPut put = new DocumentPut(data.getDocument()); - put.setCondition(data.getCondition()); - setRoute(syncSession, route); - syncSession.put(put); - metricsHelper.reportSuccessful(DocumentOperationType.PUT, startTime); - return; - } catch (DocumentAccessException documentException) { - response = createErrorResponse(documentException, restUri); - } catch (Exception e) { - response = Response.createErrorResponse(500, ExceptionUtils.getStackTraceAsString(e), restUri, RestUri.apiErrorCodes.INTERNAL_EXCEPTION); - } finally { - syncSessions.free(syncSession); - } - - metricsHelper.reportFailure(DocumentOperationType.PUT, DocumentOperationStatus.fromHttpStatusCode(response.getStatus())); - throw new RestApiException(response); - } - - @Override - public void update(RestUri restUri, FeedOperation data, Optional<String> route) throws RestApiException { - SyncSession syncSession = syncSessions.alloc(); - Response response; - try { - Instant startTime = Instant.now(); - setRoute(syncSession, route); - syncSession.update(data.getDocumentUpdate()); - metricsHelper.reportSuccessful(DocumentOperationType.UPDATE, startTime); - return; - } catch (DocumentAccessException documentException) { - response = createErrorResponse(documentException, restUri); - } catch (Exception e) { - response = Response.createErrorResponse(500, ExceptionUtils.getStackTraceAsString(e), restUri, RestUri.apiErrorCodes.INTERNAL_EXCEPTION); - } finally { - syncSessions.free(syncSession); - } - - metricsHelper.reportFailure(DocumentOperationType.UPDATE, DocumentOperationStatus.fromHttpStatusCode(response.getStatus())); - throw new RestApiException(response); - } - - @Override - public void delete(RestUri restUri, String condition, Optional<String> route) throws RestApiException { - SyncSession syncSession = syncSessions.alloc(); - Response response; - try { - Instant startTime = Instant.now(); - DocumentId id = new DocumentId(restUri.generateFullId()); - DocumentRemove documentRemove = new DocumentRemove(id); - setRoute(syncSession, route); - if (condition != null && ! condition.isEmpty()) { - documentRemove.setCondition(new TestAndSetCondition(condition)); - } - syncSession.remove(documentRemove); - metricsHelper.reportSuccessful(DocumentOperationType.REMOVE, startTime); - return; - } catch (DocumentAccessException documentException) { - if (documentException.hasConditionNotMetError()) { - 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_EXCEPTION); - } - } catch (Exception e) { - response = Response.createErrorResponse(500, ExceptionUtils.getStackTraceAsString(e), restUri, RestUri.apiErrorCodes.UNSPECIFIED); - } finally { - syncSessions.free(syncSession); - } - - metricsHelper.reportFailure(DocumentOperationType.REMOVE, DocumentOperationStatus.fromHttpStatusCode(response.getStatus())); - throw new RestApiException(response); - } - - @Override - public Optional<String> get(RestUri restUri, Optional<String> fieldSet, Optional<String> cluster) throws RestApiException { - SyncSession syncSession = syncSessions.alloc(); - // Explicit unary used instead of map() due to unhandled exceptions, blargh. - Optional<String> route = cluster.isPresent() - ? Optional.of(clusterDefToRoute(resolveClusterDef(cluster, clusterEnumerator.enumerateClusters()))) - : Optional.empty(); - setRoute(syncSession, route); - try { - DocumentId id = new DocumentId(restUri.generateFullId()); - final Document document = syncSession.get(id, fieldSet.orElse(restUri.getDocumentType() + ":[document]"), DocumentProtocol.Priority.NORMAL_1); - if (document == null) { - return Optional.empty(); - } - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - JsonWriter jsonWriter = new JsonWriter(outputStream); - jsonWriter.write(document); - return Optional.of(outputStream.toString(StandardCharsets.UTF_8.name())); - - } catch (Exception e) { - throw new RestApiException(Response.createErrorResponse(500, ExceptionUtils.getStackTraceAsString(e), restUri, RestUri.apiErrorCodes.UNSPECIFIED)); - } finally { - syncSessions.free(syncSession); - } - } - - @Override - public Optional<String> get(RestUri restUri, Optional<String> fieldSet) throws RestApiException { - return get(restUri, fieldSet, Optional.empty()); - } - - @Override - public Optional<String> get(RestUri restUri) throws RestApiException { - return get(restUri, Optional.empty()); - } - - 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); - - String targetBucketSpace; - if (!restUri.isRootOnly()) { - String docType = restUri.getDocumentType(); - Optional<String> resolvedSpace = bucketSpaceResolver.clusterBucketSpaceFromDocumentType(clusterDef.getName(), 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); - } - - protected static ClusterDef resolveClusterDef(Optional<String> wantedCluster, List<ClusterDef> clusters) throws RestApiException { - if (clusters.size() == 0) { - throw new IllegalArgumentException("Your Vespa cluster does not have any content clusters " + - "declared. Visiting feature is not available."); - } - if (! wantedCluster.isPresent()) { - if (clusters.size() != 1) { - String message = "Several clusters exist: " + - clusters.stream().map(c -> "'" + c.getName() + "'").collect(Collectors.joining(", ")) + - ". You must specify one."; - throw new RestApiException(Response.createErrorResponse(400, - message, - RestUri.apiErrorCodes.SEVERAL_CLUSTERS)); - } - return clusters.get(0); - } - - for (ClusterDef clusterDef : clusters) { - if (clusterDef.getName().equals(wantedCluster.get())) { - return clusterDef; - } - } - String message = "Your vespa cluster contains the content clusters " + - clusters.stream().map(c -> "'" + c.getName() + "'").collect(Collectors.joining(", ")) + - ", not '" + wantedCluster.get() + "'. Please select a valid vespa cluster."; - throw new RestApiException(Response.createErrorResponse(400, - message, - RestUri.apiErrorCodes.MISSING_CLUSTER)); - } - - protected static String clusterDefToRoute(ClusterDef clusterDef) { - return "[Storage:cluster=" + clusterDef.getName() + ";clusterconfigid=" + clusterDef.getConfigId() + "]"; - } - - 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()) { - 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 static int computeEffectiveConcurrency(Optional<Integer> requestConcurrency) { - int wantedConcurrency = requestConcurrency.orElse(1); - return Math.min(Math.max(wantedConcurrency, 1), CONCURRENCY_UPPER_BOUND); - } - - 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(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() ? AllFields.NAME : restUri.getDocumentType() + ":[document]")); - params.setMaxBucketsPerVisitor(1); - params.setMaxPending(32); - params.setMaxFirstPassHits(1); - params.setMaxTotalHits(options.wantedDocumentCount - .map(n -> Math.min(Math.max(n, 1), WANTED_DOCUMENT_COUNT_UPPER_BOUND)) - .orElse(1)); - params.setThrottlePolicy(new StaticThrottlePolicy().setMaxPendingCount(computeEffectiveConcurrency(options.concurrency))); - params.setToTimestamp(0L); - params.setFromTimestamp(0L); - params.setSessionTimeoutMs(VISIT_TIMEOUT_MS); - - params.visitInconsistentBuckets(true); // TODO document this as part of consistency doc - - BucketSpaceRoute bucketSpaceRoute = resolveBucketSpaceRoute(options.cluster, options.bucketSpace, restUri); - params.setRoute(bucketSpaceRoute.getClusterRoute()); - params.setBucketSpace(bucketSpaceRoute.getBucketSpace()); - - params.setTraceLevel(0); - params.setPriority(DocumentProtocol.Priority.NORMAL_4); - params.setVisitRemoves(false); - - if (options.continuation.isPresent()) { - try { - params.setResumeToken(ProgressToken.fromSerializedString(options.continuation.get())); - } catch (Exception e) { - throw new RestApiException(Response.createErrorResponse(500, ExceptionUtils.getStackTraceAsString(e), restUri, RestUri.apiErrorCodes.UNSPECIFIED)); - } - } - return params; - } - -} diff --git a/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/Response.java b/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/Response.java deleted file mode 100644 index 663f77e7eea..00000000000 --- a/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/Response.java +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.document.restapi; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.yahoo.container.jdisc.HttpResponse; - -import java.io.IOException; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.util.Optional; - -public class Response extends HttpResponse { - - private final static ObjectMapper objectMapper = new ObjectMapper(); - private final String jsonMessage; - - public Response(int code, Optional<ObjectNode> element, Optional<RestUri> restPath) { - super(code); - ObjectNode objectNode = element.orElse(objectMapper.createObjectNode()); - if (restPath.isPresent()) { - objectNode.put("id", restPath.get().generateFullId()); - objectNode.put("pathId", restPath.get().getRawPath()); - } - jsonMessage = objectNode.toString(); - } - - public static Response createErrorResponse(int code, String errorMessage, RestUri.apiErrorCodes errorID) { - return createErrorResponse(code, errorMessage, null, errorID); - } - - public static Response createErrorResponse(int code, String errorMessage, RestUri restUri, RestUri.apiErrorCodes errorID) { - ObjectNode errorNode = objectMapper.createObjectNode(); - errorNode.put("description", errorID.name() + " " + errorMessage); - errorNode.put("id", errorID.value); - - ObjectNode objectNode = objectMapper.createObjectNode(); - objectNode.putArray("errors").add(errorNode); - return new Response(code, Optional.of(objectNode), Optional.ofNullable(restUri)); - } - - @Override - public void render(OutputStream stream) throws IOException { - stream.write(jsonMessage.getBytes(StandardCharsets.UTF_8)); - } - - @Override - public String getContentType() { return "application/json"; } - -} diff --git a/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/RestApiException.java b/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/RestApiException.java deleted file mode 100644 index 29843801bee..00000000000 --- a/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/RestApiException.java +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.document.restapi; - -/** - * Exceptions for Rest API - * - * @author dybis - */ -public class RestApiException extends Exception { - - private final Response response; - - public RestApiException(Response response) { - this.response = response; - } - - public Response getResponse() { - return response; - } - -} 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 deleted file mode 100644 index 975075fd2fa..00000000000 --- a/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/RestUri.java +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.document.restapi; - -import com.google.common.base.Joiner; -import com.google.common.base.Splitter; - -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Optional; -import static com.yahoo.jdisc.Response.Status.*; - -/** - * Represents the request URI with its values. - * - * @author dybis - */ -public class RestUri { - - public static final char NUMBER_STREAMING = 'n'; - public static final char GROUP_STREAMING = 'g'; - public static final String DOCUMENT = "document"; - public static final String V_1 = "v1"; - public static final String ID = "id:"; - - public enum apiErrorCodes { - ERROR_ID_BASIC_USAGE(-1), - ERROR_ID_DECODING_PATH(-2), - VISITOR_ERROR(-3), - NO_ROUTE_WHEN_NOT_PART_OF_MESSAGEBUS(-4), - SEVERAL_CLUSTERS(-5), - URL_PARSING(-6), - INVALID_CREATE_VALUE(-7), - TOO_MANY_PARALLEL_REQUESTS(-8), - MISSING_CLUSTER(-9), INTERNAL_EXCEPTION(-9), - DOCUMENT_CONDITION_NOT_MET(-10), - DOCUMENT_EXCEPTION(-11), - PARSER_ERROR(-11), - GROUP_AND_EXPRESSION_ERROR(-12), - TIME_OUT(-13), - INTERRUPTED(-14), - UNSPECIFIED(-15), - UNKNOWN_BUCKET_SPACE(-16); - - public final long value; - apiErrorCodes(long value) { - this.value = value; - } - } - - /** - * Represents the "grouping" part of document id which can be used with streaming model. - */ - public static class Group { - public final char name; - public final String value; - Group(char name, String value) { - this.name = name; - this.value = value; - } - } - private final String namespace; - private final String documentType; - private final String docId; - private Optional<Group> group = Optional.empty(); - private final String rawPath; - - public boolean isRootOnly() { - return namespace == null; - } - - public String getRawPath() { - return rawPath; - } - - public String getNamespace() { - return namespace; - } - - public String getDocumentType() { - return documentType; - } - - public String getDocId() { - return docId; - } - - public Optional<Group> getGroup() { - return group; - } - - public String generateFullId() { - return ID + namespace + ":" + documentType + ":" - + group.map(g -> String.format("%s=%s", g.name, g.value)).orElse("") - + ":" + docId; - } - - static class PathParser { - public static final long ERROR_ID_DECODING_PATH = -10L; - final List<String> rawParts; - final String originalPath; - int readPos = 0; - public PathParser(String path) { - 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); - } - String nextToken = rawParts.get(readPos++); - return urlDecodeOrException(nextToken); - } - - String restOfPath() throws RestApiException { - String rawId = Joiner.on("/").join(rawParts.listIterator(readPos)); - return urlDecodeOrException(rawId); - } - - String urlDecodeOrException(String url) throws RestApiException { - try { - return URLDecoder.decode(url, StandardCharsets.UTF_8.name()); - } catch (UnsupportedEncodingException e) { - throw new RestApiException(Response.createErrorResponse(BAD_REQUEST,"Problems decoding the URI: " + e.getMessage(), apiErrorCodes.ERROR_ID_DECODING_PATH)); - } - } - } - - public RestUri(URI uri) throws RestApiException { - rawPath = uri.getRawPath(); - PathParser pathParser = new PathParser(rawPath); - if (! pathParser.nextTokenOrException().equals("") || - ! pathParser.nextTokenOrException().equals(DOCUMENT) || - ! pathParser.nextTokenOrException().equals(V_1)) { - throwUsage(uri.getRawPath()); - } - // 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": - group = Optional.of(new Group(NUMBER_STREAMING, pathParser.nextTokenOrException())); - break; - case "docid": - group = Optional.empty(); - break; - case "group": - group = Optional.of(new Group(GROUP_STREAMING, pathParser.nextTokenOrException())); - break; - default: throwUsage(uri.getRawPath()); - } - docId = pathParser.restOfPath(); - } - - private static void throwUsage(String inputPath) throws RestApiException { - throw new RestApiException(Response.createErrorResponse(BAD_REQUEST, - "Expected: " + - ".../{namespace}/{document-type}/group/{name}/[{user-specified}] " + - ".../{namespace}/{document-type}/docid/[{user-specified}] : but got " + inputPath, apiErrorCodes.ERROR_ID_BASIC_USAGE)); - } - -} - 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 e603a150b34..bd63a2ecbfc 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 @@ -1,443 +1,26 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.document.restapi.resource; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.inject.Inject; -import com.yahoo.cloud.config.ClusterListConfig; -import com.yahoo.container.handler.ThreadpoolConfig; -import com.yahoo.container.handler.threadpool.ContainerThreadPool; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.container.jdisc.LoggingRequestHandler; -import com.yahoo.container.logging.AccessLog; -import com.yahoo.document.DocumentTypeManager; -import com.yahoo.document.TestAndSetCondition; -import com.yahoo.document.config.DocumentmanagerConfig; -import com.yahoo.document.json.SingleDocumentParser; -import com.yahoo.document.restapi.OperationHandler; -import com.yahoo.document.restapi.OperationHandlerImpl; -import com.yahoo.document.restapi.Response; -import com.yahoo.document.restapi.RestApiException; -import com.yahoo.document.restapi.RestUri; -import com.yahoo.document.select.DocumentSelector; -import com.yahoo.document.select.parser.ParseException; -import com.yahoo.documentapi.DocumentAccess; -import com.yahoo.documentapi.messagebus.MessageBusDocumentAccess; -import com.yahoo.documentapi.messagebus.MessageBusParams; -import com.yahoo.documentapi.messagebus.loadtypes.LoadTypeSet; -import com.yahoo.jdisc.Metric; -import com.yahoo.metrics.simple.MetricReceiver; -import com.yahoo.text.Text; -import com.yahoo.vespa.config.content.AllClustersBucketSpacesConfig; -import com.yahoo.vespa.config.content.LoadTypeConfig; -import com.yahoo.vespaclient.ClusterDef; -import com.yahoo.vespaclient.ClusterList; -import com.yahoo.vespaxmlparser.DocumentFeedOperation; -import com.yahoo.vespaxmlparser.FeedOperation; -import com.yahoo.yolean.Exceptions; - -import java.io.IOException; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.Level; - -import static com.yahoo.jdisc.Response.Status.BAD_REQUEST; /** - * API for handling single operation on a document and visiting. + * Dummy for internal use. * - * @author Haakon Dybdahl + * @author jonmv */ public class RestApi extends LoggingRequestHandler { - private static final String CREATE_PARAMETER_NAME = "create"; - private static final String CONDITION_PARAMETER_NAME = "condition"; - private static final String ROUTE_PARAMETER_NAME = "route"; - private static final String DOCUMENTS = "documents"; - private static final String FIELDS = "fields"; - private static final String DOC_ID_NAME = "id"; - private static final String PATH_NAME = "pathId"; - private static final String SELECTION = "selection"; - private static final String CLUSTER = "cluster"; - private static final String CONTINUATION = "continuation"; - 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; - private final ObjectMapper mapper = new ObjectMapper(); - private final AtomicInteger threadsAvailableForApi; - @Inject - public RestApi(ContainerThreadPool threadpool, - AccessLog accessLog, - Metric metric, - DocumentmanagerConfig documentManagerConfig, - LoadTypeConfig loadTypeConfig, - ThreadpoolConfig threadpoolConfig, - AllClustersBucketSpacesConfig bucketSpacesConfig, - ClusterListConfig clusterListConfig, - MetricReceiver metricReceiver) { - super(threadpool.executor(), accessLog, metric); - MessageBusParams params = new MessageBusParams(new LoadTypeSet(loadTypeConfig)); - params.setDocumentmanagerConfig(documentManagerConfig); - this.operationHandler = new OperationHandlerImpl(new MessageBusDocumentAccess(params), - fixedClusterEnumeratorFromConfig(clusterListConfig), - fixedBucketSpaceResolverFromConfig(bucketSpacesConfig), - metricReceiver); - this.singleDocumentParser = new SingleDocumentParser(new DocumentTypeManager(documentManagerConfig)); - // 40% of the threads can be blocked before we deny requests. - if (threadpoolConfig != null) { - threadsAvailableForApi = new AtomicInteger(Math.max((int) (0.4 * threadpoolConfig.maxthreads()), 1)); - } else { - log.warning("No config for threadpool, using 200 for max blocking threads for document rest API."); - threadsAvailableForApi = new AtomicInteger(200); - } - } - - // For testing and development - RestApi(Executor executor, AccessLog accessLog, OperationHandler operationHandler, int threadsAvailable) { - super(executor, accessLog, null); - this.operationHandler = operationHandler; - this.threadsAvailableForApi = new AtomicInteger(threadsAvailable); - } - - @Override - public void destroy() { - operationHandler.shutdown(); - } - - // For testing and development - protected void setDocTypeManagerForTests(DocumentTypeManager docTypeManager) { - this.singleDocumentParser = new SingleDocumentParser(docTypeManager); - } - - private static OperationHandlerImpl.ClusterEnumerator fixedClusterEnumeratorFromConfig(ClusterListConfig config) { - List<ClusterDef> clusters = Collections.unmodifiableList(new ClusterList(config).getStorageClusters()); - return () -> clusters; - } - - private static OperationHandlerImpl.BucketSpaceResolver fixedBucketSpaceResolverFromConfig(AllClustersBucketSpacesConfig bucketSpacesConfig) { - return (clusterId, docType) -> - Optional.ofNullable(bucketSpacesConfig.cluster(clusterId)) - .map(cluster -> cluster.documentType(docType)) - .map(type -> type.bucketSpace()); - } - - private static Optional<String> requestProperty(String parameter, HttpRequest request) { - String property = request.getProperty(parameter); - if (property != null && ! property.isEmpty()) { - return Optional.of(property); - } - return Optional.empty(); - } - - private static boolean parseBooleanStrict(String value) { - if ("true".equalsIgnoreCase(value)) { - return true; - } else if ("false".equalsIgnoreCase(value)) { - return false; - } - throw new IllegalArgumentException(String.format("Value not convertible to bool: '%s'", value)); - } - - private static Optional<Boolean> parseBoolean(String parameter, HttpRequest request) { - try { - Optional<String> property = requestProperty(parameter, request); - return property.map(RestApi::parseBooleanStrict); - } - catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Invalid value for '" + parameter + "' parameter: " + - "Must be empty, true, or false but was '" + - request.getProperty(parameter) + "'"); - } - } - - private static int parsePositiveInt(String str) throws NumberFormatException { - int parsed = Integer.parseInt(str); - if (parsed <= 0) { - throw new IllegalArgumentException("Parsed number was negative or zero"); - } - return parsed; + public RestApi() { + super(ignored -> { throw new IllegalStateException("Not supposed to handle anything"); }, null, null); } @Override public HttpResponse handle(HttpRequest request) { - try { - if (threadsAvailableForApi.decrementAndGet() < 1) { - return Response.createErrorResponse(429 /* Too Many Requests */, - "Too many parallel requests, consider using http-vespa-java-client. Please try again later.", - RestUri.apiErrorCodes.TOO_MANY_PARALLEL_REQUESTS); - } - return handleInternal(request); - } finally { - threadsAvailableForApi.incrementAndGet(); - } - } - - 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) { - RestUri restUri = null; - try { - restUri = new RestUri(request.getUri()); - validateUriStructureForRequestMethod(restUri, request.getMethod()); - - Optional<Boolean> create; - try { - create = parseBoolean(CREATE_PARAMETER_NAME, request); - } - catch (IllegalArgumentException e) { - return Response.createErrorResponse(400, e.getMessage(), RestUri.apiErrorCodes.INVALID_CREATE_VALUE); - } - - String condition = request.getProperty(CONDITION_PARAMETER_NAME); - Optional<String> route = Optional.ofNullable(nonEmpty(request.getProperty(ROUTE_PARAMETER_NAME), ROUTE_PARAMETER_NAME)); - - Optional<ObjectNode> resultJson = Optional.empty(); - switch (request.getMethod()) { - case GET: // Vespa Visit/Get - return isVisitRequestUri(restUri) ? handleVisit(restUri, request) : handleGet(restUri, request); - case POST: // Vespa Put - operationHandler.put(restUri, createPutOperation(request, restUri.generateFullId(), condition), route); - break; - case PUT: // Vespa Update - operationHandler.update(restUri, createUpdateOperation(request, restUri.generateFullId(), condition, create), route); - break; - case DELETE: // Vespa Delete - operationHandler.delete(restUri, condition, route); - break; - default: - return new Response(405, Optional.empty(), Optional.of(restUri)); - } - return new Response(200, resultJson, Optional.of(restUri)); - } - catch (RestApiException e) { - return e.getResponse(); - } - catch (IllegalArgumentException userException) { - return Response.createErrorResponse(400, Exceptions.toMessageString(userException), - restUri, - RestUri.apiErrorCodes.PARSER_ERROR); - } - catch (RuntimeException systemException) { - log.log(Level.WARNING, "Internal runtime exception during Document V1 request handling", systemException); - return Response.createErrorResponse(500, Exceptions.toMessageString(systemException), - restUri, - RestUri.apiErrorCodes.UNSPECIFIED); - } - } - - private FeedOperation createPutOperation(HttpRequest request, String id, String condition) { - FeedOperation put = singleDocumentParser.parsePut(request.getData(), id); - if (condition != null && ! condition.isEmpty()) { - return new DocumentFeedOperation(put.getDocument(), new TestAndSetCondition(condition)); - } - return put; + throw new IllegalStateException("Not supposed to handle anything"); } - private FeedOperation createUpdateOperation(HttpRequest request, String id, String condition, Optional<Boolean> create) { - FeedOperation update = singleDocumentParser.parseUpdate(request.getData(), id); - if (condition != null && ! condition.isEmpty()) { - update.getDocumentUpdate().setCondition(new TestAndSetCondition(condition)); - } - create.ifPresent(c -> update.getDocumentUpdate().setCreateIfNonExistent(c)); - return update; - } - - private HttpResponse handleGet(RestUri restUri, HttpRequest request) throws RestApiException { - final Optional<String> fieldSet = requestProperty(FIELD_SET, request); - final Optional<String> cluster = requestProperty(CLUSTER, request); - final Optional<String> getDocument = operationHandler.get(restUri, fieldSet, cluster); - final ObjectNode resultNode = mapper.createObjectNode(); - if (getDocument.isPresent()) { - final JsonNode parseNode; - try { - parseNode = mapper.readTree(getDocument.get()); - } catch (IOException e) { - throw new RuntimeException("Failed while parsing my own results", e); - } - resultNode.putPOJO(FIELDS, parseNode.get(FIELDS)); - } - resultNode.put(DOC_ID_NAME, restUri.generateFullId()); - resultNode.put(PATH_NAME, restUri.getRawPath()); - - return new HttpResponse(getDocument.isPresent() ? 200 : 404) { - @Override - public String getContentType() { return APPLICATION_JSON; } - @Override - public void render(OutputStream outputStream) throws IOException { - outputStream.write(resultNode.toString().getBytes(StandardCharsets.UTF_8.name())); - } - }; - } - - private static HttpResponse createInvalidParameterResponse(String parameter, String explanation) { - return Response.createErrorResponse(400, String.format("Invalid '%s' value. %s", parameter, explanation), RestUri.apiErrorCodes.UNSPECIFIED); - } - - static class BadRequestParameterException extends IllegalArgumentException { - private String parameter; - - BadRequestParameterException(String parameter, String message) { - super(message); - this.parameter = parameter; - } - - String getParameter() { - return parameter; - } - } - - private static Optional<Integer> parsePositiveIntegerRequestParameter(String parameter, HttpRequest request) { - Optional<String> property = requestProperty(parameter, request); - if (!property.isPresent()) { - return Optional.empty(); - } - try { - return property.map(RestApi::parsePositiveInt); - } catch (IllegalArgumentException e) { - throw new BadRequestParameterException(parameter, "Expected positive integer"); - } - } - - private static OperationHandler.VisitOptions visitOptionsFromRequest(HttpRequest request) { - final OperationHandler.VisitOptions.Builder optionsBuilder = OperationHandler.VisitOptions.builder(); - - 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)); - - return optionsBuilder.build(); - } - - /** - * Escapes all single quotes in input string. - * @param original non-escaped string that may contain single quotes - * @return original if no quotes to escaped were found, otherwise a quote-escaped string - */ - private static String singleQuoteEscapedString(String original) { - if (original.indexOf('\'') == -1) { - return original; - } - StringBuilder builder = new StringBuilder(original.length() + 1); - for (int i = 0; i < original.length(); ++i) { - char c = original.charAt(i); - if (c != '\'') { - builder.append(c); - } else { - builder.append("\\'"); - } - } - return builder.toString(); - } - - - private String nonEmpty(String value, String name) { - if (value != null && value.isEmpty()) - throw new IllegalArgumentException("'" + name + "' cannot be empty"); - return value; - } - - private static long parseAndValidateVisitNumericId(String value) { - try { - return Long.parseLong(value); - } catch (NumberFormatException e) { - throw new BadRequestParameterException(SELECTION, "Failed to parse numeric part of selection URI"); - } - } - - private static String validateAndBuildLocationSubExpression(RestUri.Group group) { - if (group.name == 'n') { - return String.format("id.user==%d", parseAndValidateVisitNumericId(group.value)); - } else { - // Cannot feed documents with groups that don't pass this test, so it makes sense - // to enforce this symmetry when trying to retrieve them as well. - Text.validateTextString(group.value).ifPresent(codepoint -> { - throw new BadRequestParameterException(SELECTION, String.format( - "Failed to parse group part of selection URI; contains invalid text code point U%04X", codepoint)); - }); - return String.format("id.group=='%s'", singleQuoteEscapedString(group.value)); - } - } - - private static void validateDocumentSelectionSyntax(String expression) { - try { - new DocumentSelector(expression); - } catch (ParseException e) { - throw new BadRequestParameterException(SELECTION, String.format("Failed to parse expression given in 'selection'" + - " parameter. Must be a complete and valid sub-expression. Error: %s", e.getMessage())); - } - } - - private static String documentSelectionFromRequest(RestUri restUri, HttpRequest request) throws BadRequestParameterException { - String documentSelection = Optional.ofNullable(request.getProperty(SELECTION)).orElse(""); - if (!documentSelection.isEmpty()) { - // Ensure that the selection parameter sub-expression is complete and valid by itself. - validateDocumentSelectionSyntax(documentSelection); - } - if (restUri.getGroup().isPresent() && ! restUri.getGroup().get().value.isEmpty()) { - String locationSubExpression = validateAndBuildLocationSubExpression(restUri.getGroup().get()); - if (documentSelection.isEmpty()) { - documentSelection = locationSubExpression; - } else { - documentSelection = String.format("%s and (%s)", locationSubExpression, documentSelection); - } - } - return documentSelection; - } - - private HttpResponse handleVisit(RestUri restUri, HttpRequest request) throws RestApiException { - String documentSelection; - OperationHandler.VisitOptions options; - try { - documentSelection = documentSelectionFromRequest(restUri, request); - options = visitOptionsFromRequest(request); - } catch (BadRequestParameterException e) { - return createInvalidParameterResponse(e.getParameter(), e.getMessage()); - } - OperationHandler.VisitResult visit = operationHandler.visit(restUri, documentSelection, options); - ObjectNode resultNode = mapper.createObjectNode(); - visit.token.ifPresent(t -> resultNode.put(CONTINUATION, t)); - resultNode.putArray(DOCUMENTS).addPOJO(visit.documentsAsJsonList); - resultNode.put(PATH_NAME, restUri.getRawPath()); - - HttpResponse httpResponse = new HttpResponse(200) { - @Override - public String getContentType() { return APPLICATION_JSON; } - @Override - public void render(OutputStream outputStream) throws IOException { - try { - outputStream.write(resultNode.toString().getBytes(StandardCharsets.UTF_8)); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - }; - return httpResponse; - } } diff --git a/vespaclient-container-plugin/src/main/java/com/yahoo/documentapi/metrics/DocumentOperationStatus.java b/vespaclient-container-plugin/src/main/java/com/yahoo/documentapi/metrics/DocumentOperationStatus.java index f0529f3d55a..c665eca1cac 100644 --- a/vespaclient-container-plugin/src/main/java/com/yahoo/documentapi/metrics/DocumentOperationStatus.java +++ b/vespaclient-container-plugin/src/main/java/com/yahoo/documentapi/metrics/DocumentOperationStatus.java @@ -1,7 +1,7 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.documentapi.metrics; -import com.yahoo.document.restapi.OperationHandlerImpl; +import com.yahoo.documentapi.messagebus.protocol.DocumentProtocol; import java.util.Set; @@ -29,7 +29,10 @@ public enum DocumentOperationStatus { } public static DocumentOperationStatus fromMessageBusErrorCodes(Set<Integer> errorCodes) { - return fromHttpStatusCode(OperationHandlerImpl.getHTTPStatusCode(errorCodes)); + if (errorCodes.size() == 1 && errorCodes.contains(DocumentProtocol.ERROR_NO_SPACE)) + return SERVER_ERROR; + + return REQUEST_ERROR; } } |