org.openmrs.module.webservices.docs.ResourceDocCreator.java Source code

Java tutorial

Introduction

Here is the source code for org.openmrs.module.webservices.docs.ResourceDocCreator.java

Source

/**
 * The contents of this file are subject to the OpenMRS Public License
 * Version 1.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://license.openmrs.org
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations
 * under the License.
 *
 * Copyright (C) OpenMRS, LLC.  All Rights Reserved.
 */
package org.openmrs.module.webservices.docs;

import org.apache.commons.lang.StringUtils;
import org.openmrs.api.context.Context;
import org.openmrs.module.webservices.rest.SimpleObject;
import org.openmrs.module.webservices.rest.web.annotation.WSDoc;
import org.openmrs.module.webservices.rest.web.api.RestService;
import org.openmrs.module.webservices.rest.web.representation.Representation;
import org.openmrs.module.webservices.rest.web.resource.api.Converter;
import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceDescription;
import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceDescription.Property;
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.ConversionException;
import org.openmrs.module.webservices.rest.web.response.ResourceDoesNotSupportOperationException;
import org.openmrs.module.webservices.rest.web.v1_0.controller.MainResourceController;
import org.openmrs.module.webservices.rest.web.v1_0.controller.MainSubResourceController;
import org.openmrs.util.OpenmrsUtil;
import org.springframework.web.bind.annotation.RequestMapping;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

/**
 * Creates documentation about web service resources.
 */
public class ResourceDocCreator {

    /**
     * Creates a map of resource names and their documentation objects.
     * 
     * @param baseUrl the base or root for all the urls. e.g http://localhost:8080/openmrs
     * @return a map of ResourceData objects keyed by their resource names.
     * @throws IOException
     */
    public static Map<String, ResourceDoc> createDocMap(String baseUrl)
            throws IllegalAccessException, InstantiationException, IOException, ConversionException {

        Map<String, ResourceDoc> resouceDocMap = new HashMap<String, ResourceDoc>();

        List<DelegatingResourceHandler<?>> resourceHandlers = Context.getService(RestService.class)
                .getResourceHandlers();

        fillRepresentations(resourceHandlers, resouceDocMap);
        //fillOperations(resourceDocMap);
        fillUrls(baseUrl, resouceDocMap);

        return resouceDocMap;
    }

    /**
     * Creates a list of resource documentation objects.
     * 
     * @return the documentation object list.
     * @throws IllegalAccessException
     * @throws InstantiationException
     */
    public static List<ResourceDoc> create(String baseUrl)
            throws IllegalAccessException, InstantiationException, IOException, ConversionException {

        List<ResourceDoc> docs = new ArrayList<ResourceDoc>();

        docs.addAll(createDocMap(baseUrl).values());

        //Remove resources for subtype handlers
        for (Iterator<ResourceDoc> it = docs.iterator(); it.hasNext();) {
            ResourceDoc resourceDoc = it.next();
            if (resourceDoc.isSubtypeHandler()) {
                it.remove();
            }
        }
        Collections.sort(docs);

        return docs;
    }

