org.openmrs.module.webservices.rest.web.RestUtil.java Source code

Java tutorial

Introduction

Here is the source code for org.openmrs.module.webservices.rest.web.RestUtil.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.rest.web;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openmrs.GlobalProperty;
import org.openmrs.OpenmrsData;
import org.openmrs.OpenmrsMetadata;
import org.openmrs.api.GlobalPropertyListener;
import org.openmrs.api.context.Context;
import org.openmrs.messagesource.MessageSourceService;
import org.openmrs.module.webservices.rest.SimpleObject;
import org.openmrs.module.webservices.rest.web.api.RestService;
import org.openmrs.module.webservices.rest.web.representation.Representation;
import org.openmrs.module.webservices.validation.ValidationException;
import org.openmrs.util.OpenmrsClassLoader;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.context.request.WebRequest;

/**
 * Convenient helper methods for the Rest Web Services module.
 */
public class RestUtil implements GlobalPropertyListener {

    private static Log log = LogFactory.getLog(RestUtil.class);

    private static boolean contextEnabled = true;

    /**
     * Looks up the admin defined global property for the system limit
     * 
     * @return Integer limit
     * @see #getLimit(WebRequest)
     * @see RestConstants#MAX_RESULTS_DEFAULT_GLOBAL_PROPERTY_NAME
     */
    public static Integer getDefaultLimit() {
        String limit = Context.getAdministrationService()
                .getGlobalProperty(RestConstants.MAX_RESULTS_DEFAULT_GLOBAL_PROPERTY_NAME);
        if (StringUtils.isNotEmpty(limit)) {
            try {
                return Integer.parseInt(limit);
            } catch (NumberFormatException nfex) {
                log.error(RestConstants.MAX_RESULTS_DEFAULT_GLOBAL_PROPERTY_NAME + " must be an integer. "
                        + nfex.getMessage());
                return RestConstants.MAX_RESULTS_DEFAULT;
            }
        } else {
            return RestConstants.MAX_RESULTS_DEFAULT;
        }
    }

    /**
     * Looks up the admin defined global property for the absolute limit to results of REST calls
     * 
     * @return Integer limit
     * @see #getLimit(WebRequest)
     * @see RestConstants#MAX_RESULTS_ABSOLUTE_GLOBAL_PROPERTY_NAME
     */
    public static Integer getAbsoluteLimit() {
        String limit = Context.getAdministrationService()
                .getGlobalProperty(RestConstants.MAX_RESULTS_ABSOLUTE_GLOBAL_PROPERTY_NAME);
        if (StringUtils.isNotEmpty(limit)) {
            try {
                return Integer.parseInt(limit);
            } catch (NumberFormatException nfex) {
                log.error(RestConstants.MAX_RESULTS_ABSOLUTE_GLOBAL_PROPERTY_NAME + " must be an integer. "
                        + nfex.getMessage());
                return RestConstants.MAX_RESULTS_ABSOLUTE;
            }
        } else {
            return RestConstants.MAX_RESULTS_ABSOLUTE;
        }
    }

    /**
     * Tests whether or not a client's IP address is allowed to have access to the REST API (based
     * on a admin-settable global property).
     * 
     * @param ip address of the client
     * @return <code>true</code> if client should be allowed access
     * @see RestConstants#ALLOWED_IPS_GLOBAL_PROPERTY_NAME
     */
    public static boolean isIpAllowed(String ip) {
        return ipMatches(ip, getAllowedIps());
    }

