/** * */ package org.javabuilders; import java.beans.PropertyChangeSupport; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.ResourceBundle; import java.util.Set; import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.javabuilders.event.BackgroundEventListener; import org.javabuilders.event.BuildListener; import org.javabuilders.event.IBackgroundProcessingHandler; import org.javabuilders.handler.DefaultPropertyHandler; import org.javabuilders.handler.DefaultTypeHandler; import org.javabuilders.handler.IPropertyHandler; import org.javabuilders.handler.ITypeHandler; import org.javabuilders.handler.IntegerAsValueHandler; import org.javabuilders.handler.binding.BuilderBindings; import org.javabuilders.handler.type.ClassAsValueHandler; import org.javabuilders.handler.type.IntArrayAsValueHandler; import org.javabuilders.handler.type.IntegerArrayAsValueHandler; import org.javabuilders.handler.validation.BuilderValidators; import org.javabuilders.handler.validation.DefaultValidatorTypeHandler; import org.javabuilders.handler.validation.IValidationMessageHandler; import org.javabuilders.util.BuilderUtils; import org.javabuilders.util.PropertyUtils; /** * Represents the configuration for a builder (e.g. Swing vs SWT, etc) * @author Jacek Furmankiewicz */ public class BuilderConfig { private static final Pattern nameExtractor = Pattern.compile(".+\\((?:\\s*|.*,\\s*)name\\s*=\\s*([a-zA-Z0-9_]+)(?:\\s*|,|).*\\).*"); static ITypeHandler defaultTypeHandler = new DefaultTypeHandler(); static IPropertyHandler defaultPropertyHandler = DefaultPropertyHandler.getInstance(); private static String devSourceFolder = null; public static final String SOURCE = "javabuilders.dev.src"; public final static String CUSTOM_COMMAND_REGEX = "\\$[a-zA-Z0-9]+"; //e.g. "$validate" public final static String GLOBAL_VARIABLE_REGEX = "\\$\\$\\{[a-zA-Z0-9]+\\}"; //e.g. "$${dateFormat}" public static String PROPERY_STRING_LITERAL_CONTROL_PREFIX = "StringLiteralControl.Prefix"; public static String PROPERY_STRING_LITERAL_CONTROL_SUFFIX = "StringLiteralControl.Suffix"; /** * Static constructor */ static { devSourceFolder = System.getProperty(SOURCE); } /** * @return Current dev source folder (usually null unless overwritted with -Djavabuilders.dev.src) */ public static String getDevSourceFolder() { return devSourceFolder; } /** * @param devSourceFolder Development source folder. Allows to hot deploy YAML content without restarting the whole app */ public static void setDevSourceFolder(String devSourceFolder) { BuilderConfig.devSourceFolder = devSourceFolder; } private Map<Class<?>,Map<String,IPropertyHandler>> propertyHandlers = new HashMap<Class<?>,Map<String,IPropertyHandler>>(); protected Map<Class<?>,Map<String,IPropertyHandler>> getPropertyHandlers() { return propertyHandlers; } private Map<Class<?>,TypeDefinition> typeDefinitions = new HashMap<Class<?>, TypeDefinition>(); private Map<String,Class<?>> typeAliases = new HashMap<String, Class<?>>(); private boolean markInvalidResourceBundleKeys = false; //internal cache used to avoid re-creating the hierarchy of type definitions with every request //loaded lazily, upon demand private Map<Class<?>,Set<TypeDefinition>> typeDefinitionsForClassCache = new HashMap<Class<?>, Set<TypeDefinition>>(); private IBackgroundProcessingHandler backgroundProcessingHandler = null; private IValidationMessageHandler validationMessageHandler = null; private Map<String,ICustomCommand<? extends Object>> customCommands = new HashMap<String, ICustomCommand<? extends Object>>(); private Set<BuildListener> buildListeners = new LinkedHashSet<BuildListener>(); private Set<BackgroundEventListener> backgroundEventListeners = new LinkedHashSet<BackgroundEventListener>(); private Map<String,Object> customProperties = new HashMap<String, Object>(); private Map<String,Object> globals = new HashMap<String, Object>(); private String namePropertyName = Builder.NAME; private Map<String,PrefixControlDefinition> prefixes = new HashMap<String, PrefixControlDefinition>(); private Map<String,String> prototypes = new HashMap<String, String>(); private Set<ResourceBundle> bundles = new LinkedHashSet<ResourceBundle>(); private String yamlExtension = ".yml"; /** * Constructor * @param backgroundProcessingHandler Domain-specific background processing handler */ public BuilderConfig(IBackgroundProcessingHandler backgroundProcessingHandler, IValidationMessageHandler validationMessageHandler, ICustomCommand<Boolean> confirmCommand) { this.backgroundProcessingHandler = backgroundProcessingHandler; this.validationMessageHandler = validationMessageHandler; customCommands.put(Builder.CONFIRM_CUSTOM_COMMAND, confirmCommand); addType(Builder.BIND, BuilderBindings.class); addType(Builder.VALIDATE, BuilderValidators.class); forType(Class.class).valueHandler(ClassAsValueHandler.getInstance()); forType(BuilderValidators.class).typeHandler(DefaultValidatorTypeHandler.getInstance()); //handler for static final int constants forType(int.class).valueHandler(IntegerAsValueHandler.getInstance()); forType(int[].class).valueHandler(IntArrayAsValueHandler.getInstance()); forType(Integer[].class).valueHandler(IntegerArrayAsValueHandler.getInstance()); //default custom commands addCustomCommand(Builder.VALIDATE_CUSTOM_COMMAND, new ICustomCommand<Boolean>() { public Boolean process(BuildResult result, Object source) { return result.validate(); } }); } /** * Defines metadata about a type, e.g.<br/> * <code> * defineType(JFrame.class).requires("name","text").requires(LayoutManager.class).delay(LayoutManager.class); * @param applicableClass Class to which this type definition applies * @return The created type definition so that it can be used via the Builder pattern */ public TypeDefinition forType(Class<?> applicableClass) { if (applicableClass == null) { throw new NullPointerException("applicableClass cannot be null"); } TypeDefinition def = null; //lazy creation if (typeDefinitions.containsKey(applicableClass)) { def = typeDefinitions.get(applicableClass); } else { def = new TypeDefinition(applicableClass); typeDefinitions.put(applicableClass, def); //every time a new type definition is created //let's clear the cache to be on the safe side typeDefinitionsForClassCache.clear(); } return def; } /** * Adds multiple classes with aliases that correspond to the class names,, e.g. * <code> * addType(JFrame.class, JButton.class); * <code> * which corresponds to the type names in the builder file, e.g. * <code> * <b>JFrame</b>: * name: myFrame * title: My Frame * <b>JButton</b>: * name: myButton * text: My Button * </code> * @param classTypes Class types * @return BuilderConfig (for use in Builder pattern) */ public BuilderConfig addType(Class<?>... classTypes) { for(Class<?> type : classTypes) { addType(type); } return this; } /** * Adds an alias and the class type that corresponds to it * (assuming both names are the same), e.g. * <code> * addType(JFrame.class); * addType(JButton.class); * <code> * which corresponds to the type names in the builder file, e.g. * <code> * <b>JFrame</b>: * name: myFrame * title: My Frame * <b>JButton</b>: * name: myButton * text: My Button * </code> * @param classType Class type * @return BuilderConfig (for use in Builder pattern) */ public BuilderConfig addType(Class<?> classType) { addType(classType.getSimpleName(),classType); return this; } /** * Adds an alias and the class type that corresponds to it, e.g. * <code> * addType("JFrame",JFrame.class); * addType("JButton",JButton.class); * <code> * which corresponds to the type names in the builder file, e.g. * <code> * <b>JFrame</b>: * name: myFrame * title: My Frame * <b>JButton</b>: * name: myButton * text: My Button * </code> * @param alias Alias * @param classType Class type * @return BuilderConfig (for use in Builder pattern) */ public BuilderConfig addType(String alias, Class<?> classType) { if (alias == null || alias.length() == 0) { throw new NullPointerException("alias cannot be null or empty"); } if (classType == null) { throw new NullPointerException("classType cannot be null"); } if (typeAliases.containsKey(alias)) { String error = String.format("Duplicate alias '%s' for class '%s'. One already exists for '%s'", alias,classType.getName(),(typeAliases.get(alias)).getName()); throw new DuplicateAliasException(error); } typeAliases.put(alias, classType); return this; } /** * Removes a class from the list of recognized types * @param classType Class type * @return */ public BuilderConfig removeType(Class<?> classType) { List<String> keys = new ArrayList<String>(); for(String alias: typeAliases.keySet()) { Class<?> type = typeAliases.get(alias); if (type == classType) { keys.add(alias); } } //remove all flagged keys for(String key:keys) { typeAliases.remove(key); } return this; } /** * Gets all the defined type definitions, by class * @return */ public Collection<TypeDefinition> getTypeDefinitions() { return typeDefinitions.values(); } /** * Returns the exact type definition for a particular class * @param classType Class type * @return Type definition or null if none found */ public TypeDefinition getTypeDefinition(Class<?> classType) { return typeDefinitions.get(classType); } /** * Gets a list of all the pertinent type definitions. * @param classType Class type * @return Type definitions, sorted by inheritance tree */ public Set<TypeDefinition> getTypeDefinitions(Class<?> classType) { if (classType == null) { throw new NullPointerException("classType cannot be null"); } Set<TypeDefinition> defs = null; if (typeDefinitionsForClassCache.containsKey(classType)) { defs = typeDefinitionsForClassCache.get(classType); } else { //first request - lazy creation defs = new TreeSet<TypeDefinition>(new TypeDefinitionClassHierarchyComparator()); //classes Set<Class<?>> interfaces = new HashSet<Class<?>>(); Class<?> superClass = classType; while (superClass != null) { interfaces.addAll(Arrays.asList(superClass.getInterfaces())); if (typeDefinitions.containsKey(superClass)) { defs.add(typeDefinitions.get(superClass)); } superClass = superClass.getSuperclass(); } //interfaces //Class<?>[] interfaces = classType.getInterfaces(); for(Class<?> interfaceType : interfaces) { superClass = interfaceType; while (superClass != null) { if (typeDefinitions.containsKey(superClass)) { defs.add(typeDefinitions.get(superClass)); } superClass = superClass.getSuperclass(); } } typeDefinitionsForClassCache.put(classType, defs); } return defs; } /** * Indicates if a class is defined as a type * @param classType Class type * @return */ public boolean isTypeDefined(Class<?> classType) { return typeDefinitions.containsKey(classType); } /** * Returns the class type associated with a particular alias. Null if none found. * Should never be called directly, use BuilderUtils.getClassFromAlias() instead. * @param alias Alias * @return Class (null if none found) */ public Class<?> getClassType(String alias) { Class<?> classType = typeAliases.get(alias); return classType; } /** * Checks the raw data (before an object has been even handled) to extract its name, * if it has been specified * @param data Raw parses data * @return Name or null if not found */ public String getNameIfAvailable(Map<String,Object> data) { String name = (String) data.get(namePropertyName); return name; } /** * Returns the name of an object instance * @param instance * @return Object name * @throws ConfigurationException */ public String getObjectName(Object instance) throws ConfigurationException { String name = null; try { Object value = PropertyUtils.getProperty(instance, namePropertyName); if (value != null) { name = String.valueOf(value); } } catch (Exception ex) { //ignore } return name; } /** * Defines the property name that will be used for defining named objects (usually "name"). * Even if class type does not have a name property, the builder will handle it transparently */ public void setNamePropertyName(String namePropertyName) { this.namePropertyName = namePropertyName; } /** * @return The name of the property used to define names (usually "name") */ public String getNamePropertyName() { return this.namePropertyName; } /** * Returns the flag that controls if resource keys in the builder file * are surrounded with "#" if not found in the list of ResourceBundles * @return The markInvalidResourceBundleKeys flag */ public boolean isMarkInvalidResourceBundleKeys() { return markInvalidResourceBundleKeys; } /** * Sets the flag that controls if resource keys in the builder file * are surrounded with "#" if not found in the list of ResourceBundles * @param markInvalidResourceBundleKeys The markInvalidResourceBundleKeys flag */ public void setMarkInvalidResourceBundleKeys( boolean markInvalidResourceBundleKeys) { this.markInvalidResourceBundleKeys = markInvalidResourceBundleKeys; } /** * @return The domain-specific background processing handler */ public IBackgroundProcessingHandler getBackgroundProcessingHandler() { return backgroundProcessingHandler; } /** * @param backgroundProcessingHandler The domain-specific background processing handler */ public void setBackgroundProcessingHandler( IBackgroundProcessingHandler backgroundProcessingHandler) { this.backgroundProcessingHandler = backgroundProcessingHandler; } /** * @return the domain-specific validation message handler */ public IValidationMessageHandler getValidationMessageHandler() { return validationMessageHandler; } /** * @param validationMessageHandler the domain-specific validation message handler to set */ public void setValidationMessageHandler( IValidationMessageHandler validationMessageHandler) { this.validationMessageHandler = validationMessageHandler; } /** * Allows adding of custom commands * @param globalName Global name * @param command Command * @return Current config */ public BuilderConfig addCustomCommand(String globalName, ICustomCommand<Boolean> command) { BuilderUtils.validateNotNullAndNotEmpty("globalName", globalName); BuilderUtils.validateNotNullAndNotEmpty("command", command); if (globalName.matches(CUSTOM_COMMAND_REGEX)) { if (customCommands.containsKey(globalName)) { throw new BuildException("A custom command with the global name " + globalName + " is already defined"); } else { customCommands.put(globalName, command); } } else { throw new BuildException(globalName + " is not a valid custom command name. Must start with '$'"); } return this; } /** * @return Custom commands */ public Map<String,ICustomCommand<? extends Object>> getCustomCommands() { return customCommands; } /** * @return Global resource bundles */ public Set<ResourceBundle> getResourceBundles() { return bundles; } /** * Gets string resource from the specified bundles * @param key Key * @return String (or null if none found) */ public String getResource(String key) { String value = null; for(ResourceBundle bundle : getResourceBundles()) { if (bundle.containsKey(key)) { value = bundle.getString(key); break; } } return value; } /** * Add a global resource bundle * @param resourceBundleName Bundle name */ public void addResourceBundle(String resourceBundleName) { getResourceBundles().add(ResourceBundle.getBundle(resourceBundleName)); } /** * Add a global resource bundle * @param resourceBundle Bundle */ public void addResourceBundle(ResourceBundle resourceBundle) { getResourceBundles().add(resourceBundle); } /** * Adds a build listener * @param listener Build listener */ public void addBuildListener(BuildListener listener) { buildListeners.add(listener); } /** * Removes a build listener * @param listener Build listener */ public void removeBuildListener(BuildListener listener) { if (buildListeners.contains(listener)) { buildListeners.remove(listener); } } /** * @return Build listeners */ public BuildListener[] getBuildListeners() { return buildListeners.toArray(new BuildListener[buildListeners.size()]); } /** * Adds a background event listener * @param listener Listener */ public void addBackgroundEventListener(BackgroundEventListener listener) { backgroundEventListeners.add(listener); } /** * Removes a build listener * @param listener Build listener */ public void removeBackgroundEventListener(BackgroundEventListener listener) { if (backgroundEventListeners.contains(listener)) { backgroundEventListeners.remove(listener); } } /** * @return Background event listeners */ public BackgroundEventListener[] getBackgroundEventListeners() { return backgroundEventListeners.toArray(new BackgroundEventListener[backgroundEventListeners.size()]); } /** * Gets the collection of custom properties that allow to store any additional domain-specific settings * @return the customProperties */ public Map<String, Object> getCustomProperties() { return customProperties; } /** * Factory method that should be overriden for each toolkit with the * property change support that is proper for that toolkit's threading rules * @return Domain-specific property change support */ public PropertyChangeSupport createPropertyChangeSupport(Object source) { return new PropertyChangeSupport(source); } /** * Adds a global variable * @param name Name * @param value Value * @return This */ public BuilderConfig addGlobalVariable(String name, Object value) { BuilderUtils.validateNotNullAndNotEmpty("name", name); BuilderUtils.validateNotNullAndNotEmpty("value", value); if (name.matches(GLOBAL_VARIABLE_REGEX)) { if (globals.containsKey(globals)) { throw new BuildException("A global variable {0} already exists", name); } else { globals.put(name, value); } } else { throw new BuildException("{0} is not a valid global variable. Must start with '$'", name); } return this; } /** * Gets global variable value * @param name Name * @param expectedType Expected variable type * @return Value */ public Object getGlobalVariable(String name, Class<?> expectedType) { BuilderUtils.validateNotNullAndNotEmpty("name", name); BuilderUtils.validateNotNullAndNotEmpty("expectedType", expectedType); Object value = globals.get(name); if (value == null) { throw new BuildException("Global variable {0} is null",name); } if (!expectedType.isAssignableFrom(value.getClass())) { throw new BuildException("Global variable {0} is not compatible with expected type {1}", name, expectedType); } return value; } /** * @deprecated Use forType(Class<?>).typeHandler() instead */ public BuilderConfig addTypeHandler(ITypeHandler typeHandler) { forType(typeHandler.getApplicableClass()).typeHandler(typeHandler); return this; } /** * @deprecated Use TypeDefinition.getTypeHandler(BuilderConfig, Class) instead */ public ITypeHandler getTypeHandler(Class<?> classType) { return TypeDefinition.getTypeHandler(this, classType); } /** * @deprecated use forType(Class).propertyHandler() instead */ public BuilderConfig addPropertyHandler(Class<?> type, IPropertyHandler handler) { forType(type).propertyHandler(handler); return this; } /** * @deprecated user TypeDefinition.getPropertyHandler(BuilderConfig,Class,String) instead */ public IPropertyHandler getPropertyHandler(Class<?> classType, String key) { return TypeDefinition.getPropertyHandler(this, classType, key); } /** * Defines a control prefix (e.g. "btn", "cbx") which will be associated with a particular type. * This can allow types to be instantiated automatically based on name (e.g. "btnOK" could create * a JButton named "btnOK" with onAction=ok wired in automatically) * @param prefix * @param clazz */ public void prefix(String prefix, Class<?> clazz) { prefix(prefix,clazz,null); } /** * Defines a control prefix (e.g. "btn", "cbx") which will be associated with a particular type. * This can allow types to be instantiated automatically based on name (e.g. "btnOK" could create * a JButton named "btnOK" with onAction=ok wired in automatically) * @param prefix * @param clazz * @param defaults A map of default values. Allowed placeholders for values: PrefixControlDefinition.SUFFIX_LABEL,PrefixControlDefinition.SUFFIX_PASCAL_CASE * @see PrefixControlDefinition */ public void prefix(String prefix, Class<?> clazz, Map<String,String> defaults) { PrefixControlDefinition def = new PrefixControlDefinition(); def.setType(clazz); def.setDefaults(defaults); prefixes.put(prefix, def); } /** * @param prefix Control prefix * @return Control associated with prefix, null if not found */ public PrefixControlDefinition getPrefix(String prefix) { return prefixes.get(prefix); } /** * Defines a control prototype via a YAML snippet. When a control with the name specified in YAML * is encountered in the MigLayout section (or equivalent), it will be auto-created using * this snippet * @return */ public String prototype(String yaml) { Matcher m = nameExtractor.matcher(yaml); if (m.find() && m.groupCount() >= 1) { String name = m.group(1); prototypes.put(name, yaml); return name; } else { throw new RuntimeException("Unable to extract 'name' property from YAML: " + yaml); } } /** * Defines multiple control prototypes in one call */ public void prototypes(String... yamls) { for(String yaml : yamls) { prototype(yaml); } } /** * @param name Control name * @return YAML snippet associated with the prototype control name or null if not found */ public String getPrototype(String name) { return prototypes.get(name); } /** * @return the default extension for YAML files */ public String getYamlExtension() { return yamlExtension; } /** * @param yamlExtension the default extension for YAML files */ public void setYamlExtension(String yamlExtension) { this.yamlExtension = yamlExtension; } }