/**
* 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;
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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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)) {
RestConstants.URI_PREFIX = "";
}
// 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 + " [" + ex.getMessage() + "]");
} else
map.put("message", "[" + ex.getMessage() + "]");
StackTraceElement[] steElements = ex.getStackTrace();
if (steElements.length > 0) {
StackTraceElement ste = ex.getStackTrace()[0];
map.put("code", ste.getClassName() + ":" + ste.getLineNumber());
map.put("detail", ExceptionUtils.getStackTrace(ex));
} else {
map.put("code", "");
map.put("detail", "");
}
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);
}
}