org.javabuilders.Builder Maven / Gradle / Ivy
The newest version!
/**
*
*/
package org.javabuilders;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.beanutils.PropertyUtils;
import org.javabuilders.event.BuildEvent;
import org.javabuilders.event.BuildListener;
import org.javabuilders.handler.GlobalVariablePropertyHandler;
import org.javabuilders.handler.IPropertyHandler;
import org.javabuilders.handler.ITypeAsValueHandler;
import org.javabuilders.handler.ITypeChildrenHandler;
import org.javabuilders.handler.ITypeHandler;
import org.javabuilders.handler.ITypeHandlerAfterCreationProcessor;
import org.javabuilders.handler.ITypeHandlerFinishProcessor;
import org.jvyaml.YAML;
/**
* Ancestor for all builders
* @author Jacek Furmankiewicz
*/
public class Builder {
private final static Logger logger = Logger.getLogger(Builder.class.getSimpleName());
public final static Map EMPTY_PROPERTIES = new HashMap();
/**
* The standard property names used throughout the builder types
*/
public final static String CONTENT = "content";
/**
* The standard property name used for layout constraints
*/
public final static String CONSTRAINTS = "constraints";
/**
* The standard property name used to indicate a binding node
*/
public final static String BIND = "bind";
/**
* The standard property name used to indicate a validation node
*/
public final static String VALIDATE = "validate";
/**
* The standard property name used for storing a single value that represents an entire object
*/
public final static String VALUE = "value";
/**
* The standard property name used for visually laying out components in a YAML file
*/
public final static String LAYOUT = "layout";
/**
* The standard property name used to indicate on onAction event
*/
public final static String ON_ACTION = "onAction";
/**
* The standard property name used to indicate on onFocus event
*/
public final static String ON_FOCUS = "onFocus";
/**
* The standard property name used to indicate on onFocusLost event
*/
public final static String ON_FOCUS_LOST = "onFocusLost";
/**
* Reserved keyword used to indicate the caller : "this"
*/
public final static String THIS = "this";
/**
* Prefix for named objects($), e.g. "$$saveButton"
*/
public final static String NAMED_OBJECT_REGEX = "\\$\\{[a-zA-Z0-9]+\\}";
private final static int NAMED_OBJECT_PREFIX_LENGTH = 2;
private final static int NAMED_OBJECT_SUFFIX_LENGTH = 1;
/**
* Name of the $validate custom command
*/
public final static String VALIDATE_CUSTOM_COMMAND = "$validate";
/**
* Name of the $confirm custom command
*/
public final static String CONFIRM_CUSTOM_COMMAND = "$confirm";
/**
* String boolean false
*/
public final static String BOOLEAN_FALSE = "false";
/**
* @return Standard JavaBuilders resource bundle
*/
public static ResourceBundle getResourceBundle() {
ResourceBundle bundle = ResourceBundle.getBundle("org/javabuilders/Resources");
return bundle;
}
/**
* Builds assuming the root object has already been instantiated
* (e.g. for loading from within the constructor of an object and creating
* it dynamically at that time)
* @param caller The caller
* @return Build result
* @throws IOException
*/
public static BuildResult build(BuilderConfig config, Object caller, ResourceBundle...resourceBundles) {
return build(config, caller, EMPTY_PROPERTIES, resourceBundles);
}
/**
* Builds assuming the root object has already been instantiated
* (e.g. for loading from within the constructor of an object and creating
* it dynamically at that time)
* @param caller The caller
* @param customProperties Optional custom properties
* @return Build result
* @throws IOException
*/
public static BuildResult build(BuilderConfig config,Object caller, Map customProperties,
ResourceBundle...resourceBundles) {
String fileName = caller.getClass().getSimpleName() + ".yaml";
BuildResult result;
result = build(config, caller, fileName, customProperties, resourceBundles);
return result;
}
/**
* Builds assuming the root object has already been instantiated
* (e.g. for loading from within the constructor of an object and creating
* it dynamically at that time)
* @param caller The caller
* @return Build result
* @throws IOException
* @throws BuildException
*/
public static BuildResult build(BuilderConfig config,Object caller, String fileName, ResourceBundle...resourceBundles) {
return build(config, caller, fileName, EMPTY_PROPERTIES, resourceBundles);
}
/**
* Builds assuming the root object has already been instantiated
* (e.g. for loading from within the constructor of an object and creating
* it dynamically at that time)
* @param caller The caller
* @return Build result
* @throws IOException
* @throws BuildException
*/
@SuppressWarnings("unchecked")
public static BuildResult build(BuilderConfig config,Object caller, String fileName,
Map customProperties, ResourceBundle...resourceBundles) {
if (caller == null) {
throw new NullPointerException("Caller cannot be null or empty");
}
BuildProcess process = new BuildProcess(config, caller, resourceBundles);
//apply custom properties
if (customProperties != null) {
for(String key : customProperties.keySet()) {
process.getBuildResult().getProperties().put(key, customProperties.get(key));
}
}
//handle case when during development time the YAML content is loaded from the IDEs /src folder
//instead of the app's /bin folder (allows hot-deploy of YAML without restarting the whole app)
//needs to be ignored for the builders' built-in dialogs (e.g. BackgroundDialog)
InputStream input = null;
if (BuilderConfig.getDevSourceFolder() == null ||
caller.getClass().getPackage().getName().startsWith(Builder.class.getPackage().getName())) {
input = caller.getClass().getResourceAsStream(fileName);
} else {
//during dev time overriden source of YAML files
String yamlFileName = fileName;
try {
URI bin = caller.getClass().getProtectionDomain().getCodeSource().getLocation().toURI();
URI src = bin.resolve(new URI(BuilderConfig.getDevSourceFolder() +
"/" +
caller.getClass().getPackage().getName().replace(".", "/") +
"/" + fileName));
yamlFileName = src.toString();
input = new FileInputStream(new File(src));
} catch (Exception e) {
throw new BuildException(e,"Unable to process file {0}.\n{1}",yamlFileName, e);
}
}
//check for missing file (Issue #20)
if (input == null) {
throw new BuildException("No YAML file found: {0}", fileName);
}
//read in string manually so that we can pre-validate it for invalid characters
StringBuilder bld = new StringBuilder();
try {
BufferedReader rdr = new BufferedReader(new InputStreamReader(input));
String line = rdr.readLine();
while( line != null) {
bld.append(line).append("\n");
line = rdr.readLine();
}
rdr.close();
} catch (IOException ex) {
throw new BuildException(ex);
}
Object document = YAML.load(bld.toString());
executeBuild(document, config, process);
return process.getBuildResult();
}
/**
* Builds assuming the root object has already been instantiated
* (e.g. for loading from within the constructor of an object and creating
* it dynamically at that time)
* @param caller The caller
* @return Build result
* @throws IOException
* @throws BuildException
*/
public static BuildResult buildFromString(BuilderConfig config,Object caller, String yamlContent, ResourceBundle...resourceBundles) {
if (caller == null) {
throw new NullPointerException("Caller cannot be null or empty");
}
BuilderUtils.validateYamlContent(yamlContent);
BuildProcess result = new BuildProcess(config, caller, resourceBundles);
Object document = YAML.load(yamlContent);
executeBuild(document, config, result);
return result.getBuildResult();
}
/**
* Actual building logic happens here
* @throws BuildException
*/
@SuppressWarnings("unchecked")
private static void executeBuild(Object document, BuilderConfig config, BuildProcess process) throws BuildException {
//build started events
BuildListener[] listeners = config.getBuildListeners();
BuildEvent buildEvent = new BuildEvent(process.getCaller(),process.getBuildResult());
if (listeners.length > 0) {
for (BuildListener listener : listeners) {
listener.buildStarted(buildEvent);
}
}
//pre-processing
document = BuilderPreProcessor.preprocess(config, process, document, null);
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "Building from YAML document:\n%s",document);
}
if (document instanceof Map) {
Map map = (Map)document;
//system nodes need to be processed after the regular builder nodes
Set systemNodes = new LinkedHashSet();
systemNodes.add(BIND);
systemNodes.add(VALIDATE);
//control the order in which the keys get executed
List priorities = new LinkedList();
for(String key : map.keySet()) {
//make sure the 'system' nodes get executed last
if (!systemNodes.contains(key)) {
priorities.add(key);
}
}
for (String systemNode : systemNodes) {
if (map.containsKey(systemNode)) {
priorities.add(systemNode);
}
}
for(String key : priorities) {
Object docNode = map.get(key);
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE,"Processing root node: %s",key);
}
//handle the different potential formats of the root nodes
if (docNode instanceof Map) {
processDocumentNode(config,process, null, key,docNode);
} else if (docNode instanceof List) {
//root nodes that come in as collection automatically get moved to "content"
Map content = new HashMap();
content.put(Builder.CONTENT, docNode);
docNode = content;
processDocumentNode(config,process, null, key,docNode);
} else if (docNode instanceof String) {
//root nodes that come in as string automatically get moved to "value"
Map value = new HashMap();
value.put(Builder.VALUE, docNode);
docNode = value;
processDocumentNode(config,process, null, key,docNode);
} else {
throw new BuildException("Unable to handle the root node :" + key);
}
//handle the properties as named objects, once the regular builder node has run
if (!systemNodes.contains(key)) {
for(NamedObjectPropertyValue request : process.getPropertiesAsNamedObjects()) {
request.setReference(process);
}
process.getPropertiesAsNamedObjects().clear(); //run it only once
}
}
} else {
processDocumentNode(config, process, null, null, document);
}
//finish up the build
BuilderUtils.updateNamedObjectReferencesInCaller(process);
//fire build ended events
listeners = config.getBuildListeners();
for(BuildListener listener: listeners) {
listener.buildEnded(buildEvent);
}
}
/**
* Processes a particular document node
* @param rawDocumentNode
* @throws BuildException
*/
@SuppressWarnings("unchecked")
private static void processDocumentNode(BuilderConfig config, BuildProcess process, Node parent,
String currentKey, Object rawDocumentNode) throws BuildException {
if (rawDocumentNode instanceof Map) {
if (logger.isLoggable(Level.FINE)) {
logger.fine("Started creating object defined by alias: " + currentKey);
}
Map data = (Map)rawDocumentNode;
Class> currentType = BuilderUtils.getClassFromAlias(process, currentKey, null);
if (currentType == null) {
throw new InvalidTypeException(currentKey);
}
handleType(config, process, parent, currentKey, data, currentType);
} else if (rawDocumentNode instanceof List){
//collection of objects or values
List items = (List)rawDocumentNode;
Node itemsNode = new Node(parent,currentKey);
if (parent != null) {
parent.addChildNode(itemsNode);
//propagate parent properties to collection node - makes it faster for child nodes to get to parent's
//most important attributes
itemsNode.setMainObject(parent.getMainObject());
itemsNode.setConsumedKeys(parent.getConsumedKeys());
}
boolean treatListAsPropertyValue = true;
for(Object item : items) {
if (item instanceof Map) {
Map itemMap = (Map)item;
for(String itemKey : itemMap.keySet()) {
Object itemValue = itemMap.get(itemKey);
processDocumentNode(config, process, itemsNode, itemKey, itemValue);
treatListAsPropertyValue = false;
}
}
}
if (treatListAsPropertyValue) {
handleProperty(config, process, parent, currentKey);
}
} else {
//PROPERTY VALUE
handleProperty(config, process, parent, currentKey);
}
}
//handles creating types
private static void handleType(BuilderConfig config, BuildProcess process, Node parent, String currentKey, Map data,
Class> classType) throws BuildException {
Class> currentType = BuilderUtils.getClassFromAlias(process, currentKey, null);
if (currentType == null) {
throw new InvalidTypeException(currentKey);
}
//make sure any missing defaults are there
handleDefaults(config, process, parent, currentKey, data, currentType);
//validate the current entry - can throw exceptions in there...
validate(config, process, parent, currentKey, data, currentType);
ITypeHandler typeHandler = config.getTypeHandler(currentType);
Node current = null;
//handle passing a pre-instantiated object
Object existingInstance = BuilderUtils.getExistingInstanceIfAvailable(process.getCaller(), currentType, config, data);
if (existingInstance != null) {
//existing instance handle
current = typeHandler.useExistingInstance(config, process, parent, currentKey, data, existingInstance);
} else if (parent == null && process.getCaller() != null &&
currentType.isAssignableFrom(process.getCaller().getClass())) {
//root entry in the file
current = typeHandler.useExistingInstance(config, process, parent, currentKey, data, process.getCaller());
} else {
//new
current = typeHandler.createNewInstance(config, process, parent, currentKey, data);
}
//handle result of what the type handler returned
if (current == null) {
//handler run...but has nothing to, we can abort any further processing for this node
return;
} else if (current.getMainObject() == null) {
throw new BuildException("ITypeHandler for alias " + currentKey + " did not set Node.mainObject to a value");
}
//add all the keys that the type handler consumed
for(String typeHandlerKey : typeHandler.getConsumedKeys()) {
current.getConsumedKeys().add(typeHandlerKey);
}
//same thing for finish processor
TypeDefinition def = config.getTypeDefinition(currentType);
if (def != null) {
ITypeHandlerFinishProcessor finishProcessor = config.getTypeDefinition(currentType).getFinishProcessor();
if (finishProcessor != null && finishProcessor instanceof IKeyValueConsumer) {
IKeyValueConsumer consumer = (IKeyValueConsumer) finishProcessor;
for(String key : consumer.getConsumedKeys()) {
current.getConsumedKeys().add(key);
}
}
}
Class> createdClassType = current.getMainObject().getClass();
Set typeDefinitions = config.getTypeDefinitions(createdClassType);
Set ignored = TypeDefinition.getIgnored(config, current.getMainObject().getClass());
//after creation processors
List afterCreationProcessors = TypeDefinition.getAfterCreationProcessors(config, createdClassType);
for(ITypeHandlerAfterCreationProcessor processor : afterCreationProcessors) {
processor.afterCreation(config, process, current, currentKey, data);
}
//is the type mapped to a method on the parent?
if (parent != null) {
Class> parentClass = parent.getMainObject().getClass();
Method method = TypeDefinition.getTypeAsMethod(config, parentClass, createdClassType);
if (method != null) {
try {
method.invoke(parent.getMainObject(), current.getMainObject());
} catch (Exception e) {
throw new BuildException("Unable to call {0}.{1} with type {2}",
parentClass.getSimpleName(), method.getName(), createdClassType.getSimpleName(),e);
}
}
}
if (!(typeHandler instanceof ITypeChildrenHandler)) {
Map> delayedKeysByWeight = new TreeMap>();
//recursively process all the child nodes
for(String childKey : data.keySet()) {
Object childValue = data.get(childKey);
//skip keys that have already been processed
if (current.getConsumedKeys().contains(childKey)) {
continue;
}
if (logger.isLoggable(Level.FINE)) {
logger.fine("Processing child key: " + childKey);
}
//handle internationalization if any resource bundles have been passed
if ((config.getResourceBundles().size() > 0 || process.getResourceBundles().size() > 0) && childValue instanceof String) {
if (TypeDefinition.isLocalizableProperty(childKey, typeDefinitions)) {
data.put(childKey,process.getBuildResult().getResource(String.valueOf(childValue)));
}
}
Class> childClass = BuilderUtils.getClassFromAlias(process, childKey, null);
if (childClass != null) {
//is a type, but may need to be delayed till the end
ITypeHandler childTypeHandler = config.getTypeHandler(childClass);
Integer delayedWeight = TypeDefinition.getDelayedWeight(childTypeHandler, config.getTypeDefinitions(createdClassType));
if (childTypeHandler != null && delayedWeight > 0) {
//this key needs to be processed after all the other children
addToDelayedKeys(delayedKeysByWeight, delayedWeight, childKey);
} else {
processDocumentNode(config,process,current,childKey, childValue);
}
} else {
//a property
if (!ignored.contains(childKey)) {
Integer delayedWeight = TypeDefinition.getDelayedWeight(typeHandler, childKey,
config.getTypeDefinitions(createdClassType));
if (delayedWeight > 0) {
//this key needs to be processed after all the other children
addToDelayedKeys(delayedKeysByWeight, delayedWeight, childKey);
} else {
processDocumentNode(config,process,current,childKey, childValue);
}
}
}
}
//process the delayed children
for(Integer delayWeight : delayedKeysByWeight.keySet()) {
List keys = delayedKeysByWeight.get(delayWeight);
for(String delayedKey : keys) {
Object delayedValue = data.get(delayedKey);
if (logger.isLoggable(Level.FINE)) {
logger.fine("Processing delayed weight: " + delayWeight + " / child key: " + delayedKey);
}
processDocumentNode(config,process,current,delayedKey, delayedValue);
}
}
}
//register main object as root if we are running this at the root level
if (parent == null && current.getMainObject() != null) {
process.getBuildResult().getRoots().add(current.getMainObject());
}
//we're done handling this type - do all the post processing stuff
if (typeHandler instanceof ITypeHandlerFinishProcessor) {
ITypeHandlerFinishProcessor postHandler = (ITypeHandlerFinishProcessor)typeHandler;
postHandler.finish(config, process, current, currentKey, data);
}
List postProcessors = TypeDefinition.getFinishProcessors(config, createdClassType);
for(ITypeHandlerFinishProcessor postProcessor : postProcessors) {
postProcessor.finish(config, process, current, currentKey, data);
}
//should it be added to the list of named objects?
if (config.isNamedObject(current.getMainObject())) {
String name = config.getObjectName(current.getMainObject());
process.addNamedObject(name, current.getMainObject());
}
if (logger.isLoggable(Level.FINE)) {
logger.fine("Finished creating object defined by alias: " + currentKey);
}
}
//common logic to populate the list of delayed keys
private static void addToDelayedKeys(Map> delayedKeysByWeight,
Integer delayWeight, String key) {
List delayedKeys = delayedKeysByWeight.get(delayWeight);
if (delayedKeys == null) {
delayedKeys = new LinkedList();
delayedKeysByWeight.put(delayWeight, delayedKeys);
}
delayedKeys.add(key);
}
private static void handleProperty(BuilderConfig config, BuildProcess process, Node parent, String currentKey) throws BuildException {
//PROPERTY VALUE
if (!parent.getConsumedKeys().contains(currentKey)) { //each property should be processed only once by any handler
//does this property point to a named object?
if (parent.getProperty(currentKey) instanceof String) {
String sValue =parent.getStringProperty(currentKey);
if (sValue.matches(Builder.NAMED_OBJECT_REGEX)) {
NamedObjectPropertyValue nopValue = new NamedObjectPropertyValue(parent.getMainObject(),currentKey,
sValue.substring(NAMED_OBJECT_PREFIX_LENGTH,sValue.length() - NAMED_OBJECT_SUFFIX_LENGTH));
process.getPropertiesAsNamedObjects().add(nopValue);
parent.getConsumedKeys().add(currentKey);
return;
}
}
//does this property point to a global variable?
if (parent.getProperty(currentKey) instanceof String) {
String sValue =parent.getStringProperty(currentKey);
if (sValue.matches(BuilderConfig.GLOBAL_VARIABLE_REGEX)) {
GlobalVariablePropertyHandler.getInstance().handle(config, process, parent, currentKey);
return;
}
}
//regular property handling starts here...
IPropertyHandler handler = config.getPropertyHandler(parent.getMainObject().getClass(),currentKey);
//debug info
if (logger.isLoggable(Level.FINE)) {
if (handler.getConsumedKeys().size() == 0) {
logger.fine(String.format("Handling property '%s' of value '%s' for type alias '%s'", currentKey,
parent.getProperties().get(currentKey),
parent.getKey()));
} else {
for(String consumedKey : handler.getConsumedKeys()) {
if (parent.getProperties().containsKey(consumedKey)) {
logger.fine(String.format("Handling property '%s' of value '%s' for type alias '%s'",
consumedKey,
parent.getProperties().get(consumedKey),
parent.getKey()));
}
}
}
}
//handle properties flagged as lists
if (handler instanceof IPropertyList) {
IPropertyList propertyList = (IPropertyList)handler;
for(String consumedKey : handler.getConsumedKeys()) {
if (propertyList.isList(consumedKey)) {
//once a property has been flagged as a list, handle to make sure the value gets converted into one
Object value = parent.getProperties().get(consumedKey);
if (value != null && !(value instanceof List)) {
List