| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| package org.apache.datasketches.server; |
| |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| |
| import java.io.IOException; |
| import java.io.Reader; |
| import java.net.URLDecoder; |
| |
| import org.apache.datasketches.Family; |
| import org.eclipse.jetty.server.Request; |
| import org.eclipse.jetty.server.handler.AbstractHandler; |
| |
| import com.google.gson.GsonBuilder; |
| import com.google.gson.JsonArray; |
| import com.google.gson.JsonElement; |
| import com.google.gson.JsonObject; |
| import com.google.gson.JsonParser; |
| |
| import static org.apache.datasketches.server.SketchConstants.*; |
| |
| /** |
| * Provides a common request handler for the sketches server. This gives us several benefits: |
| * <ul> |
| * <li>Extracts JSON query from querystring or POST body, as appropriate, to allow multiple input types.</li> |
| * <li>Sketches are stateful, and even reading can be disrupted by writes on another thread. A real |
| * database would have a more sophisticated locking system, but this class lets us synchronize across |
| * query types.</li> |
| * <li>Handles both JSON arrays or single JSON objects as inputs, letting the query handlers avoid |
| * code duplication. |
| * </ul> |
| * By using this class, the individual query handlers are able to consume and emit only JSON objects; they |
| * need not worry about details of the HTTP request or response. |
| */ |
| public abstract class BaseSketchesQueryHandler extends AbstractHandler { |
| final SketchStorage sketches; |
| final boolean queryExempt; |
| |
| /** |
| * Basic query handler. Assumes calls must include a JSON query. |
| * @param sketches The sketches database to use |
| */ |
| BaseSketchesQueryHandler(final SketchStorage sketches) { |
| this(sketches, false); |
| } |
| |
| /** |
| * Basic query handler, allowing the derived type to explicitly declare if an input query is optional. |
| * @param sketches The sketches database to use |
| * @param queryExempt <tt>true</tt> if a query is not required, otherwise <tt>false</tt> |
| */ |
| BaseSketchesQueryHandler(final SketchStorage sketches, final boolean queryExempt) { |
| if (sketches == null) { |
| throw new IllegalArgumentException("Cannot initialize handler with SketchStorage == null"); |
| } |
| this.sketches = sketches; |
| this.queryExempt = queryExempt; |
| } |
| |
| static JsonElement checkMethodAndReadJson(final Request baseRequest, |
| final HttpServletRequest request, |
| final HttpServletResponse response) throws IOException { |
| JsonElement query = null; |
| if (request.getMethod().equals("POST")) { |
| response.setContentType("application/json"); |
| try (final Reader reader = request.getReader()) { |
| query = JsonParser.parseReader(reader); |
| } |
| } else if (request.getMethod().equals("GET")) { |
| response.setContentType("text/html"); |
| query = JsonParser.parseString(URLDecoder.decode(request.getQueryString(), "utf-8")); |
| } else { |
| response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED); |
| baseRequest.setHandled(true); |
| } |
| |
| return query; |
| } |
| |
| /** |
| * Query handler to be implemented by subclasses |
| * @param query A JSON query to process |
| * @return A JSON response |
| */ |
| protected abstract JsonObject processQuery(JsonObject query); |
| |
| /** |
| * Internal method to synchronize calls to subclasses |
| * @param query A JSON query to process |
| * @return A JSON response |
| */ |
| final JsonObject callProcessQuery(final JsonObject query) { |
| return processQuery(query); |
| } |
| |
| @Override |
| public void handle(final String target, |
| final Request baseRequest, |
| final HttpServletRequest request, |
| final HttpServletResponse response) throws IOException { |
| JsonElement query = null; |
| if (!queryExempt && ((query = checkMethodAndReadJson(baseRequest, request, response)) == null)) { |
| return; |
| } |
| |
| // error messages will be wrapped in json |
| response.setCharacterEncoding("utf-8"); |
| response.setContentType("application/json"); |
| |
| JsonElement result = null; |
| |
| try { |
| if (query == null) { |
| result = callProcessQuery(null); |
| } else if (query.isJsonArray()) { |
| for (final JsonElement subQuery : query.getAsJsonArray()) { |
| final JsonObject subResult = callProcessQuery(subQuery.getAsJsonObject()); |
| if (subResult != null) { |
| // lazy initialization to avoid possibly empty array |
| if (result == null) { |
| result = new JsonArray(((JsonArray) query).size()); |
| } |
| ((JsonArray) result).add(subResult); |
| } |
| } |
| } else { |
| result = callProcessQuery((JsonObject) query); |
| } |
| |
| if (result != null) { |
| //response.getWriter().print(result.toString()); |
| response.getWriter().print(new GsonBuilder().setPrettyPrinting().create().toJson(result)); |
| } |
| |
| // we're ok if we reach here without an exception |
| response.setStatus(HttpServletResponse.SC_OK); |
| } catch (final Exception e) { |
| final JsonObject error = new JsonObject(); |
| error.addProperty(ERROR_KEY, e.getMessage()); |
| response.setStatus(UNPROCESSABLE_ENTITY); |
| } |
| |
| baseRequest.setHandled(true); |
| } |
| |
| static Family familyFromString(final String type) throws IllegalArgumentException { |
| switch (type.toLowerCase()) { |
| case SKETCH_FAMILY_THETA: |
| return Family.QUICKSELECT; |
| |
| case SKETCH_FAMILY_KLL: |
| return Family.KLL; |
| |
| case SKETCH_FAMILY_FREQUENCY: |
| return Family.FREQUENCY; |
| |
| case SKETCH_FAMILY_HLL: |
| return Family.HLL; |
| |
| case SKETCH_FAMILY_CPC: |
| return Family.CPC; |
| |
| case SKETCH_FAMILY_RESERVOIR: |
| return Family.RESERVOIR; |
| |
| case SKETCH_FAMILY_VAROPT: |
| return Family.VAROPT; |
| |
| default: |
| throw new IllegalArgumentException("Unrecognized sketch type: " + type); |
| } |
| } |
| } |