// 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.codehaus.jackson.map.ObjectMapper; import org.openmrs.Concept; import org.openmrs.Field; import org.openmrs.Form; import org.openmrs.FormField; import org.openmrs.api.ConceptService; import org.openmrs.api.FormService; import org.openmrs.api.context.Context; import org.openmrs.module.webservices.rest.SimpleObject; import org.openmrs.module.webservices.rest.web.RequestContext; 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.response.ResponseException; import org.openmrs.util.FormUtil; import org.projectbuendia.openmrs.webservices.rest.RestController; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.SortedMap; import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * REST resource for charts. These are stored as OpenMRS forms, but that's * primarily to allow for ease of maintenance (OpenMRS provides an editing UI). * @see AbstractReadOnlyResource */ @Resource(name = RestController.REST_VERSION_1_AND_NAMESPACE + "/charts", supportedClass = Form.class, supportedOpenmrsVersions = "1.10.*,1.11.*") public class ChartResource extends AbstractReadOnlyResource<Form> { private static final Pattern COMPRESSIBLE_UUID = Pattern.compile("^([0-9]+)A+$"); private final FormService formService; private final ConceptService conceptService; public ChartResource() { super("chart", Representation.DEFAULT, Representation.FULL); formService = Context.getFormService(); conceptService = Context.getConceptService(); } /** * Retrieves a single form with the given UUID. * @param context the request context; specify the URL query parameter * "?v=full" to get a list of the groups and concepts in the form. * @see AbstractReadOnlyResource#retrieve(String, RequestContext) */ @Override public Form retrieveImpl(String uuid, RequestContext context, long snapshotTime) throws ResponseException { return formService.getFormByUuid(uuid); } /** * Populates the {@link SimpleObject} with 'version' (the version number of the form) * and, if details are requested with the query parameter "?v=full", adds the list of * chart sections in 'sections' containing the details of each tile or row in the chart. * @param context the request context; specify the URL query parameter * "?v=full" to get a list of the groups and concepts in the form. */ @Override protected void populateJsonProperties(Form form, RequestContext context, SimpleObject json, long snapshotTime) { json.put("version", form.getVersion()); if (context.getRepresentation() != Representation.FULL) return; List<Map> sections = new ArrayList<>(); SortedMap<Integer, TreeSet<FormField>> structure = FormUtil.getFormStructure(form); for (FormField sectionFormField : structure.get(0)) { Field sectionField = sectionFormField.getField(); Map<String, Object> section = parseFieldDescription(sectionField); section.put("label", sectionField.getName()); List<Map> items = new ArrayList<>(); for (FormField itemFormField : structure.get(sectionFormField.getId())) { Field itemField = itemFormField.getField(); Map<String, Object> item = parseFieldDescription(itemField); item.put("label", itemField.getName()); item.put("concepts", getConceptUuids(item.get("concepts"), itemField)); item.put("required", itemFormField.getRequired()); item.remove("concept"); items.add(item); } section.put("items", items); sections.add(section); } json.put("sections", sections); } /** * Returns a list of compressed concept UUIDs (see compressUuid), given the "concepts" * member of a field's description JSON. If the "concepts" members is missing, returns * the field's concept. The description is assumed to be correctly formatted, i.e. the * concepts argument should be a list of integer IDs of existing concepts. Otherwise, an * uncaught exception will be thrown (as with any other case of internal database corruption). */ private List<Object> getConceptUuids(Object concepts, Field field) { if (concepts == null) { return Collections.singletonList(compressUuid(field.getConcept().getUuid())); } // If the casts in this method fail, we want it to produce an uncaught exception and // HTTP error 500 (not error 4xx, which is for invalid user input). List conceptIds = (List) concepts; List<Object> result = new ArrayList<>(); for (Object item : conceptIds) { int id = (Integer) item; Concept concept = conceptService.getConcept(id); if (concept == null) { throw new IllegalStateException("Concept " + id + " not found"); } result.add(compressUuid(concept.getUuid())); } return result; } /** * Saves a bit of JSON space by using integers to represent UUIDs that are exactly 36 * consist of an integer followed by a string of "A"s. All other UUIDs are left as strings. */ private static Object compressUuid(String uuid) { Matcher matcher = COMPRESSIBLE_UUID.matcher(uuid); if (uuid.length() == 36 && matcher.matches()) { return Integer.valueOf(matcher.group(1)); } return uuid; } private static Map<String, Object> parseFieldDescription(Field field) { String description = field.getDescription().trim(); if (description.startsWith("{\"")) { try { Map<String, Object> config = new ObjectMapper().readValue(field.getDescription(), Map.class); for (String key : config.keySet()) { if (config.get(key) == null || "".equals(config.get(key))) { config.remove(key); } } return config; } catch (IOException e) { throw new InvalidObjectDataException( "Invalid JSON in description of field " + field.getId()); } } else return new HashMap<String, Object>(); } /** * Returns all charts (there is no support for searching or filtering). * @param context the request context; specify the URL query parameter * "?v=full" to get a list of the groups and concepts in each form. * @see AbstractReadOnlyResource#search(RequestContext) */ @Override protected Iterable<Form> searchImpl(RequestContext context, long snapshotTime) { return getCharts(formService); } public static List<Form> getCharts(FormService formService) { List<Form> charts = new ArrayList<>(); String[] uuids = Context.getAdministrationService() .getGlobalProperty(GlobalProperties.CHART_UUIDS) .split(","); for (String uuid : uuids) { Form form = formService.getFormByUuid(uuid); if (form == null) { throw new ConfigurationException(GlobalProperties.CHART_UUIDS + " property is incorrect; cannot find form " + uuid); } charts.add(form); } return charts; } }