/** * */ package org.javabuilders.util; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.net.URL; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.javabuilders.BuildException; import org.javabuilders.BuildProcess; import org.javabuilders.BuildResult; import org.javabuilders.BuilderConfig; import org.javabuilders.BuilderPreProcessor; import org.javabuilders.ICustomCommand; import org.javabuilders.NamedObjectProperty; import org.javabuilders.Node; import org.javabuilders.Values; import org.javabuilders.annotations.Alias; import org.javabuilders.annotations.DoInBackground; import org.javabuilders.event.BackgroundEvent; import org.javabuilders.event.BackgroundEventListener; import org.javabuilders.event.CancelStatus; import org.javabuilders.event.IBackgroundCallback; import org.javabuilders.event.IBindingListener; import org.javabuilders.event.IBindingListenerProvider; import org.javabuilders.event.ObjectMethod; import org.javabuilders.exception.InvalidFormatException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.yaml.snakeyaml.Yaml; /** * Various common utilities * * @author Jacek Furmankiewicz * */ public class BuilderUtils { private final static Logger logger = LoggerFactory.getLogger(BuilderUtils.class); private static OperatingSystem os = OperatingSystem.Windows; // it's the most likely // 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 bean pattern:either // "propertyName" // or // "object.propertyName" // or // "object.propertyName.nestedProperty" private static Pattern namePattern = Pattern.compile("([A-Z]{0,1}[0-9a-z]+)"); /** * 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.<br/> * The following order of preference is used, based on the arguments:<br/> * <ol> * <li>method(caller-compatible class, event class) * <li>method(caller-compatible class) * <li>method(event class) * <li>method() * </ol> * 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.<br/> * The following order of preference is used, based on the arguments:<br/> * <ol> * <li>method(caller-compatible class, event class) * <li>method(caller-compatible class) * <li>method(event class) * <li>method() * </ol> * 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<Method> methods = new HashSet<Method>(); // 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.error(ex.getMessage(),ex); 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<Integer, Method> methodsByPreference = new TreeMap<Integer, Method>(); 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<ObjectMethod> 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<ObjectMethod> 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); 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.isDebugEnabled()) { logger.debug("Executing background method: {}", method.getMethod().getName()); } // create the list of methods that should be executed // after this background one // completes final Collection<ObjectMethod> outstandingMethods = new ArrayList<ObjectMethod>(); 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()); } } }; //implement background event listeners (Issue 69) try { //global listeners for(BackgroundEventListener l : result.getConfig().getBackgroundEventListeners()) { l.backgroundTaskStarted(result, event); } //local listeners for(BackgroundEventListener l : result.getBackgroundEventListeners()) { l.backgroundTaskStarted(result, event); } //do default handling result.getConfig().getBackgroundProcessingHandler().doInBackground(result, result.getCaller(), method.getMethod(), event, callback); } finally { //notify local that task has ended for(BackgroundEventListener l : result.getBackgroundEventListeners()) { l.backgroundTaskEnded(result, event); } //notify global that task has ended for(BackgroundEventListener l : result.getConfig().getBackgroundEventListeners()) { l.backgroundTaskEnded(result, event); } } if (logger.isDebugEnabled()) { logger.debug("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.isDebugEnabled()) { logger.debug("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.<br/> * The following rules are followed:<br/> * <ul> * <li>The caller's instance variable reference must be null (i.e. cannot * override existing one) * <li>It must match exactly by name to the named object * <li>The instance variables type must be compatible with the one created * during the build</li> * </ul> */ public static void updateNamedObjectReferencesInCaller(BuildProcess result) { Object caller = result.getCaller(); //get a list of all the fields in the full object hierarchy, so that //setting references on superclass fields is possible too (Issue #51) List<Field> fields = new LinkedList<Field>(); Class<?> parent = caller.getClass(); while (parent != null) { fields.addAll(Arrays.asList(parent.getDeclaredFields())); parent = parent.getSuperclass(); } if (caller != null) { 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.isDebugEnabled()) { logger.debug("Successfully set reference to caller's variable: " + name); } continue; } else { if (logger.isInfoEnabled()) { logger.info("Failed to set value for caller's variable: " + name + ". Incompatible types."); } } } else { // instance can be pre-existing if (logger.isInfoEnabled()) { logger.info("Unable to set caller's instance variable: " + name + ". It is not null."); } } } catch (IllegalArgumentException e) { if (logger.isInfoEnabled()) { logger.info("Failed to access property " + name, e); } } catch (IllegalAccessException e) { if (logger.isInfoEnabled()) { logger.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<Object> convertToList(Object value) { if (value instanceof List) { return (List<Object>) value; } else { List<Object> list = new ArrayList<Object>(); list.add(value); return list; } } /** * Converts a list to a string * * @param list * List * @param delimiter * Delimiter * @param estimatedLength * Estimated length (for performance reasons) * @return String */ public static String convertListToString(List<Object> list, char delimiter, int estimatedLength) { StringBuilder builder = new StringBuilder(estimatedLength); for (Object value : list) { if (builder.length() > 0) { builder.append(delimiter).append(" "); } builder.append(value); } return builder.toString(); } /** * Method to automatically populate the properties of an object from a map * of values * * @param target * Target * @param properties * Map of propeties and their values * @throws BuildException * Thrown if unable to set property */ public static void populateObjectPropertiesFromMap(Object target, Map<String, Object> properties) throws BuildException { for (String property : properties.keySet()) { Object value = properties.get(property); try { PropertyUtils.setProperty(target, property, value); } catch (Exception e) { throw new BuildException("Unable to set value on object for key: " + property, e); } } } /** * Used for parsing property paths in binding and validation expression * * @param text * Input text (with properties for binding/validation, in EL or * bean-style) * @return List of properties * @throws BuildException */ public static List<NamedObjectProperty> getParsedPropertyExpression(String text) throws BuildException { List<NamedObjectProperty> properties = new ArrayList<NamedObjectProperty>(); // look for EL expressions in the source path Matcher m = elPattern.matcher(text); while (m.find()) { // add, but removing the EL ${ and } brackets String path = m.group().split("[${]|[}]")[1]; // split the object name and property path into separate fields NamedObjectProperty property = getParsedProperty(path); properties.add(property); } // if none found assume it's a bean property if (properties.size() == 0) { if (text.matches(beanPattern)) { NamedObjectProperty property = getParsedProperty(text); properties.add(property); } else { throw new BuildException("Unable to parse property expression. It is recognized as neither EL or regular Bean: " + text); } } return properties; } /** * Parses a named object/property path * * @param property * Property path in "objectName.propertyPath" format * @return Parsed properety * @throws BuildException */ public static NamedObjectProperty getParsedProperty(String property) throws BuildException { if (property.indexOf('.') <= 0) { property = String.format("this.%s", property); } int pos = property.indexOf('.'); if (pos > 0) { NamedObjectProperty objectProperty = new NamedObjectProperty(property.substring(0, pos), property.substring(pos + 1)); return objectProperty; } else { throw new BuildException("Unable to parse named object property: " + property); } } /** * Finds the class type from a name referenced in YAML by looking at the * instance variables on the caller class * * @param caller * @param className * @param objectName * @return Class (or null if none found) */ public static Class<?> getClassFromCallerFields(Object caller, String className, String objectName) { Class<?> typeClass = null; Map<String, Class<?>> classes = new HashMap<String, Class<?>>(); Field[] fields = caller.getClass().getDeclaredFields(); for (Field field : fields) { if (field.getType().getSimpleName().equals(className)) { classes.put(field.getName(), field.getType()); typeClass = field.getType(); } } if (classes.size() > 1) { // rare case...multiple declarations of the same custom class for (String name : classes.keySet()) { if (name.equals(objectName)) { typeClass = classes.get(name); break; } } } return typeClass; } /** * Gets the class from a class alias in the YAML file * * @param process * @param key * @param instanceName * (optionall name of the class instance) * @return Class or null if not found */ public static Class<?> getClassFromAlias(BuildProcess process, String key, String instanceName) { Class<?> typeClass = process.getConfig().getClassType(key); if (typeClass == null) { // try treating the key as if it was a complete class name typeClass = BuilderUtils.getClassFromCallerFields(process.getCaller(), key, instanceName); } //if still null, check if the name is in the class hierarchy of the caller if (typeClass == null) { Class<?> parent = process.getCaller().getClass(); while (parent != null) { if (parent.getSimpleName().equals(key)) { typeClass = parent; break; } parent = parent.getSuperclass(); } } return typeClass; } /** * @return All fields, including protected fields from superclasses */ public static Map<String, Field> getAllFields(Class<? extends Object> typeClass) { Map<String, Field> allFields = new HashMap<String, Field>(); for (Field field : typeClass.getDeclaredFields()) { allFields.put(field.getName(), field); field.setAccessible(true); } typeClass = typeClass.getSuperclass(); while (typeClass != null) { Field[] fields = typeClass.getDeclaredFields(); for (Field field : fields) { int mod = field.getModifiers(); if (!Modifier.isStatic(mod) && (Modifier.isProtected(mod) || Modifier.isPublic(mod))) { allFields.put(field.getName(), field); field.setAccessible(true); } } typeClass = typeClass.getSuperclass(); } return allFields; } /** * Looks for a field with a particular name and type * @param name Name * @param typeClass Type class * @return Field or null if not found */ public static Field getField(Object caller, String name, Class<? extends Object> typeClass) { Map<String,Field> fields = getAllFields(caller.getClass()); Field field = null; for(String fieldName : fields.keySet()) { if (name.equals(fieldName)) { Field temp = fields.get(fieldName); if (typeClass.isAssignableFrom(temp.getType())) { field = temp; } break; } } return field; } /** * Gets all accessible method from the entire object hierarchy * * @param typeClass * Type class * @return List of methods */ public static List<Method> getAllMethods(Class<? extends Object> typeClass) { List<Method> allMethods = new ArrayList<Method>(); for (Method method : typeClass.getDeclaredMethods()) { allMethods.add(method); method.setAccessible(true); } typeClass = typeClass.getSuperclass(); while (typeClass != null) { Method[] methods = typeClass.getDeclaredMethods(); for (Method method : methods) { int mod = method.getModifiers(); if (!Modifier.isStatic(mod) && (Modifier.isProtected(mod) || Modifier.isPublic(mod))) { allMethods.add(method); method.setAccessible(true); } } typeClass = typeClass.getSuperclass(); } return allMethods; } /** * Checks if a listener is needed, depending if any of the passed method * lists have any value in them * * @param methods * Method lists * @return true if needed, false if not */ public static boolean isListenerNeeded(Values<String, ObjectMethod>... methods) { boolean needed = false; for (Values<String, ObjectMethod> methodList : methods) { if (methodList != null && methodList.size() > 0) { needed = true; break; } } return needed; } /** * Checks YAML for tabs...since the jvyaml error reporting is so poor that * it gives you no idea as to where the error is * * @param yaml */ public static void validateYamlContent(String yaml, String fileName) { StringBuilder errors = new StringBuilder(); String[] lines = yaml.split("\n"); String previousLine = null; for(int i = 0; i < lines.length;i++) { String line = lines[i]; //check for tabs int pos = line.indexOf("\t"); if (pos >= 0) { errors.append(MessageFormat.format("Found a tab in line {0} starting at\n{1}\n", (i+1), line.substring(pos))); } //check for unmatched parentheses int left = 0, right = 0; int startingQuotes = 0, endingQuotes = 0; boolean inQuotes = false; for(int c = 0; c < line.length(); c++) { char charAt = line.charAt(c); if (charAt == '"') { inQuotes = !inQuotes; if (inQuotes) { startingQuotes++; } else { endingQuotes++; } } else if (charAt == '(' && !inQuotes) { left++; } else if (charAt == ')' && !inQuotes) { right++; } else if (charAt == '#' && !inQuotes) { //'#' means start of comment, disregard everything afterwards break; } } if (left != right) { errors.append(MessageFormat.format("Unmatched number of left and right parentheses in line {0}: {1}\n", (i+1), line)); } if (startingQuotes != endingQuotes) { errors.append(MessageFormat.format("Unmatched number of opening and closing quotes in line {0}: {1}\n", (i+1), line)); } //check if collection started without a ":" on the parent if (previousLine != null) { String trimmed = line.trim(); if (trimmed.startsWith("-")) { pos = line.indexOf("-"); int prevPos = previousLine.indexOf("-"); if (pos != prevPos) { //previous line is a parent and not another element in the same collection //it has to end with ":" then String noCommentsLine = getYamlLineWithoutTrailingComments(previousLine); int lineIndent = getFirstNoSpacePosition(line); int previousLineIndent = getFirstNoSpacePosition(previousLine); if(!noCommentsLine.trim().endsWith(":") && lineIndent > previousLineIndent) { throw new BuildException("\":\" is missing after \"{0}\", as list is started on next line",noCommentsLine); } } } } //remember for the next line to validate against previousLine = line; } if (errors.length() > 0) { if (fileName != null) { errors.insert(0, MessageFormat.format("Errors found in file: {0}\n", fileName)); } throw new InvalidFormatException(errors.toString()); } } private static int getFirstNoSpacePosition(String line) { for(int i = 0; i < line.length();i++) { if (line.charAt(i) != ' ') { return i; } } return -1; } private static String getYamlLineWithoutTrailingComments(String line) { if (line.indexOf("#") < 0) { return line; } else { boolean inQuotes = false; for(int i = 0; i < line.length();i++) { char c = line.charAt(i); if (c == '"') { inQuotes = !inQuotes; } else if (c == '#' && !inQuotes) { //comment begins return line.substring(0,i); } } return line; } } /** * Attempts to find an existing instance * * @param caller * @return */ public static Object getExistingInstanceIfAvailable(Object caller, Class<?> expectedClass, BuilderConfig config, Map<String, Object> data) { Object instance = null; if (caller != null) { String name = config.getNameIfAvailable(data); instance = getExistingInstanceIfAvailable(caller, expectedClass, config, name); } return instance; } /** * Attempts to find an existing instance * * @param caller * @return */ public static Object getExistingInstanceIfAvailable(Object caller, Class<?> expectedClass, BuilderConfig config, String name) { Object instance = null; if (caller != null && name != null) { Map<String, Field> fields = getAllFields(caller.getClass()); Object possible = null; try { if (fields.containsKey(name)) { possible = fields.get(name).get(caller); } } catch (Exception e) { throw new BuildException(e, "Failed to get value for {0}: {1}", name, e.getMessage()); } if (possible != null) { if (expectedClass.isAssignableFrom(possible.getClass())) { // class types are compatible instance = possible; } else { throw new BuildException( "Found a variable called {0}, but it was of incompatible type {1}, instead of the expected {2}", name, possible.getClass(), expectedClass); } } } return instance; } /** * Standard logic to get a URL from a path in YAML. Looks in the class * filesystem first, if not found looks it up in the resource bundles * instead. Enhancement <a * href="http://code.google.com/p/javabuilders/issues/detail?id=18">#18</a> * * @param process * Process * @param path * Path (can be an actual path, e.g. "resources/picture.png" or a * key in the resource bundle. e.g. "image.fileNew" where in the * bundle it would say "image.fileNew=/resources/picture.png") * @return URL or null if not found */ public static URL getResourceURL(BuildProcess process, String path) { URL url = process.getCaller().getClass().getResource(path); if (url == null) { // path not found...check if the path points to a key in the // resource bundle instead if (process.getBuildResult().isInternationalizationActive()) { path = process.getBuildResult().getResource(path); if (path != null) { url = process.getCaller().getClass().getResource(path); } } } return url; } /** * Standard logic to get an input stream from a path in YAML. Looks in the * class filesystem first, if not found looks it up in the resource bundles * instead. Enhancement <a * href="http://code.google.com/p/javabuilders/issues/detail?id=18">#18</a> * * @param process * Process * @param path * Path (can be an actual path, e.g. "resources/picture.png" or a * key in the resource bundle. e.g. "image.fileNew" where in the * bundle it would say "image.fileNew=/resources/picture.png") * @return URL or null if not found */ public static InputStream getResourceInputStream(BuildProcess process, String path) { InputStream is = process.getCaller().getClass().getResourceAsStream(path); if (is == null) { // path not found...check if the path points to a key in the // resource bundle instead if (process.getBuildResult().isInternationalizationActive()) { path = process.getBuildResult().getResource(path); if (path != null) { is = process.getCaller().getClass().getResourceAsStream(path); } } } return is; } /** * Generates a Java-safe name from an input string. * Ensures it is unique in the context of the current build (issue #118) * * @param input * @return */ public static String generateName(BuildResult result, String input, String prefix, String suffix) { return generateName(result,input,prefix,suffix,1); } /** * Generates a Java-safe name from an input string. * Ensures it is unique in the context of the current build (issue #118) * * @param input * @return */ private static String generateName(BuildResult result, String input, String prefix, String suffix, int instance) { Matcher m = namePattern.matcher(input); StringBuilder bld = new StringBuilder(input.length()); if (prefix != null) { bld.append(prefix); } while (m.find()) { String group = m.group(); if (group.length() > 1) { if (bld.length() == 0) { bld.append(group.substring(0, 1).toLowerCase()); bld.append(group.substring(1)); } else { bld.append(group.substring(0, 1).toUpperCase()); bld.append(group.substring(1)); } } else { bld.append(group.toUpperCase()); } } //append instance, if larger than 1 (required to ensure unique control names) if (instance > 1) { bld.append(instance); } if (suffix != null) { bld.append(suffix); } String name = bld.toString(); //check if this name already exists, need to generate a unique one //by adding a number at the end if (result.get(name) != null) { return generateName(result,input,prefix,suffix,++instance); } else { return name; } } /** * Reads the YAML file for a particular class * * @param baseClass * Class * @return YAML content for that class * @throws IOException */ public static String getYamlContent(BuilderConfig config, Class<?> baseClass) throws IOException { return getYamlContent(config, baseClass,null); } /** * Reads the YAML file for a particular class * * @param baseClass * Class * @return YAML content for that class * @throws IOException */ public static String getYamlContent(BuilderConfig config, Class<?> baseClass, String yamlFileName) throws IOException { StringBuilder builder = new StringBuilder(); if (yamlFileName == null) { yamlFileName = baseClass.getSimpleName() + config.getYamlExtension(); } InputStream is = baseClass.getResourceAsStream(yamlFileName); InputStreamReader isr = null; BufferedReader rdr = null; try { isr = new InputStreamReader(is); rdr = new BufferedReader(isr); String line = ""; while ((line = rdr.readLine()) != null) { builder.append(line).append("\n"); } } finally { if (rdr != null) { rdr.close(); } if (isr != null) { isr.close(); } } return builder.toString(); } /** * Gets generics info despite runtime type erasure. Hack alert, obviously. * @param field Field * @return Class (if found) or null if not */ public static Class<?> getGenericsTypeFromCollectionField(Field field) { Class<?> clazz = null; if (List.class.isAssignableFrom(field.getType()) || Set.class.isAssignableFrom(field.getType())) { field.setAccessible(true); ParameterizedType ptype = (ParameterizedType) field.getGenericType(); Type[] types = ptype.getActualTypeArguments(); if (types != null && types.length > 0) { clazz = (Class<?>)types[0]; } } return clazz; } /** * Massages an argument into MessageFormat to stop it from blowing up MessageFormat (if it has embedded {} values) * @param argument Argument * @return MessageFormat-safe argument string */ public static Object[] getMessageFormatSafeArguments(Object... arguments) { for(int i = 0; i < arguments.length;i++) { Object argument = arguments[i]; String str = null; if (argument instanceof String) { str = (String) argument; } else if (argument != null) { str = String.valueOf(str); } if (str != null && str.indexOf('{') >= 0) { arguments[i] = str.replace("{","\\{"); } } return arguments; } /** * Gets value from a map and substitutes it with a default if not found * @param map * @param key * @param defaultValue * @return */ public static Object getValue(Map<String,Object> map, Object key, Object defaultValue) { Object value = map.get(key); if (value == null) { value = defaultValue; } return value; } /** * Extracts actual key from compressed YAML (e.g: JButton from JButton(name=button,onAction=save)) * @param key Key * @return Actual key */ public static String getRealKey(String key) { if (key.indexOf('(') > 0 && key.endsWith(")")) { key = key.substring(0,key.indexOf('(')); } return key; } /** * Wraps <html> tags around text if HTML content fount * @param text * @return */ public static String handlePotentialHtmlContent(String text) { String html = text; if (text.contains("<") && text.contains("</")) { html = String.format("<html>%s</html>",text); } return html; } /** * Explodes the compressed YAML (virtual constructor flow) into a proper map of parsed values * @param key Key (e.g. "JButton(name=ok)" or just a simple "(name=ok)") */ public static void uncompressYaml(String key, Map<String,Object> current) throws BuildException { StringBuilder nameValuePair = new StringBuilder(key.length()); //parse the virtual constructor values int start = key.indexOf("("); if (start > 0 && key.endsWith(")")) { String constructorText = key.substring(start + 1, key.length() - 1); List<String> keyValuePairs = new LinkedList<String>(); int nestedParentheses = 0; boolean isEmbeddedInString = false; Character listEnd = null; for(int i = 0; i < constructorText.length(); i++) { char currentChar = constructorText.charAt(i); if (currentChar == '"') { isEmbeddedInString = !isEmbeddedInString; } if (!isEmbeddedInString) { if (BuilderPreProcessor.listIndicators.containsKey(currentChar)) { listEnd = BuilderPreProcessor.listIndicators.get(currentChar); if (listEnd == ')') { //TODO : deprecate after 1.0 if (BuilderPreProcessor.logger.isWarnEnabled()) { BuilderPreProcessor.logger.warn("'[]' is the new format for lists, '()' is deprecated and will be removed: %s",constructorText); } } nestedParentheses++; } else if (listEnd != null && currentChar == listEnd) { listEnd = null; nestedParentheses--; } else if (currentChar == ',' && nestedParentheses == 0) { //we have a real separator keyValuePairs.add(nameValuePair.toString()); nameValuePair.setLength(0); continue; } } nameValuePair.append(currentChar); } //process last one keyValuePairs.add(nameValuePair.toString()); StringBuilder temp = new StringBuilder(); for(String keyValuePair : keyValuePairs) { if (keyValuePair.length() > 0) { String[] pair = keyValuePair.split("=",2); if (pair.length == 2) { pair[0] = pair[0].trim(); temp.setLength(0); temp.append(pair[1].trim()); //if the argument is a Java-style collection () replace it wit the equivalent YAML [] //handle embedded values in a String (Issue #14) boolean isEmbedded = false; for (int i = 0; i < temp.length();i++) { char c = temp.charAt(i); if (c == '"') { isEmbedded = !isEmbedded; } if (!isEmbedded) { if (c == '(') { temp.setCharAt(i, '['); } else if (c == ')') { temp.setCharAt(i, ']'); } } } //put it into the map, but only if it does not exist there already if (!current.containsKey(pair[0])) { //parse the value with YAML to make sure it makes it into the correct default type Yaml yaml = new Yaml(); Object value = yaml.load(temp.toString()); current.put(pair[0],value); } } else { //could be an empty constructor, but if not...it's a format error throw new BuildException("Key/value {0} from virtual constructor {1} not in valid format",keyValuePair,constructorText); } } } } else { throw new BuildException("Unable to parse virtual constructor: {0}", key); } } /** * Standard logic to fire binding events, if the config supports them * @param result * @param binding */ @SuppressWarnings("unchecked") public static <T extends Object> void fireBindingEvent(BuildResult result, T binding) { if (result.getConfig() instanceof IBindingListenerProvider) { IBindingListenerProvider<T> provider = (IBindingListenerProvider<T>) result.getConfig(); for(IBindingListener<T> listener : provider.getBindingListeners()) { listener.bindingCreated(result, binding); } } } }