/**
* 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.aop;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Set;
import org.openmrs.OpenmrsObject;
import org.openmrs.Retireable;
import org.openmrs.User;
import org.openmrs.Voidable;
import org.openmrs.annotation.AllowDirectAccess;
import org.openmrs.annotation.DisableHandlers;
import org.openmrs.annotation.Independent;
import org.openmrs.api.APIException;
import org.openmrs.api.context.Context;
import org.openmrs.api.handler.ConceptNameSaveHandler;
import org.openmrs.api.handler.RequiredDataHandler;
import org.openmrs.api.handler.RetireHandler;
import org.openmrs.api.handler.SaveHandler;
import org.openmrs.api.handler.UnretireHandler;
import org.openmrs.api.handler.UnvoidHandler;
import org.openmrs.api.handler.VoidHandler;
import org.openmrs.util.HandlerUtil;
import org.openmrs.util.Reflect;
import org.openmrs.validator.ValidateUtil;
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.util.StringUtils;
/**
* This class provides the AOP around each save, (un)void, and (un)retire method in the service
* layer so that the required data (like creator, dateChanged, dateVoided, etc) can be set
* automatically and the developer doesn't have to worry about doing it explicitly in the service
* impl method. <br>
* <br>
* See /metadata/api/spring/applicationContext-service.xml for the mapping of this bean. <br>
* <br>
* For an Openmrs Service to use this AOP advice class and take advantage of its automatic variable
* setting, it must have "<ref local="requiredDataInterceptor"/>" in its "preInterceptors".<br>
* <br>
* By default, this should take care of any child collections on the object being acted on. Any
* child collection of {@link OpenmrsObject}s will get "handled" (i.e., void data set up, save data
* set up, or retire data set up, etc) by the same handler type that the parent object was handled
* with.<br>
* <br>
* To add a new action to happen for a save* method, create a new class that extends
* {@link RequiredDataHandler}. Add any <b>unique</b> code that needs to be done automatically
* before the save. See {@link ConceptNameSaveHandler} as an example. (The code should be
* <b>unique</b> because all other {@link SaveHandler}s will still be called <i>in addition to</i>
* your new handler.) Be sure to add the {@link org.openmrs.annotation.Handler} annotation (like
* "@Handler(supports=YourPojoThatHasUniqueSaveNeeds.class)") to your class so that it is picked up
* by Spring automatically.<br>
* <br>
* To add a new action for a void* or retire* method, extend the {@link VoidHandler}/
* {@link RetireHandler} class and override the handle method. Do not call super, because that code
* would then be run twice because both handlers are registered. Be sure to add the
* {@link org.openmrs.annotation.Handler} annotation (like
* "@Handler(supports=YourPojoThatHasUniqueSaveNeeds.class)") to your class so that it is picked up
* by Spring automatically.
*
* @see RequiredDataHandler
* @see SaveHandler
* @see VoidHandler
* @since 1.5
*/
public class RequiredDataAdvice implements MethodBeforeAdvice {
/**
* @see org.springframework.aop.MethodBeforeAdvice#before(java.lang.reflect.Method,
* java.lang.Object[], java.lang.Object)
* @should not fail on update method with no arguments
*/
@Override
@SuppressWarnings("unchecked")
public void before(Method method, Object[] args, Object target) throws Throwable {
String methodName = method.getName();
// skip out early if there are no arguments
if (args == null || args.length == 0) {
return;
}
Object mainArgument = args[0];
// fail early on a null parameter
if (mainArgument == null) {
return;
}
// the "create" is there to cover old deprecated methods since AOP doesn't occur
// on method calls within a class, only on calls to methods from external classes to methods
// "update" is not an option here because there are multiple methods that start with "update" but is
// not updating the primary argument. eg: ConceptService.updateConceptWord(Concept)
if (methodName.startsWith("save") || methodName.startsWith("create")) {
// if the first argument is an OpenmrsObject, handle it now
Reflect reflect = new Reflect(OpenmrsObject.class);
if (reflect.isSuperClass(mainArgument)) {
// fail early if the method name is not like saveXyz(Xyz)
if (!methodNameEndsWithClassName(method, mainArgument.getClass())) {
return;
}
// if a second argument exists, pass that to the save handler as well
// (with current code, it means we're either in an obs save or a user save)
String other = null;
if (args.length > 1 && args[1] instanceof String) {
other = (String) args[1];
}
ValidateUtil.validate(mainArgument);
recursivelyHandle(SaveHandler.class, (OpenmrsObject) mainArgument, other);
}
// if the first argument is a list of openmrs objects, handle them all now
else if (Reflect.isCollection(mainArgument) && isOpenmrsObjectCollection(mainArgument)) {
// ideally we would fail early if the method name is not like savePluralOfXyz(Collection<Xyz>)
// but this only occurs once in the API (AdministrationService.saveGlobalProperties
// so it is not worth handling this case
// if a second argument exists, pass that to the save handler as well
// (with current code, it means we're either in an obs save or a user save)
String other = null;
if (args.length > 1) {
other = (String) args[1];
}
Collection<OpenmrsObject> openmrsObjects = (Collection<OpenmrsObject>) mainArgument;
for (OpenmrsObject object : openmrsObjects) {
ValidateUtil.validate(mainArgument);
recursivelyHandle(SaveHandler.class, object, other);
}
}
} else {
// fail early if the method name is not like retirePatient or retireConcept when dealing
// with Patients or Concepts as the first argument
if (!methodNameEndsWithClassName(method, mainArgument.getClass())) {
return;
}
if (methodName.startsWith("void")) {
Voidable voidable = (Voidable) args[0];
Date dateVoided = voidable.getDateVoided() == null ? new Date() : voidable.getDateVoided();
String voidReason = (String) args[1];
recursivelyHandle(VoidHandler.class, voidable, Context.getAuthenticatedUser(), dateVoided, voidReason, null);
} else if (methodName.startsWith("unvoid")) {
Voidable voidable = (Voidable) args[0];
Date originalDateVoided = voidable.getDateVoided();
User originalVoidingUser = voidable.getVoidedBy();
recursivelyHandle(UnvoidHandler.class, voidable, originalVoidingUser, originalDateVoided, null, null);
} else if (methodName.startsWith("retire")) {
Retireable retirable = (Retireable) args[0];
String retireReason = (String) args[1];
recursivelyHandle(RetireHandler.class, retirable, retireReason);
} else if (methodName.startsWith("unretire")) {
Retireable retirable = (Retireable) args[0];
Date originalDateRetired = retirable.getDateRetired();
recursivelyHandle(UnretireHandler.class, retirable, Context.getAuthenticatedUser(), originalDateRetired,
null, null);
}
}
}
/**
* Convenience method to change the given method to make sure it ends with
* the given class name. <br>
* This will recurse to the super class to check that as well.
*
* @param method
* the method name (like savePatient, voidEncounter,
* retireConcept)
* @param mainArgumentClass
* class to compare
* @return true if method's name ends with the mainArgumentClasses simple
* name
*/
private boolean methodNameEndsWithClassName(Method method, Class<?> mainArgumentClass) {
if (method.getName().endsWith(mainArgumentClass.getSimpleName())) {
return true;
} else {
mainArgumentClass = mainArgumentClass.getSuperclass();
// stop recursing if no super class
if (mainArgumentClass != null) {
return methodNameEndsWithClassName(method, mainArgumentClass);
}
}
return false;
}
/**
* Convenience method for {@link #recursivelyHandle(Class, OpenmrsObject, User, Date, String, List)}.
* Calls that method with the current user and the current Date.
*
* @param <H> the type of Handler to get (should extend {@link RequiredDataHandler})
* @param handlerType the type of Handler to get (should extend {@link RequiredDataHandler})
* @param openmrsObject the object that is being acted upon
* @param reason an optional second argument that was passed to the service method (usually a
* void/retire reason)
* @see #recursivelyHandle(Class, OpenmrsObject, User, Date, String, List)
*/
public static <H extends RequiredDataHandler> void recursivelyHandle(Class<H> handlerType, OpenmrsObject openmrsObject,
String reason) {
recursivelyHandle(handlerType, openmrsObject, Context.getAuthenticatedUser(), new Date(), reason, null);
}
/**
* This loops over all declared collections on the given object and all declared collections on
* parent objects to use the given <code>handlerType</code>.
*
* @param <H> the type of Handler to get (should extend {@link RequiredDataHandler})
* @param handlerType the type of Handler to get (should extend {@link RequiredDataHandler})
* @param openmrsObject the object that is being acted upon
* @param currentUser the current user to set recursively on the object
* @param currentDate the date to set recursively on the object
* @param other an optional second argument that was passed to the service method (usually a
* void/retire reason)
* @param alreadyHandled an optional list of objects that have already been handled and should
* not be processed again. this is intended to prevent infinite recursion when
* handling collection properties.
* @see HandlerUtil#getHandlersForType(Class, Class)
*/
@SuppressWarnings("unchecked")
public static <H extends RequiredDataHandler> void recursivelyHandle(Class<H> handlerType, OpenmrsObject openmrsObject,
User currentUser, Date currentDate, String other, List<OpenmrsObject> alreadyHandled) {
if (openmrsObject == null) {
return;
}
Class<? extends OpenmrsObject> openmrsObjectClass = openmrsObject.getClass();
if (alreadyHandled == null) {
alreadyHandled = new ArrayList<OpenmrsObject>();
}
// fetch all handlers for the object being saved
List<H> handlers = HandlerUtil.getHandlersForType(handlerType, openmrsObjectClass);
// loop over all handlers, calling onSave on each
for (H handler : handlers) {
handler.handle(openmrsObject, currentUser, currentDate, other);
}
alreadyHandled.add(openmrsObject);
Reflect reflect = new Reflect(OpenmrsObject.class);
List<Field> allInheritedFields = reflect.getInheritedFields(openmrsObjectClass);
// loop over all child collections of OpenmrsObjects and recursively save on those
for (Field field : allInheritedFields) {
// skip field if it's declared independent
if (Reflect.isAnnotationPresent(openmrsObjectClass, field.getName(), Independent.class)) {
continue;
}
if (reflect.isCollectionField(field) && !isHandlerMarkedAsDisabled(handlerType, field)) {
// the collection we'll be looping over
Collection<OpenmrsObject> childCollection = getChildCollection(openmrsObject, field);
if (childCollection != null) {
for (Object collectionElement : childCollection) {
if (!alreadyHandled.contains(collectionElement)) {
recursivelyHandle(handlerType, (OpenmrsObject) collectionElement, currentUser, currentDate,
other, alreadyHandled);
}
}
}
}
}
}
/**
* This method gets a child attribute off of an OpenmrsObject. It usually uses the getter for
* the attribute, but can use the direct field (even if its private) if told to by the
* {@link AllowDirectAccess} annotation.
*
* @param openmrsObject the object to get the collection off of
* @param field the name of the field that is the collection
* @return the actual collection of objects that is on the given <code>openmrsObject</code>
* @should get value of given child collection on given field
* @should should be able to get annotated private fields
* @should throw APIException if getter method not found
*/
@SuppressWarnings("unchecked")
protected static Collection<OpenmrsObject> getChildCollection(OpenmrsObject openmrsObject, Field field) {
String fieldName = field.getName();
String getterName = "get" + StringUtils.capitalize(fieldName);
try {
// checks if direct access is allowed
if (field.isAnnotationPresent(AllowDirectAccess.class)) {
boolean previousFieldAccessibility = field.isAccessible();
field.setAccessible(true);
Collection<OpenmrsObject> childCollection = (Collection<OpenmrsObject>) field.get(openmrsObject);
field.setAccessible(previousFieldAccessibility);
return childCollection;
} else {
// access the field via its getter method
Class<? extends OpenmrsObject> openmrsObjectClass = openmrsObject.getClass();
Method getterMethod = openmrsObjectClass.getMethod(getterName, (Class[]) null);
return (Collection<OpenmrsObject>) getterMethod.invoke(openmrsObject, new Object[] {});
}
}
catch (IllegalAccessException e) {
if (field.isAnnotationPresent(AllowDirectAccess.class)) {
throw new APIException("unable.get.field", new Object[] { fieldName, openmrsObject.getClass() });
} else {
throw new APIException("unable.getter.method", new Object[] { "use", getterName, fieldName,
openmrsObject.getClass() });
}
}
catch (InvocationTargetException e) {
throw new APIException("unable.getter.method", new Object[] { "run", getterName, fieldName,
openmrsObject.getClass() });
}
catch (NoSuchMethodException e) {
throw new APIException("unable.getter.method", new Object[] { "find", getterName, fieldName,
openmrsObject.getClass() });
}
}
/**
* Checks the given {@link Class} to see if it A) is a {@link Collection}/{@link Set}/
* {@link List}, and B) contains {@link OpenmrsObject}s
*
* @param arg the actual object being passed in
* @return true if it is a Collection of some kind of OpenmrsObject
* @should return true if class is openmrsObject list
* @should return true if class is openmrsObject set
* @should return false if collection is empty regardless of type held
*/
protected static boolean isOpenmrsObjectCollection(Object arg) {
if (arg instanceof Collection) {
Collection<?> col = (Collection<?>) arg;
return !col.isEmpty() && col.iterator().next() instanceof OpenmrsObject;
}
return false;
}
/**
* Checks if the given field is annotated with a @DisableHandler annotation to specify
* that the given handlerType should be disabled
*
* @param handlerType
* @param field
* @return true if the handlerType has been marked as disabled, false otherwise
*/
protected static boolean isHandlerMarkedAsDisabled(Class<? extends RequiredDataHandler> handlerType, Field field) {
// if the annotation isn't present, return false
if (!field.isAnnotationPresent(DisableHandlers.class)) {
return false;
} else {
// otherwise we need to see if the handler type is one of the types specified in the annotation
for (Class<? extends RequiredDataHandler> h : field.getAnnotation(DisableHandlers.class).handlerTypes()) {
if (h.isAssignableFrom(handlerType)) {
return true;
}
}
}
return false;
}
}