package org.javabuilders; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.MissingResourceException; import java.util.ResourceBundle; import java.util.Set; import org.javabuilders.event.BackgroundEventListener; import org.javabuilders.handler.validation.BuilderValidators; import org.javabuilders.handler.validation.IValidationMessageHandler; import org.javabuilders.handler.validation.ValidationMessageList; import org.javabuilders.util.BuilderUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The return of any build process. That's what both the YAML and Java side see and * can communicate with * @author Jacek Furmankiewicz * */ @SuppressWarnings("serial") public class BuildResult extends HashMap<String, Object> { private static final Logger logger = LoggerFactory.getLogger(BuildResult.class); private BuilderConfig config; private Object caller = null; private ResourceBundle defaultBundle = null; private List<Object> roots = new ArrayList<Object>(); private boolean isDirty = false; private BuilderValidators validators = new BuilderValidators(this); private PropertyChangeSupport support = new PropertyChangeSupport(this); private IValidationMessageHandler validationMessageHandler; private Set<ResourceBundle> resourceBundles = new HashSet<ResourceBundle>(); private Map<String, Field> allFields = null; private Object bindingContext = null; private Map<String,?> properties = new HashMap<String, Object>(); private Set<BackgroundEventListener> backgroundEventListeners = new LinkedHashSet<BackgroundEventListener>(); //poor man's version of functional programming private IResourceFallback defaultResourceFallback = new IResourceFallback() { public String get(String key) { if (config.getResourceBundles().size() > 0 || getResourceBundles().size() > 0) { //unable to find key - pass the key as the value return getInvalidResource(key); } else { return key; } } }; /** * @param config Config */ public BuildResult(BuilderConfig config, Object caller) { this.config = config; this.caller = caller; allFields = BuilderUtils.getAllFields(caller.getClass()); } /* (non-Javadoc) * @see java.util.HashMap#put(java.lang.Object, java.lang.Object) */ @Override public Object put(String key, Object value) { //Additional validation to ensure duplicate names are not allowed (Issue #20) if (key == null) { throw new BuildException("Null keys are not allowed in BuildResult: {0}", value); } else if (keySet().contains(key)) { throw new BuildException("Duplicate name \"{0}\" specified in YAML file : {1}", key, value.getClass().getSimpleName()); } else { return super.put(key, value); } } /* (non-Javadoc) * @see java.util.HashMap#get(java.lang.Object) */ @Override public Object get(Object key) { Object value = super.get(key); if (value == null && allFields.containsKey(key)) { //named object not created during build..look for a field or property with the same name Field field = allFields.get(key); try { value = field.get(caller); } catch (Exception e) { logger.error("Unable to access field {}: {}", key, e.getMessage()); } } return value; } /** * JavaBean support * @param listener */ public void addPropertyChangeListener(PropertyChangeListener listener) { support.addPropertyChangeListener(listener); } /** * JavaBean support * @param propertyName * @param listener */ public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) { support.addPropertyChangeListener(propertyName,listener); } /** * Flag used to track if any changes have been made to properties flagged for tracking * @return the isDirty flag */ public boolean isDirty() { return isDirty; } /** * JavaBean support * @param listener */ public void removePropertyChangeListener(PropertyChangeListener listener) { support.removePropertyChangeListener(listener); } /** * JavaBean support * @param propertyName * @param listener */ public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) { support.removePropertyChangeListener(propertyName,listener); } /** * Used to reset the dirty flag * @param isDirty the isDirty flag to set */ public void setDirty(boolean isDirty) { boolean oldValue = this.isDirty; this.isDirty = isDirty; support.firePropertyChange("dirty", oldValue, isDirty); } /** * Syncs the marked variables between the YAML and Java side. * Can be called mnaually at any time, builder calls it when required * @throws BuildException */ public void sync() throws BuildException { //TODO: sync variables across both domains } /** * Validates the YAML/Java values, depending on how validations where defined in the * YAML domain * @return true if valid, false if not */ public boolean validate() { return this.validate(true); } /** * Validates the YAML/Java values, depending on how validations where defined in the * YAML domain * @param handleValidationMessages Flags if supposed to show validation messages or not * @return true if valid, false if not */ public boolean validate(boolean handleValidationMessages) { ValidationMessageList list = getValidationMessages(); if (list.size() > 0) { if (handleValidationMessages) { validationMessageHandler.handleValidationMessages(list, this); } return false; } else { //all fine, no input return true; } } /** * @return List of validation messages. If size == 0, means no validation issues exist */ public ValidationMessageList getValidationMessages() { return getValidators().getValidationMessages(validationMessageHandler); } /** * @return The property validators defined in the YAML file */ public BuilderValidators getValidators() { return validators; } /** * @return the validation message handler */ public IValidationMessageHandler getValidationMessageHandler() { return validationMessageHandler; } /** * @param validationMessageHandler the validation message handler */ public void setValidationMessageHandler( IValidationMessageHandler validationMessageHandler) { this.validationMessageHandler = validationMessageHandler; } /** * return Resource bundles */ public Set<ResourceBundle> getResourceBundles() { return this.resourceBundles; } /** * @param bundles Resource bundles */ void setResourceBundles(Set<ResourceBundle> bundles) { this.resourceBundles = bundles; } /** * @param key * @return */ public String getResource(String key) { return getResource(key,defaultResourceFallback); } /** * @param key * @return */ public String getResource(String key, IResourceFallback resourceFallback) { String resource = null; //look in the process bundles first for(ResourceBundle bundle : getResourceBundles()) { try { resource = bundle.getString(key); break; } catch (MissingResourceException ex) { // intentionally ignored; continue with lookup } } //look in the global bundles next if (resource == null) { for(ResourceBundle bundle : config.getResourceBundles()) { try { resource = bundle.getString(key); break; } catch (MissingResourceException ex) { // intentionally ignored; continue with lookup } } } //look in base resource bundle last if (resource == null) { try { if (defaultBundle == null) { defaultBundle = ResourceBundle.getBundle(Builder.RESOURCE_BUNDLE); } resource = defaultBundle.getString(key); } catch (Exception ex) {} } //fallback strategy - different depending if internationalization is active or not if (resource == null) { resource = resourceFallback.get(key); } return resource; } /** * @param key Key * @return Key formatted as as an invalid (i.e. not internationalized) value */ public String getInvalidResource(String key) { String resource = config.isMarkInvalidResourceBundleKeys() ? String.format("#%s#",key) : key; if (logger.isInfoEnabled()) { logger.info("Unable to find value in any resource bundle for key: {}", key); } return resource; } /** * @return the caller that initiated the build */ public Object getCaller() { return caller; } /** * @return the caller that initiated the build */ public BuilderConfig getConfig() { return config; } /** * Gets the first root object, if defined * @return Root object * @throws MultipleRootsException */ public Object getRoot() { if (roots.size() > 0) { return roots.get(0); } else { return null; } } /** * Gets the root objects * @return Root objects */ public List<Object> getRoots() { return roots; } /** * @return Domain-specific binding contenxt */ public Object getBindingContext() { return bindingContext; } /** * @param bindingContext Domain specific binding context */ public void setBindingContext(Object bindingContext) { this.bindingContext = bindingContext; } /** * @return Build-specific custom properties */ @SuppressWarnings({ "rawtypes" }) public Map getProperties() { return properties; } /** * Flag that indicates if the build process has internationalization logic activates * @return */ public boolean isInternationalizationActive() { return getConfig().getResourceBundles().size() > 0 || getResourceBundles().size() > 0; } /** * 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()]); } }