/**
*
*/
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.regex.Matcher;
import java.util.regex.Pattern;
import org.javabuilders.annotations.BuildFile;
import org.javabuilders.event.BuildEvent;
import org.javabuilders.event.BuildListener;
import org.javabuilders.exception.InvalidFormatException;
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.javabuilders.util.BuilderUtils;
import org.javabuilders.util.ChildrenCardinalityUtils;
import org.javabuilders.util.PropertyUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yaml.snakeyaml.Yaml;
/**
* Ancestor for all builders
* @author Jacek Furmankiewicz
*/
public class Builder {
private final static Logger logger = LoggerFactory.getLogger(Builder.class);
private static final Pattern prefixNameSplitter = Pattern.compile("([a-z]+)([A-Z0-9]+[a-zA-Z0-9]*)");
public final static Map<String,?> EMPTY_PROPERTIES = null;
public final static String NAME = "name";
/**
* 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;
public static final String RESOURCE_BUNDLE = "org/javabuilders/Resources";
/**
* 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";
/**
* String boolean true
*/
public final static String BOOLEAN_TRUE = "true";
public final static String INTERNAL_FIELD_PREFIX = "__";
public final static String PROTOTYPE_FIELD_PREFIX = "$";
/**
* 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<String, ?> customProperties,
ResourceBundle...resourceBundles) {
Class<?> type = caller.getClass();
String fileName = null;
//@BuildFile annotation overrides any convention over configuration
if (type.isAnnotationPresent(BuildFile.class)) {
fileName = type.getAnnotation(BuildFile.class).value();
} else {
Class<?> declaringType = type.getDeclaringClass();
if (declaringType == null) {
fileName = type.getSimpleName() + config.getYamlExtension();
} else {
//build a nested name from the class hierarchy
StringBuilder bld = new StringBuilder(type.getSimpleName());
while (declaringType != null) {
bld.insert(0, declaringType.getSimpleName());
bld.insert(declaringType.getSimpleName().length(),".");
declaringType = declaringType.getDeclaringClass();
}
bld.append(config.getYamlExtension());
fileName = bld.toString();
}
}
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, null, 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
*/
public static BuildResult build(BuilderConfig config,Object caller, String fileName,
Map<String,?> customProperties, ResourceBundle...resourceBundles) {
if (caller == null) {
throw new NullPointerException("Caller cannot be null or empty");
}
//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}.\nMaybe you are using an older extension (.yaml) " +
" and need to change it via config.setYamlExtension(String) in your main().\n" +
"The default was changed to ''.yml'' as of version 1.1 to be compatible with the YAML standard.", 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);
}
return buildFromString(config, caller, bld.toString(), fileName, customProperties, 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
*/
public static BuildResult buildFromString(BuilderConfig config,Object caller, String yamlContent, ResourceBundle...resourceBundles) {
return buildFromString(config,caller, yamlContent,null,null,resourceBundles);
}
/**
* Builds from YAML string
* @param config
* @param caller
* @param yamlContent
* @param customProperties
* @param resourceBundles
* @return
*/
public static BuildResult buildFromString(BuilderConfig config,Object caller, String yamlContent, Map<String,?> customProperties,
ResourceBundle...resourceBundles) {
return buildFromString(config, caller, yamlContent, null, customProperties, 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 fileName File name (null means no actual file, just raw string content)
* @return Build result
* @throws IOException
* @throws BuildException
*/
@SuppressWarnings("unchecked")
public static BuildResult buildFromString(BuilderConfig config,Object caller, String yamlContent, String fileName, Map<String,?> 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));
}
}
BuilderUtils.validateYamlContent(yamlContent, fileName);
Yaml yaml = new Yaml();
Object document = yaml.load(yamlContent);
executeBuild(document, config, process);
return process.getBuildResult();
}
/**
* Builds a single control within the context of an already running build process.
* Used for auto-building from control name
* @return
*/
public static Object buildControlFromName(BuildProcess process, Node parent, String name) {
if (logger.isDebugEnabled()) {
logger.debug("Build control from name: {}, name");
}
Class<?> clazz = null;
String yamlContent = null;
//first check if it is a globally defined control
if (name.startsWith(PROTOTYPE_FIELD_PREFIX)) {
yamlContent = process.getConfig().getPrototype(name.substring(1));
if (yamlContent != null) {
yamlContent = yamlContent.trim();
String key = BuilderUtils.getRealKey(yamlContent);
clazz = process.getConfig().getClassType(key);
if (clazz != null) {
Map<String,Object> map = new HashMap<String, Object>();
BuilderUtils.uncompressYaml(yamlContent, map);
processDocumentNode(process.getConfig(), process, parent, clazz.getSimpleName(), map);
return process.getByName(name.substring(1));
//massage the YAML to be in the same format as for default controls
//yaml= yaml.replace(key, "");
//yaml = yaml.substring(1,yaml.length() - 1); //trim the first and last parentheses
} else {
throw new BuildException("Unable to find type for protype {0}",yamlContent);
}
} else {
throw new BuildException("Unable to find prototype definition for {0}",name);
}
} else {
//not global - may be prefix based
Matcher m = prefixNameSplitter.matcher(name);
if (m.find() && m.groupCount() == 2) {
String prefix = m.group(1);
String suffix = m.group(2);
PrefixControlDefinition def = process.getConfig().getPrefix(prefix);
if (def != null) {
clazz = def.getType();
yamlContent = def.getDefaultsAsYaml(process, name, suffix);
} else {
throw new BuildException("Unable to find type for prefix {0} for {1}",prefix,name);
}
} else {
throw new BuildException("Unable to parse prefix and suffix from control name: {0}",name);
}
}
//now that we have the YAML, let's process it as if it was part of the actual build file
Yaml yaml = new Yaml();
Object rawDocumentNode = yaml.load(yamlContent);
processDocumentNode(process.getConfig(), process, parent, clazz.getSimpleName(), rawDocumentNode);
return process.getByName(name);
}
/**
* 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);
process.setDocument(document);
if (logger.isDebugEnabled()) {
logger.debug("Building from YAML document:\n{}",document);
}
if (document instanceof Map) {
Map<String,Object> map = (Map<String,Object>)document;
//system nodes need to be processed after the regular builder nodes
Set<String> systemNodes = new LinkedHashSet<String>();
systemNodes.add(BIND);
systemNodes.add(VALIDATE);
//control the order in which the keys get executed
List<String> priorities = new LinkedList<String>();
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.isDebugEnabled()) {
logger.debug("Processing root node: {}",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<String,Object> content = new HashMap<String,Object>();
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<String,Object> value = new HashMap<String,Object>();
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
* Called either from the Builder or from AutoBuilder
* @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.isDebugEnabled()) {
logger.debug("Started creating object defined by alias: {}",currentKey);
}
Map<String,Object> data = (Map<String,Object>)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){
//cannot be root of a document - issue 79
if (parent == null) {
throw new BuildException("Yaml cannot start with a List as root: {0}",rawDocumentNode);
}
//collection of objects or values
@SuppressWarnings("rawtypes")
List items = (List)rawDocumentNode;
Class<?> type = BuilderUtils.getClassFromAlias(process, currentKey, null);
//create child nodes only for types, not for properties that are lists (e.g. event listeners with multiple events)
if (Builder.CONTENT.equals(currentKey) || type != null) {
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<String,Object> itemMap = (Map<String,Object>)item;
for(String itemKey : itemMap.keySet()) {
Object itemValue = itemMap.get(itemKey);
processDocumentNode(config, process, itemsNode, itemKey, itemValue);
treatListAsPropertyValue = false;
}
}
}
//TODO: evaluate if this code can be removed
if (treatListAsPropertyValue) {
handleProperty(config, process, parent, currentKey);
}
} else {
//propertie as lists (issue #47)
handleProperty(config, process, parent, currentKey);
}
} else {
if (parent != null) {
//PROPERTY VALUE
handleProperty(config, process, parent, currentKey);
} else {
throw new InvalidFormatException("Unable to process document node : {0}", rawDocumentNode);
}
}
}
public static Object createControlFromCompressedYaml(BuildProcess process, Node parent, String compressedYaml) {
Map<String,Object> data = new HashMap<String, Object>();
BuilderUtils.uncompressYaml(compressedYaml,data);
String key = BuilderUtils.getRealKey(compressedYaml);
Class<?> classType = BuilderUtils.getClassFromAlias(process, key, null);
return handleType(process.getConfig(),process,parent,key,data,classType).getMainObject();
}
//handles creating types
private static Node handleType(BuilderConfig config, BuildProcess process, Node parent, String currentKey, Map<String,Object> 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 = TypeDefinition.getTypeHandler(config, 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 null;
} 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<TypeDefinition> typeDefinitions = config.getTypeDefinitions(createdClassType);
Set<String> ignored = TypeDefinition.getIgnored(config, current.getMainObject().getClass());
//after creation processors
List<ITypeHandlerAfterCreationProcessor> 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 {
Object target = parent.getMainObject();
Object argument = current.getMainObject();
method.invoke(target, argument);
} catch (Exception e) {
throw new BuildException(e,"Unable to call {0} on {1} with type {2}. Error: {3}",
method, parentClass.getSimpleName(), createdClassType.getSimpleName(),e.getMessage());
}
}
}
if (!(typeHandler instanceof ITypeChildrenHandler)) {
Map<Integer,List<String>> delayedKeysByWeight = new TreeMap<Integer,List<String>>();
//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.isDebugEnabled()) {
logger.debug("Processing child key: {}", childKey);
}
//handle potentially internationalized resources
if (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 = TypeDefinition.getTypeHandler(config, 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) && !isInternal(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<String> keys = delayedKeysByWeight.get(delayWeight);
for(String delayedKey : keys) {
Object delayedValue = data.get(delayedKey);
if (logger.isDebugEnabled()) {
logger.debug("Processing delayed weight: {} / {}", delayWeight, delayedKey);
}
processDocumentNode(config,process,current,delayedKey, delayedValue);
}
}
//check cardinality
ChildrenCardinalityUtils.checkChildrenCardinality(config,current);
}
//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<ITypeHandlerFinishProcessor> 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?
String name = config.getNameIfAvailable(data);
if (name != null) {
process.addNamedObject(name, current.getMainObject());
}
if (logger.isDebugEnabled()) {
logger.debug("Finished creating object defined by alias: {}", currentKey);
}
return current;
}
//helper method - all keys prefixed with "__" are treated as internal use and ignored
private static boolean isInternal(String childKey) {
return childKey.startsWith(INTERNAL_FIELD_PREFIX);
}
//common logic to populate the list of delayed keys
private static void addToDelayedKeys(Map<Integer,List<String>> delayedKeysByWeight,
Integer delayWeight, String key) {
List<String> delayedKeys = delayedKeysByWeight.get(delayWeight);
if (delayedKeys == null) {
delayedKeys = new LinkedList<String>();
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 = TypeDefinition.getPropertyHandler(config, parent.getMainObject().getClass(),currentKey);
//debug info
if (logger.isDebugEnabled()) {
if (handler.getConsumedKeys().size() == 0) {
logger.debug("Handling property '{}' of value '{}' for type alias '{}'", new Object[]{currentKey,
parent.getProperties().get(currentKey), parent.getKey()});
} else {
for(String consumedKey : handler.getConsumedKeys()) {
if (parent.getProperties().containsKey(consumedKey)) {
logger.debug("Handling property '{}' of value '{}' for type alias '{}'",
new Object[]{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<Object> valueList = new ArrayList<Object>();
valueList.add(value);
parent.getProperties().put(consumedKey, valueList);
if (logger.isDebugEnabled()) {
logger.debug("Converted single value {} to a single item list for property ", consumedKey);
}
}
}
}
}
//handling regular properties that are types and have specialized "type as value" handlers
try {
if (handler instanceof ITypeAsValueSupport && PropertyUtils.isValid(parent.getMainObject(), currentKey)) {
Class<?> propertyType = PropertyUtils.getPropertyType(parent.getMainObject(),currentKey);
ITypeAsValueHandler<? extends Object> asValueHandler = TypeDefinition.getTypeAsValueHandler(config, propertyType);
if (asValueHandler != null) {
String sValue = parent.getStringProperty(currentKey);
if (sValue.matches(asValueHandler.getRegex())) {
//replace the value from the file with the corresponding object instance
parent.getProperties().put(currentKey, asValueHandler.getValue(process, parent, currentKey,
parent.getProperty(currentKey)));
} else {
throw new BuildException("Invalid {0} value \"{1}\" for {2}.{3}. Must in be in \"{4}\" format, e.g. \"{5}\"",
propertyType.getSimpleName(), sValue, parent.getMainObject().getClass().getSimpleName(),
currentKey, asValueHandler.getRegex(), asValueHandler.getInputValueSample());
}
}
}
} catch (Exception e) {
throw new BuildException(e,"Unable to process property {0}.{1} : {2}",
parent.getMainObject().getClass().getSimpleName(),currentKey, e.getMessage());
}
validateProperty(handler,config, process, parent, currentKey);
handler.handle(config, process, parent, currentKey);
//tell the parent what keys have been handled
parent.getConsumedKeys().add(currentKey); //required for the default type handler to work, since it handles all undefined properties
for(String key : handler.getConsumedKeys()) {
parent.getConsumedKeys().add(key);
}
}
}
//performs property value validation
@SuppressWarnings({ "unchecked", "rawtypes" })
private static void validateProperty(IPropertyHandler handler, BuilderConfig config, BuildProcess result,
Node parent, String currentKey) throws BuildException {
//allowed values
if (handler instanceof IAllowedValues) {
String value = parent.getStringProperty(currentKey);
IAllowedValues list = (IAllowedValues)handler;
if (!list.getAllowedValues().contains(value)) {
//display detailed error message
throw new InvalidPropertyValueException(parent.getKey(),currentKey, value, list.getAllowedValues());
}
}
//valid formats
if (handler instanceof IAllowedPropertyFormat) {
IAllowedPropertyFormat format = (IAllowedPropertyFormat)handler;
//check all properties that the handler consumes
for(String consumedKey : handler.getConsumedKeys()) {
if (parent.containsProperty(consumedKey)) {
String value = parent.getStringProperty(consumedKey);
if (!value.matches(format.getRegexPattern(currentKey))) {
throw new InvalidPropertyValueException(parent.getKey(),currentKey, value,
format.getRegexPattern(currentKey), format.getValidSample(currentKey));
}
}
}
}
//valid combinations
if (handler instanceof IAllowedPropertyCombinations) {
IAllowedPropertyCombinations combination = (IAllowedPropertyCombinations)handler;
if (!combination.getAllowedCombinations().isValid(parent.getProperties().keySet())) {
throw new BuildException("Invalid combination of properties. Valid are: " +
combination.getAllowedCombinations());
}
}
//a list?
if (handler instanceof IPropertyList) {
IPropertyList listHandler = (IPropertyList)handler;
//we must match the list of values to the value list definitions
//check all properties that the handler consumes
for(String consumedKey : handler.getConsumedKeys()) {
if (parent.containsProperty(consumedKey) && listHandler.isList(consumedKey)) {
List<Object> values = (List<Object>)parent.getProperties().get(consumedKey);
//match to a value list definition
Values valueList = null;
for (ValueListDefinition vlDef : listHandler.getValueListDefinitions(consumedKey)) {
if (vlDef.isExactMatch(values)) {
valueList = new Values(vlDef);
vlDef.validateValues(values, result, parent,valueList);
if (!valueList.isValid()) {
throw new BuildException(valueList.getErrors());
}
break;
}
}
if (valueList == null) {
throw new BuildException(String.format("Values '%s' did not match to any defined value list definition for property '%s'",
values, consumedKey));
} else {
//all good - replace the original value with the full
//ValueList
parent.getProperties().put(consumedKey, valueList);
}
}
}
}
}
/**
* Validates if the current data is correct.
* @return Error message (empty means no errors, all good)
* @throws InvalidParentTypeException
* @throws MissingRequiredPropertyException
* @throws MissingRequiredTypeException
*/
private static void validate(BuilderConfig config, BuildProcess process, Node parent, String currentKey, Map<String,Object> currentProperties, Class<?> classType) throws InvalidParentTypeException, MissingRequiredPropertyException, MissingRequiredTypeException {
//check for allowed parent
if (parent != null && !TypeDefinition.isParentAllowed(parent, config.getTypeDefinitions(classType))) {
throw new InvalidParentTypeException(classType,parent.getMainObject().getClass(),
TypeDefinition.getAllowedParents(config, classType));
}
//check for required properties
for(String requiredKey : TypeDefinition.getRequiredKeys(config, classType)) {
if (!currentProperties.containsKey(requiredKey)) {
throw new MissingRequiredPropertyException(currentKey,
requiredKey, currentProperties);
}
}
//check for required types
for(Class<?> requiredType : TypeDefinition.getRequiredTypes(config, classType)) {
boolean found = false;
keySearch:
for(String key : currentProperties.keySet()) {
if (currentProperties.get(key) instanceof Map) {
//dealing with a type
Class<?> type = BuilderUtils.getClassFromAlias(process, key, null);
if (type != null && requiredType.isAssignableFrom(type)) {
found = true;
break keySearch;
}
}
}
if (!found) {
throw new MissingRequiredTypeException(currentKey,requiredType,currentProperties);
}
}
}
/**
* Subroutine to handle adding of default values, in case they are missing
*/
private static void handleDefaults(BuilderConfig config, BuildProcess result, Node parent, String currentKey, Map<String,Object> currentProperties, Class<?> classType) {
Map<String,Object> defaults = TypeDefinition.getDefaults(config, classType);
for(String key : defaults.keySet()) {
if (!currentProperties.containsKey(key)) {
currentProperties.put(key, defaults.get(key));
}
}
}
}