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.xml;
009    
010    import static org.picocontainer.script.xml.XMLConstants.COMPONENT_INSTANCE_FACTORY;
011    import static org.picocontainer.script.xml.XMLConstants.PARAMETER_ZERO;
012    
013    import java.io.IOException;
014    import java.io.Reader;
015    import java.net.URL;
016    import java.util.ArrayList;
017    import java.util.List;
018    
019    import javax.xml.parsers.DocumentBuilderFactory;
020    import javax.xml.parsers.ParserConfigurationException;
021    
022    import org.picocontainer.ComponentAdapter;
023    import org.picocontainer.ComponentFactory;
024    import org.picocontainer.DefaultPicoContainer;
025    import org.picocontainer.MutablePicoContainer;
026    import org.picocontainer.Parameter;
027    import org.picocontainer.PicoClassNotFoundException;
028    import org.picocontainer.PicoCompositionException;
029    import org.picocontainer.PicoContainer;
030    import org.picocontainer.classname.DefaultClassLoadingPicoContainer;
031    import org.picocontainer.behaviors.Caching;
032    import org.picocontainer.injectors.ConstructorInjection;
033    import org.picocontainer.parameters.ComponentParameter;
034    import org.picocontainer.parameters.ConstantParameter;
035    import org.picocontainer.script.LifecycleMode;
036    import org.picocontainer.script.ScriptedBuilder;
037    import org.picocontainer.script.ScriptedContainerBuilder;
038    import org.picocontainer.script.ScriptedPicoContainerMarkupException;
039    import org.w3c.dom.Document;
040    import org.w3c.dom.Element;
041    import org.w3c.dom.Node;
042    import org.w3c.dom.NodeList;
043    import org.xml.sax.InputSource;
044    import org.xml.sax.SAXException;
045    
046    import com.thoughtworks.xstream.XStream;
047    import com.thoughtworks.xstream.io.HierarchicalStreamDriver;
048    import com.thoughtworks.xstream.io.xml.DomDriver;
049    import com.thoughtworks.xstream.io.xml.DomReader;
050    
051    /**
052     * This class builds up a hierarchy of PicoContainers from an XML configuration
053     * file.
054     * 
055     * @author Konstantin Pribluda
056     */
057    public class XStreamContainerBuilder extends ScriptedContainerBuilder  {
058        private final Element rootElement;
059    
060        private final static String IMPLEMENTATION = "implementation";
061        private final static String INSTANCE = "instance";
062        private final static String ADAPTER = "adapter";
063        private final static String CLASS = "class";
064        private final static String KEY = "key";
065        private final static String CONSTANT = "constant";
066        private final static String DEPENDENCY = "dependency";
067        private final static String CONSTRUCTOR = "constructor";
068    
069        private final HierarchicalStreamDriver xsdriver;
070    
071        /**
072         * construct with just reader, use context classloader
073         * 
074         * @param script
075         */
076        public XStreamContainerBuilder(Reader script) {
077            this(script, Thread.currentThread().getContextClassLoader());
078        }
079    
080        /**
081         * construct with given script and specified classloader
082         * 
083         * @param classLoader
084         * @param script
085         */
086        public XStreamContainerBuilder(Reader script, ClassLoader classLoader) {
087            this(script, classLoader, new DomDriver());
088        }
089    
090        public XStreamContainerBuilder(Reader script, ClassLoader classLoader, HierarchicalStreamDriver driver) {
091            this(script, classLoader, driver, LifecycleMode.AUTO_LIFECYCLE);
092        }
093    
094        public XStreamContainerBuilder(Reader script, ClassLoader classLoader, HierarchicalStreamDriver driver,
095                LifecycleMode lifecycleMode) {
096            super(script, classLoader, lifecycleMode);
097            xsdriver = driver;
098            InputSource inputSource = new InputSource(script);
099            try {
100                rootElement = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputSource)
101                        .getDocumentElement();
102            } catch (SAXException e) {
103                throw new ScriptedPicoContainerMarkupException(e);
104            } catch (IOException e) {
105                throw new ScriptedPicoContainerMarkupException(e);
106            } catch (ParserConfigurationException e) {
107                throw new ScriptedPicoContainerMarkupException(e);
108            }
109        }
110    
111        public XStreamContainerBuilder(URL script, ClassLoader classLoader, HierarchicalStreamDriver driver) {
112            this(script, classLoader, driver, LifecycleMode.AUTO_LIFECYCLE);
113        }
114    
115        public XStreamContainerBuilder(URL script, ClassLoader classLoader, HierarchicalStreamDriver driver,
116                LifecycleMode lifecycleMode) {
117            super(script, classLoader, lifecycleMode);
118            xsdriver = driver;
119            try {
120                InputSource inputSource = new InputSource(getScriptReader());
121                rootElement = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputSource)
122                        .getDocumentElement();
123            } catch (SAXException e) {
124                throw new ScriptedPicoContainerMarkupException(e);
125            } catch (IOException e) {
126                throw new ScriptedPicoContainerMarkupException(e);
127            } catch (ParserConfigurationException e) {
128                throw new ScriptedPicoContainerMarkupException(e);
129            }
130        }
131    
132        public void populateContainer(MutablePicoContainer container) {
133            populateContainer(container, rootElement);
134        }
135    
136        /**
137         * just a convenience method, so we can work recursively with subcontainers
138         * for whatever puproses we see cool.
139         * 
140         * @param container
141         * @param rootElement
142         */
143        private void populateContainer(MutablePicoContainer container, Element rootElement) {
144            NodeList children = rootElement.getChildNodes();
145            Node child;
146            String name;
147            short type;
148            for (int i = 0; i < children.getLength(); i++) {
149                child = children.item(i);
150                type = child.getNodeType();
151    
152                if (type == Document.ELEMENT_NODE) {
153                    name = child.getNodeName();
154                    if (IMPLEMENTATION.equals(name)) {
155                        try {
156                            insertImplementation(container, (Element) child);
157                        } catch (ClassNotFoundException e) {
158                            throw new ScriptedPicoContainerMarkupException(e);
159                        }
160                    } else if (INSTANCE.equals(name)) {
161                        insertInstance(container, (Element) child);
162                    } else if (ADAPTER.equals(name)) {
163                        insertAdapter(container, (Element) child);
164                    } else {
165                        throw new ScriptedPicoContainerMarkupException("Unsupported element:" + name);
166                    }
167                }
168            }
169    
170        }
171    
172        /**
173         * process adapter node
174         * 
175         * @param container
176         * @param rootElement
177         */
178        @SuppressWarnings("unchecked")
179        protected void insertAdapter(MutablePicoContainer container, Element rootElement) {
180            String key = rootElement.getAttribute(KEY);
181            String klass = rootElement.getAttribute(CLASS);
182            try {
183                DefaultPicoContainer nested = new DefaultPicoContainer();
184                populateContainer(nested, rootElement);
185    
186                if (key != null) {
187                    container.addAdapter((ComponentAdapter) nested.getComponent(key));
188                } else if (klass != null) {
189                    Class clazz = getClassLoader().loadClass(klass);
190                    container.addAdapter((ComponentAdapter) nested.getComponent(clazz));
191                } else {
192                    container.addAdapter(nested.getComponent(ComponentAdapter.class));
193                }
194            } catch (ClassNotFoundException ex) {
195                throw new ScriptedPicoContainerMarkupException(ex);
196            }
197    
198        }
199    
200        /**
201         * process implementation node
202         * 
203         * @param container
204         * @param rootElement
205         * @throws ClassNotFoundException
206         */
207        protected void insertImplementation(MutablePicoContainer container, Element rootElement)
208                throws ClassNotFoundException {
209            String key = rootElement.getAttribute(KEY);
210            String klass = rootElement.getAttribute(CLASS);
211            String constructor = rootElement.getAttribute(CONSTRUCTOR);
212            if (klass == null || "".equals(klass)) {
213                throw new ScriptedPicoContainerMarkupException(
214                        "class specification is required for component implementation");
215            }
216    
217            Class<?> clazz = getClassLoader().loadClass(klass);
218    
219    
220            // ok , we processed our children. insert implementation
221            Parameter[] parameterArray =  getParameters(rootElement);
222            if ("default".equals(constructor)){
223                    parameterArray = Parameter.ZERO;
224            }
225            
226            NodeList children = rootElement.getChildNodes();
227            if (children.getLength() > 0 || "default".equals(constructor)) {
228                if (key == null || "".equals(key)) {
229                    // without key. clazz is our key
230                    container.addComponent(clazz, clazz, parameterArray);
231                } else {
232                    // with key
233                    container.addComponent(key, clazz, parameterArray);
234                }
235            } else {
236                if (key == null || "".equals(key)) {
237                    // without key. clazz is our key
238                    container.addComponent(clazz, clazz);
239                } else {
240                    // with key
241                    container.addComponent(key, clazz);
242                }
243    
244            }
245        }
246    
247            private Parameter[] getParameters(Element rootElement) throws ClassNotFoundException {
248                List<Parameter> parameters = new ArrayList<Parameter>();
249    
250            NodeList children = rootElement.getChildNodes();
251            Node child;
252            String name;
253            String dependencyKey;
254            String dependencyClass;
255            Object parseResult;
256    
257            for (int i = 0; i < children.getLength(); i++) {
258                child = children.item(i);
259                if (child.getNodeType() == Document.ELEMENT_NODE) {
260                    name = child.getNodeName();
261                    // constant parameter. it does not have any attributes.
262                    if (CONSTANT.equals(name)) {
263                        // create constant with xstream
264                        parseResult = parseElementChild((Element) child);
265                        if (parseResult == null) {
266                            throw new ScriptedPicoContainerMarkupException("could not parse constant parameter");
267                        }
268                        parameters.add(new ConstantParameter(parseResult));
269                    } else if (DEPENDENCY.equals(name)) {
270                        // either key or class must be present. not both
271                        // key has prececence
272                        dependencyKey = ((Element) child).getAttribute(KEY);
273                        if (dependencyKey == null || "".equals(dependencyKey)) {
274                            dependencyClass = ((Element) child).getAttribute(CLASS);
275                            if (dependencyClass == null || "".equals(dependencyClass)) {
276                                throw new ScriptedPicoContainerMarkupException(
277                                        "either key or class must be present for dependency");
278                            } else {
279                                parameters.add(new ComponentParameter(getClassLoader().loadClass(dependencyClass)));
280                            }
281                        } else {
282                            parameters.add(new ComponentParameter(dependencyKey));
283                        }
284                    } else if (PARAMETER_ZERO.equals(child.getNodeName())) {
285                            //Check:  We can't check everything here since we aren't schema validating
286                            //But it will at least catch some goofs.
287                            if (!parameters.isEmpty()) {
288                                    throw new PicoCompositionException("Cannot mix other parameters with '" + PARAMETER_ZERO +"' nodes." );
289                            }
290                            
291                            return Parameter.ZERO;
292                    }
293                }
294            }
295            return (Parameter[]) parameters.toArray(new Parameter[parameters.size()]);
296        }
297    
298        /**
299         * process instance node. we get key from atributes ( if any ) and leave
300         * content to xstream. we allow only one child node inside. ( first one wins )
301         * 
302         * @param container
303         * @param rootElement
304         */
305        protected void insertInstance(MutablePicoContainer container, Element rootElement) {
306            String key = rootElement.getAttribute(KEY);
307            Object result = parseElementChild(rootElement);
308            if (result == null) {
309                throw new ScriptedPicoContainerMarkupException("no content could be parsed in instance");
310            }
311            if (key != null && !"".equals(key)) {
312                // insert with key
313                container.addComponent(key, result);
314            } else {
315                // or without
316                container.addComponent(result);
317            }
318        }
319    
320        /**
321         * parse element child with xstream and provide object
322         * 
323         * @return
324         * @param rootElement
325         */
326        protected Object parseElementChild(Element rootElement) {
327            NodeList children = rootElement.getChildNodes();
328            Node child;
329            for (int i = 0; i < children.getLength(); i++) {
330                child = children.item(i);
331                if (child.getNodeType() == Document.ELEMENT_NODE) {
332                    return (new XStream(xsdriver)).unmarshal(new DomReader((Element) child));
333                }
334            }
335            return null;
336        }
337    
338        protected PicoContainer createContainerFromScript(PicoContainer parentContainer, Object assemblyScope) {
339            try {
340                // create ComponentInstanceFactory for the container
341                MutablePicoContainer childContainer = createMutablePicoContainer(
342                         parentContainer, new ContainerOptions(rootElement));
343                populateContainer(childContainer);
344                return childContainer;
345            } catch (PicoClassNotFoundException e) {
346                throw new ScriptedPicoContainerMarkupException("Class not found:" + e.getMessage(), e);
347            }
348        }
349    
350        private MutablePicoContainer createMutablePicoContainer(PicoContainer parentContainer, ContainerOptions containerOptions) throws PicoCompositionException {
351            boolean caching = containerOptions.isCaching();
352            boolean inherit = containerOptions.isInheritParentBehaviors();
353            String monitorName = containerOptions.getMonitorName();
354            String componentFactoryName = containerOptions.getComponentFactoryName();
355            
356            if (inherit) {
357                    if (!(parentContainer instanceof MutablePicoContainer)) {
358                            throw new PicoCompositionException("For behavior inheritance to be used, the parent picocontainer must be of type MutablePicoContainer");
359                    }
360                    
361                    MutablePicoContainer parentPico = (MutablePicoContainer)parentContainer;
362                    return parentPico.makeChildContainer();
363            }
364            
365            ScriptedBuilder builder = new ScriptedBuilder(parentContainer);
366            if (caching) builder.withCaching();
367            return builder
368                .withClassLoader(getClassLoader())
369                .withLifecycle()
370                .withComponentFactory(componentFactoryName)
371                .withMonitor(monitorName)
372                .build();
373    
374        }
375    
376    }