// Copyright 2015 The Project Buendia Authors // // 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 distrib- // uted 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 // specific language governing permissions and limitations under the License. package org.openmrs.projectbuendia.webservices.rest; import org.openmrs.OpenmrsObject; import org.openmrs.module.webservices.rest.SimpleObject; import org.openmrs.module.webservices.rest.web.RequestContext; import org.openmrs.module.webservices.rest.web.RestConstants; import org.openmrs.module.webservices.rest.web.annotation.Resource; import org.openmrs.module.webservices.rest.web.representation.Representation; import org.openmrs.module.webservices.rest.web.resource.api.Listable; import org.openmrs.module.webservices.rest.web.resource.api.Retrievable; import org.openmrs.module.webservices.rest.web.resource.api.Searchable; import org.openmrs.module.webservices.rest.web.response.ObjectNotFoundException; import org.openmrs.module.webservices.rest.web.response.ResponseException; import org.openmrs.projectbuendia.Utils; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; /** * Abstract superclass for resources whose REST API only supports read * operations. Subclasses must provide the {@link Resource} annotation with a * "name" parameter specifying the URL path, "supportedClass" giving the class * of items in the REST collection, and "supportedOpenmrsVersions" giving a * comma-separated list of supported OpenMRS Platform version numbers. * <p/> * <p>Every AbstractReadOnlyResource provides the operations: * <ul> * <li>Retrieve an item: {@link #retrieve(String, RequestContext)} * <li>List all items: {@link #getAll(RequestContext)} * <li>Search for items: {@link #search(RequestContext)} * </ul> * <p>Each of these methods returns a {@link SimpleObject}, which is converted * to a JSON response. If an error occurs, the returned JSON has the form: * <pre> * { * "error": { * "message": "[error message]", * "code": "[breakpoint]", * "detail": "[stack trace]" * } * } * </pre> * <p>For more details about each operation, see the method-level comments. */ public abstract class AbstractReadOnlyResource<T extends OpenmrsObject> implements Listable, Retrievable, Searchable { static final RequestLogger logger = RequestLogger.LOGGER; private final String resourceAlias; private final List<Representation> availableRepresentations; @Override public String getUri(Object instance) { // This will return an incorrect URI if the webservices.rest.uriPrefix // property is incorrect. We don't use getUri() at all in our resource // classes, but the Resource interface requires that we implement it. OpenmrsObject mrsObject = (OpenmrsObject) instance; Resource res = getClass().getAnnotation(Resource.class); return RestConstants.URI_PREFIX + res.name() + "/" + mrsObject.getUuid(); } /** * Retrieves the item with a specified UUID. * <p/> * <p>Responds to: {@code GET [API root]/[resource type]/[UUID]} * @param uuid the UUID of the desired item * @param context the request context; see individual implementations of * {@link #searchImpl(RequestContext, long)} for parameter details. * @return a {@link SimpleObject} with a "uuid" field and additional fields * provided by {@link #populateJsonProperties(T, RequestContext, SimpleObject, long)} * </ul> * @throws ResponseException if anything goes wrong */ @Override public Object retrieve(String uuid, RequestContext context) throws ResponseException { try { logger.request(context, this, "retrieve", uuid); Object result = retrieveInner(uuid, context, System.currentTimeMillis()); logger.reply(context, this, "retrieve", result); return result; } catch (Exception e) { logger.error(context, this, "retrieve", e); throw e; } } private Object retrieveInner(String uuid, RequestContext context, long snapshotTime) throws ResponseException { T item = retrieveImpl(uuid, context, snapshotTime); if (item == null) { throw new ObjectNotFoundException(); } return convertToJson(item, context, snapshotTime); } /** Retrieves a single item by UUID, returning null if it can't be found. */ protected abstract T retrieveImpl(String uuid, RequestContext context, long snapshotTime); /** * Converts a single item to JSON. By default, this populates the UUID * automatically, then delegates to populateJsonProperties to add the * remaining information. This is expected to be sufficient for most cases, * but subclasses can override this method if they want more flexibility. */ protected SimpleObject convertToJson(T item, RequestContext context, long snapshotTime) { SimpleObject json = new SimpleObject(); json.put("uuid", item.getUuid()); populateJsonProperties(item, context, json, snapshotTime); return json; } /** Populates the given SimpleObject with data from the given item. */ protected abstract void populateJsonProperties( T item, RequestContext context, SimpleObject json, long snapshotTime); @Override public List<Representation> getAvailableRepresentations() { return availableRepresentations; } /** * Delegates to {@link #search(RequestContext)}, which returns all the items * in the collection if no search parameters are specified. */ @Override public SimpleObject getAll(RequestContext context) throws ResponseException { return search(context); } /** * Performs a search using the given {@link RequestContext}. The real work * is done by the {@link #searchImpl(RequestContext, long)} function, which * also defines how the search is controlled by URL parameters. If no * search parameters are given, returns all the items in the collection. * @param context the request context; see individual implementations of * {@link #searchImpl(RequestContext, long)} for the search parameters. * @return a {@link SimpleObject} with the following keys: * <ul> * <li>"results": a {@link List} of {@link SimpleObject}s representing * items that match the search parameters, in the same form as that * returned by {@link #retrieve(String, RequestContext)} * <li>"snapshotTime": a timestamp in ISO 8601 UTC format, indicating * the server clock time at which the results were retrieved; for * resources that support incremental fetch, clients can pass in * this snapshotTime as the "sm" query parameter of the next request * to get just the data added or changed since this request * </ul> * TODO: It's nuts that snapshotTime and the "sm" parameter are in different * formats (ISO 8601 vs. millis) when the only purpose of snapshotTime is to * be passed back in as "sm". Make them both use millis. * @throws ResponseException if anything goes wrong */ @Override public SimpleObject search(RequestContext context) throws ResponseException { try { logger.request(context, this, "search"); long snapshotTime = System.currentTimeMillis(); SimpleObject result = searchInner(context, snapshotTime); logger.reply(context, this, "search", result); return result; } catch (Exception e) { logger.error(context, this, "search", e); throw e; } } /** * Wraps searchImpl, converting items (of type T) to SimpleObjects. * @param context the request context; see individual implementations of * {@link #searchImpl(RequestContext, long)} for the search parameters. * @param snapshotTime a timestamp (in millis) to use as the time at which * to take a consistent snapshot; all the returned results will reflect * the state of the database at that particular point in time. */ private SimpleObject searchInner(RequestContext context, long snapshotTime) throws ResponseException { List<SimpleObject> results = new ArrayList<>(); for (T item : searchImpl(context, snapshotTime)) { results.add(convertToJson(item, context, snapshotTime)); } SimpleObject response = new SimpleObject(); response.put("results", results); response.put("snapshotTime", Utils.toIso8601(new Date(snapshotTime))); return response; } /** * Lists all the items matching the specified search parameters. Note that * this method is also used for "get all", so it's entirely possible for the * context to not contain any query parameters. */ protected abstract Iterable<T> searchImpl(RequestContext context, long snapshotTime); protected AbstractReadOnlyResource(String resourceAlias, Representation... representations) { availableRepresentations = Arrays.asList(representations); this.resourceAlias = resourceAlias; } }