/* * Copyright 2013-2017 Erudika. https://erudika.com * * 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. * * For issues and patches go to: https://github.com/erudika */ package com.erudika.para.rest; import com.erudika.para.Para; import com.erudika.para.annotations.Locked; import com.erudika.para.core.App; import com.erudika.para.core.utils.CoreUtils; import com.erudika.para.core.ParaObject; import com.erudika.para.core.utils.ParaObjectUtils; import com.erudika.para.core.User; import com.erudika.para.security.SecurityUtils; import com.erudika.para.utils.Config; import com.erudika.para.utils.Pager; import com.erudika.para.utils.Utils; import com.erudika.para.validation.ValidationUtils; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.databind.JsonMappingException; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.net.URI; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A few helper methods for handling REST requests and responses. * @author Alex Bogdanovski [alex@erudika.com] */ @SuppressWarnings("unchecked") public final class RestUtils { private static final Logger logger = LoggerFactory.getLogger(RestUtils.class); private RestUtils() { } ///////////////////////////////////////////// // REST REQUEST UTILS ///////////////////////////////////////////// /** * Extracts the access key from a request. It can be a header or a parameter. * @param request a request * @return the access key */ public static String extractAccessKey(HttpServletRequest request) { if (request == null) { return ""; } String accessKey = ""; String auth = request.getHeader(HttpHeaders.AUTHORIZATION); boolean isAnonymousRequest = isAnonymousRequest(request); if (StringUtils.isBlank(auth)) { auth = request.getParameter("X-Amz-Credential"); if (!StringUtils.isBlank(auth)) { accessKey = StringUtils.substringBetween(auth, "=", "/"); } } else { if (isAnonymousRequest) { accessKey = StringUtils.substringAfter(auth, "Anonymous").trim(); } else { accessKey = StringUtils.substringBetween(auth, "=", "/"); } } if (isAnonymousRequest && StringUtils.isBlank(accessKey)) { // try to get access key from parameter accessKey = request.getParameter("accessKey"); } return accessKey; } /** * Check if Authorization header starts with 'Anonymous'. Used for guest access. * @param request a request * @return true if user is unauthenticated */ public static boolean isAnonymousRequest(HttpServletRequest request) { return request != null && (StringUtils.startsWith(request.getHeader(HttpHeaders.AUTHORIZATION), "Anonymous") || StringUtils.isBlank(request.getHeader(HttpHeaders.AUTHORIZATION))); } /** * Extracts the date field from a request. It can be a header or a parameter. * @param request a request * @return the date */ public static String extractDate(HttpServletRequest request) { if (request == null) { return ""; } String date = request.getHeader("X-Amz-Date"); if (StringUtils.isBlank(date)) { return request.getParameter("X-Amz-Date"); } else { return date; } } /** * Extracts the resource name, for example '/v1/_resource/path' * returns '_resource/path'. * @param request a request * @return the resource path */ public static String extractResourcePath(HttpServletRequest request) { if (request == null || request.getRequestURI().length() <= 3) { return ""; } // get request path, strip first slash '/' String uri = request.getRequestURI().substring(1); // skip to the end of API version prefix '/v1/' int start = uri.indexOf('/'); if (start >= 0 && start + 1 < uri.length()) { return uri.substring(start + 1); } else { return ""; } } /** * Reads an object from a given resource path. * Assumes that the "id" is located after the first slash "/" like this: {type}/{id}/... * @param appid app id * @param path resource path * @return the object found at this path or null */ public static ParaObject readResourcePath(String appid, String path) { if (StringUtils.isBlank(appid) || StringUtils.isBlank(path) || !path.contains("/")) { return null; } try { URI uri = new URI(path); if (path.length() > 1) { path = path.startsWith("/") ? uri.getPath().substring(1) : uri.getPath(); String[] parts = path.split("/"); String id; if (parts.length == 1) { id = parts[0]; // case: {id} } else if (parts.length >= 2) { id = parts[1]; // case: {type}/{id}/... } else { return null; } return Para.getDAO().read(appid, id); } } catch (Exception e) { logger.debug("Invalid resource path {}: {}", path, e); } return null; } /** * Returns the current authenticated {@link App} object. * @return an App object or null */ public static App getPrincipalApp() { App app = SecurityUtils.getAuthenticatedApp(); User user = SecurityUtils.getAuthenticatedUser(); if (app != null) { return app; } else if (user != null) { return Para.getDAO().read(Config.APP_NAME_NS, App.id(user.getAppid())); } logger.info("Unauthenticated request - returning root App: {}", Config.APP_NAME_NS); return Para.getDAO().read(Config.APP_NAME_NS, App.id(Config.APP_NAME_NS)); } /** * Returns a Response with the entity object inside it and 200 status code. * If there was and error the status code is different than 200. * * @param is the entity input stream * @param type the type to convert the entity into, for example a Map. If null, this returns the InputStream. * @return response with 200 or error status */ public static Response getEntity(InputStream is, Class<?> type) { Object entity; try { if (is != null && is.available() > 0) { if (is.available() > Config.MAX_ENTITY_SIZE_BYTES) { return getStatusResponse(Response.Status.BAD_REQUEST, "Request is too large - the maximum is " + (Config.MAX_ENTITY_SIZE_BYTES / 1024) + " KB."); } if (type == null) { entity = is; } else { entity = ParaObjectUtils.getJsonReader(type).readValue(is); } } else { return getStatusResponse(Response.Status.BAD_REQUEST, "Missing request body."); } } catch (JsonMappingException e) { return getStatusResponse(Response.Status.BAD_REQUEST, e.getMessage()); } catch (JsonParseException e) { return getStatusResponse(Response.Status.BAD_REQUEST, e.getMessage()); } catch (IOException e) { logger.error(null, e); return getStatusResponse(Response.Status.INTERNAL_SERVER_ERROR, e.toString()); } return Response.ok(entity).build(); } /** * An app can edit itself or delete itself. It can't read, edit, overwrite or delete other apps, * unless it is the root app. * @param app an app * @param object another object * @return true if app passes the check */ private static boolean checkImplicitAppPermissions(App app, ParaObject object) { if (app != null && object != null) { return !app.getType().equals(object.getType()) || app.getId().equals(object.getId()) || app.isRootApp(); } return false; } /** * Reads the bytes from an InputStream. * @param in an InputStream * @return byte[] */ public static byte[] readEntityBytes(InputStream in) { byte[] jsonEntity = null; try { if (in != null && in.available() > 0) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buf = new byte[1024]; int length; while ((length = in.read(buf)) > 0) { baos.write(buf, 0, length); } jsonEntity = baos.toByteArray(); } else { jsonEntity = new byte[0]; } } catch (IOException ex) { logger.error(null, ex); } finally { try { if (in != null) { in.close(); } } catch (IOException ex) { logger.error(null, ex); } } return jsonEntity; } /** * Process voting request and create vote object. * @param object the object to cast vote on * @param entity request entity data * @return status codetrue if vote was successful */ public static Response getVotingResponse(ParaObject object, Map<String, Object> entity) { boolean voteSuccess = false; if (object != null && entity != null) { String upvoterId = (String) entity.get("_voteup"); String downvoterId = (String) entity.get("_votedown"); if (!StringUtils.isBlank(upvoterId)) { voteSuccess = object.voteUp(upvoterId); } else if (!StringUtils.isBlank(downvoterId)) { voteSuccess = object.voteDown(downvoterId); } if (voteSuccess) { object.update(); } } return Response.ok(voteSuccess).build(); } ///////////////////////////////////////////// // CORE REST RESPONSE HANDLERS ///////////////////////////////////////////// /** * Read response as JSON. * @param app the app object * @param content the object that was read * @return status code 200 or 404 */ public static Response getReadResponse(App app, ParaObject content) { // app can't modify other apps except itself if (app != null && content != null && checkImplicitAppPermissions(app, content)) { return Response.ok(content).build(); } return getStatusResponse(Response.Status.NOT_FOUND); } /** * Create response as JSON. * @param type type of the object to create * @param is entity input stream * @param app the app object * @return a status code 201 or 400 */ public static Response getCreateResponse(App app, String type, InputStream is) { ParaObject content; Response entityRes = getEntity(is, Map.class); if (entityRes.getStatusInfo() == Response.Status.OK) { Map<String, Object> newContent = (Map<String, Object>) entityRes.getEntity(); if (!StringUtils.isBlank(type)) { newContent.put(Config._TYPE, type); } content = ParaObjectUtils.setAnnotatedFields(newContent); if (app != null && content != null && !app.getType().equals(type)) { content.setAppid(app.getAppIdentifier()); int typesCount = app.getDatatypes().size(); app.addDatatypes(content); // The reason why we do two validation passes is because we want to return // the errors through the API and notify the end user. // This is the primary validation pass (validates not only core POJOS but also user defined objects). String[] errors = ValidationUtils.validateObject(app, content); if (errors.length == 0) { // Secondary validation pass called here. Object is validated again before being created // See: IndexAndCacheAspect.java String id = content.create(); if (id != null) { // new type added so update app object if (typesCount < app.getDatatypes().size()) { app.update(); } return Response.created(URI.create(Utils.urlEncode(content.getObjectURI()))). entity(content).build(); } } return getStatusResponse(Response.Status.BAD_REQUEST, errors); } return getStatusResponse(Response.Status.BAD_REQUEST, "Failed to create object."); } return entityRes; } /** * Overwrite response as JSON. * @param id the object id * @param type type of the object to create * @param is entity input stream * @param app the app object * @return a status code 200 or 400 */ public static Response getOverwriteResponse(App app, String id, String type, InputStream is) { ParaObject content; Response entityRes = getEntity(is, Map.class); if (entityRes.getStatusInfo() == Response.Status.OK) { Map<String, Object> newContent = (Map<String, Object>) entityRes.getEntity(); if (!StringUtils.isBlank(type)) { newContent.put(Config._TYPE, type); } content = ParaObjectUtils.setAnnotatedFields(newContent); if (app != null && content != null && !StringUtils.isBlank(id) && !app.getType().equals(type)) { content.setType(type); content.setAppid(app.getAppIdentifier()); content.setId(id); int typesCount = app.getDatatypes().size(); app.addDatatypes(content); // The reason why we do two validation passes is because we want to return // the errors through the API and notify the end user. // This is the primary validation pass (validates not only core POJOS but also user defined objects). String[] errors = ValidationUtils.validateObject(app, content); if (errors.length == 0) { // Secondary validation pass called here. Object is validated again before being created // See: IndexAndCacheAspect.java CoreUtils.getInstance().overwrite(app.getAppIdentifier(), content); // new type added so update app object if (typesCount < app.getDatatypes().size()) { app.update(); } return Response.ok(content).build(); } return getStatusResponse(Response.Status.BAD_REQUEST, errors); } return getStatusResponse(Response.Status.BAD_REQUEST, "Failed to overwrite object."); } return entityRes; } /** * Update response as JSON. * @param object object to validate and update * @param is entity input stream * @param app the app object * @return a status code 200 or 400 or 404 */ public static Response getUpdateResponse(App app, ParaObject object, InputStream is) { if (app != null && object != null) { Map<String, Object> newContent; Response entityRes = getEntity(is, Map.class); String[] errors = {}; if (entityRes.getStatusInfo() == Response.Status.OK) { newContent = (Map<String, Object>) entityRes.getEntity(); } else { return entityRes; } object.setAppid(app.getAppIdentifier()); if (newContent.containsKey("_voteup") || newContent.containsKey("_votedown")) { return getVotingResponse(object, newContent); } else { ParaObjectUtils.setAnnotatedFields(object, newContent, Locked.class); // app can't modify other apps except itself if (checkImplicitAppPermissions(app, object)) { // This is the primary validation pass (validates not only core POJOS but also user defined objects). errors = ValidationUtils.validateObject(app, object); if (errors.length == 0) { // Secondary validation pass: object is validated again before being updated object.update(); return Response.ok(object).build(); } } } return getStatusResponse(Response.Status.BAD_REQUEST, errors); } return getStatusResponse(Response.Status.NOT_FOUND); } /** * Delete response as JSON. * @param content the object to delete * @param app the current App object * @return a status code 200 or 400 */ public static Response getDeleteResponse(App app, ParaObject content) { if (app != null && content != null && content.getId() != null && content.getAppid() != null) { if (checkImplicitAppPermissions(app, content)) { content.setAppid(app.getAppIdentifier()); content.delete(); return Response.ok().build(); } return getStatusResponse(Response.Status.BAD_REQUEST); } return getStatusResponse(Response.Status.NOT_FOUND); } /** * Batch read response as JSON. * @param app the current App object * @param ids list of ids * @return status code 200 or 400 */ public static Response getBatchReadResponse(App app, List<String> ids) { if (app != null && ids != null && !ids.isEmpty()) { return Response.ok(Para.getDAO().readAll(app.getAppIdentifier(), ids, true).values()).build(); } else { return getStatusResponse(Response.Status.BAD_REQUEST, "Missing ids."); } } /** * Batch create response as JSON. * @param app the current App object * @param is entity input stream * @return a status code 200 or 400 */ public static Response getBatchCreateResponse(final App app, InputStream is) { if (app != null) { final LinkedList<ParaObject> newObjects = new LinkedList<ParaObject>(); Response entityRes = getEntity(is, List.class); if (entityRes.getStatusInfo() == Response.Status.OK) { List<Map<String, Object>> items = (List<Map<String, Object>>) entityRes.getEntity(); for (Map<String, Object> object : items) { // can't create multiple apps in batch if (!app.getType().equals(object.get(Config._TYPE))) { ParaObject pobj = ParaObjectUtils.setAnnotatedFields(object); if (pobj != null && ValidationUtils.isValidObject(pobj)) { pobj.setAppid(app.getAppIdentifier()); newObjects.add(pobj); } } } Para.getDAO().createAll(app.getAppIdentifier(), newObjects); Para.asyncExecute(new Runnable() { public void run() { int typesCount = app.getDatatypes().size(); app.addDatatypes(newObjects.toArray(new ParaObject[0])); if (typesCount < app.getDatatypes().size()) { app.update(); } } }); } else { return entityRes; } return Response.ok(newObjects).build(); } else { return getStatusResponse(Response.Status.BAD_REQUEST); } } /** * Batch update response as JSON. * @param app the current App object * @param oldObjects a list of old objects read from DB * @param newProperties a list of new object properties to be updated * @return a status code 200 or 400 */ public static Response getBatchUpdateResponse(App app, Map<String, ParaObject> oldObjects, List<Map<String, Object>> newProperties) { if (app != null && oldObjects != null && newProperties != null) { LinkedList<ParaObject> updatedObjects = new LinkedList<ParaObject>(); for (Map<String, Object> newProps : newProperties) { if (newProps != null && newProps.containsKey(Config._ID)) { ParaObject oldObject = oldObjects.get((String) newProps.get(Config._ID)); // updating apps in batch is not allowed if (oldObject != null && checkImplicitAppPermissions(app, oldObject)) { ParaObject updatedObject = ParaObjectUtils.setAnnotatedFields(oldObject, newProps, Locked.class); if (ValidationUtils.isValidObject(app, updatedObject)) { updatedObject.setAppid(app.getAppIdentifier()); updatedObjects.add(updatedObject); } } } } Para.getDAO().updateAll(app.getAppIdentifier(), updatedObjects); return Response.ok(updatedObjects).build(); } else { return getStatusResponse(Response.Status.BAD_REQUEST); } } /** * Batch delete response as JSON. * @param app the current App object * @param ids list of ids to delete * @return a status code 200 or 400 */ public static Response getBatchDeleteResponse(App app, List<String> ids) { LinkedList<ParaObject> objects = new LinkedList<ParaObject>(); if (ids != null && !ids.isEmpty()) { if (ids.size() <= Config.MAX_ITEMS_PER_PAGE) { for (ParaObject pobj : Para.getDAO().readAll(app.getAppIdentifier(), ids, false).values()) { if (pobj != null && pobj.getId() != null && pobj.getType() != null) { // deleting apps in batch is not allowed if (!app.getType().equals(pobj.getType())) { objects.add(pobj); } } } Para.getDAO().deleteAll(app.getAppIdentifier(), objects); } else { return getStatusResponse(Response.Status.BAD_REQUEST, "Limit reached. Maximum number of items to delete is " + Config.MAX_ITEMS_PER_PAGE); } } else { return getStatusResponse(Response.Status.BAD_REQUEST, "Missing ids."); } return Response.ok().build(); } ///////////////////////////////////////////// // LINKS REST RESPONSE HANDLERS ///////////////////////////////////////////// /** * Handles requests to search for linked objects. * @param pobj the object to operate on * @param id2 the id of the second object (optional) * @param type2 the type of the second object * @param params query parameters * @param pager a {@link Pager} object * @param childrenOnly find only directly linked objects in 1-to-many relationship * @return a Response */ public static Response readLinksHandler(ParaObject pobj, String id2, String type2, MultivaluedMap<String, String> params, Pager pager, boolean childrenOnly) { String query = params.getFirst("q"); if (type2 != null) { if (id2 != null) { return Response.ok(pobj.isLinked(type2, id2), MediaType.TEXT_PLAIN_TYPE).build(); } else { List<ParaObject> items = new ArrayList<ParaObject>(); if (childrenOnly) { if (params.containsKey("count")) { pager.setCount(pobj.countChildren(type2)); } else { if (params.containsKey("field") && params.containsKey("term")) { items = pobj.getChildren(type2, params.getFirst("field"), params.getFirst("term"), pager); } else { if (StringUtils.isBlank(query)) { items = pobj.getChildren(type2, pager); } else { items = pobj.findChildren(type2, query, pager); } } } } else { if (params.containsKey("count")) { pager.setCount(pobj.countLinks(type2)); } else { if (StringUtils.isBlank(query)) { items = pobj.getLinkedObjects(type2, pager); } else { items = pobj.findLinkedObjects(type2, params.getFirst("field"), query, pager); } } } return Response.ok(getResponseObject(items, pager)).build(); } } else { return getStatusResponse(Response.Status.BAD_REQUEST, "Parameter 'type' is missing."); } } /** * Handles requests to delete linked objects. * @param pobj the object to operate on * @param id2 the id of the second object (optional) * @param type2 the type of the second object * @param childrenOnly find only directly linked objects in 1-to-many relationship * @return a Response */ public static Response deleteLinksHandler(ParaObject pobj, String id2, String type2, boolean childrenOnly) { if (type2 == null && id2 == null) { pobj.unlinkAll(); } else if (type2 != null) { if (id2 != null) { pobj.unlink(type2, id2); } else if (childrenOnly) { pobj.deleteChildren(type2); } } return Response.ok().build(); } /** * Handles requests to link an object to other objects. * @param pobj the object to operate on * @param id2 the id of the second object (optional) * @return a Response */ public static Response createLinksHandler(ParaObject pobj, String id2) { if (id2 != null && pobj != null) { String linkid = pobj.link(id2); if (linkid == null) { return getStatusResponse(Response.Status.BAD_REQUEST, "Failed to create link."); } else { return Response.ok(linkid, MediaType.TEXT_PLAIN_TYPE).build(); } } else { return getStatusResponse(Response.Status.BAD_REQUEST, "Parameters 'type' and 'id' are missing."); } } ///////////////////////////////////////////// // SEARCH RESPONSE HANDLERS ///////////////////////////////////////////// static <P extends ParaObject> Map<String, Object> buildQueryAndSearch(App app, String querytype, MultivaluedMap<String, String> params, String typeOverride) { String query = paramOrDefault(params, "q", "*"); String appid = app.getAppIdentifier(); Pager pager = getPagerFromParams(params); List<P> items = Collections.emptyList(); String queryType = paramOrDefault(params, "querytype", querytype); String type = paramOrDefault(params, Config._TYPE, null); if (!StringUtils.isBlank(typeOverride) && !"search".equals(typeOverride)) { type = typeOverride; } if (params == null) { return getResponseObject(Para.getSearch().findQuery(appid, type, query, pager), pager); } if ("id".equals(queryType)) { items = findByIdQuery(params, appid, pager); } else if ("ids".equals(queryType)) { items = Para.getSearch().findByIds(appid, params.get("ids")); pager.setCount(items.size()); } else if ("nested".equals(queryType)) { items = Para.getSearch().findNestedQuery(appid, type, params.getFirst("field"), query, pager); } else if ("nearby".equals(queryType)) { items = findNearbyQuery(params, appid, type, query, pager); } else if ("prefix".equals(queryType)) { items = Para.getSearch().findPrefix(appid, type, params.getFirst("field"), params.getFirst("prefix"), pager); } else if ("similar".equals(queryType)) { items = findSimilarQuery(params, appid, type, pager); } else if ("tagged".equals(queryType)) { items = findTaggedQuery(params, appid, type, pager); } else if ("in".equals(queryType)) { items = Para.getSearch().findTermInList(appid, type, params.getFirst("field"), params.get("terms"), pager); } else if ("terms".equals(queryType)) { items = findTermsQuery(params, pager, appid, type); } else if ("wildcard".equals(queryType)) { items = Para.getSearch().findWildcard(appid, type, params.getFirst("field"), query, pager); } else if ("count".equals(queryType)) { pager.setCount(Para.getSearch().getCount(appid, type)); } else { items = Para.getSearch().findQuery(appid, type, query, pager); } return getResponseObject(items, pager); } private static Pager getPagerFromParams(MultivaluedMap<String, String> params) { Pager pager = new Pager(); boolean isPaginationLimited = Config.getConfigBoolean("limited_pagination", true); long page = NumberUtils.toLong(paramOrDefault(params, "page", ""), 0); int limit = NumberUtils.toInt(paramOrDefault(params, "limit", ""), pager.getLimit()); pager.setPage(isPaginationLimited && page > 1000 ? 1000 : page); pager.setSortby(paramOrDefault(params, "sort", pager.getSortby())); pager.setDesc(Boolean.parseBoolean(paramOrDefault(params, "desc", "true"))); pager.setLimit(isPaginationLimited && limit > (3 * Config.MAX_ITEMS_PER_PAGE) ? limit / 3 : limit); return pager; } private static <P extends ParaObject> List<P> findTermsQuery(MultivaluedMap<String, String> params, Pager pager, String appid, String type) { if (params == null) { return Collections.emptyList(); } String matchAll = paramOrDefault(params, "matchall", "true"); List<String> termsList = params.get("terms"); if (termsList != null) { Map<String, String> terms = new HashMap<String, String>(termsList.size()); for (String termTuple : termsList) { if (!StringUtils.isBlank(termTuple) && termTuple.contains(Config.SEPARATOR)) { String[] split = termTuple.split(Config.SEPARATOR, 2); terms.put(split[0], split[1]); } } if (params.containsKey("count")) { pager.setCount(Para.getSearch().getCount(appid, type, terms)); } else { return Para.getSearch().findTerms(appid, type, terms, Boolean.parseBoolean(matchAll), pager); } } return Collections.emptyList(); } private static <P extends ParaObject> List<P> findTaggedQuery(MultivaluedMap<String, String> params, String appid, String type, Pager pager) { List<String> tags = params.get("tags"); String[] tagz = tags != null ? tags.toArray(new String[0]) : null; return Para.getSearch().findTagged(appid, type, tagz, pager); } private static <P extends ParaObject> List<P> findSimilarQuery(MultivaluedMap<String, String> params, String appid, String type, Pager pager) { List<String> fields = params.get("fields"); String[] fieldz = (fields != null) ? fields.toArray(new String[0]) : null; return Para.getSearch().findSimilar(appid, type, params.getFirst("filterid"), fieldz, params.getFirst("like"), pager); } private static <P extends ParaObject> List<P> findNearbyQuery(MultivaluedMap<String, String> params, String appid, String type, String query, Pager pager) { String latlng = params.getFirst("latlng"); if (StringUtils.contains(latlng, ",")) { String[] coords = latlng.split(",", 2); String rad = paramOrDefault(params, "radius", null); int radius = NumberUtils.toInt(rad, 10); double lat = NumberUtils.toDouble(coords[0], 0); double lng = NumberUtils.toDouble(coords[1], 0); return Para.getSearch().findNearby(appid, type, query, radius, lat, lng, pager); } return Collections.emptyList(); } private static <P extends ParaObject> List<P> findByIdQuery(MultivaluedMap<String, String> params, String appid, Pager pager) { String id = paramOrDefault(params, Config._ID, null); P obj = Para.getSearch().findById(appid, id); if (obj != null) { pager.setCount(1); return Collections.singletonList(obj); } return Collections.emptyList(); } private static String paramOrDefault(MultivaluedMap<String, String> params, String name, String defaultValue) { return params != null && params.containsKey(name) ? params.getFirst(name) : defaultValue; } private static <P extends ParaObject> Map<String, Object> getResponseObject(List<P> items, Pager pager) { Map<String, Object> result = new HashMap<String, Object>(); result.put("items", items); result.put("page", pager.getPage()); result.put("totalHits", pager.getCount()); return result; } ///////////////////////////////////////////// // MISC RESPONSE HANDLERS ///////////////////////////////////////////// /** * A generic JSON response handler. * @param status status code * @param messages zero or more errors * @return a response as JSON */ public static Response getStatusResponse(Response.Status status, String... messages) { if (status == null) { return Response.status(Response.Status.BAD_REQUEST).build(); } String msg = StringUtils.join(messages, ". "); if (StringUtils.isBlank(msg)) { msg = status.getReasonPhrase(); } try { return GenericExceptionMapper.getExceptionResponse(status.getStatusCode(), msg); } catch (Exception ex) { logger.error(null, ex); return Response.status(Response.Status.BAD_REQUEST).build(); } } /** * A generic JSON response handler. Returns a message and response code. * @param response the response to write to * @param status status code * @param message error message */ public static void returnStatusResponse(HttpServletResponse response, int status, String message) { if (response == null) { return; } PrintWriter out = null; try { response.setStatus(status); response.setContentType(MediaType.APPLICATION_JSON); out = response.getWriter(); ParaObjectUtils.getJsonWriter().writeValue(out, getStatusResponse(Response.Status. fromStatusCode(status), message).getEntity()); } catch (Exception ex) { logger.error(null, ex); } finally { if (out != null) { out.close(); } } } /** * A generic JSON response returning an object. Status code is always {@code 200}. * @param response the response to write to * @param obj an object */ public static void returnObjectResponse(HttpServletResponse response, Object obj) { if (response == null) { return; } PrintWriter out = null; try { response.setStatus(HttpServletResponse.SC_OK); response.setContentType(MediaType.APPLICATION_JSON); out = response.getWriter(); ParaObjectUtils.getJsonWriter().writeValue(out, obj); } catch (Exception ex) { logger.error(null, ex); } finally { if (out != null) { out.close(); } } } /** * Returns the path parameter value. * @param param a parameter name * @param ctx ctx * @return a value */ public static String pathParam(String param, ContainerRequestContext ctx) { return ctx.getUriInfo().getPathParameters().getFirst(param); } /** * Returns the path parameters values. * @param param a parameter name * @param ctx ctx * @return a list of parameter values */ public static List<String> pathParams(String param, ContainerRequestContext ctx) { return ctx.getUriInfo().getPathParameters().get(param); } /** * Returns the query parameter value. * @param param a parameter name * @param ctx ctx * @return parameter value */ public static String queryParam(String param, ContainerRequestContext ctx) { return ctx.getUriInfo().getQueryParameters().getFirst(param); } /** * Returns the query parameter values. * @param param a parameter name * @param ctx ctx * @return a list of values */ public static List<String> queryParams(String param, ContainerRequestContext ctx) { return ctx.getUriInfo().getQueryParameters().get(param); } /** * Returns true if parameter exists. * @param param a parameter name * @param ctx ctx * @return true if parameter is set */ public static boolean hasQueryParam(String param, ContainerRequestContext ctx) { return ctx.getUriInfo().getQueryParameters().containsKey(param); } }