    /**
     * Tests whether or not there is a match between the given IP address and the candidates.
     * 
     * @param ip
     * @param candidateIps
     * @return <code>true</code> if there is a match
     * @should return true if list is empty
     * @should return false if there is no match
     * @should return true for exact match
     * @should return true for match with submask
     * @should return false if there is no match with submask
     * @should return true for exact ipv6 match
     * @should throw IllegalArgumentException for invalid mask
     */
    public static boolean ipMatches(String ip, List<String> candidateIps) {
        if (candidateIps.isEmpty()) {
            return true;
        }

        InetAddress address;
        try {
            address = InetAddress.getByName(ip);
        } catch (UnknownHostException e) {
            throw new IllegalArgumentException("Invalid IP in the ip parameter" + ip, e);
        }

        for (String candidateIp : candidateIps) {
            // split IP and mask
            String[] candidateIpPattern = candidateIp.split("/");

            InetAddress candidateAddress;
            try {
                candidateAddress = InetAddress.getByName(candidateIpPattern[0]);
            } catch (UnknownHostException e) {
                throw new IllegalArgumentException("Invalid IP in the candidateIps parameter", e);
            }

            if (candidateIpPattern.length == 1) { // there's no mask
                if (address.equals(candidateAddress)) {
                    return true;
                }
            } else {
                if (address.getAddress().length != candidateAddress.getAddress().length) {
                    continue;
                }

                int bits = Integer.parseInt(candidateIpPattern[1]);
                if (candidateAddress.getAddress().length < Math.ceil((double) bits / 8)) {
                    throw new IllegalArgumentException(
                            "Invalid mask " + bits + " for IP " + candidateIp + " in the candidateIps parameter");
                }

                // compare bytes based on the given mask
                boolean matched = true;
                for (int bytes = 0; bits > 0; bytes++, bits -= 8) {
                    int mask = 0x000000FF; // mask the entire byte
                    if (bits < 8) {
                        // mask only some first bits of a byte
                        mask = (mask << (8 - bits));
                    }
                    if ((address.getAddress()[bytes] & mask) != (candidateAddress.getAddress()[bytes] & mask)) {
                        matched = false;
                        break;
                    }
                }
                if (matched) {
                    return true;
                }
            }

        }
        return false;
    }

    /**
     * Returns a list of IPs which can access the REST API based on a global property. In case the
     * property is empty, returns an empty list.
     * <p>
     * IPs should be separated by a whitespace or a comma. IPs can be declared with bit masks e.g.
     * <code>10.0.0.0/30</code> matches <code>10.0.0.0 - 10.0.0.3</code> and
     * <code>10.0.0.0/24</code> matches <code>10.0.0.0 - 10.0.0.255</code>.
     * 
     * @see RestConstants#ALLOWED_IPS_GLOBAL_PROPERTY_NAME
     * @return the list of IPs
     */
    public static List<String> getAllowedIps() {
        String allowedIpsProperty = Context.getAdministrationService()
                .getGlobalProperty(RestConstants.ALLOWED_IPS_GLOBAL_PROPERTY_NAME, "");

        if (allowedIpsProperty.isEmpty()) {
            return Collections.emptyList();
        } else {
            String[] allowedIps = allowedIpsProperty.split("[\\s,]+");
            return Arrays.asList(allowedIps);
        }
    }

