summaryrefslogtreecommitdiffstats
path: root/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
committerJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
commit72231250ed81e10d66bfe70701e64fa5fe50f712 (patch)
tree2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi
Publish
Diffstat (limited to 'vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi')
-rw-r--r--vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/LocalDataVisitorHandler.java69
-rw-r--r--vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/OperationHandler.java39
-rw-r--r--vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/OperationHandlerImpl.java260
-rw-r--r--vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/Response.java48
-rw-r--r--vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/RestApiException.java21
-rw-r--r--vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/RestUri.java128
-rw-r--r--vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/resource/RestApi.java229
-rw-r--r--vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/resource/package-info.java5
8 files changed, 799 insertions, 0 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
new file mode 100644
index 00000000000..4dc47f20889
--- /dev/null
+++ b/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/LocalDataVisitorHandler.java
@@ -0,0 +1,69 @@
+// Copyright 2016 Yahoo Inc. 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 org.apache.commons.lang3.exception.ExceptionUtils;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Handling data from visit.
+ *
+ * @author dybdahl
+ */
+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.getStackTrace(e)).append("\n");
+ }
+ }
+ }
+
+ // TODO: Not sure if we should support removeal 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.getStackTrace(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
new file mode 100644
index 00000000000..240cc3c3c61
--- /dev/null
+++ b/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/OperationHandler.java
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. 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.VespaXMLFeedReader;
+
+import java.util.Optional;
+
+/**
+ * Abstract the backend stuff for the REST API, such as retrieving or updating documents.
+ *
+ * @author 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;
+ }
+ }
+
+ VisitResult visit(RestUri restUri, String documentSelection, Optional<String> cluster, Optional<String> continuation) throws RestApiException;
+
+ void put(RestUri restUri, VespaXMLFeedReader.Operation data) throws RestApiException;
+
+ void update(RestUri restUri, VespaXMLFeedReader.Operation data) throws RestApiException;
+
+ void delete(RestUri restUri, String condition) throws RestApiException;
+
+ Optional<String> get(RestUri restUri) throws RestApiException;
+
+ /** 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
new file mode 100644
index 00000000000..21fd930e9d5
--- /dev/null
+++ b/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/OperationHandlerImpl.java
@@ -0,0 +1,260 @@
+// Copyright 2016 Yahoo Inc. 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.TestAndSetCondition;
+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.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.protocol.DocumentProtocol;
+import com.yahoo.messagebus.StaticThrottlePolicy;
+import com.yahoo.storage.searcher.ContinuationHit;
+import com.yahoo.vdslib.VisitorOrdering;
+import com.yahoo.vespaclient.ClusterDef;
+import com.yahoo.vespaclient.ClusterList;
+import com.yahoo.vespaxmlparser.VespaXMLFeedReader;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Sends operations to messagebus via document api.
+ *
+ * @author dybdahl
+ */
+public class OperationHandlerImpl implements OperationHandler {
+
+ public static final int VISIT_TIMEOUT_MS = 120000;
+ private final DocumentAccess documentAccess;
+
+ public OperationHandlerImpl(DocumentAccess documentAccess) {
+ this.documentAccess = documentAccess;
+ }
+
+ @Override
+ public void shutdown() {
+ documentAccess.shutdown();
+ }
+
+ private static final int HTTP_STATUS_BAD_REQUEST = 400;
+ private static final int HTTP_STATUS_INSUFFICIENT_STORAGE = 507;
+
+ private static int getHTTPStatusCode(Set<Integer> errorCodes) {
+ if (errorCodes.size() == 1 && errorCodes.contains(DocumentProtocol.ERROR_NO_SPACE)) {
+ return HTTP_STATUS_INSUFFICIENT_STORAGE;
+ }
+ return HTTP_STATUS_BAD_REQUEST;
+ }
+
+ private static Response createErrorResponse(DocumentAccessException documentException, RestUri restUri) {
+ return Response.createErrorResponse(getHTTPStatusCode(documentException.getErrorCodes()), documentException.getMessage(), restUri);
+ }
+
+ @Override
+ public VisitResult visit(
+ RestUri restUri,
+ String documentSelection,
+ Optional<String> cluster,
+ Optional<String> continuation) throws RestApiException {
+
+ VisitorParameters visitorParameters = createVisitorParameters(restUri, documentSelection, cluster, continuation);
+
+ 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.getStackTrace(e),
+ restUri));
+ }
+ try {
+ if (! visitorControlHandler.waitUntilDone(VISIT_TIMEOUT_MS)) {
+ throw new RestApiException(Response.createErrorResponse(500, "Timed out", restUri));
+ }
+ if (visitorControlHandler.getResult().code != VisitorControlHandler.CompletionCode.SUCCESS) {
+ throw new RestApiException(Response.createErrorResponse(400, visitorControlHandler.getResult().toString()));
+ }
+ } catch (InterruptedException e) {
+ throw new RestApiException(Response.createErrorResponse(500, ExceptionUtils.getStackTrace(e), restUri));
+ }
+ if (localDataVisitorHandler.getErrors().isEmpty()) {
+ final Optional<String> continuationToken;
+ if (! visitorControlHandler.getProgress().isFinished()) {
+ final ContinuationHit continuationHit = new ContinuationHit(visitorControlHandler.getProgress());
+ continuationToken = Optional.of(continuationHit.getValue());
+ } else {
+ continuationToken = Optional.empty();
+ }
+ return new VisitResult(continuationToken, localDataVisitorHandler.getCommaSeparatedJsonDocuments());
+ }
+ throw new RestApiException(Response.createErrorResponse(500, localDataVisitorHandler.getErrors(), restUri));
+ }
+
+ @Override
+ public void put(RestUri restUri, VespaXMLFeedReader.Operation data) throws RestApiException {
+ try {
+ SyncSession syncSession = documentAccess.createSyncSession(new SyncParameters());
+ DocumentPut put = new DocumentPut(data.getDocument());
+ put.setCondition(data.getCondition());
+ syncSession.put(put);
+ } catch (DocumentAccessException documentException) {
+ throw new RestApiException(createErrorResponse(documentException, restUri));
+ } catch (Exception e) {
+ throw new RestApiException(Response.createErrorResponse(500, ExceptionUtils.getStackTrace(e), restUri));
+ }
+ }
+
+ @Override
+ public void update(RestUri restUri, VespaXMLFeedReader.Operation data) throws RestApiException {
+ try {
+ SyncSession syncSession = documentAccess.createSyncSession(new SyncParameters());
+ syncSession.update(data.getDocumentUpdate());
+ } catch (DocumentAccessException documentException) {
+ throw new RestApiException(createErrorResponse(documentException, restUri));
+ } catch (Exception e) {
+ throw new RestApiException(Response.createErrorResponse(500, ExceptionUtils.getStackTrace(e), restUri));
+ }
+ }
+
+ @Override
+ public void delete(RestUri restUri, String condition) throws RestApiException {
+ try {
+ DocumentId id = new DocumentId(restUri.generateFullId());
+ SyncSession syncSession = documentAccess.createSyncSession(new SyncParameters());
+ DocumentRemove documentRemove = new DocumentRemove(id);
+ if (condition != null && ! condition.isEmpty()) {
+ documentRemove.setCondition(new TestAndSetCondition(condition));
+ }
+ syncSession.remove(documentRemove);
+ } catch (DocumentAccessException documentException) {
+ throw new RestApiException(Response.createErrorResponse(400, documentException.getMessage(), restUri));
+ } catch (Exception e) {
+ throw new RestApiException(Response.createErrorResponse(500, ExceptionUtils.getStackTrace(e), restUri));
+ }
+ }
+
+ @Override
+ public Optional<String> get(RestUri restUri) throws RestApiException {
+ try {
+ DocumentId id = new DocumentId(restUri.generateFullId());
+ SyncSession syncSession = documentAccess.createSyncSession(new SyncParameters());
+ final Document document = syncSession.get(id);
+ 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.getStackTrace(e), restUri));
+ }
+ }
+
+ private static String resolveClusterRoute(Optional<String> wantedCluster) throws RestApiException {
+ List<ClusterDef> clusters = new ClusterList("client").getStorageClusters();
+ return resolveClusterRoute(wantedCluster, clusters);
+ }
+
+ // Based on resolveClusterRoute in VdsVisit, protected for testability
+ protected static String resolveClusterRoute(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) {
+ new RestApiException(Response.createErrorResponse(400, "Several clusters exist: " +
+ clusterListToString(clusters) + " you must specify one.. "));
+ }
+ return clusterDefToRoute(clusters.get(0));
+ }
+
+ for (ClusterDef clusterDef : clusters) {
+ if (clusterDef.getName().equals(wantedCluster.get())) {
+ return clusterDefToRoute(clusterDef);
+ }
+ }
+ throw new RestApiException(Response.createErrorResponse(400, "Your vespa cluster contains the content clusters " +
+ clusterListToString(clusters) + " not " + wantedCluster.get() + ". Please select a valid vespa cluster."));
+
+ }
+
+ private static String clusterDefToRoute(ClusterDef clusterDef) {
+ return "[Storage:cluster=" + clusterDef.getName() + ";clusterconfigid=" + clusterDef.getConfigId() + "]";
+ }
+
+ private static String clusterListToString(List<ClusterDef> clusters) {
+ StringBuilder clusterListString = new StringBuilder();
+ clusters.forEach(x -> clusterListString.append(x.getName()).append(" (").append(x.getConfigId()).append("), "));
+ return clusterListString.toString();
+ }
+
+ private VisitorParameters createVisitorParameters(
+ RestUri restUri,
+ String documentSelection,
+ Optional<String> clusterName,
+ Optional<String> continuation)
+ throws RestApiException {
+
+ 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(")");
+ }
+
+ VisitorParameters params = new VisitorParameters(selection.toString());
+
+ params.setMaxBucketsPerVisitor(1);
+ params.setMaxPending(32);
+ params.setMaxFirstPassHits(1);
+ params.setMaxTotalHits(10);
+ params.setThrottlePolicy(new StaticThrottlePolicy().setMaxPendingCount(1));
+ params.setToTimestamp(0L);
+ params.setFromTimestamp(0L);
+
+ params.visitInconsistentBuckets(true);
+ params.setVisitorOrdering(VisitorOrdering.ASCENDING);
+
+ params.setRoute(resolveClusterRoute(clusterName));
+
+ params.setTraceLevel(0);
+ params.setPriority(DocumentProtocol.Priority.NORMAL_4);
+ params.setVisitRemoves(false);
+
+ if (continuation.isPresent()) {
+ try {
+ params.setResumeToken(ContinuationHit.getToken(continuation.get()));
+ } catch (Exception e) {
+ throw new RestApiException(Response.createErrorResponse(500, ExceptionUtils.getStackTrace(e), restUri));
+ }
+ }
+ 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
new file mode 100644
index 00000000000..9c846e9ce38
--- /dev/null
+++ b/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/Response.java
@@ -0,0 +1,48 @@
+// Copyright 2016 Yahoo Inc. 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) {
+ ObjectNode objectNode = objectMapper.createObjectNode();
+ objectNode.putArray("errors").add(errorMessage);
+ return new Response(code, Optional.of(objectNode), Optional.<RestUri>empty());
+ }
+
+ public static Response createErrorResponse(int code, String errorMessage, RestUri restUri) {
+ ObjectNode objectNode = objectMapper.createObjectNode();
+ objectNode.putArray("errors").add(errorMessage);
+ return new Response(code, Optional.of(objectNode), Optional.of(restUri));
+ }
+
+ @Override
+ public void render(OutputStream stream) throws IOException {
+ stream.write(jsonMessage.getBytes(StandardCharsets.UTF_8));
+ }
+
+ @Override
+ public String getContentType() { return "application/json"; }
+
+} \ No newline at end of file
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
new file mode 100644
index 00000000000..b553d83d848
--- /dev/null
+++ b/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/RestApiException.java
@@ -0,0 +1,21 @@
+// Copyright 2016 Yahoo Inc. 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 dybdahl
+ */
+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
new file mode 100644
index 00000000000..98293508168
--- /dev/null
+++ b/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/RestUri.java
@@ -0,0 +1,128 @@
+// Copyright 2016 Yahoo Inc. 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 dybdahl
+ */
+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:";
+
+ /**
+ * 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 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 + ":"
+ + (getGroup().isPresent() ? group.get().name + "=" + group.get().value : "")
+ + ":" + docId;
+ }
+
+ static class PathParser {
+ final List<String> rawParts;
+ final String originalPath;
+ int readPos = 0;
+ public PathParser(String path) {
+ this.originalPath = path;
+ this.rawParts = Splitter.on('/').splitToList(path);
+ }
+ String nextTokenOrException() throws RestApiException {
+ if (readPos >= rawParts.size()) {
+ throwUsage(originalPath);
+ }
+ return rawParts.get(readPos++);
+ }
+
+ String restOfPath() throws RestApiException {
+ String rawId = Joiner.on("/").join(rawParts.listIterator(readPos));
+ try {
+ return URLDecoder.decode(rawId, StandardCharsets.UTF_8.name());
+ } catch (UnsupportedEncodingException e) {
+ throw new RestApiException(Response.createErrorResponse(BAD_REQUEST,"Problems decoding the URI: " + e.getMessage()));
+ }
+ }
+ }
+
+ 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());
+ }
+ namespace = pathParser.nextTokenOrException();
+ 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:\n" +
+ ".../{namespace}/{document-type}/group/{name}/[{user-specified}]\n" +
+ ".../{namespace}/{document-type}/docid/[{user-specified}]\n: but got " + inputPath));
+ }
+
+}
+
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
new file mode 100644
index 00000000000..8b863a69f5c
--- /dev/null
+++ b/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/resource/RestApi.java
@@ -0,0 +1,229 @@
+// Copyright 2016 Yahoo Inc. 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.google.inject.Inject;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+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.documentapi.messagebus.MessageBusDocumentAccess;
+import com.yahoo.documentapi.messagebus.MessageBusParams;
+import com.yahoo.documentapi.messagebus.loadtypes.LoadTypeSet;
+import com.yahoo.vespaxmlparser.VespaXMLFeedReader;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * API for handling single operation on a document and visiting.
+ *
+ * @author dybdahl
+ */
+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 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 final OperationHandler operationHandler;
+ private SingleDocumentParser singleDocumentParser;
+ private ObjectMapper mapper = new ObjectMapper();
+ private AtomicInteger threadsAvailableForApi = new AtomicInteger(20 /*max concurrent requests */);
+
+ @Inject
+ public RestApi(Executor executor, AccessLog accessLog, DocumentmanagerConfig documentManagerConfig) {
+ super(executor, accessLog);
+ final LoadTypeSet loadTypes = new LoadTypeSet("client");
+ this.operationHandler = new OperationHandlerImpl(new MessageBusDocumentAccess(new MessageBusParams(loadTypes)));
+ this.singleDocumentParser = new SingleDocumentParser(new DocumentTypeManager(documentManagerConfig));
+ }
+
+ // For testing and development
+ public RestApi(
+ Executor executor,
+ AccessLog accessLog,
+ OperationHandler operationHandler) {
+ super(executor, accessLog);
+ this.operationHandler = operationHandler;
+ }
+
+ @Override
+ public void destroy() {
+ operationHandler.shutdown();
+ }
+
+ // For testing and development
+ protected void setDocTypeManagerForTests(DocumentTypeManager docTypeManager) {
+ this.singleDocumentParser = new SingleDocumentParser(docTypeManager);
+ }
+
+ // Returns null if invalid value.
+ private Optional<Boolean> parseBoolean(String parameter, HttpRequest request) {
+ final String property = request.getProperty(parameter);
+ if (property != null && ! property.isEmpty()) {
+ switch (property) {
+ case "true" : return Optional.of(true);
+ case "false": return Optional.of(false);
+ default : return null;
+ }
+ }
+ return Optional.empty();
+ }
+
+ @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.");
+ }
+ return handleInternal(request);
+ } finally {
+ threadsAvailableForApi.incrementAndGet();
+ }
+ }
+
+ // protected for testing
+ protected HttpResponse handleInternal(HttpRequest request) {
+ final RestUri restUri;
+ try {
+ restUri = new RestUri(request.getUri());
+ } catch (RestApiException e) {
+ return e.getResponse();
+ } catch (Exception e2) {
+ return Response.createErrorResponse(500, "Exception while parsing URI: " + e2.getMessage());
+ }
+
+ Optional<Boolean> create = parseBoolean(CREATE_PARAMETER_NAME, request);
+ if (create == null) {
+ return Response.createErrorResponse(403, "Non valid value for 'create' parameter, must be empty, true, or " +
+ "false: " + request.getProperty(CREATE_PARAMETER_NAME));
+ }
+ String condition = request.getProperty(CONDITION_PARAMETER_NAME);
+ Optional<ObjectNode> resultJson = Optional.empty();
+ try {
+ switch (request.getMethod()) {
+ case GET: // Vespa Visit/Get
+ return restUri.getDocId().isEmpty() ? handleVisit(restUri, request) : handleGet(restUri);
+ case POST: // Vespa Put
+ operationHandler.put(restUri, createPutOperation(request, restUri.generateFullId(), condition));
+ break;
+ case PUT: // Vespa Update
+ operationHandler.update(restUri, createUpdateOperation(request, restUri.generateFullId(), condition, create));
+ break;
+ case DELETE: // Vespa Delete
+ operationHandler.delete(restUri, condition);
+ break;
+ default:
+ return new Response(405, Optional.empty(), Optional.of(restUri));
+ }
+ } catch (RestApiException e) {
+ return e.getResponse();
+ } catch (Exception e2) {
+ // We always blame the user. This might be a bit nasty, but the parser throws various kind of exception
+ // types, but with nice descriptions.
+ return Response.createErrorResponse(400, e2.getMessage(), restUri);
+ }
+ return new Response(200, resultJson, Optional.of(restUri));
+ }
+
+ private VespaXMLFeedReader.Operation createPutOperation(HttpRequest request, String id, String condition) {
+ final VespaXMLFeedReader.Operation operationPut =
+ singleDocumentParser.parsePut(request.getData(), id);
+ if (condition != null && ! condition.isEmpty()) {
+ operationPut.setCondition(new TestAndSetCondition(condition));
+ }
+ return operationPut;
+ }
+
+ private VespaXMLFeedReader.Operation createUpdateOperation(HttpRequest request, String id, String condition, Optional<Boolean> create) {
+ final VespaXMLFeedReader.Operation operationUpdate =
+ singleDocumentParser.parseUpdate(request.getData(), id);
+ if (condition != null && ! condition.isEmpty()) {
+ operationUpdate.getDocumentUpdate().setCondition(new TestAndSetCondition(condition));
+ }
+ if (create.isPresent()) {
+ operationUpdate.getDocumentUpdate().setCreateIfNonExistent(create.get());
+ }
+ return operationUpdate;
+ }
+
+ private HttpResponse handleGet(RestUri restUri) throws RestApiException {
+ final Optional<String> getDocument = operationHandler.get(restUri);
+ 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 void render(OutputStream outputStream) throws IOException {
+ outputStream.write(resultNode.toString().getBytes(StandardCharsets.UTF_8.name()));
+ }
+ };
+ }
+
+ private HttpResponse handleVisit(RestUri restUri, HttpRequest request) throws RestApiException {
+ if (restUri.getGroup().isPresent() && ! restUri.getGroup().get().value.isEmpty()) {
+ return Response.createErrorResponse(
+ 400,
+ "Visiting does not support setting value for group/value, try using expression parameter instead.",
+ restUri);
+
+ }
+ String documentSelection = Optional.ofNullable(request.getProperty(SELECTION)).orElse("");
+ Optional<String> cluster = Optional.ofNullable(request.getProperty(CLUSTER));
+ Optional<String> continuation = Optional.ofNullable(request.getProperty(CONTINUATION));
+ final OperationHandler.VisitResult visit = operationHandler.visit(restUri, documentSelection, cluster, continuation);
+ final ObjectNode resultNode = mapper.createObjectNode();
+ if (visit.token.isPresent()) {
+ resultNode.put(CONTINUATION, visit.token.get());
+ }
+ resultNode.putArray(DOCUMENTS).addPOJO(visit.documentsAsJsonList);
+ resultNode.put(PATH_NAME, restUri.getRawPath());
+
+ HttpResponse httpResponse = new HttpResponse(200) {
+ @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;
+ }
+} \ No newline at end of file
diff --git a/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/resource/package-info.java b/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/resource/package-info.java
new file mode 100644
index 00000000000..52e32009e00
--- /dev/null
+++ b/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/resource/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.document.restapi.resource;
+
+import com.yahoo.osgi.annotation.ExportPackage;