/** * 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.api.impl; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.apache.commons.lang.StringUtils; import org.hibernate.proxy.HibernateProxy; import org.openmrs.api.APIException; import org.openmrs.module.ModuleUtil; import org.openmrs.module.webservices.rest.web.OpenmrsClassScanner; import org.openmrs.module.webservices.rest.web.RestConstants; import org.openmrs.module.webservices.rest.web.annotation.SubResource; import org.openmrs.module.webservices.rest.web.api.RestHelperService; import org.openmrs.module.webservices.rest.web.api.RestService; import org.openmrs.module.webservices.rest.web.representation.CustomRepresentation; import org.openmrs.module.webservices.rest.web.representation.NamedRepresentation; import org.openmrs.module.webservices.rest.web.representation.Representation; import org.openmrs.module.webservices.rest.web.resource.api.Resource; import org.openmrs.module.webservices.rest.web.resource.api.SearchConfig; import org.openmrs.module.webservices.rest.web.resource.api.SearchHandler; import org.openmrs.module.webservices.rest.web.resource.api.SearchParameter; import org.openmrs.module.webservices.rest.web.resource.api.SearchQuery; import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceHandler; import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingSubclassHandler; import org.openmrs.module.webservices.rest.web.response.InvalidSearchException; import org.openmrs.module.webservices.rest.web.response.UnknownResourceException; import org.openmrs.util.OpenmrsConstants; /** * Default implementation of the {@link RestService} */ public class RestServiceImpl implements RestService { volatile Map<String, ResourceDefinition> resourceDefinitionsByNames; volatile Map<Class<?>, Resource> resourcesBySupportedClasses; private volatile Map<CompositeSearchHandlerKeyValue, Set<SearchHandler>> searchHandlersByParameter; private volatile Map<CompositeSearchHandlerKeyValue, SearchHandler> searchHandlersByIds; private volatile Map<String, Set<SearchHandler>> searchHandlersByResource; private volatile List<SearchHandler> allSearchHandlers; private RestHelperService restHelperService; private OpenmrsClassScanner openmrsClassScanner; public RestHelperService getRestHelperService() { return restHelperService; } public void setRestHelperService(RestHelperService restHelperService) { this.restHelperService = restHelperService; } public OpenmrsClassScanner getOpenmrsClassScanner() { return openmrsClassScanner; } public void setOpenmrsClassScanner(OpenmrsClassScanner openmrsClassScanner) { this.openmrsClassScanner = openmrsClassScanner; } public RestServiceImpl() { } static class ResourceDefinition { public Resource resource; public int order; public ResourceDefinition(Resource resource, int order) { this.resource = resource; this.order = order; } } /** * Wraps {@code Resource} name and an additional string-based key into a composite key. */ private static class CompositeSearchHandlerKeyValue { public final String supportedResource; public final String secondKey; public final String secondKeyValue; public CompositeSearchHandlerKeyValue(String supportedResource, String additionalKeyProperty) { this.supportedResource = supportedResource; this.secondKey = additionalKeyProperty; this.secondKeyValue = null; } public CompositeSearchHandlerKeyValue(String supportedResource, String additionalKeyProperty, String additionalKeyPropertyValue) { this.supportedResource = supportedResource; this.secondKey = additionalKeyProperty; this.secondKeyValue = additionalKeyPropertyValue; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; CompositeSearchHandlerKeyValue that = (CompositeSearchHandlerKeyValue) o; if (!supportedResource.equals(that.supportedResource)) return false; if (!secondKey.equals(that.secondKey)) return false; return secondKeyValue != null ? secondKeyValue.equals(that.secondKeyValue) : that.secondKeyValue == null; } @Override public int hashCode() { int result = supportedResource.hashCode(); result = 31 * result + secondKey.hashCode(); result = 31 * result + (secondKeyValue != null ? secondKeyValue.hashCode() : 0); return result; } } private void initializeResources() { if (resourceDefinitionsByNames != null) { return; } Map<String, ResourceDefinition> tempResourceDefinitionsByNames = new HashMap<String, ResourceDefinition>(); Map<Class<?>, Resource> tempResourcesBySupportedClasses = new HashMap<Class<?>, Resource>(); List<Class<? extends Resource>> resources; try { resources = openmrsClassScanner.getClasses(Resource.class, true); } catch (IOException e) { throw new APIException("Cannot access REST resources", e); } for (Class<? extends Resource> resource : resources) { ResourceMetadata resourceMetadata = getResourceMetadata(resource); if (resourceMetadata == null) continue; if (isResourceToBeAdded(resourceMetadata, tempResourceDefinitionsByNames.get(resourceMetadata.getName()))) { Resource newResource = newResource(resource); tempResourceDefinitionsByNames.put(resourceMetadata.getName(), new ResourceDefinition(newResource, resourceMetadata.getOrder())); tempResourcesBySupportedClasses.put(resourceMetadata.getSupportedClass(), newResource); } } resourcesBySupportedClasses = tempResourcesBySupportedClasses; resourceDefinitionsByNames = tempResourceDefinitionsByNames; } /** * Determines whether a {@code Resource} should be added to the cache. * * @param resourceMetadata the resource metadata of the resource to be added * @param existingResourceDefinition the resource definition of resource * @return true if the resource should be added and false otherwise */ private boolean isResourceToBeAdded(ResourceMetadata resourceMetadata, ResourceDefinition existingResourceDefinition) { if (existingResourceDefinition == null) { return true; } if (existingResourceDefinition.order == resourceMetadata.getOrder()) { throw new IllegalStateException("Two resources with the same name (" + resourceMetadata.getName() + ") must not have the same order"); } if (existingResourceDefinition.order < resourceMetadata.getOrder()) { return false; } return true; } /** * Gets {@code ResourceMetadata} from a {@code Resource} classes annotations. * * @param resource the resource to get the metadata from * @return the metadata of a resource */ private ResourceMetadata getResourceMetadata(Class<? extends Resource> resource) { ResourceMetadata resourceMetadata; org.openmrs.module.webservices.rest.web.annotation.Resource resourceAnnotation = resource .getAnnotation(org.openmrs.module.webservices.rest.web.annotation.Resource.class); if (resourceAnnotation == null) { SubResource subresourceAnnotation = resource.getAnnotation(SubResource.class); if (subresourceAnnotation == null || !isOpenmrsVersionInVersions(subresourceAnnotation.supportedOpenmrsVersions())) { return null; } org.openmrs.module.webservices.rest.web.annotation.Resource parentResourceAnnotation = subresourceAnnotation .parent().getAnnotation(org.openmrs.module.webservices.rest.web.annotation.Resource.class); if (parentResourceAnnotation == null) { return null; } resourceMetadata = new ResourceMetadata(parentResourceAnnotation.name() + "/" + subresourceAnnotation.path(), subresourceAnnotation.supportedClass(), subresourceAnnotation.order()); } else { if (!isOpenmrsVersionInVersions(resourceAnnotation.supportedOpenmrsVersions())) { return null; } resourceMetadata = new ResourceMetadata(resourceAnnotation.name(), resourceAnnotation.supportedClass(), resourceAnnotation.order()); } return resourceMetadata; } private static class ResourceMetadata { private final String name; private final Class<?> supportedClass; private final int order; public ResourceMetadata(String name, Class<?> supportedClass, int order) { this.name = name; this.supportedClass = supportedClass; this.order = order; } public String getName() { return name; } public Class<?> getSupportedClass() { return supportedClass; } public int getOrder() { return order; } } /** * Checks if OpenMRS version is in given array of versions. * * @param versions the array of versions to be checked for the openmrs version * @return true if the openmrs version is in versions and false otherwise */ private boolean isOpenmrsVersionInVersions(String[] versions) { if (versions.length == 0) { return false; } boolean result = false; for (String version : versions) { if (ModuleUtil.matchRequiredVersions(OpenmrsConstants.OPENMRS_VERSION_SHORT, version)) { result = true; break; } } return result; } private void initializeSearchHandlers() { if (searchHandlersByIds != null) { return; } Map<CompositeSearchHandlerKeyValue, SearchHandler> tempSearchHandlersByIds = new HashMap<CompositeSearchHandlerKeyValue, SearchHandler>(); Map<CompositeSearchHandlerKeyValue, Set<SearchHandler>> tempSearchHandlersByParameters = new HashMap<CompositeSearchHandlerKeyValue, Set<SearchHandler>>(); Map<String, Set<SearchHandler>> tempSearchHandlersByResource = new HashMap<String, Set<SearchHandler>>(); List<SearchHandler> allSearchHandlers = restHelperService.getRegisteredSearchHandlers(); for (SearchHandler searchHandler : allSearchHandlers) { addSearchHandler(tempSearchHandlersByIds, tempSearchHandlersByParameters, tempSearchHandlersByResource, searchHandler); } this.allSearchHandlers = allSearchHandlers; searchHandlersByParameter = tempSearchHandlersByParameters; searchHandlersByIds = tempSearchHandlersByIds; searchHandlersByResource = tempSearchHandlersByResource; } private void addSearchHandler(Map<CompositeSearchHandlerKeyValue, SearchHandler> tempSearchHandlersByIds, Map<CompositeSearchHandlerKeyValue, Set<SearchHandler>> tempSearchHandlersByParameters, Map<String, Set<SearchHandler>> tempSearchHandlersByResource, SearchHandler searchHandler) { for (String supportedVersion : searchHandler.getSearchConfig().getSupportedOpenmrsVersions()) { if (ModuleUtil.matchRequiredVersions(OpenmrsConstants.OPENMRS_VERSION_SHORT, supportedVersion)) { addSupportedSearchHandler(tempSearchHandlersByIds, tempSearchHandlersByParameters, searchHandler); addSearchHandlerToResourceMap(tempSearchHandlersByResource, searchHandler); } } } private void addSupportedSearchHandler(Map<CompositeSearchHandlerKeyValue, SearchHandler> tempSearchHandlersByIds, Map<CompositeSearchHandlerKeyValue, Set<SearchHandler>> tempSearchHandlersByParameters, SearchHandler searchHandler) { CompositeSearchHandlerKeyValue searchHanlderIdKey = new CompositeSearchHandlerKeyValue(searchHandler .getSearchConfig().getSupportedResource(), searchHandler.getSearchConfig().getId()); SearchHandler previousSearchHandler = tempSearchHandlersByIds.put(searchHanlderIdKey, searchHandler); if (previousSearchHandler != null) { SearchConfig config = searchHandler.getSearchConfig(); throw new IllegalStateException("Two search handlers (" + searchHandler.getClass() + ", " + previousSearchHandler.getClass() + ") for the same resource (" + config.getSupportedResource() + ") must not have the same ID (" + config.getId() + ")"); } addSearchHandlerToParametersMap(tempSearchHandlersByParameters, searchHandler); } private void addSearchHandlerToParametersMap( Map<CompositeSearchHandlerKeyValue, Set<SearchHandler>> tempSearchHandlersByParameters, SearchHandler searchHandler) { for (SearchQuery searchQueries : searchHandler.getSearchConfig().getSearchQueries()) { Set<SearchParameter> parameters = new HashSet<SearchParameter>(searchQueries.getRequiredParameters()); parameters.addAll(searchQueries.getOptionalParameters()); for (SearchParameter parameter : parameters) { CompositeSearchHandlerKeyValue parameterKey = new CompositeSearchHandlerKeyValue(searchHandler .getSearchConfig().getSupportedResource(), parameter.getName(), parameter.getValue()); Set<SearchHandler> list = tempSearchHandlersByParameters.get(parameterKey); if (list == null) { list = new HashSet<SearchHandler>(); tempSearchHandlersByParameters.put(parameterKey, list); } list.add(searchHandler); } } } private void addSearchHandlerToResourceMap(Map<String, Set<SearchHandler>> tempSearchHandlersByResource, SearchHandler searchHandler) { SearchConfig config = searchHandler.getSearchConfig(); Set<SearchHandler> handlers = tempSearchHandlersByResource.get(config.getSupportedResource()); if (handlers == null) { handlers = new HashSet<SearchHandler>(); tempSearchHandlersByResource.put(config.getSupportedResource(), handlers); } handlers.add(searchHandler); } /** * @see org.openmrs.module.webservices.rest.web.api.RestService#getRepresentation(java.lang.String) * @should return default representation if given null * @should return default representation if given string is empty * @should return reference representation if given string matches the ref representation * constant * @should return default representation if given string matches the default representation * constant * @should return full representation if given string matches the full representation constant * @should return an instance of custom representation if given string starts with the custom * representation prefix * @should return an instance of named representation for given string if it is not empty and * does not match any other case */ @Override public Representation getRepresentation(String requested) { if (StringUtils.isEmpty(requested)) { return Representation.DEFAULT; } if (RestConstants.REPRESENTATION_REF.equals(requested)) { return Representation.REF; } else if (RestConstants.REPRESENTATION_DEFAULT.equals(requested)) { return Representation.DEFAULT; } else if (RestConstants.REPRESENTATION_FULL.equals(requested)) { return Representation.FULL; } else if (requested.startsWith(RestConstants.REPRESENTATION_CUSTOM_PREFIX)) { return new CustomRepresentation(requested.replace(RestConstants.REPRESENTATION_CUSTOM_PREFIX, "")); } return new NamedRepresentation(requested); } /** * @see org.openmrs.module.webservices.rest.web.api.RestService#getResourceByName(String) * @should return resource for given name * @should return resource for given name and ignore unannotated resources * @should fail if failed to get resource classes * @should fail if resource for given name cannot be found * @should fail if resource for given name does not support the current openmrs version * @should return subresource for given name * @should fail if subresource for given name does not support the current openmrs version * @should fail if two resources with same name and order are found for given name * @should return resource with lower order value if two resources with the same name are found * for given name */ @Override public Resource getResourceByName(String name) throws APIException { initializeResources(); ResourceDefinition resourceDefinition = resourceDefinitionsByNames.get(name); if (resourceDefinition == null) { throw new UnknownResourceException("Unknown resource: " + name); } else { return resourceDefinition.resource; } } /** * @see org.openmrs.module.webservices.rest.web.api.RestService#getResourceBySupportedClass(Class) * @should return resource supporting given class and current openmrs version * @should fail if no resource supporting given class and current openmrs version was found * @should fail if no resource supporting given class was found * @should return resource supporting superclass of given class if given class is a hibernate * proxy * @should return resource supporting superclass of given class if no resource supporting given * class was found * @should return resource supporting direct superclass of given class if no resource supporting * given class was found but multiple resources supporting multiple superclasses exist * @should fail if failed to get resource classes * @should fail if two resources with same name and order are found for given class * @should return resource with lower order value if two resources with the same name are found * for given class */ @Override public Resource getResourceBySupportedClass(Class<?> resourceClass) throws APIException { initializeResources(); if (HibernateProxy.class.isAssignableFrom(resourceClass)) { resourceClass = resourceClass.getSuperclass(); } Resource resource = resourcesBySupportedClasses.get(resourceClass); if (resource == null) { Entry<Class<?>, Resource> bestResourceEntry = null; for (Entry<Class<?>, Resource> resourceEntry : resourcesBySupportedClasses.entrySet()) { if (resourceEntry.getKey().isAssignableFrom(resourceClass)) { if (bestResourceEntry == null || bestResourceEntry.getKey().isAssignableFrom(resourceEntry.getKey())) { bestResourceEntry = resourceEntry; } } } if (bestResourceEntry != null) { resource = bestResourceEntry.getValue(); } } if (resource == null) { throw new APIException("Unknown resource: " + resourceClass); } else { return resource; } } /** * @throws InstantiationException */ private Resource newResource(Class<? extends Resource> resourceClass) { try { Resource resource = resourceClass.newInstance(); return resource; } catch (Exception ex) { throw new APIException("Failed to instantiate " + resourceClass, ex); } } /** * Returns a search handler, which supports the given resource and the map of parameters and * values. * <p> * A {@code SearchHandler} is selected according to following steps (in this order): * <ul> * <li>Lookup a {@code SearchHandler} based on its {@code id} ({@code SearchConfig#id}) if * specified in given {@code parameters}. This lookup can fail if no or two * {@code SearchHandler}'s is/are found for given {@code id} and {@code resourceName}.</li> * <li>Lookup a {@code SearchHandler} based on given {@code parameters} if no {@code id} is * specified. The lookup returns the {@code SearcHandler} supporting all requested * {@code parameters} and with {@code parameters} satisfying the {@code SearchHandler}'s * {@code SearchConfig}'s required parameters. This lookup can fail if more than 1 * {@code SearchHandler} satisfies the requirements mentioned before.</li> * </ul> * If no {@code SearchHandler} is found, {@code NULL} is returned. * </p> * * @see org.openmrs.module.webservices.rest.web.api.RestService#getSearchHandler(java.lang.String, * java.util.Map) * @should return search handler matching id set in given parameters * @should fail if parameters contain a search handler id which cannot be found * @should fail if two search handlers for the same resource have the same id * @should return null if parameters do not contain a search handler id and no other non special * request parameters * @should return search handler providing all request parameters and parameters satisfying its * required parameters * @should return null if given parameters are missing a parameter required by search handlers * eligible for given resource name and parameters * @should fail if two search handlers match given resource and parameters and no search handler * id is specified * @should return null if a non special request parameter in given parameters cannot be found in * any search handler * @should return null if no search handler is found for given resource name * @should return null if no search handler is found for current openmrs version */ @Override public SearchHandler getSearchHandler(String resourceName, Map<String, String[]> parameters) throws APIException { initializeSearchHandlers(); Set<SearchParameter> searchParameters = new HashSet<SearchParameter>(); for (Map.Entry<String, String[]> parameter : parameters.entrySet()) { if (!RestConstants.SPECIAL_REQUEST_PARAMETERS.contains(parameter.getKey()) || RestConstants.REQUEST_PROPERTY_FOR_TYPE.equals(parameter.getKey())) { searchParameters.add(new SearchParameter(parameter.getKey(), parameter.getValue()[0])); } } String[] searchIds = parameters.get(RestConstants.REQUEST_PROPERTY_FOR_SEARCH_ID); if (searchIds != null && searchIds.length > 0) { SearchHandler searchHandler = searchHandlersByIds.get(new CompositeSearchHandlerKeyValue(resourceName, searchIds[0])); if (searchHandler == null) { throw new InvalidSearchException("The search with id '" + searchIds[0] + "' for '" + resourceName + "' resource is not recognized"); } else { return searchHandler; } } Set<SearchHandler> candidateSearchHandlers = null; for (SearchParameter param : searchParameters) { Set<SearchHandler> searchHandlers = searchHandlersByParameter.get(new CompositeSearchHandlerKeyValue( resourceName, param.getName(), param.getValue())); if (searchHandlers == null) { searchHandlers = searchHandlersByParameter.get(new CompositeSearchHandlerKeyValue(resourceName, param .getName())); if (searchHandlers == null) return null; //Missing parameter so there's no handler. } if (candidateSearchHandlers == null) { candidateSearchHandlers = new HashSet<SearchHandler>(); candidateSearchHandlers.addAll(searchHandlers); } else { //Eliminate candidate search handlers that do not include all parameters candidateSearchHandlers.retainAll(searchHandlers); } } if (candidateSearchHandlers == null) { return null; } else { eliminateCandidateSearchHandlersWithMissingRequiredParameters(candidateSearchHandlers, searchParameters); if (candidateSearchHandlers.isEmpty()) { return null; } else if (candidateSearchHandlers.size() == 1) { return candidateSearchHandlers.iterator().next(); } else { List<String> candidateSearchHandlerIds = new ArrayList<String>(); for (SearchHandler candidateSearchHandler : candidateSearchHandlers) { candidateSearchHandlerIds.add(RestConstants.REQUEST_PROPERTY_FOR_SEARCH_ID + "=" + candidateSearchHandler.getSearchConfig().getId()); } throw new InvalidSearchException("The search is ambiguous. Please specify " + StringUtils.join(candidateSearchHandlerIds, " or ")); } } } /** * Eliminate search handlers with at least one required parameter that is not provided in * {@code searchParameters}. * * @param candidateSearchHandlers the search handlers to filter for required parameters * @param searchParameters the search parameters to be checked against search handlers required * parameters */ private void eliminateCandidateSearchHandlersWithMissingRequiredParameters(Set<SearchHandler> candidateSearchHandlers, Set<SearchParameter> searchParameters) { Iterator<SearchHandler> it = candidateSearchHandlers.iterator(); while (it.hasNext()) { SearchHandler candidateSearchHandler = it.next(); boolean remove = true; for (SearchQuery candidateSearchQueries : candidateSearchHandler.getSearchConfig().getSearchQueries()) { Set<SearchParameter> requiredParameters = new HashSet<SearchParameter>( candidateSearchQueries.getRequiredParameters()); Iterator<SearchParameter> iterator = requiredParameters.iterator(); while (iterator.hasNext()) { SearchParameter requiredParameter = iterator.next(); for (SearchParameter param : searchParameters) { if (requiredParameter.getValue() == null) { if (requiredParameter.getName().equals(param.getName())) { iterator.remove(); } } else { if (requiredParameter.equals(param)) { iterator.remove(); } } } } if (requiredParameters.isEmpty()) { remove = false; break; } } if (remove) { it.remove(); } } } /** * @see org.openmrs.module.webservices.rest.web.api.RestService#getResourceHandlers() * @should return list of delegating resource handlers including subclass handlers * @should return list with delegating resource with lower order value if two resources with the * same name are found for given name * @should fail if failed to get resource classes * @should fail if two resources with same name and order are found for a class */ @Override public List<DelegatingResourceHandler<?>> getResourceHandlers() throws APIException { initializeResources(); List<DelegatingResourceHandler<?>> resourceHandlers = new ArrayList<DelegatingResourceHandler<?>>(); for (Resource resource : resourcesBySupportedClasses.values()) { if (resource instanceof DelegatingResourceHandler) { resourceHandlers.add((DelegatingResourceHandler<?>) resource); } } List<DelegatingSubclassHandler> subclassHandlers = restHelperService.getRegisteredRegisteredSubclassHandlers(); for (DelegatingSubclassHandler subclassHandler : subclassHandlers) { resourceHandlers.add(subclassHandler); } return resourceHandlers; } /** * @see org.openmrs.module.webservices.rest.web.api.RestService#getAllSearchHandlers() * @should return all search handlers if search handlers have been initialized * @should return null if search handlers have not been initialized */ public List<SearchHandler> getAllSearchHandlers() { return allSearchHandlers; } /** * @see org.openmrs.module.webservices.rest.web.api.RestService#getSearchHandlers(java.lang.String) * @should return search handlers for given resource name * @should return null if no search handler is found for given resource name * @should return null if no search handler is found for current openmrs version * @should return null given null * @should fail if two search handlers for the same resource have the same id */ @Override public Set<SearchHandler> getSearchHandlers(String resourceName) { if (searchHandlersByResource == null) { initializeSearchHandlers(); } return searchHandlersByResource.get(resourceName); } /** * @see RestService#initialize() * @should initialize resources and search handlers * @should clear cached resources and search handlers and reinitialize them * @should fail if failed to get resource classes * @should fail if failed to instantiate a resource * @should fail if two resources with same name and order are found * @should fail if two search handlers for the same resource have the same id */ @Override public void initialize() { // first clear out any existing values resourceDefinitionsByNames = null; resourcesBySupportedClasses = null; searchHandlersByIds = null; searchHandlersByParameter = null; searchHandlersByResource = null; initializeResources(); initializeSearchHandlers(); } }