    /*
     * TODO - move logic from here to a method to deal with custom
     * representations Converts the given <code>openmrsObject</code> into a
     * {@link SimpleObject} to be returned to the REST user. <br/>
     * 
     * TODO catch each possible exception in this method and log helpful error
     * msgs instead of just having the method throw the generic exception
     * 
     * TODO: change this to use a list of strings for the rep?
     * 
     * @param resource the OpenmrsResource to convert to. If null, looks up the
     * resource from the given OpenmrsObject
     * 
     * @param openmrsObject the OpenmrsObject to convert
     * 
     * @param representation the default/full/small/custom (if null, uses
     * "default")
     * 
     * @return a SimpleObject (key/value pair mapping) of the object properties
     * requested
     * 
     * @throws Exception
     * 
     * public SimpleObject convert(OpenmrsResource resource, OpenmrsObject
     * openmrsObject, String representation) throws Exception {
     * 
     * if (representation == null) representation =
     * RestConstants.REPRESENTATION_DEFAULT;
     * 
     * if (resource == null) resource =
     * HandlerUtil.getPreferredHandler(OpenmrsResource.class,
     * openmrsObject.getClass());
     * 
     * // the object to return. adds the default link/display/uuid properties
     * SimpleObject simpleObject = new SimpleObject(resource, openmrsObject);
     * 
     * // if they asked for a simple rep, we're done, just return that if
     * (RestConstants.REPRESENTATION_REF.equals(representation)) return
     * simpleObject;
     * 
     * // get the properties to show on this object String[] propsToInclude =
     * getPropsToInclude(resource, representation);
     * 
     * // loop over each prop defined and put it on the simpleObject for (String
     * prop : propsToInclude) {
     * 
     * // cut out potential white space around commas prop = prop.trim();
     * 
     * // the property field on the resource of what we're converting Field
     * propertyOnResource; try { propertyOnResource =
     * resource.getClass().getDeclaredField(prop); } catch (NoSuchFieldException
     * e) { // the user requested a field that does not exist on the //
     * resource, // so silently skip this log.debug("Skipping field: " + prop +
     * " because it does not exist on the " + resource + " resource"); continue;
     * }
     * 
     * // the name of the getter methods for this property String getterName =
     * "get" + StringUtils.capitalize(prop);
     * 
     * // first check to see if there is a getter defined on the resource, //
     * maybe its a custom translation to a string or OpenmrsObject and // we can
     * then end early Method getterOnResource = getMethod(resource.getClass(),
     * getterName, openmrsObject.getClass());
     * 
     * if (getterOnResource != null) { // e.g. if prop is "name" and a dev
     * defined // "personResource.getName(Person)" then we can stop here and //
     * just use that method's return value Object returnValue =
     * getterOnResource.invoke(resource, openmrsObject);
     * 
     * // turn OpenmrsObjects into Refs if (OpenmrsObject.class
     * .isAssignableFrom(returnValue.getClass())) {
     * 
     * String cascadeRep = getCascadeRep(propertyOnResource, representation);
     * 
     * SimpleObject so = convert(openmrsObject, cascadeRep);
     * simpleObject.put(prop, so); } else //
     * if(String.class.isAssignableFrom(returnValue.getClass())) // everything
     * else /should be/ strings. // (what special about Dates, etc?)
     * simpleObject.put(prop, returnValue); continue; }
     * 
     * // the user didn't define a getProperty(OpenmrsObject), so we // need to
     * find openmrsObject.getProperty() magically by reflection
     * 
     * // get the actual value we'll need to convert on the OpenmrsObject Method
     * getterOnObject = openmrsObject.getClass().getMethod( getterName,
     * (Class[]) null); Object propValue = getterOnObject.invoke(openmrsObject,
     * (Object[]) null);
     * 
     * Class propertyClass = propertyOnResource.getType();
     * 
     * // now convert from OpenmrsObject into this type on the resource if
     * (propertyClass.equals(SimpleObject.class)) {
     * 
     * String cascadeRep = getCascadeRep(propertyOnResource, representation);
     * SimpleObject subSimpleObject = convert(resource, openmrsObject,
     * cascadeRep); simpleObject.put(prop, subSimpleObject); } else if
     * (OpenmrsResource.class.isAssignableFrom(propertyClass)) { // the resource
     * has a resource property (like AuditInfo) OpenmrsResource openmrsResource
     * = (OpenmrsResource) propertyClass .newInstance();
     * 
     * // TODO: if representation just has "prop", assume that means // all
     * default properties on the resource
     * 
     * // TODO: if representation has "prop.x, prop.y", assume that // means
     * only those properties from the resource // see isCollection else if
     * statement for implementation of it // and possibly creating a common
     * method for getting the // strippedDownRep
     * 
     * // TODO: else if cascade is one of the standard ones, find the // rep //
     * to cascade to String cascadeRep = getCascadeRep(propertyOnResource,
     * representation);
     * 
     * SimpleObject subSimpleObject = convert(openmrsResource, openmrsObject,
     * cascadeRep); simpleObject.put(prop, subSimpleObject); } else if
     * (Reflect.isCollection(propertyClass)) {
     * 
     * // the list put onto the "simpleObject" as a list List<Object>
     * listofSimpleObjects = new ArrayList<Object>();
     * 
     * OpenmrsObject collectionContains = isOpenmrsObjectCollection(propValue);
     * if (collectionContains != null) { // we have an OpenmrsObject collection
     * 
     * OpenmrsResource collectionResource = HandlerUtil
     * .getPreferredHandler(OpenmrsResource.class,
     * collectionContains.getClass());
     * 
     * if (representation.contains(prop + ".")) { // recurse on this convert
     * method, because the user // asked for something complex by putting in //
     * "names.givenName, names.familyName" in the // representation
     * 
     * // TODO: look through the representation and take out // everything but
     * "prop.*" strings String strippedDownRep = null; // new String[] { //
     * "givenName", // "familyName", // "creator"};
     * 
     * // recurse on this current "convert" method. for (OpenmrsObject o :
     * (Collection<OpenmrsObject>) propValue) { convert(collectionResource, o,
     * strippedDownRep); } } else if (RestConstants.REPRESENTATION_FULL
     * .equals(representation) || RestConstants.REPRESENTATION_MEDIUM
     * .equals(representation)) {
     * 
     * String cascadeRep = getCascadeRep(propertyOnResource, representation);
     * 
     * for (OpenmrsObject o : (Collection<OpenmrsObject>) propValue) {
     * convert(collectionResource, o, cascadeRep); } } else { // the user didn't
     * ask for anything special in the rep, // so they get back lists of ref
     * simple objects by // default for (OpenmrsObject o :
     * (Collection<OpenmrsObject>) propValue) { // sets uuid/link/display
     * SimpleObject listMemberSimpleObject = new SimpleObject(
     * collectionResource, o); listofSimpleObjects.add(listMemberSimpleObject);
     * } } } else { // we just have a list of java objects, simply put their //
     * string values in // TODO how to use conversionservice here? for (Object o
     * : (Collection<Object>) propValue) { listofSimpleObjects.add(o); } }
     * simpleObject.put(prop, listofSimpleObjects);
     * 
     * } else { // we just have some of java object, put in its toString value
     * // TODO use conversionservice? simpleObject.put(prop, propValue); }
     * 
     * }
     * 
     * return simpleObject; }
     */

