/** * This Source Code Form is subject to the terms of the Mozilla Public License, * v. 2.0. If a copy of the MPL was not distributed with this file, You can * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. * * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS * graphic logo is a trademark of OpenMRS Inc. */ package org.openmrs.module.webservices.rest.web.v1_0.resource.openmrs1_8; import org.apache.commons.lang.StringUtils; import org.openmrs.Concept; import org.openmrs.ConceptAnswer; import org.openmrs.ConceptClass; import org.openmrs.ConceptDatatype; import org.openmrs.ConceptDescription; import org.openmrs.ConceptMap; import org.openmrs.ConceptName; import org.openmrs.ConceptNumeric; import org.openmrs.ConceptSearchResult; import org.openmrs.Drug; import org.openmrs.api.ConceptService; import org.openmrs.api.context.Context; import org.openmrs.module.webservices.helper.HibernateCollectionHelper; import org.openmrs.module.webservices.rest.SimpleObject; import org.openmrs.module.webservices.rest.web.ConversionUtil; import org.openmrs.module.webservices.rest.web.RequestContext; import org.openmrs.module.webservices.rest.web.RestConstants; import org.openmrs.module.webservices.rest.web.annotation.PropertyGetter; import org.openmrs.module.webservices.rest.web.annotation.PropertySetter; import org.openmrs.module.webservices.rest.web.annotation.RepHandler; import org.openmrs.module.webservices.rest.web.annotation.Resource; import org.openmrs.module.webservices.rest.web.representation.DefaultRepresentation; import org.openmrs.module.webservices.rest.web.representation.FullRepresentation; import org.openmrs.module.webservices.rest.web.representation.NamedRepresentation; import org.openmrs.module.webservices.rest.web.representation.RefRepresentation; import org.openmrs.module.webservices.rest.web.representation.Representation; import org.openmrs.module.webservices.rest.web.resource.api.PageableResult; import org.openmrs.module.webservices.rest.web.resource.impl.AlreadyPaged; import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingCrudResource; import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceDescription; import org.openmrs.module.webservices.rest.web.resource.impl.NeedsPaging; import org.openmrs.module.webservices.rest.web.response.ConversionException; import org.openmrs.module.webservices.rest.web.response.ResourceDoesNotSupportOperationException; import org.openmrs.module.webservices.rest.web.response.ResponseException; import org.openmrs.util.LocaleUtility; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.Set; /** * {@link Resource} for {@link Concept}, supporting standard CRUD operations */ @Resource(name = RestConstants.VERSION_1 + "/concept", order = 2, supportedClass = Concept.class, supportedOpenmrsVersions = "1.8.*") public class ConceptResource1_8 extends DelegatingCrudResource<Concept> { public ConceptResource1_8() { //RESTWS-439 //Concept numeric fields allowedMissingProperties.add("hiNormal"); allowedMissingProperties.add("hiAbsolute"); allowedMissingProperties.add("hiCritical"); allowedMissingProperties.add("lowNormal"); allowedMissingProperties.add("lowAbsolute"); allowedMissingProperties.add("lowCritical"); allowedMissingProperties.add("units"); allowedMissingProperties.add("precise"); allowedMissingProperties.add("allowDecimal"); allowedMissingProperties.add("displayPrecision"); } @RepHandler(RefRepresentation.class) public SimpleObject asRef(Concept delegate) throws ConversionException { DelegatingResourceDescription description = new DelegatingResourceDescription(); description.addProperty("uuid"); description.addProperty("display", "displayString", Representation.DEFAULT); if (delegate.isRetired()) { description.addProperty("retired"); } description.addSelfLink(); return convertDelegateToRepresentation(delegate, description); } @RepHandler(FullRepresentation.class) public SimpleObject asFull(Concept delegate) throws ConversionException { DelegatingResourceDescription description = fullRepresentationDescription(delegate); return convertDelegateToRepresentation(delegate, description); } @RepHandler(value = NamedRepresentation.class, name = "fullchildren") public SimpleObject asFullChildren(Concept delegate) throws ConversionException { Set<String> path = new HashSet<String>(); path.add(delegate.getUuid()); assertNoCycles(delegate, path); return asFullChildrenInternal(delegate); } protected void assertNoCycles(Concept delegate, Set<String> path) throws ConversionException { for (Concept member : delegate.getSetMembers()) { if (path.add(member.getUuid())) { assertNoCycles(member, path); } else { throw new ConversionException("Cycles in children are not supported. Concept with uuid " + delegate.getUuid() + " repeats in a set."); } path.remove(member.getUuid()); } } /** * It is used internally for the fullchildren representation. Contrary to the fullchildren * handler it does not check for cycles. * * @param delegate * @return * @throws ConversionException */ @RepHandler(value = NamedRepresentation.class, name = "fullchildreninternal") public SimpleObject asFullChildrenInternal(Concept delegate) throws ConversionException { DelegatingResourceDescription description = fullRepresentationDescription(delegate); description.removeProperty("setMembers"); description.addProperty("setMembers", new NamedRepresentation("fullchildreninternal")); description.removeProperty("answers"); description.addProperty("answers", Representation.FULL); return convertDelegateToRepresentation(delegate, description); } @Override public List<Representation> getAvailableRepresentations() { List<Representation> availableRepresentations = super.getAvailableRepresentations(); availableRepresentations.add(new NamedRepresentation("fullchildren")); return availableRepresentations; } protected DelegatingResourceDescription fullRepresentationDescription(Concept delegate) { DelegatingResourceDescription description = new DelegatingResourceDescription(); description.addProperty("uuid"); description.addProperty("display"); description.addProperty("name", Representation.DEFAULT); description.addProperty("datatype", Representation.DEFAULT); description.addProperty("conceptClass", Representation.DEFAULT); description.addProperty("set"); description.addProperty("version"); description.addProperty("retired"); description.addProperty("names", Representation.DEFAULT); description.addProperty("descriptions", Representation.DEFAULT); description.addProperty("mappings", Representation.DEFAULT); description.addProperty("answers", Representation.DEFAULT); description.addProperty("setMembers", Representation.DEFAULT); description.addProperty("auditInfo"); description.addSelfLink(); if (delegate.isNumeric()) { description.addProperty("hiNormal"); description.addProperty("hiAbsolute"); description.addProperty("hiCritical"); description.addProperty("lowNormal"); description.addProperty("lowAbsolute"); description.addProperty("lowCritical"); description.addProperty("units"); description.addProperty("precise"); } return description; } /** * @see DelegatingCrudResource#getRepresentationDescription(Representation) */ @Override public DelegatingResourceDescription getRepresentationDescription(Representation rep) { if (rep instanceof DefaultRepresentation) { DelegatingResourceDescription description = new DelegatingResourceDescription(); description.addProperty("uuid"); description.addProperty("display"); description.addProperty("name", Representation.DEFAULT); description.addProperty("datatype", Representation.REF); description.addProperty("conceptClass", Representation.REF); description.addProperty("set"); description.addProperty("version"); description.addProperty("retired"); description.addProperty("names", Representation.REF); description.addProperty("descriptions", Representation.REF); description.addProperty("mappings", Representation.REF); description.addProperty("answers", Representation.REF); description.addProperty("setMembers", Representation.REF); //description.addProperty("conceptMappings", Representation.REF); add as subresource description.addSelfLink(); description.addLink("full", ".?v=" + RestConstants.REPRESENTATION_FULL); return description; } return null; } /** * @see org.openmrs.module.webservices.rest.web.resource.impl.BaseDelegatingResource#getCreatableProperties() */ @Override public DelegatingResourceDescription getCreatableProperties() { DelegatingResourceDescription description = new DelegatingResourceDescription(); description.addRequiredProperty("names"); description.addRequiredProperty("datatype"); description.addRequiredProperty("conceptClass"); description.addProperty("descriptions"); description.addProperty("set"); description.addProperty("version"); description.addProperty("mappings"); description.addProperty("answers"); description.addProperty("setMembers"); //ConceptNumeric properties description.addProperty("hiNormal"); description.addProperty("hiAbsolute"); description.addProperty("hiCritical"); description.addProperty("lowNormal"); description.addProperty("lowAbsolute"); description.addProperty("lowCritical"); description.addProperty("units"); description.addProperty("allowDecimal"); description.addProperty("displayPrecision"); return description; } /** * @see org.openmrs.module.webservices.rest.web.resource.impl.BaseDelegatingResource#getUpdatableProperties() */ @Override public DelegatingResourceDescription getUpdatableProperties() throws ResourceDoesNotSupportOperationException { DelegatingResourceDescription description = super.getUpdatableProperties(); description.addProperty("name"); description.addProperty("names"); description.addProperty("descriptions"); return description; } /** * @see org.openmrs.module.webservices.rest.web.resource.impl.BaseDelegatingResource#getPropertiesToExposeAsSubResources() */ @Override public List<String> getPropertiesToExposeAsSubResources() { return Arrays.asList("names", "descriptions", "conceptMappings"); } /** * Sets the name property to be the fully specified name of the Concept in the current locale * * @param instance * @param name */ @PropertySetter("name") public static void setFullySpecifiedName(Concept instance, String name) { ConceptName fullySpecifiedName = new ConceptName(name, Context.getLocale()); instance.setFullySpecifiedName(fullySpecifiedName); } /** * It's needed, because of ConversionException: Don't know how to handle collection class: * interface java.util.Collection If request to update Concept updates ConceptName, adequate * resource takes care of it, so this method just adds new and removes deleted names. * * @param instance * @param names * @throws NoSuchMethodException * @throws InvocationTargetException * @throws IllegalAccessException */ @PropertySetter("names") public static void setNames(Concept instance, List<ConceptName> names) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { new HibernateCollectionHelper<Concept, ConceptName>( instance) { @Override public int compare(ConceptName left, ConceptName right) { if (Objects.equals(left.getUuid(), right.getUuid())) { return 0; } boolean areEqual = (Objects.equals(left.getName(), right.getName()) && Objects.equals(left.getConceptNameType(), right.getConceptNameType()) && Objects.equals( left.getLocale(), right.getLocale())); return areEqual ? 0 : 1; } @Override public Collection<ConceptName> getAll() { return instance.getNames(); } @Override public void add(ConceptName item) { instance.addName(item); } @Override public void remove(ConceptName item) { instance.removeName(item); } }.set(names); } /** * It's needed, because of ConversionException: Don't know how to handle collection class: * interface java.util.Collection * * @param instance * @param descriptions * @throws NoSuchMethodException * @throws InvocationTargetException * @throws IllegalAccessException */ @PropertySetter("descriptions") public static void setDescriptions(Concept instance, List<ConceptDescription> descriptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { new HibernateCollectionHelper<Concept, ConceptDescription>( instance) { @Override public int compare(ConceptDescription left, ConceptDescription right) { if (Objects.equals(left.getUuid(), right.getUuid())) { return 0; } boolean areEqual = (Objects.equals(left.getDescription(), right.getDescription()) && Objects.equals( left.getLocale(), right.getLocale())); return areEqual ? 0 : 1; } @Override public Collection<ConceptDescription> getAll() { return instance.getDescriptions(); } @Override public void add(ConceptDescription item) { instance.addDescription(item); } @Override public void remove(ConceptDescription item) { instance.removeDescription(item); } }.set(descriptions); } /** * It's needed, because of ConversionException: Don't know how to handle collection class: * interface java.util.Collection * * @param instance * @param mappings */ @PropertySetter("mappings") public static void setMappings(Concept instance, List<ConceptMap> mappings) { instance.getConceptMappings().clear(); for (ConceptMap map : mappings) { instance.addConceptMapping(map); } } @PropertyGetter("mappings") public static List<ConceptMap> getMappings(Concept instance) { return new ArrayList<ConceptMap>(instance.getConceptMappings()); } /** * Gets the display name of the Concept delegate * * @param instance the delegate instance to get the display name off */ @PropertyGetter("display") public String getDisplayName(Concept instance) { ConceptName cn = instance.getName(); return cn == null ? null : cn.getName(); } /** * {@link #newDelegate(SimpleObject)} is used instead to support ConceptNumeric * * @see DelegatingCrudResource#newDelegate() */ @Override public Concept newDelegate() { throw new ResourceDoesNotSupportOperationException("Should use newDelegate(SimpleObject) instead"); } @Override public Concept newDelegate(SimpleObject object) { String datatypeUuid = (String) object.get("datatype"); if (ConceptDatatype.NUMERIC_UUID.equals(datatypeUuid)) { return new ConceptNumeric(); } else { return new Concept(); } } /** * @see DelegatingCrudResource#save(java.lang.Object) */ @Override public Concept save(Concept c) { return Context.getConceptService().saveConcept(c); } /** * Fetches a concept by uuid * * @see DelegatingCrudResource#getByUniqueId(java.lang.String) */ @Override public Concept getByUniqueId(String uuid) { return Context.getConceptService().getConceptByUuid(uuid); } /** * @see org.openmrs.module.webservices.rest.web.resource.impl.DelegatingCrudResource#purge(java.lang.Object, * org.openmrs.module.webservices.rest.web.RequestContext) */ @Override public void purge(Concept concept, RequestContext context) throws ResponseException { if (concept == null) return; Context.getConceptService().purgeConcept(concept); } /** * This does not include retired concepts * * @see org.openmrs.module.webservices.rest.web.resource.impl.DelegatingCrudResource#doGetAll(org.openmrs.module.webservices.rest.web.RequestContext) */ @Override protected NeedsPaging<Concept> doGetAll(RequestContext context) { List<Concept> allConcepts = Context.getConceptService().getAllConcepts(null, true, context.getIncludeAll()); return new NeedsPaging<Concept>(allConcepts, context); } /** * Concept searches support the following additional query parameters: * <ul> * <li>answerTo=(uuid): restricts results to concepts that are answers to the given concept uuid * </li> * <li>memberOf=(uuid): restricts to concepts that are set members of the given concept set's * uuid</li> * </ul> * * @see org.openmrs.module.webservices.rest.web.resource.impl.DelegatingCrudResource#doSearch(RequestContext) */ @Override protected PageableResult doSearch(RequestContext context) { ConceptService service = Context.getConceptService(); Integer startIndex = null; Integer limit = null; boolean canPage = true; // Collect information for answerTo and memberOf query parameters String answerToUuid = context.getRequest().getParameter("answerTo"); String memberOfUuid = context.getRequest().getParameter("memberOf"); Concept answerTo = null; List<Concept> memberOfList = null; if (StringUtils.isNotBlank(answerToUuid)) { try { answerTo = (Concept) ConversionUtil.convert(answerToUuid, Concept.class); } catch (ConversionException ex) { log.error("Unexpected exception while retrieving answerTo Concept with UUID " + answerToUuid, ex); } } if (StringUtils.isNotBlank(memberOfUuid)) { Concept memberOf = service.getConceptByUuid(memberOfUuid); memberOfList = service.getConceptsByConceptSet(memberOf); canPage = false; // ConceptService does not support memberOf searches, so paging must be deferred. } // Only set startIndex and limit if we can return paged results if (canPage) { startIndex = context.getStartIndex(); limit = context.getLimit(); } List<ConceptSearchResult> searchResults; // get the user's locales...and then convert that from a set to a list List<Locale> locales = new ArrayList<Locale>(LocaleUtility.getLocalesInOrder()); searchResults = service.getConcepts(context.getParameter("q"), locales, context.getIncludeAll(), null, null, null, null, answerTo, startIndex, limit); // convert search results into list of concepts List<Concept> results = new ArrayList<Concept>(searchResults.size()); for (ConceptSearchResult csr : searchResults) { // apply memberOf filter if (memberOfList == null || memberOfList.contains(csr.getConcept())) results.add(csr.getConcept()); } PageableResult result = null; if (canPage) { Integer count = service.getCountOfConcepts(context.getParameter("q"), locales, false, Collections.<ConceptClass> emptyList(), Collections.<ConceptClass> emptyList(), Collections.<ConceptDatatype> emptyList(), Collections.<ConceptDatatype> emptyList(), answerTo); boolean hasMore = count > startIndex + limit; result = new AlreadyPaged<Concept>(context, results, hasMore); } else { result = new NeedsPaging<Concept>(results, context); } return result; } @Override protected void delete(Concept c, String reason, RequestContext context) throws ResponseException { if (c.isRetired()) { // since DELETE should be idempotent, we return success here return; } Context.getConceptService().retireConcept(c, reason); } /** * @param instance * @return the list of Concepts or Drugs */ @PropertyGetter("answers") public static Object getAnswers(Concept instance) { List<ConceptAnswer> conceptAnswers = new ArrayList<ConceptAnswer>(); conceptAnswers.addAll(instance.getAnswers(false)); Collections.sort(conceptAnswers); List<Object> answers = new ArrayList<Object>(); for (ConceptAnswer conceptAnswer : conceptAnswers) { if (conceptAnswer.getAnswerDrug() != null) { answers.add(conceptAnswer.getAnswerDrug()); } else if (conceptAnswer.getAnswerConcept() != null) { answers.add(conceptAnswer.getAnswerConcept()); } } return answers; } /** * @param instance * @param answerUuids the list of Concepts or Drugs * @throws ResourceDoesNotSupportOperationException */ @PropertySetter("answers") public static void setAnswers(Concept instance, List<String> answerUuids /*Concept or Drug uuid*/) throws ResourceDoesNotSupportOperationException { // remove answers that are not in the new list Iterator<ConceptAnswer> iterator = instance.getAnswers(false).iterator(); while (iterator.hasNext()) { ConceptAnswer answer = iterator.next(); String conceptUuid = answer.getConcept().getUuid(); String drugUuid = (answer.getAnswerDrug() != null) ? answer.getAnswerDrug().getUuid() : null; if (answerUuids.contains(conceptUuid)) { answerUuids.remove(conceptUuid); // remove from passed in list } else if (answerUuids.contains(drugUuid)) { answerUuids.remove(drugUuid); // remove from passed in list } else instance.removeAnswer(answer); // remove from concept question object } List<Object> answerObjects = new ArrayList<Object>(answerUuids.size()); for (String uuid : answerUuids) { Concept c = Context.getConceptService().getConceptByUuid(uuid); if (c != null) { answerObjects.add(c); } else { // it is a drug Drug drug = Context.getConceptService().getDrugByUuid(uuid); if (drug != null) answerObjects.add(drug); else throw new ResourceDoesNotSupportOperationException("There is no concept or drug with given uuid: " + uuid); } } // add in new answers for (Object obj : answerObjects) { ConceptAnswer answerToAdd = null; if (obj.getClass().isAssignableFrom(Concept.class)) answerToAdd = new ConceptAnswer((Concept) obj); else answerToAdd = new ConceptAnswer(((Drug) obj).getConcept(), (Drug) obj); answerToAdd.setCreator(Context.getAuthenticatedUser()); answerToAdd.setDateCreated(new Date()); instance.addAnswer(answerToAdd); } } /** * @param instance * @param setMembers the list of Concepts */ @PropertySetter("setMembers") public static void setSetMembers(Concept instance, List<Concept> setMembers) { instance.getConceptSets().clear(); if (setMembers == null || setMembers.isEmpty()) { instance.setSet(false); } else { instance.setSet(true); for (Concept setMember : setMembers) { instance.addSetMember(setMember); } } } }