/** * */ package org.javabuilders; import static org.javabuilders.util.BuilderUtils.getRealKey; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.javabuilders.exception.UnrecognizedAliasException; import org.javabuilders.handler.ITypeChildrenHandler; import org.javabuilders.handler.ITypeHandler; import org.javabuilders.util.BuilderUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Pre-processor for YAML file to handle advanced functionality specific to Java Builders. * Namely, it handles virtual constructors embedded in the types, as well as exploding shortcut types (i.e. those entered as a single value or a list) into a proper map * @author Jacek Furmankiewicz */ public class BuilderPreProcessor { public static final Logger logger = LoggerFactory.getLogger(BuilderPreProcessor.class); public static final Map<Character,Character> listIndicators = new HashMap<Character, Character>(); static { listIndicators.put('(',')'); //obsolete-for backwards compatibility listIndicators.put('[',']'); listIndicators.put(null,null); } /** * Pre-processes the YAML object to implement advanced/enhanced JB-YAML features * @param config Config * @param current Current * @throws BuildException If anything goes wrong... */ public static Object preprocess(BuilderConfig config, BuildProcess process, Object current, Object parent) throws BuildException { try { if (current instanceof Map) { current = handleMap(config, process, current, parent); } else if (current instanceof List) { current = handleList(config, process, current, parent); } else if (current instanceof String && parent == null) { //only handle String if it is the root in a file current = handleString(config, process, current, parent); } return current; } catch (BuildException be) { logger.error(be.getMessage(),be); throw be; } catch (Exception e) { logger.error(e.getMessage(),e); throw new BuildException(e); } } @SuppressWarnings("unchecked") private static Object handleMap(BuilderConfig config, BuildProcess process, Object current, Object parent) throws BuildException { Map<String,Object> map = (Map<String,Object>)current; List<String> keysToRemove = new ArrayList<String>(); Map<String,Object> propertiesToAdd = new HashMap<String, Object>(); for(String key : map.keySet()) { Object value = map.get(key); String realKey = getRealKey(key); Class<?> typeClass = BuilderUtils.getClassFromAlias(process, realKey, null); if (typeClass != null) { //we're in business...dealing with a type here... ITypeHandler handler = TypeDefinition.getTypeHandler(config, typeClass); //handle types that are expressed as a list or single value explodeShortcutTypeToMap(config, process, handler, key, map); //if exploded, we are now sure it is a proper type Map Map<String,Object> typeMap = (Map<String, Object>) map.get(key); if (typeMap == null) { //handle case where is root line in compact syntax with no children underneath typeMap = new HashMap<String, Object>(); } if (!key.equals(realKey)) { //enhanced JB-YAML : embedded virtual constructor in the type //explode the embedded property values into stand-alone entries in the document BuilderUtils.uncompressYaml(key, typeMap); propertiesToAdd.put(realKey, typeMap); keysToRemove.add(key); } //handle aliased properties handlePropertyAlias(config,typeClass,typeMap); handleMappedProperties(config, typeClass, typeMap); //handle mapped properties /* for(String typeMapKey : typeMap.keySet()) { Object typeMapValue = typeMap.get(typeMapKey); //mapped value? Object mappedValue = TypeDefinition.getPropertyValue(config, typeClass, typeMapKey, typeMapValue); if (!mappedValue.equals(typeMapValue)) { typeMap.put(typeMapKey, mappedValue); } } */ //keep goin' down... if(!(handler instanceof ITypeChildrenHandler)) { preprocess(config, process, typeMap, current); } } else { //keep looking further down the rabbit hole... preprocess(config, process, value, current); } } //remove all the keys with virtual constructors for(String key : keysToRemove) { map.remove(key); } //add their properly exploded versions for(String key : propertiesToAdd.keySet()) { map.put(key, propertiesToAdd.get(key)); } return current; } @SuppressWarnings("unchecked") private static Object handleList(BuilderConfig config, BuildProcess process, Object current, Object parent) throws BuildException { List<Object> list = (List<Object>) current; for(int i = 0; i < list.size(); i++) { Object item = list.get(i); if (item instanceof String) { String stringItem = (String) item; String realKey = getRealKey(stringItem); if (!stringItem.equals(realKey)) { Class<?> typeClass = BuilderUtils.getClassFromAlias(process, realKey, null); if (typeClass != null) { //dealing with a virtual constructor in a list Map<String,Object> rootMap = new HashMap<String, Object>(); Map<String,Object> typeMap = new HashMap<String, Object>(); rootMap.put(realKey, typeMap); BuilderUtils.uncompressYaml(stringItem, typeMap); list.remove(i); list.add(i, rootMap); //handle aliased properties handlePropertyAlias(config,typeClass,typeMap); handleMappedProperties(config, typeClass, typeMap); } else { throw new BuildException("{0} is not a recognized alias", realKey); //preprocess(config, item, current); } } else { preprocess(config, process, item, current); } } else { preprocess(config, process, item, current); } } return current; } private static Object handleString(BuilderConfig config, BuildProcess process, Object current, Object parent) throws BuildException { //dealing with a single value - could be a virtual constructor though String value = (String)current; String realKey = getRealKey(value); if (!value.equals(realKey)) { Class<?> typeClass = BuilderUtils.getClassFromAlias(process, realKey, null); if (typeClass != null) { //dealing with a virtual constructor in a list Map<String,Object> rootMap = new HashMap<String, Object>(); Map<String,Object> typeMap = new HashMap<String, Object>(); rootMap.put(realKey, typeMap); BuilderUtils.uncompressYaml(value, typeMap); current = rootMap; preprocess(config, process, rootMap, parent); //handle aliased properties handlePropertyAlias(config,typeClass,typeMap); handleMappedProperties(config, typeClass, typeMap); } else { throw new UnrecognizedAliasException("\"{0}\" is not a recognized alias", realKey); } } else { //regular string property ...nothing to do for now } return current; } //explodes a type entered as a list or single value into a proper map public static void explodeShortcutTypeToMap(BuilderConfig config, BuildProcess process, ITypeHandler handler, String key, Map<String,Object> current) throws BuildException { Object value = current.get(key); //handle special short-hand cases where it may be shown as a list //need to "move" the list to a pre-defined property name...used for short-hand formats of type entry if (value instanceof List) { Map<String,Object> childMap = new HashMap<String,Object>(); childMap.put(handler.getCollectionPropertyName(), value); value = childMap; current.put(key, childMap); } //handle special short-hand case where it is just a String //need to move the String value to a pre-defined property name if (value instanceof String) { String sValue = (String)value; //handle the case where the value may be an embedded object using virtual constructor flow boolean isType = false; String realTypeKey = getRealKey(sValue); if (!realTypeKey.equals(value)) { Class<?> valueClass = BuilderUtils.getClassFromAlias(process, realTypeKey, null); if (valueClass != null) { Map<String,Object> rootMap = new HashMap<String,Object>(); Map<String,Object> typeMap = new HashMap<String,Object>(); current.put(key, rootMap); rootMap.put(realTypeKey, typeMap); BuilderUtils.uncompressYaml(sValue, typeMap); isType = true; } } if (!isType) { Map<String,Object> childMap = new HashMap<String,Object>(); childMap.put(handler.getSimpleValuePropertyName(), value); value = childMap; current.put(key, childMap); } } } //handles property aliases private static void handlePropertyAlias(BuilderConfig config, Class<?> typeClass, Map<String,Object> typeMap) throws BuildException { Iterator<String> it = typeMap.keySet().iterator(); Map<String,Object> aliasedPropertiesToAdd = new HashMap<String, Object>(); while (it.hasNext()) { String typeMapKey = it.next(); //aliased? String actual = TypeDefinition.getPropertyForAlias(config, typeClass, typeMapKey); if (actual != null) { //the current key is an alias for a "real" property and the real property is not defined (otherwise it overrides all the aliases) if (typeMap.containsKey(actual)) { throw new BuildException("Both \"{0}\" alias ({1}) and actual \"{2}\" property ({3}) have been specified. Only can of the two is allowed.", typeMapKey, typeMap.get(typeMapKey),actual, typeMap.get(actual)); } else { //move value to actual property aliasedPropertiesToAdd.put(actual, typeMap.get(typeMapKey)); it.remove(); } } } for(String typeMapKey : aliasedPropertiesToAdd.keySet()) { typeMap.put(typeMapKey, aliasedPropertiesToAdd.get(typeMapKey)); } } private static void handleMappedProperties(BuilderConfig config, Class<?> typeClass, Map<String, Object> typeMap) throws BuildException { //handle mapped properties for(String typeMapKey : typeMap.keySet()) { Object typeMapValue = typeMap.get(typeMapKey); //mapped value? if (typeMapValue != null) { Object mappedValue = TypeDefinition.getPropertyValue(config, typeClass, typeMapKey, typeMapValue); if (!mappedValue.equals(typeMapValue)) { typeMap.put(typeMapKey, mappedValue); } } } } }