    /**
     * Fills a map of resource names and their documentation objects with resource representations.
     * 
     * @param resourceHandlers resource classes.
     * @param resourceDocMap a map of each resource name and its corresponding documentation object.
     * @throws IllegalAccessException
     * @throws InstantiationException
     */
    private static void fillRepresentations(List<DelegatingResourceHandler<?>> resourceHandlers,
            Map<String, ResourceDoc> resourceDocMap)
            throws IllegalAccessException, InstantiationException, ConversionException {

        //We want to handle all resources before their subresources
        Collections.sort(resourceHandlers, new Comparator<DelegatingResourceHandler<?>>() {

            @Override
            public int compare(DelegatingResourceHandler<?> left, DelegatingResourceHandler<?> right) {
                return isSubclass(left).compareTo(isSubclass(right));
            }

            private Boolean isSubclass(DelegatingResourceHandler<?> resourceHandler) {
                return resourceHandler.getClass().getAnnotation(
                        org.openmrs.module.webservices.rest.web.annotation.SubResource.class) != null;
            }
        });

        //Go through all resource classes asking each for its default, ref and full representation.                                                                                                   InstantiationException {
        for (DelegatingResourceHandler<?> resourceHandler : resourceHandlers) {
            if (resourceHandler.getClass().getName()
                    .equals("org.openmrs.module.webservices.rest.web.HivDrugOrderSubclassHandler")) {
                continue; //Skip the test class
            }

            Object delegate = null;
            try {
                delegate = resourceHandler.newDelegate();
            } catch (ResourceDoesNotSupportOperationException ex) {
                continue;
            }
            if (delegate == null) {
                // TODO: handle resources that don't implement newDelegate(), e.g. ConceptSearchResource1_9, all subclasses of EvaluatedResource in the reportingrest module
                continue;
            }

            String resourceClassname = delegate.getClass().getSimpleName();
            if (resourceClassname.equals("UserAndPassword1_8")) {
                resourceClassname = "User"; //Work-around for UserAndPassword to be displayed as User
            } else if (resourceClassname.equals("CohortMember1_8")) {
                resourceClassname = "CohortMember";
            } else if (resourceClassname.equals("IncomingHl7Message1_8")) {
                resourceClassname = "HL7";
            }

            String subResourceForClass = null;
            ResourceDoc resourceDoc = new ResourceDoc(resourceClassname);
            org.openmrs.module.webservices.rest.web.annotation.Resource resourceAnnotation = ((org.openmrs.module.webservices.rest.web.annotation.Resource) resourceHandler
                    .getClass().getAnnotation(org.openmrs.module.webservices.rest.web.annotation.Resource.class));
            if (resourceAnnotation != null) {
                resourceDoc.setResourceName(resourceAnnotation.name());
            } else {
                //this is a subResource, use the name of the collection
                org.openmrs.module.webservices.rest.web.annotation.SubResource subResourceAnnotation = ((org.openmrs.module.webservices.rest.web.annotation.SubResource) resourceHandler
                        .getClass()
                        .getAnnotation(org.openmrs.module.webservices.rest.web.annotation.SubResource.class));
                if (subResourceAnnotation != null) {
                    org.openmrs.module.webservices.rest.web.annotation.Resource parentResourceAnnotation = ((org.openmrs.module.webservices.rest.web.annotation.Resource) subResourceAnnotation
                            .parent()
                            .getAnnotation(org.openmrs.module.webservices.rest.web.annotation.Resource.class));

                    resourceDoc.setResourceName(parentResourceAnnotation.name());
                    resourceDoc.setSubResourceName(subResourceAnnotation.path());

                    subResourceForClass = parentResourceAnnotation.supportedClass().getSimpleName();
                }
            }

            Object instance = resourceHandler;

            // subtype handlers are not resources themselves, but further specify resources (e.g. drugorder for order)
            if (resourceHandler instanceof DelegatingSubclassHandler) {
                Class<?> resourceClass = ((DelegatingSubclassHandler<?, ?>) resourceHandler).getSuperclass();
                instance = Context.getService(RestService.class).getResourceBySupportedClass(resourceClass);
                //Add as a subtype handler
                ResourceDoc actualResourceDoc = resourceDocMap.get(resourceClass.getSimpleName());
                if (actualResourceDoc == null) {
                    actualResourceDoc = new ResourceDoc(resourceClass.getSimpleName());
                    resourceDocMap.put(actualResourceDoc.getName(), actualResourceDoc);
                }
                actualResourceDoc.addSubtypeHandler(resourceDoc);
            }

            if (resourceDoc.isSubResource()) {
                // Add as a subresource to an existing resource
                ResourceDoc parentResourceDoc = resourceDocMap.get(subResourceForClass);
                parentResourceDoc.addSubResource(resourceDoc);
            } else {
                // Add as a resource

                ResourceDoc previous = resourceDocMap.put(resourceDoc.getName(), resourceDoc);

                // in case we've put in a placeholder (since a subtype handler was processed before its resource)
                // we need to preserve its children
                if (previous != null) {
                    for (ResourceDoc subResource : previous.getSubResources()) {
                        resourceDoc.addSubResource(subResource);
                    }
                    for (ResourceDoc subtypeHandler : previous.getSubtypeHandlers()) {
                        resourceDoc.addSubtypeHandler(subtypeHandler);
                    }
                }
            }

            //GET representations
            Representation[] representations = new Representation[] { Representation.REF, Representation.DEFAULT,
                    Representation.FULL };

            for (Representation representation : representations) {
                if (instance instanceof Converter) {
                    try {
                        @SuppressWarnings("unchecked")
                        Converter<Object> converter = (Converter<Object>) instance;
                        SimpleObject simpleObject = converter.asRepresentation(delegate, representation);
                        resourceDoc.addRepresentation(new ResourceRepresentation(
                                "GET " + representation.getRepresentation(), simpleObject.keySet()));
                    } catch (Exception e) {
                        resourceDoc.addRepresentation(new ResourceRepresentation(
                                "GET " + representation.getRepresentation(), Arrays.asList("Not supported")));
                    }
                } else {
                    resourceDoc.addRepresentation(new ResourceRepresentation(
                            "GET " + representation.getRepresentation(), Arrays.asList("Not supported")));
                }
            }

            //POST create representations
            try {
                DelegatingResourceDescription description = resourceHandler.getCreatableProperties();
                List<String> properties = getPOSTProperties(description);
                resourceDoc.addRepresentation(new ResourceRepresentation("POST create", properties));
            } catch (ResourceDoesNotSupportOperationException e) {
                resourceDoc.addRepresentation(
                        new ResourceRepresentation("POST create", Arrays.asList("Not supported")));
            }

            //POST update representations
            try {
                DelegatingResourceDescription description = resourceHandler.getUpdatableProperties();
                List<String> properties = getPOSTProperties(description);
                resourceDoc.addRepresentation(new ResourceRepresentation("POST update", properties));
            } catch (ResourceDoesNotSupportOperationException e) {
                resourceDoc.addRepresentation(
                        new ResourceRepresentation("POST update", Arrays.asList("Not supported")));
            }

        }
    }