    /*
     * Used by code commented out above. Ready for possible deletion.
     * 
     * TODO: look into whether this can use PropertyUtils instead
     * 
     * /** Helper method to use the superclass of param class as well
     * 
     * @param c
     * 
     * @param name
     * 
     * @param param
     * 
     * @return
     * 
     * public Method getMethod(Class<?> c, String name, Class<?> param) {
     * 
     * Method m = null; try { m = c.getMethod(name, param); } catch
     * (NoSuchMethodException ex) { // do nothing }
     * 
     * if (m != null) return m;
     * 
     * if (param.getSuperclass() != null) { return getMethod(c, name,
     * param.getSuperclass()); }
     * 
     * return null; // throw new NoSuchMethodException("No method on class " + c
     * + // " with name " + name + " with param " + param); }
     */

    /**
     * Determines the request representation, if not provided, uses default. <br/>
     * Determines number of results to limit to, if not provided, uses default set by admin. <br/>
     * Determines how far into a list to start with given the startIndex param. <br/>
     * 
     * @param request the current http web request
     * @param response the current http web response
     * @param defaultView the representation to use if none specified
     * @return a {@link RequestContext} object filled with all the necessary values
     * @see RestConstants#REQUEST_PROPERTY_FOR_LIMIT
     * @see RestConstants#REQUEST_PROPERTY_FOR_REPRESENTATION
     * @see RestConstants#REQUEST_PROPERTY_FOR_START_INDEX
     * @see RestConstants#REQUEST_PROPERTY_FOR_INCLUDE_ALL
     */
    public static RequestContext getRequestContext(HttpServletRequest request, HttpServletResponse response,
            Representation defaultView) {
        if (defaultView == null)
            defaultView = Representation.DEFAULT;

        RequestContext ret = new RequestContext();
        ret.setRequest(request);
        ret.setResponse(response);

        // get the "v" param for the representations
        String temp = request.getParameter(RestConstants.REQUEST_PROPERTY_FOR_REPRESENTATION);
        if ("".equals(temp)) {
            throw new IllegalArgumentException("?v=(empty string) is not allowed");
        } else if (temp == null) {
            ret.setRepresentation(defaultView);
        } else if (temp.equals(defaultView.getRepresentation())) {
            throw new IllegalArgumentException(
                    "Do not specify ?v=" + temp + " because it is the default behavior for this request");
        } else {
            ret.setRepresentation(Context.getService(RestService.class).getRepresentation(temp));
        }

        // get the "t" param for subclass-specific requests
        temp = request.getParameter(RestConstants.REQUEST_PROPERTY_FOR_TYPE);
        if ("".equals(temp)) {
            throw new IllegalArgumentException(
                    "?" + RestConstants.REQUEST_PROPERTY_FOR_TYPE + "=(empty string) is not allowed");
        } else {
            ret.setType(temp);
        }

        // fetch the "limit" param
        Integer limit = getIntegerParam(request, RestConstants.REQUEST_PROPERTY_FOR_LIMIT);
        if (limit != null) {
            ret.setLimit(limit);
        }

        // fetch the startIndex param
        Integer startIndex = getIntegerParam(request, RestConstants.REQUEST_PROPERTY_FOR_START_INDEX);
        if (startIndex != null) {
            ret.setStartIndex(startIndex);
        }

        Boolean includeAll = getBooleanParam(request, RestConstants.REQUEST_PROPERTY_FOR_INCLUDE_ALL);
        if (includeAll != null) {
            ret.setIncludeAll(includeAll);
        }
        return ret;
    }

