// 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.Concept;
import org.openmrs.ConceptAnswer;
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.hl7.HL7Constants;
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.projectbuendia.ClientConceptNamer;
import org.openmrs.projectbuendia.Utils;
import org.projectbuendia.openmrs.webservices.rest.RestController;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/**
* REST collection of all the concepts that are present in at least one of the
* charts returned by {@link ChartResource}.
* @see AbstractReadOnlyResource
*/
@Resource(name = RestController.REST_VERSION_1_AND_NAMESPACE + "/concepts",
supportedClass = Concept.class, supportedOpenmrsVersions = "1.10.*,1.11.*")
public class ConceptResource extends AbstractReadOnlyResource<Concept> {
/** A map from HL7 type abbreviations to short names used in JSON output. */
private static final Map<String, String> HL7_TYPE_NAMES = new HashMap<>();
static {
HL7_TYPE_NAMES.put(HL7Constants.HL7_BOOLEAN, "coded");
HL7_TYPE_NAMES.put(HL7Constants.HL7_CODED, "coded");
HL7_TYPE_NAMES.put(HL7Constants.HL7_CODED_WITH_EXCEPTIONS, "coded");
HL7_TYPE_NAMES.put(HL7Constants.HL7_TEXT, "text");
HL7_TYPE_NAMES.put(HL7Constants.HL7_NUMERIC, "numeric");
HL7_TYPE_NAMES.put(HL7Constants.HL7_DATE, "date");
HL7_TYPE_NAMES.put(HL7Constants.HL7_DATETIME, "datetime");
HL7_TYPE_NAMES.put("ZZ", "none");
}
private final FormService formService;
private final ConceptService conceptService;
private final ClientConceptNamer namer;
public ConceptResource() {
super("concept", Representation.DEFAULT);
formService = Context.getFormService();
conceptService = Context.getConceptService();
namer = new ClientConceptNamer(Context.getLocale());
}
/**
* Retrieves a single concept with the given UUID.
* @param context the request context; specify the URL query parameter
* "locales=[comma-separated list]" to get localized concept names for
* specific locales; otherwise, all available locales will be returned.
* @see AbstractReadOnlyResource#retrieve(String, RequestContext)
*/
@Override
protected Concept retrieveImpl(String uuid, RequestContext context, long snapshotTime) {
return conceptService.getConceptByUuid(uuid);
}
/**
* Returns all concepts present in at least one of the charts returned by
* {@link ChartResource} (there is no support for searching or filtering).
* @param context the request context; specify the URL query parameter
* "locales=[comma-separated list]" to get localized concept names for
* specific locales; otherwise, all available locales will be returned.
* @see AbstractReadOnlyResource#search(RequestContext)
*/
@Override protected Iterable<Concept> searchImpl(RequestContext context, long snapshotTime) {
// Retrieves all the concepts that the client needs to know about
// (the concepts within all the charts served by ChartResource).
Set<Concept> ret = new HashSet<>();
for (Form chart : ChartResource.getCharts(formService)) {
for (FormField formField : chart.getFormFields()) {
Field field = formField.getField();
Concept fieldConcept = field.getConcept();
if (fieldConcept != null) {
ret.add(fieldConcept);
for (ConceptAnswer answer : fieldConcept.getAnswers(false)) {
ret.add(answer.getAnswerConcept());
}
}
// Fetch and add additional concepts that might have been stored in the field
// description.
for (Concept additionalConcept :
getConceptsFromFieldDescription(field.getDescription())) {
ret.add(additionalConcept);
}
}
}
return ret;
}
/**
* Buendia supports multiple concepts per field, but OpenMRS does not. To handle this, Buendia
* packs additional concept IDs into the JSON field description that it generates in the
* {@code profile_apply} script.
* <p>
* This method extracts those concept IDs and converts them into {@link Concept}s.
*
* @param fieldDescription The output of {@link Field#getDescription()}.
* @return a list of referenced concepts, or an empty list if the field description doesn't
* match the expected input format.
*/
private static List<Concept> getConceptsFromFieldDescription(
@Nullable String fieldDescription) {
if (fieldDescription == null) {
return Collections.emptyList();
}
List<Concept> returnValue = new ArrayList<>();
try {
SimpleObject extendedData = SimpleObject.parseJson(fieldDescription);
Object object = extendedData.get("concepts");
if (! (object instanceof List)) {
return Collections.emptyList();
}
List list = (List) object;
for (Object obj : list) {
if (obj == null) {
continue;
}
// We're ok with a ClassCastException here if this isn't an Integer.
Integer conceptId = (Integer) obj;
Concept concept = Context.getConceptService().getConcept(conceptId);
if (concept == null) {
continue;
}
returnValue.add(concept);
}
} catch (IOException ignore) {
}
return returnValue;
}
/**
* Adds the following fields to the {@link SimpleObject}:
* <ul>
* <li>"xform_id": the ID used to identify the concept in an xform
* (not the concept's UUID)
* <li>"type": one of the following type names, indicating the concept
* datatype as defined by OpenMRS:
* <ul>
* <li>coded
* <li>text
* <li>numeric
* <li>datetime
* <li>date
* <li>none
* </ul>
* <li>names: a {@link Map} of locales to concept names. If locales
* are specified in the "locales" URL query parameter, only those
* locales will appear; otherwise all allowed locales will appear.
* </ul>
* @param context the request context; specify the URL query parameter
* "locales=[comma-separated list]" to get localized concept names for
* specific locales; otherwise, all available locales will be returned.
*/
@Override protected void populateJsonProperties(
Concept concept, RequestContext context, SimpleObject json, long snapshotTime) {
// TODO: Cache this in the request context?
List<Locale> locales = getLocalesForRequest(context);
String jsonType = HL7_TYPE_NAMES.get(concept.getDatatype().getHl7Abbreviation());
if (jsonType == null) {
throw new ConfigurationException("Concept %s has unmapped HL7 data type %s",
concept.getName().getName(), concept.getDatatype().getHl7Abbreviation());
}
json.put("xform_id", concept.getId());
json.put("type", jsonType);
Map<String, String> names = new HashMap<>();
for (Locale locale : locales) {
names.put(locale.toString(), namer.getClientName(concept));
}
json.put("names", names);
}
private List<Locale> getLocalesForRequest(RequestContext context) {
// TODO: Make this cheap to call multiple times for a single request.
String localeIds = context.getRequest().getParameter("locales");
if (localeIds == null || localeIds.trim().equals("")) {
return Context.getAdministrationService().getAllowedLocales();
}
List<Locale> locales = new ArrayList<>();
for (String localeId : localeIds.split(",")) {
locales.add(Locale.forLanguageTag(localeId.trim()));
}
return locales;
}
}