001    /*******************************************************************************
002     * Copyright (C) PicoContainer Organization. All rights reserved.
003     * ---------------------------------------------------------------------------
004     * The software in this package is published under the terms of the BSD style
005     * license a copy of which has been included with this distribution in the
006     * LICENSE.txt file.
007     ******************************************************************************/
008    package org.picocontainer.script.groovy.nodes;
009    
010    import java.io.Serializable;
011    import java.util.Collections;
012    import java.util.HashSet;
013    import java.util.Set;
014    
015    import org.picocontainer.script.groovy.BuilderNode;
016    import java.util.Map;
017    
018    import org.picocontainer.script.ScriptedPicoContainerMarkupException;
019    
020    /**
021     * Abstract base class for custom nodes. Also provides basic services and
022     * construction capabilities.
023     * 
024     * @author James Strachan
025     * @author Paul Hammant
026     * @author Aslak Hellesøy
027     * @author Michael Rimov
028     * @author Mauro Talevi
029     */
030    public abstract class AbstractBuilderNode implements BuilderNode, Serializable {
031    
032        /**
033         * The name of the node we're working with.
034         */
035        private final String nodeName;
036    
037        /**
038         * A set of all possible supported attribute names.
039         */
040        private final Set<String> supportedAttributes = new HashSet<String>();
041    
042        /**
043         * Constructs a custom node builder. In derived classes you would typically
044         * create a default constructor and call addPossibleParent()/addAttribute()
045         * to customize the validation capabilities of the Node.
046         * 
047         * @param nodeName the name of the node we're constructing.
048         */
049        public AbstractBuilderNode(final String nodeName) {
050            this.nodeName = nodeName;
051    
052        }
053    
054        /**
055         * Add an attribute to the list of ones supported by this node.
056         * 
057         * @param name String the name of the attribute we support.
058         * @return AbstractBuilderNode (this) to allow for method chaining.
059         */
060        protected AbstractBuilderNode addAttribute(final String name) {
061            supportedAttributes.add(name);
062            return this;
063        }
064    
065        public String getNodeName() {
066            return nodeName;
067        }
068    
069        public Set<String> getSupportedAttributeNames() {
070            return Collections.unmodifiableSet(supportedAttributes);
071        }
072    
073        public String toString() {
074            return "BuilderNode: " + this.getClass().getName() + " (\"" + getNodeName() + "\")";
075        }
076    
077        /**
078         * Checks that an attribute actually exists in the attirbute map. (The key
079         * exists and the value is non-null)
080         * 
081         * @param attributes Map the current node's attributes.
082         * @param key String the attribute key we're looking for.
083         * @return boolean true if the attribute exists for the current node.
084         */
085        protected boolean isAttribute(final Map<String, Object> attributes, final String key) {
086            return attributes.containsKey(key) && attributes.get(key) != null;
087        }
088    
089        /**
090         * {@inheritDoc}
091         * <p>
092         * This particular implementation checks all specified attribute keynames
093         * against the names supported in the node type. It does not type checking
094         * against the values passed in via the attributes.
095         * </p>
096         * 
097         * @param specifiedAttributes the attributes as passed in by the groovy
098         *            script.
099         * @throws ScriptedPicoContainerMarkupException if an attribute is specified
100         *             that is not recognized.
101         */
102        public void validateScriptedAttributes(final Map<String, Object> specifiedAttributes)
103                throws ScriptedPicoContainerMarkupException {
104            Set<String> specifiedAttributeNames = specifiedAttributes.keySet();
105            if (this.getSupportedAttributeNames().containsAll(specifiedAttributeNames)) {
106                return;
107            }
108    
109            Set<String> unknownAttributeNames = new HashSet<String>(specifiedAttributeNames);
110            unknownAttributeNames.removeAll(this.getSupportedAttributeNames());
111    
112            StringBuffer errorMessage = new StringBuffer();
113            errorMessage.append("Found one or more unknown attributes for builder node '");
114            errorMessage.append(this.getNodeName());
115            errorMessage.append("': ");
116            errorMessage.append(toCSV(unknownAttributeNames));
117            errorMessage.append(".  Recognized Attributes For this node are [");
118            errorMessage.append(toCSV(this.getSupportedAttributeNames()));
119            errorMessage.append("].");
120    
121            throw new ScriptedPicoContainerMarkupException(errorMessage.toString());
122        }
123    
124        /**
125         * Utility function that takes a set and converts it to a comma delimited
126         * String with the format: key1, key2,.....
127         * 
128         * @param set Set the set to convert. For each object in the set, its
129         *            toString() is called.
130         * @return String
131         */
132        private String toCSV(final Set<String> set) {
133    
134            StringBuffer result = new StringBuffer();
135            boolean needComma = false;
136            for (String element : set) {
137                if (needComma) {
138                    result.append(",");
139                } else {
140                    needComma = true;
141                }
142    
143                result.append(element.toString());
144            }
145            return result.toString();
146        }
147    
148    }