    /**
     * Determines the request representation with Representation.DEFAULT as the default view.
     * 
     * @param request the current http web request
     * @param response the current http web response
     * @return a {@link RequestContext} object filled with all the necessary values
     * @see getRequestContext(javax.servlet.http.HttpServletRequest,
     *      org.openmrs.module.webservices.rest.web.representation.Representation)
     */
    public static RequestContext getRequestContext(HttpServletRequest request, HttpServletResponse response) {
        return getRequestContext(request, response, Representation.DEFAULT);
    }

    /**
     * Convenience method to get the given param out of the given request.
     * 
     * @param request the WebRequest to look in
     * @param param the string name to fetch
     * @return null if the param doesn't exist or is not a valid integer
     */
    private static Integer getIntegerParam(HttpServletRequest request, String param) {
        String paramString = request.getParameter(param);

        if (paramString != null) {
            try {
                Integer tempInt = new Integer(paramString);
                return tempInt; // return the valid value
            } catch (NumberFormatException e) {
                log.debug("unable to parse '" + param + "' parameter into a valid integer: " + paramString);
            }
        }

        return null;
    }

    /**
     * Convenience method to get the given param out of the given request as a boolean.
     * 
     * @param request the WebRequest to look in
     * @param param the string name to fetch
     * @return <code>true</code> if the param is equal to 'true', <code>false</code> for any empty
     *         value, null value, or not equal to 'true', or missing param.
     * @should return true only if request param is 'true'
     */
    public static Boolean getBooleanParam(HttpServletRequest request, String param) {
        try {
            return ServletRequestUtils.getBooleanParameter(request, param);
        } catch (ServletRequestBindingException e) {
            return false;
        }
    }