    /**
     * Returns a list of POST properties of the given description.
     * 
     * @param description
     */
    private static List<String> getPOSTProperties(DelegatingResourceDescription description) {
        List<String> properties = new ArrayList<String>();
        for (Entry<String, Property> property : description.getProperties().entrySet()) {
            if (property.getValue().isRequired()) {
                properties.add("*" + property.getKey() + "*");
            } else {
                properties.add(property.getKey());
            }
        }
        return properties;
    }

    /**
     * Fills a map of resource names and their documentation objects with resource operations.
     * 
     * @param resouceDocMap a map of each resource name and its corresponding documentation object.
     */
    private static void fillOperations(Map<String, ResourceDoc> resouceDocMap) throws IOException {

        File directory = new File(
                new File("").getAbsolutePath() + "/src/main/java/org/openmrs/module/webservices/rest/web/resource");

        //Look for all resource files in the resource folder.
        String[] files = directory.list();
        if (files == null)
            return;

        for (int i = 0; i < files.length; i++) {
            String file = files[i];

            //We are only interested in ......Resource.java files
            if (file.endsWith("Resource.java")) {

                //Resource name is class name without the .java extension
                String name = file.subSequence(0, file.length() - "Resource.java".length()).toString();
                ResourceDoc resourceDoc = resouceDocMap.get(name);

                //Get the complete path and name of the java source file.
                String fullPathName = directory.getAbsolutePath() + File.separator + file;

                String source = OpenmrsUtil.getFileAsString(new File(fullPathName));

                //Parse the file's JavaDoc annotations to get the supported web service operations.
                resourceDoc.setOperations(JavadocParser.parse(source));
            }
        }
    }

    /**
     * Fills a map of resource names and their documentation objects with resource urls.
     * 
     * @param baseUrl the base or root for all the urls. e.g http://localhost:8080/openmrs
     * @param resouceDocMap a map of each resource name and its corresponding documentation object.
     */
    private static void fillUrls(String baseUrl, Map<String, ResourceDoc> resouceDocMap) throws IOException {
        List<ResourceOperation> resourceOperations = null;
        List<ResourceOperation> subResourceOperations = null;
        String resourceUrl = baseUrl + "/rest";

        for (ResourceDoc doc : resouceDocMap.values()) {
            //skip subclass handlers e.g DrugOrderSubclassHandler
            if (doc.isSubtypeHandler()) {
                continue;
            }

            if (doc.getSubResourceName() == null) {
                if (resourceOperations == null)
                    resourceOperations = getResourceOperations(resourceUrl, MainResourceController.class, true);

                for (ResourceOperation ro : resourceOperations) {
                    //Add the actual urls after replacing the {resource} string with the resource name
                    doc.addOperation(new ResourceOperation(
                            StringUtils.replace(ro.getName(), "{resource}", doc.getResourceName()),
                            ro.getDescription()));
                }

                //Set the root url.
                doc.setUrl(resourceUrl + "/" + doc.getResourceName());
                setUrlInSubResources(doc.getUrl(), doc);
            } else {
                //This is a sub resource that has a parent
                if (subResourceOperations == null)
                    subResourceOperations = getResourceOperations(resourceUrl, MainSubResourceController.class,
                            false);

                for (ResourceOperation sro : subResourceOperations) {
                    //generate the actual url to match the parent resource and subresource
                    String operationUrl = StringUtils.replaceEach(sro.getName(),
                            new String[] { "{resource}", "{subResource}" },
                            new String[] { doc.getResourceName(), doc.getSubResourceName() });
                    doc.addOperation(new ResourceOperation(operationUrl, sro.getDescription()));
                }

                doc.setUrl(resourceUrl + "/" + doc.getResourceName() + "/{parentUuid}/" + doc.getSubResourceName());
                setUrlInSubResources(resourceUrl, doc);
            }
        }
    }

