All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.javabuilders.BuilderConfig Maven / Gradle / Ivy

The newest version!
/**
 * 
 */
package org.javabuilders;

import java.beans.PropertyChangeSupport;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Logger;

import org.apache.commons.beanutils.PropertyUtils;
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.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;

/**
 * Represents the configuration for a builder (e.g. Swing vs SWT, etc)
 * @author Jacek Furmankiewicz
 */
public class BuilderConfig {

	@SuppressWarnings("unused")
	private static Logger logger = Logger.getLogger(BuilderConfig.class.getSimpleName());
	
	private static ITypeHandler defaultTypeHandler = new DefaultTypeHandler();
	private 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}"
	private static Set bundles = new LinkedHashSet();

	
	/**
	 * 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,Map>  propertyHandlers = new HashMap,Map>();
	
	protected Map,Map> getPropertyHandlers() {
		return propertyHandlers;
	}
	private Map,ITypeHandler> typeHandlers = new HashMap,ITypeHandler>();
	
	private Map,TypeDefinition> typeDefinitions = new HashMap, TypeDefinition>();
	private Map> typeAliases = new HashMap>();
	
	private Map,String> namedObjectCriteria = new HashMap,String>();
	
	private boolean markInvalidResourceBundleKeys = true;

	//internal cache used to avoid re-creating the hierarchy of type definitions with every request
	//loaded lazily, upon demand
	private Map,Set> typeDefinitionsForClassCache = new HashMap, Set>();
	
	private IBackgroundProcessingHandler backgroundProcessingHandler = null;
	private IValidationMessageHandler validationMessageHandler = null;
	
	private Map> customCommands = new HashMap>();
	
	private Set buildListeners = new LinkedHashSet();
	
	private Map customProperties = new HashMap();
	
	private Map globals = new HashMap();
	
	/**
	 * Constructor
	 * @param backgroundProcessingHandler Domain-specific background processing handler
	 */
	public BuilderConfig(IBackgroundProcessingHandler backgroundProcessingHandler, 
			ITypeHandler bindingTypeHandler,
			IValidationMessageHandler validationMessageHandler, ICustomCommand confirmCommand) {
		this.backgroundProcessingHandler = backgroundProcessingHandler;
		this.validationMessageHandler = validationMessageHandler;

		customCommands.put(Builder.CONFIRM_CUSTOM_COMMAND, confirmCommand);
		
		addType(Builder.BIND, BuilderBindings.class);
		addType(Builder.VALIDATE, BuilderValidators.class);

		if (bindingTypeHandler != null) {
			addTypeHandler(bindingTypeHandler);
		}
		addTypeHandler(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() {
			public Boolean process(BuildResult result, Object source) {
				return result.validate();
			}
		});
	}
	
	/**
	 * Registers a handler for one or more key/value pairs for a particular class type (including all of its descendants)
	 * @param handler Property handler
	 * @return Current instance, for use in builder pattern
	 */
	public BuilderConfig addPropertyHandler(IPropertyHandler handler) {
		if (handler == null) {
			throw new NullPointerException("handler cannot be null");
		}
		if (handler.getApplicableClass() == null) {
			throw new NullPointerException("IPropertyHandler.getApplicableClass() cannot be null");
		}
		//register the handler for each of the keys it is supposed to consume
		for(String key : handler.getConsumedKeys()) {
			Map handlers = propertyHandlers.get(handler.getApplicableClass());
			if (handlers == null) {
				handlers = new HashMap();
				propertyHandlers.put(handler.getApplicableClass(), handlers);
			}
			handlers.put(key, handler);
		}
		return this;
	}
	
	/**
	 * Adds a type handler for a particular class and all of its children
	 * @param typeHandler Type handler
	 * @return Current instance, for use in Builder pattern
	 */
	public BuilderConfig addTypeHandler(ITypeHandler typeHandler) {
		if (typeHandler == null) {
			throw new NullPointerException("typeHandler cannot be null");
		}		
		if (typeHandler.getApplicableClass() == null) {
			throw new NullPointerException("ITypeHandler.getApplicableClass() cannot be null");
		}		
		typeHandlers.put(typeHandler.getApplicableClass(),typeHandler);
		return this;
	}
	
	/**
	 * Defines metadata about a type, e.g.
* * 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. * * addType(JFrame.class, JButton.class); * * which corresponds to the type names in the builder file, e.g. * * JFrame: * name: myFrame * title: My Frame * JButton: * name: myButton * text: My Button * * @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. * * addType(JFrame.class); * addType(JButton.class); * * which corresponds to the type names in the builder file, e.g. * * JFrame: * name: myFrame * title: My Frame * JButton: * name: myButton * text: My Button * * @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. * * addType("JFrame",JFrame.class); * addType("JButton",JButton.class); * * which corresponds to the type names in the builder file, e.g. * * JFrame: * name: myFrame * title: My Frame * JButton: * name: myButton * text: My Button * * @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; } /** * Gets all the defined type definitions, by class * @return */ public Collection 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 getTypeDefinitions(Class classType) { if (classType == null) { throw new NullPointerException("classType cannot be null"); } Set defs = null; if (typeDefinitionsForClassCache.containsKey(classType)) { defs = typeDefinitionsForClassCache.get(classType); } else { //first request - lazy creation defs = new TreeSet(new TypeDefinitionClassHierarchyComparator()); Class superClass = classType; while (superClass != null) { if (typeDefinitions.containsKey(superClass)) { defs.add(typeDefinitions.get(superClass)); } superClass = superClass.getSuperclass(); } typeDefinitionsForClassCache.put(classType, defs); } return defs; } /** * Gets the type handler for a specific type alias * @param classType Class type * @return Type handler */ public ITypeHandler getTypeHandler(Class classType) { if (classType == null) { throw new NullPointerException("classType cannot be null"); } //this alias is a type, not just a property ITypeHandler typeHandler = defaultTypeHandler; Class parentType = classType; //go from the bottom up the hierarchy tree...the lowest //type handler wins while (parentType != null) { if (typeHandlers.containsKey(parentType)) { typeHandler = typeHandlers.get(parentType); break; } parentType = parentType.getSuperclass(); } return typeHandler; } /** * Returns the handler for a specific property * @param classType The type of the object being processed * @param key The name of the property * @return The handler for that property (and potentially others, if it consumes multiple ones) */ public IPropertyHandler getPropertyHandler(Class classType, String key) { IPropertyHandler handler = defaultPropertyHandler; Class parentClass = classType; //find the first applicable property handler in the object hierarchy, //unless one is found that handles ALL properties while (parentClass != null) { Map handlers = getPropertyHandlers().get(parentClass); if (handlers != null && handlers.containsKey(key)) { handler = handlers.get(key); break; } parentClass = parentClass.getSuperclass(); } return handler; } /** * 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) */ Class getClassType(String alias) { Class classType = typeAliases.get(alias); return classType; } /** * Indicates if an object instance should be treated as a unique * named objects and added to the list of named objects in the * BuildResult * @param instance * @return true if named, false if not */ public boolean isNamedObject(Object instance) { boolean isNamed = false; for(Class classType : namedObjectCriteria.keySet()) { if (classType.isInstance(instance)) { isNamed = true; break; } } return isNamed; } /** * Checks the raw data (before an object has been even handled) to extract its name, * if it has been specified * @param currentType Class type * @param data Raw parses data * @return Name or null if not found */ public String getNameIfAvailable(Class currentType, Map data) { String name = null; for(Class classType : namedObjectCriteria.keySet()) { if (classType.isAssignableFrom(currentType)) { String propertyName = namedObjectCriteria.get(classType); name = (String) data.get(propertyName); break; } } 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; for(Class classType : namedObjectCriteria.keySet()) { if (classType.isInstance(instance)) { String nameProperty = namedObjectCriteria.get(classType); try { //Issue #20 - fix for null names Object value = PropertyUtils.getProperty(instance,nameProperty); if (value == null) { name = null; } else { name = String.valueOf(value); } } catch (Exception e) { throw new ConfigurationException("Invalid named objects configuration",e); } } } return name; } /** * Specifies that any object of this type with this property should be * added to the list of named objects in the BuildResult, using the * value of the specified property as the name * @param classType Class type * @param nameProperty Property name */ public void addNamedObjectCriteria(Class classType, String nameProperty) { namedObjectCriteria.put(classType, nameProperty); } /** * 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 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 */ Map> getCustomCommands() { return customCommands; } /** * @return Global resource bundles */ public Set 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[0]); } /** * Gets the collection of custom properties that allow to store any additional domain-specific settings * @return the customProperties */ public Map 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; } }