    /**
     * Sets the HTTP status on the response according to the exception
     * 
     * @param ex
     * @param response
     */
    public static void setResponseStatus(Throwable ex, HttpServletResponse response) {
        ResponseStatus ann = ex.getClass().getAnnotation(ResponseStatus.class);
        if (ann != null) {
            if (StringUtils.isNotBlank(ann.reason())) {
                response.setStatus(ann.value().value(), ann.reason());
            } else {
                response.setStatus(ann.value().value());
            }
        } else {
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
    }

    /**
     * Sets the HTTP status on the response to no content, and returns an empty value, suitable for
     * returning from a @ResponseBody annotated Spring controller method.
     * 
     * @param response
     * @return
     */
    public static Object noContent(HttpServletResponse response) {
        response.setStatus(HttpServletResponse.SC_NO_CONTENT);
        return "";
    }

    /**
     * Sets the HTTP status for CREATED and (if 'created' has a uri) the Location header attribute
     * 
     * @param response
     * @param created
     * @return the object passed in
     */
    public static Object created(HttpServletResponse response, Object created) {
        response.setStatus(HttpServletResponse.SC_CREATED);
        try {
            String uri = (String) PropertyUtils.getProperty(created, "uri");
            response.addHeader("Location", uri);
        } catch (Exception ex) {
        }
        return created;
    }

    /**
     * Sets the HTTP status for UPDATED and (if 'updated' has a uri) the Location header attribute
     * 
     * @param response
     * @param updated
     * @return the object passed in
     */
    public static Object updated(HttpServletResponse response, Object updated) {
        response.setStatus(HttpServletResponse.SC_OK);
        try {
            String uri = (String) PropertyUtils.getProperty(updated, "uri");
            response.addHeader("Location", uri);
        } catch (Exception ex) {
        }
        return updated;
    }

    /**
     * Updates the Uri prefix through which clients consuming web services will connect to the web
     * app
     * 
     * @return the webapp's Url prefix
     */
    public static void setUriPrefix() {
        if (contextEnabled) {
            RestConstants.URI_PREFIX = Context.getAdministrationService()
                    .getGlobalProperty(RestConstants.URI_PREFIX_GLOBAL_PROPERTY_NAME);
        }

        if (StringUtils.isBlank(RestConstants.URI_PREFIX)) {
            // reset just in case it is a white space character or empty
            // string
            RestConstants.URI_PREFIX = RestConstants.URI_PREFIX_GP_DEFAULT_VALUE;
        }

        // append the trailing slash in case the user forgot it
        if (!RestConstants.URI_PREFIX.endsWith("/")) {
            RestConstants.URI_PREFIX += "/";
        }

        RestConstants.URI_PREFIX = RestConstants.URI_PREFIX + "ws/rest/";
    }

    /**
     * It allows to disable calls to Context. It should be used in TESTS ONLY.
     */
    public static void disableContext() {
        contextEnabled = false;
    }

    /**
     * A Set is returned by removing voided data from passed Collection. The Collection passed as
     * parameter is not modified
     * 
     * @param input collection of OpenmrsData
     * @return non-voided OpenmrsData
     */
    public static <D extends OpenmrsData, C extends Collection<D>> Set<D> removeVoidedData(C input) {
        Set<D> data = new LinkedHashSet<D>();
        for (D d : input) {
            if (!d.isVoided()) {
                data.add(d);
            }
        }
        return data;
    }

    /**
     * A Set is returned by removing retired data from passed Collection. The Collection passed as
     * parameter is not modified
     * 
     * @param input collection of OpenmrsMetadata
     * @return non-retired OpenmrsMetaData
     */
    public static <M extends OpenmrsMetadata, C extends Collection<M>> Set<M> removeRetiredData(C input) {
        Set<M> data = new LinkedHashSet<M>();
        for (M m : input) {
            if (!m.isRetired()) {
                data.add(m);
            }
        }
        return data;
    }

    /**
     * @see org.openmrs.api.GlobalPropertyListener#supportsPropertyName(java.lang.String)
     */
    @Override
    public boolean supportsPropertyName(String propertyName) {
        return propertyName.equals(RestConstants.URI_PREFIX_GLOBAL_PROPERTY_NAME);
    }

    /**
     * @see org.openmrs.api.GlobalPropertyListener#globalPropertyChanged(org.openmrs.GlobalProperty)
     */
    @Override
    public void globalPropertyChanged(GlobalProperty newValue) {
        setUriPrefix();
    }

    /**
     * @see org.openmrs.api.GlobalPropertyListener#globalPropertyDeleted(java.lang.String)
     */
    @Override
    public void globalPropertyDeleted(String propertyName) {
        setUriPrefix();
    }

    /**
     * Inspects the cause chain for the given throwable, looking for an exception of the given class
     * (e.g. to find an APIAuthenticationException wrapped in an InvocationTargetException)
     * 
     * @param throwable
     * @param causeClassToLookFor
     * @return whether any exception in the cause chain of throwable is an instance of
     *         causeClassToLookFor
     */
    public static boolean hasCause(Throwable throwable, Class<? extends Throwable> causeClassToLookFor) {
        return ExceptionUtils.indexOfType(throwable, causeClassToLookFor) >= 0;
    }

    /**
     * Gets a list of classes in a given package. Note that interfaces are not returned.
     * 
     * @param pkgname the package name.
     * @param suffix the ending text on name. eg "Resource.class"
     * @return the list of classes.
     */
    public static ArrayList<Class<?>> getClassesForPackage(String pkgname, String suffix) throws IOException {
        ArrayList<Class<?>> classes = new ArrayList<Class<?>>();

        //Get a File object for the package
        File directory = null;
        String relPath = pkgname.replace('.', '/');
        Enumeration<URL> resources = OpenmrsClassLoader.getInstance().getResources(relPath);
        while (resources.hasMoreElements()) {

            URL resource = resources.nextElement();
            if (resource == null) {
                throw new RuntimeException("No resource for " + relPath);
            }

            try {
                directory = new File(resource.toURI());
            } catch (URISyntaxException e) {
                throw new RuntimeException(pkgname + " (" + resource
                        + ") does not appear to be a valid URL / URI.  Strange, since we got it from the system...",
                        e);
            } catch (IllegalArgumentException ex) {
                //ex.printStackTrace();
            }

            //If folder exists, look for all resource class files in it.
            if (directory != null && directory.exists()) {

                //Get the list of the files contained in the package
                String[] files = directory.list();

                for (int i = 0; i < files.length; i++) {

                    //We are only interested in Resource.class files
                    if (files[i].endsWith(suffix)) {

                        //Remove the .class extension
                        String className = pkgname + '.' + files[i].substring(0, files[i].length() - 6);

                        try {
                            Class<?> cls = Class.forName(className);
                            if (!cls.isInterface())
                                classes.add(cls);
                        } catch (ClassNotFoundException e) {
                            throw new RuntimeException("ClassNotFoundException loading " + className);
                        }
                    }
                }
            } else {

                //Directory does not exist, look in jar file.
                try {
                    String fullPath = resource.getFile();
                    String jarPath = fullPath.replaceFirst("[.]jar[!].*", ".jar").replaceFirst("file:", "");
                    JarFile jarFile = new JarFile(jarPath);

                    Enumeration<JarEntry> entries = jarFile.entries();
                    while (entries.hasMoreElements()) {
                        JarEntry entry = entries.nextElement();

                        String entryName = entry.getName();

                        if (!entryName.endsWith(suffix))
                            continue;

                        if (entryName.startsWith(relPath)
                                && entryName.length() > (relPath.length() + "/".length())) {
                            String className = entryName.replace('/', '.').replace('\\', '.').replace(".class", "");

                            try {
                                Class<?> cls = Class.forName(className);
                                if (!cls.isInterface())
                                    classes.add(cls);
                            } catch (ClassNotFoundException e) {
                                throw new RuntimeException("ClassNotFoundException loading " + className);
                            }
                        }
                    }
                } catch (IOException e) {
                    throw new RuntimeException(
                            pkgname + " (" + directory + ") does not appear to be a valid package", e);
                }
            }
        }

        return classes;
    }

    /**
     * Wraps the exception message as a SimpleObject to be sent to client
     * 
     * @param ex
     * @param reason
     * @return
     */
    public static SimpleObject wrapErrorResponse(Exception ex, String reason) {
        LinkedHashMap map = new LinkedHashMap();
        if (reason != null && !reason.isEmpty()) {
            map.put("message", reason);
        } else
            map.put("message", ex.getMessage());
        StackTraceElement ste = ex.getStackTrace()[0];
        map.put("code", ste.getClassName() + ":" + ste.getLineNumber());
        map.put("detail", ExceptionUtils.getStackTrace(ex));
        return new SimpleObject().add("error", map);
    }

    /**
     * Creates a SimpleObject to sent to the client with all validation errors (with message codes
     * resolved)
     * 
     * @param ex
     * @return
     */
    public static SimpleObject wrapValidationErrorResponse(ValidationException ex) {

        MessageSourceService messageSourceService = Context.getMessageSourceService();

        SimpleObject errors = new SimpleObject();
        errors.add("message", messageSourceService.getMessage("webservices.rest.error.invalid.submission"));
        errors.add("code", "webservices.rest.error.invalid.submission");

        List<SimpleObject> globalErrors = new ArrayList<SimpleObject>();
        SimpleObject fieldErrors = new SimpleObject();

        if (ex.getErrors().hasGlobalErrors()) {

            for (Object errObj : ex.getErrors().getGlobalErrors()) {

                ObjectError err = (ObjectError) errObj;
                String message = messageSourceService.getMessage(err.getCode());

                SimpleObject globalError = new SimpleObject();
                globalError.put("code", err.getCode());
                globalError.put("message", message);
                globalErrors.add(globalError);
            }

        }

        if (ex.getErrors().hasFieldErrors()) {

            for (Object errObj : ex.getErrors().getFieldErrors()) {
                FieldError err = (FieldError) errObj;
                String message = messageSourceService.getMessage(err.getCode());

                SimpleObject fieldError = new SimpleObject();
                fieldError.put("code", err.getCode());
                fieldError.put("message", message);

                if (!fieldErrors.containsKey(err.getField())) {
                    fieldErrors.put(err.getField(), new ArrayList<SimpleObject>());
                }

                ((List<SimpleObject>) fieldErrors.get(err.getField())).add(fieldError);
            }

        }

        errors.put("globalErrors", globalErrors);
        errors.put("fieldErrors", fieldErrors);

        return new SimpleObject().add("error", errors);
    }
}