org.javabuilders.BuilderUtils Maven / Gradle / Ivy
The newest version!
/**
*
*/
package org.javabuilders;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.beanutils.PropertyUtils;
import org.javabuilders.annotations.Alias;
import org.javabuilders.annotations.DoInBackground;
import org.javabuilders.event.BackgroundEvent;
import org.javabuilders.event.CancelStatus;
import org.javabuilders.event.IBackgroundCallback;
import org.javabuilders.event.ObjectMethod;
/**
* Various common utilities
* @author Jacek Furmankiewicz
*
*/
public class BuilderUtils {
private final static java.util.logging.Logger logger = Logger.getLogger(BuilderUtils.class.getSimpleName());
private static OperatingSystem os = OperatingSystem.Windows; //it's the most likely default, let's be honest
//should accept both ${propertyName} and ${object.propertyName}
private static Pattern elPattern = Pattern.compile("[${][a-z][a-zA-Z0-9]*(\\.?[a-z][a-zA-Z0-9]*)*}"); //check for EL pattern
private static String beanPattern = "[a-zA-Z][a-zA-Z09]*(\\.?[a-z]?[a-zA-Z0-9]*)*"; //check for bean pattern : either "propertyName" or "object.propertyName" or "object.propertyName.nestedProperty"
/**
* Static constructor
*/
static {
String name = System.getProperty("os.name").toLowerCase();
if (name.indexOf("unix") >= 0 || name.indexOf("linux") >= 0) {
os = OperatingSystem.LinuxUnix;
} else if (name.indexOf("mac") >= 0) {
os = OperatingSystem.Mac;
}
}
/**
* Returns the current operating system. Used for platform-specific fixes
* @return OS
*/
public static OperatingSystem getOS() {
return os;
}
/**
* Simple enum to represent the different OS types
* @author Jacek Furmankiewicz
*/
public enum OperatingSystem {
Windows, LinuxUnix, Mac, Other
}
/**
* Returns the method that should be invoked based on the name defined in the build file.
* The following order of preference is used, based on the arguments:
*
* - method(caller-compatible class, event class)
*
- method(caller-compatible class)
*
- method(event class)
*
- method()
*
* Caller-compatible class has to be the caller's class or any of its superclasses/interfaces.
* Support for mapping to methods annotated with "@Name".
* @param result The build result
* @param node Source node whose object generates the event
* @param methodKey The base name of the method to be invoked (e.g. "save")
* @param eventClasses an optional list of classes specific to the event (e.g. KeyEvent or MouseEvent for Swing key event listeners)
* @return
*/
public static ObjectMethod getCallerEventMethod(BuildProcess result, Node node, String methodKey, Class>... eventClasses) throws BuildException {
String methodName = String.valueOf(node.getProperties().get(methodKey));
return getCallerEventMethod(result, methodName, node.getMainObject().getClass(), eventClasses);
}
/**
* Returns the method that should be invoked based on the name defined in the build file.
* The following order of preference is used, based on the arguments:
*
* - method(caller-compatible class, event class)
*
- method(caller-compatible class)
*
- method(event class)
*
- method()
*
* Caller-compatible class has to be the caller's class or any of its superclasses/interfaces.
* Support for mapping to methods annotated with "@Name".
* @param node Source node whose object generates the event
* @param result The build result
* @param methodName Method name
* @param mainObjectClass Main object (i.e. the object that generates the call to the method, e.g. a button) class
* @param eventClasses an optional list of classes specific to the event (e.g. KeyEvent or MouseEvent for Swing key event listeners)
* @return Method to call or null if none found
*/
public static ObjectMethod getCallerEventMethod(BuildProcess result, String methodName, Class> mainObjectClass, Class>... eventClasses) throws BuildException {
Set methods = new HashSet();
//custom command
if (result.getConfig().getCustomCommands().containsKey(methodName)) {
ICustomCommand extends Object> command = result.getConfig().getCustomCommands().get(methodName);
try {
Method method = command.getClass().getMethod("process", BuildResult.class, Object.class);
ObjectMethod noMethod = new ObjectMethod(command,method, ObjectMethod.MethodType.CustomCommand);
return noMethod;
} catch (Exception ex) {
logger.severe(ex.getMessage());
throw new BuildException(ex,"Unable to get custom command method: {0}", ex.getMessage());
}
}
Object target = result.getCaller();
//regular method
for(Method method : getAllMethods(target.getClass())) {
if (method.getParameterTypes().length <= 2) {
//find methods with the specified name or annotated with @Name with the same value
if (method.isAnnotationPresent(Alias.class)) {
if (method.getAnnotation(Alias.class).value().equals(methodName)) {
methods.add(method);
}
} else if (method.getName().equals(methodName)) {
methods.add(method);
}
}
}
//we have a set of methods with the same name..find the appropriate one to call
Method methodToCall = null;
TreeMap methodsByPreference = new TreeMap();
preferenceSearch: //start the search for compatible methods by preference
for(Method method : methods) {
//find the best methods to call
switch(method.getParameterTypes().length) {
case 0:
//no arguments - lowest preference
methodsByPreference.put(0, method);
break;
case 1:
//one argument - can be either caller or event class
Class> parameterType = method.getParameterTypes()[0];
if (parameterType.isAssignableFrom(mainObjectClass)) {
methodsByPreference.put(2, method); //second in terms of preference
} else if (eventClasses != null && eventClasses.length > 0) {
for(Class> eventClass : eventClasses) {
if (parameterType.isAssignableFrom(eventClass)) {
methodsByPreference.put(3, method); //third in terms of preference
break;
} else if (method.isAnnotationPresent(DoInBackground.class) &&
BackgroundEvent.class.isAssignableFrom(method.getParameterTypes()[0])) {
//background event method
methodsByPreference.put(5, method); //background methods have highest preference
}
}
}
break;
case 2:
//two arguments - should be caller class / event class
Class> firstParameterType = method.getParameterTypes()[0];
Class> secondParameterType = method.getParameterTypes()[1];
boolean isSecondParameterAnEventClass = false;
for(Class> eventClass : eventClasses) {
if (secondParameterType.isAssignableFrom(eventClass)) {
isSecondParameterAnEventClass = true;
break;
}
}
if (firstParameterType.isAssignableFrom(mainObjectClass) && isSecondParameterAnEventClass) {
methodsByPreference.put(4, method); //best preference
break preferenceSearch; //no need to search further - we already found the best method
}
break;
}
}
//find the best method to call based on preference
for(int i = 5; i >= 0; i--) {
if (methodsByPreference.containsKey(i)) {
methodToCall = methodsByPreference.get(i);
break;
}
}
if (methodToCall != null) {
methodToCall.setAccessible(true); //make sure we can call it, even if it's private
} else {
throw new BuildException("Unable to find method to call for name \"{0}\"",methodName);
}
return new ObjectMethod(target,methodToCall);
}
/**
* Invoked the method mapped to an event on a caller
* @param result Build result
* @param node Source that generated the event
* @param methods The methods to be called (with zero parameters or one that is compatible with the source)
* @param eventClassInstance The event-specific class type (can be null)
* @see getCallerEventMethod()
*/
public static void invokeCallerEventMethods(final BuildResult result, Node node, Collection methods, Object eventClassInstance) {
invokeCallerEventMethods(result, node.getMainObject(), methods, eventClassInstance);
}
/**
* Invoked the method mapped to an event on a caller
* @param result Build result
* @param mainObject Source that generated the event
* @param methods The methods to be called (with zero parameters or one that is compatible with the source)
* @param eventClassInstance The event-specific class type (can be null)
* @see getCallerEventMethod()
*/
public static void invokeCallerEventMethods(final BuildResult result,
Object mainObject, Collection methods, Object eventClassInstance) {
Object invocationResult = null;
for(ObjectMethod method : methods) {
try {
//custom command?
if (method.getType() == ObjectMethod.MethodType.CustomCommand) {
method.getMethod().setAccessible(true);
invocationResult = method.getMethod().invoke(method.getInstance(),result, mainObject);
if (Boolean.FALSE.equals(invocationResult)) {
//abort
break;
}
} else {
//is this a background method?
if (method.getMethod().isAnnotationPresent(DoInBackground.class)) {
DoInBackground ann = method.getMethod().getAnnotation(DoInBackground.class);
final BackgroundEvent event = new BackgroundEvent(mainObject, eventClassInstance,
ann.blocking(), result.getConfig());
event.setCancelable(ann.cancelable());
event.setProgressIndeterminate(ann.indeterminateProgress());
event.setProgressStart(ann.progressStart());
event.setProgressEnd(ann.progressEnd());
event.setProgressValue(ann.progressValue());
//handle internationalizing the progress message
String resource = result.getResource(ann.progressMessage());
event.setProgressMessage(resource);
if(logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE,"Executing background method: %s", method.getMethod().getName());
}
//create the list of methods that should be executed after this background one
//completes
final Collection outstandingMethods = new ArrayList();
boolean next = false;
for(ObjectMethod nextMethod : methods) {
if (method.equals(nextMethod)) {
next = true;
} else if (next) {
outstandingMethods.add(nextMethod);
}
; }
//when background method is done, execute the remaining methods
//via a recursive call
final IBackgroundCallback callback = new IBackgroundCallback() {
public void done(Object returnValue) {
//only continue if task was not cancelled
if (event.getCancelStatus().getStatus() <= CancelStatus.NONE.getStatus() &&
outstandingMethods != null && outstandingMethods.size() > 0) {
BuilderUtils.invokeCallerEventMethods(result, event.getSource(), outstandingMethods, event.getOriginalEvent());
}
}
};
result.getConfig().getBackgroundProcessingHandler().doInBackground(result, result.getCaller(),
method.getMethod(), event, callback);
if(logger.isLoggable(Level.FINE)) {
logger.fine("Finished executing background method: " + method.getMethod().getName());
}
//stop executing the methods - it is the background handler's responsibility
//to execute them after the background one completes successfully
break;
}
switch (method.getMethod().getParameterTypes().length) {
case 0:
//no arguments
invocationResult = method.getMethod().invoke(method.getInstance());
break;
case 1:
//one argument - can be caller or event class
Class> parameterType = method.getMethod().getParameterTypes()[0];
if (parameterType.isAssignableFrom(mainObject.getClass())) {
invocationResult = method.getMethod().invoke(method.getInstance(), mainObject);
} else {
invocationResult = method.getMethod().invoke(method.getInstance(), eventClassInstance);
}
break;
case 2:
//two arguments - must be caller, event class instance
invocationResult = method.getMethod().invoke(result.getCaller(), mainObject, eventClassInstance);
break;
}
//if invoked method return false, abort calling the rest of the methods
if (invocationResult != null && invocationResult.equals(Boolean.FALSE)) {
break;
}
if (logger.isLoggable(Level.FINE)) {
logger.fine("Finished executing method: " + method.getMethod().getName());
}
}
} catch (Exception ex) {
throw new BuildException(ex, "Failed to invoke method: {0}. {1}", method.getMethod().getName(), ex.getMessage() );
}
}
}
/**
* If a caller is defined, updates any of the caller's instance variables references to the
* corresponding ones that were created during the build.
* The following rules are followed:
*
* - The caller's instance variable reference must be null (i.e. cannot override existing one)
*
- It must match exactly by name to the named object
*
- The instance variables type must be compatible with the one created during the build
*
*
*/
public static void updateNamedObjectReferencesInCaller(BuildProcess result) {
Object caller = result.getCaller();
if (caller != null) {
Field[] fields = caller.getClass().getDeclaredFields();
for(String name : result.getBuildResult().keySet()) {
for(Field field : fields) {
String fromName = field.getName();
if (field.isAnnotationPresent(Alias.class)) {
fromName = field.getAnnotation(Alias.class).value();
}
if (fromName.equals(name)) {
field.setAccessible(true); //ensure we have access to the field, even if private
Object value = null;
try {
value = field.get(caller);
if (value == null) {
Object namedObject = result.getBuildResult().get(name);
if (field.getType().isAssignableFrom(namedObject.getClass())) {
field.set(caller, namedObject);
if (logger.isLoggable(Level.FINE)) {
logger.fine("Successfully set reference to caller's variable: " + name);
}
} else {
if (logger.isLoggable(Level.INFO)) {
logger.info("Failed to set value for caller's variable: " + name + ". Incompatible types.");
}
}
} else {
//instance can be pre-existing
if (logger.isLoggable(Level.FINE)) {
logger.info("Unable to set caller's instance variable: " + name + ". It is not null.");
}
}
} catch (IllegalArgumentException e) {
if (logger.isLoggable(Level.INFO)) {
logger.log(Level.INFO,"Failed to access property " + name,e);
}
} catch (IllegalAccessException e) {
if (logger.isLoggable(Level.INFO)) {
logger.log(Level.INFO,"Failed to access property " + name,e);
}
}
}
}
}
}
}
/**
* Validates a value is not null and not empty
* @param name Value name
* @param value Value
*/
public static void validateNotNullAndNotEmpty(String name, Object value) {
if (value == null) {
throw new NullPointerException(
String.format("%s cannot be null", name));
}
if (value instanceof String && ((String)value).length() == 0) {
throw new NullPointerException(
String.format("%s cannot be empty String", name));
}
}
/**
* Simple utility to quickly convert a single value to a list, or return a list if
* the value is already a list to begin with
* @param value
* @return Value converted to list
*/
@SuppressWarnings("unchecked")
public static List