diff options
author | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
---|---|---|
committer | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
commit | 72231250ed81e10d66bfe70701e64fa5fe50f712 (patch) | |
tree | 2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi |
Publish
Diffstat (limited to 'vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi')
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; |