/* * Copyright 2015-2016 OpenCB * * Licensed 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.opencb.opencga.client.rest; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.media.multipart.FormDataMultiPart; import org.glassfish.jersey.media.multipart.MultiPartFeature; import org.glassfish.jersey.media.multipart.file.FileDataBodyPart; import org.opencb.commons.datastore.core.ObjectMap; import org.opencb.commons.datastore.core.QueryOptions; import org.opencb.commons.datastore.core.QueryResponse; import org.opencb.commons.datastore.core.QueryResult; import org.opencb.opencga.catalog.exceptions.CatalogException; import org.opencb.opencga.client.config.ClientConfiguration; import org.opencb.opencga.core.results.VariantQueryResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.Entity; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.Response; import java.io.File; import java.io.IOException; import java.util.Collection; import java.util.Collections; import java.util.Map; import java.util.stream.Collectors; /** * Created by imedina on 04/05/16. */ public abstract class AbstractParentClient { protected Client client; private String userId; private String sessionId; private ClientConfiguration configuration; protected ObjectMapper jsonObjectMapper; private static int timeout = 10000; private static int batchSize = 2000; private static int defaultLimit = 2000; private static final int DEFAULT_SKIP = 0; protected static final String GET = "GET"; protected static final String POST = "POST"; protected Logger logger; protected AbstractParentClient(String userId, String sessionId, ClientConfiguration configuration) { this.userId = userId; this.sessionId = sessionId; this.configuration = configuration; init(); } private void init() { this.logger = LoggerFactory.getLogger(this.getClass().toString()); this.client = ClientBuilder.newClient(); jsonObjectMapper = new ObjectMapper(); if (configuration.getRest() != null) { if (configuration.getRest().getTimeout() > 0) { timeout = configuration.getRest().getTimeout(); } if (configuration.getRest().getBatchQuerySize() > 0) { batchSize = configuration.getRest().getBatchQuerySize(); } if (configuration.getRest().getDefaultLimit() > 0) { defaultLimit = configuration.getRest().getDefaultLimit(); } } } protected <T> VariantQueryResult<T> executeVariantQuery(String category, String action, Map<String, Object> params, String method, Class<T> clazz) throws IOException { QueryResponse<T> queryResponse = execute(category, null, action, params, method, clazz); return (VariantQueryResult<T>) queryResponse.first(); } protected <T> QueryResponse<T> execute(String category, String action, Map<String, Object> params, String method, Class<T> clazz) throws IOException { return execute(category, null, action, params, method, clazz); } protected <T> QueryResponse<T> execute(String category, String id, String action, Map<String, Object> params, String method, Class<T> clazz) throws IOException { return execute(category, id, null, null, action, params, method, clazz); } protected <T> QueryResponse<T> execute(String category1, String id1, String category2, String id2, String action, Map<String, Object> paramsMap, String method, Class<T> clazz) throws IOException { ObjectMap params; if (paramsMap == null) { params = new ObjectMap(); } else { params = new ObjectMap(paramsMap); } // // Remove null or empty params // for (Map.Entry<String, Object> param : params.entrySet()) { // Object value = param.getValue(); // if (value == null || (value instanceof String && ((String) value).isEmpty())) { // params.remove(param.getKey()); // } // } client.property(ClientProperties.CONNECT_TIMEOUT, 1000); client.property(ClientProperties.READ_TIMEOUT, timeout); // Build the basic URL WebTarget path = client .target(configuration.getRest().getHost()) .path("webservices") .path("rest") .path("v1") .path(category1); // TODO we still have to check if there are multiple IDs, the limit is 200 pero query, this can be parallelized // Some WS do not have IDs such as 'create' if (id1 != null && !id1.isEmpty()) { path = path.path(id1); } if (category2 != null && !category2.isEmpty()) { path = path.path(category2); } if (id2 != null && !id2.isEmpty()) { path = path.path(id2); } // Add the last URL part, the 'action' path = path.path(action); int numRequiredFeatures = params.getInt(QueryOptions.LIMIT, defaultLimit); int limit = Math.min(numRequiredFeatures, batchSize); int skip = params.getInt(QueryOptions.SKIP, DEFAULT_SKIP); // Session ID is needed almost always, the only exceptions are 'create/user', 'login' and 'changePassword' if (this.sessionId != null && !this.sessionId.isEmpty()) { path = path.queryParam("sid", this.sessionId); } QueryResponse<T> finalQueryResponse = null; QueryResponse<T> queryResponse; while (true) { params.put(QueryOptions.SKIP, skip); params.put(QueryOptions.LIMIT, limit); params.put(QueryOptions.TIMEOUT, timeout); if (!action.equals("upload")) { queryResponse = callRest(path, params, clazz, method); } else { queryResponse = callUploadRest(path, params, clazz); } int numResults = queryResponse.getResponse().isEmpty() ? 0 : queryResponse.getResponse().get(0).getNumResults(); if (finalQueryResponse == null) { finalQueryResponse = queryResponse; } else { if (numResults > 0) { finalQueryResponse.getResponse().get(0).getResult().addAll(queryResponse.getResponse().get(0).getResult()); finalQueryResponse.getResponse().get(0).setNumResults(finalQueryResponse.getResponse().get(0).getResult().size()); } } int numTotalResults = queryResponse.getResponse().isEmpty() ? 0 : finalQueryResponse.getResponse().get(0).getNumResults(); if (numResults < limit || numTotalResults >= numRequiredFeatures || numResults == 0) { break; } // DO NOT CHANGE THE ORDER OF THE FOLLOWING CODE skip += numResults; if (skip + batchSize < numRequiredFeatures) { limit = batchSize; } else { limit = numRequiredFeatures - numTotalResults; } } return finalQueryResponse; } /** * Call to WS using get or post method. * * @param path Path of the WS. * @param params Params to be passed to the WS. * @param clazz Expected return class. * @param method Method by which the query will be done (GET or POST). * @return A queryResponse object containing the results of the query. * @throws IOException if the path is wrong and cannot be converted to a proper url. */ private <T> QueryResponse<T> callRest(WebTarget path, Map<String, Object> params, Class clazz, String method) throws IOException { String jsonString = "{}"; if (method.equalsIgnoreCase(GET)) { // TODO we still have to check the limit of the query, and keep querying while there are more results if (params != null) { for (String s : params.keySet()) { Object o = params.get(s); if (o instanceof Collection) { String value = ((Collection<?>) o).stream().map(Object::toString).collect(Collectors.joining(",")); path = path.queryParam(s, value); } else { path = path.queryParam(s, o); } } } logger.debug("GET URL: " + path.getUri().toURL()); jsonString = path.request().get().readEntity(String.class); } else if (method.equalsIgnoreCase(POST)) { // TODO we still have to check the limit of the query, and keep querying while there are more results // Form form = new Form(); // if (params != null) { // for (String s : params.keySet()) { // Object value = params.get(s); // if (value instanceof Number) { // form.param(s, (Integer.toString((int) params.get(s)))); // } else { // form.param(s, ((String) params.get(s))); // } // } // } if (params != null) { for (String s : params.keySet()) { if (!s.equals("body")) { path = path.queryParam(s, params.get(s)); } } } // ObjectMap json = new ObjectMap("body", params.get("body")); logger.debug("POST URL: " + path.getUri().toURL()); Response body = path.request().post(Entity.json(params.get("body"))); jsonString = body.readEntity(String.class); // jsonString = path.request().post(Entity.json(params.get("body")), String.class); } return parseResult(jsonString, clazz); } /** * Call to upload WS. * * @param path Path of the WS. * @param params Params to be passed to the WS. * @param clazz Expected return class. * @return A queryResponse object containing the results of the query. * @throws IOException if the path is wrong and cannot be converted to a proper url. */ private <T> QueryResponse<T> callUploadRest(WebTarget path, Map<String, Object> params, Class<T> clazz) throws IOException { String jsonString; String filePath = ((String) params.get("file")); params.remove("file"); path.register(MultiPartFeature.class); final FileDataBodyPart filePart = new FileDataBodyPart("file", new File(filePath)); FormDataMultiPart formDataMultiPart = new FormDataMultiPart(); // Add the rest of the parameters to the form for (Map.Entry<String, Object> stringObjectEntry : params.entrySet()) { formDataMultiPart.field(stringObjectEntry.getKey(), stringObjectEntry.getValue().toString()); } final FormDataMultiPart multipart = (FormDataMultiPart) formDataMultiPart.bodyPart(filePart); jsonString = path.request().post(Entity.entity(multipart, multipart.getMediaType()), String.class); formDataMultiPart.close(); multipart.close(); return parseResult(jsonString, clazz); } private <T> QueryResponse<T> parseResult(String json, Class<T> clazz) throws IOException { if (json != null && !json.isEmpty()) { ObjectReader reader = jsonObjectMapper .readerFor(jsonObjectMapper.getTypeFactory().constructParametrizedType(QueryResponse.class, QueryResult.class, clazz)); try { return reader.readValue(json); } catch (JsonParseException e) { if (json.startsWith("<html>")) { if (json.contains("504 Gateway Time-out")) { return new QueryResponse<>("", 0, "", "Error 504 Gateway Time-out. The server didn't respond in time.", null, Collections.emptyList()); } } throw e; } } else { return new QueryResponse<>(); } } private ObjectMap createIfNull(ObjectMap objectMap) { if (objectMap == null) { objectMap = new ObjectMap(); } return objectMap; } protected ObjectMap addParamsToObjectMap(ObjectMap objectMap, String key, Object value, Object ... params) { objectMap = createIfNull(objectMap); objectMap.put(key, value); if (params != null && params.length > 0) { for (int i = 0; i < params.length; i += 2) { objectMap.put(params[i].toString(), params[i + 1]); } } return objectMap; } public String getSessionId() { return sessionId; } public AbstractParentClient setSessionId(String sessionId) { this.sessionId = sessionId; return this; } public ClientConfiguration getConfiguration() { return configuration; } public AbstractParentClient setConfiguration(ClientConfiguration configuration) { this.configuration = configuration; return this; } public String getUserId(ObjectMap options) throws CatalogException { String userId = this.userId; if (options != null && options.containsKey("userId")) { userId = options.getString("userId"); } if (userId == null || userId.isEmpty()) { throw new CatalogException("Missing user id"); } return userId; } public AbstractParentClient setUserId(String userId) { this.userId = userId; return this; } }