    /**
     * Sets URL in subResources.
     * 
     * @param parentUrl
     * @param doc
     */
    private static void setUrlInSubResources(String parentUrl, ResourceDoc doc) {
        for (ResourceDoc subResource : doc.getSubResources()) {
            subResource.setUrl(parentUrl + "/{uuid}/" + subResource.getSubResourceName());
            setUrlInSubResources(subResource.getUrl(), subResource);
        }
    }

    /**
     * Gets a request method description specified for a given web service request handling method.
     * 
     * @param requestMapping the request mapping annotation.
     * @param operation the HTTP operation method.
     * @param method the method.
     * @param supportsSearching specified if the controller the method belongs supports searching
     * @return the method description string.
     */
    private static String getMethodDescription(RequestMapping requestMapping, String operation, Method method,
            boolean supportsSearching) {

        //If the documentation annotation exists, then no need for auto generating the description.
        WSDoc docAnnotation = (WSDoc) method.getAnnotation(WSDoc.class);
        if (docAnnotation != null)
            return docAnnotation.value();

        String value = null;
        if (requestMapping.value().length > 0 && StringUtils.isNotEmpty(requestMapping.value()[0]))
            value = requestMapping.value()[0];

        if (operation.equals("GET")) {
            if (value != null && value.contains("uuid"))
                return "Fetch by unique uuid";
            if (!supportsSearching)
                return "Fetch all non-retired";
            return "Fetch all non-retired that match any specified parameters otherwise fetch all non-retired";

        } else if (operation.equals("POST")) {
            if (value != null && value.contains("uuid"))
                return "Edit with given uuid, only modifying properties in request";
            else
                return "Create with properties in request";

        } else if (operation.equals("DELETE")) {
            if (!requestMapping.params()[0].startsWith("!"))
                return "Delete this object from the database";
            else
                return "Retire/Void this object";
        }

        return null;
    }

    /**
     * Generates {@link ResourceOperation}s corresponding to the supported http methods
     * 
     * @param url
     * @param clazz
     * @param supportsSearching specified if the controller the method belongs supports searching
     * @return
     */
    private static List<ResourceOperation> getResourceOperations(String url, Class<?> clazz,
            boolean supportsSearching) {
        List<ResourceOperation> resourceOperations = new ArrayList<ResourceOperation>();
        Method[] methods = clazz.getMethods();
        for (Method method : methods) {
            RequestMapping antn = (RequestMapping) method.getAnnotation(RequestMapping.class);
            if (antn == null)
                continue;

            String requestMethod = antn.method()[0].name();

            if (requestMethod.equals("TRACE")) {
                //Skip TRACE, which is used to disable a method in HL7MessageController.
                continue;
            }

            String operationUrl = requestMethod + " " + url;

            if (antn.value().length > 0)
                operationUrl += antn.value()[0];

            String paramString = null;
            for (String param : antn.params()) {
                if (paramString == null)
                    paramString = param;
                else
                    paramString += ("&" + param);
            }

            if (paramString != null)
                operationUrl += "?" + paramString;

            resourceOperations.add(new ResourceOperation(operationUrl,
                    getMethodDescription(antn, requestMethod, method, supportsSearching)));
        }

        Collections.sort(resourceOperations);

        return resourceOperations;
    }
}