001    // Copyright 2004, 2005 The Apache Software Foundation
002    //
003    // Licensed under the Apache License, Version 2.0 (the "License");
004    // you may not use this file except in compliance with the License.
005    // You may obtain a copy of the License at
006    //
007    //     http://www.apache.org/licenses/LICENSE-2.0
008    //
009    // Unless required by applicable law or agreed to in writing, software
010    // distributed under the License is distributed on an "AS IS" BASIS,
011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012    // See the License for the specific language governing permissions and
013    // limitations under the License.
014    
015    package org.apache.tapestry.test;
016    
017    import java.util.ArrayList;
018    import java.util.HashMap;
019    import java.util.Iterator;
020    import java.util.List;
021    import java.util.Map;
022    
023    import org.apache.hivemind.ApplicationRuntimeException;
024    import org.apache.hivemind.ClassResolver;
025    import org.apache.hivemind.Location;
026    import org.apache.hivemind.Resource;
027    import org.apache.hivemind.impl.DefaultClassResolver;
028    import org.apache.hivemind.service.ClassFactory;
029    import org.apache.hivemind.service.impl.ClassFactoryImpl;
030    import org.apache.hivemind.util.ClasspathResource;
031    import org.apache.hivemind.util.PropertyUtils;
032    import org.apache.tapestry.Tapestry;
033    import org.apache.tapestry.enhance.AbstractPropertyWorker;
034    import org.apache.tapestry.enhance.EnhancementOperationImpl;
035    import org.apache.tapestry.enhance.EnhancementWorker;
036    import org.apache.tapestry.services.ComponentConstructor;
037    import org.apache.tapestry.spec.ComponentSpecification;
038    import org.apache.tapestry.spec.IComponentSpecification;
039    import org.apache.tapestry.util.DescribedLocation;
040    
041    /**
042     * A utility class that is used to instantiate abstract Tapestry pages and components. It creates,
043     * at runtime, a subclass where all abstract properties are filled in (each property complete with
044     * an instance variable, an accessor method and a mutator method). This isn't quite the same as how
045     * the class is enhanced at runtime (though it does use a subset of the same
046     * {@link org.apache.tapestry.enhance.EnhancementWorker code}), but is sufficient to unit test the
047     * class, especially listener methods.
048     * <p>
049     * One part of the enhancement is that the
050     * {@link org.apache.tapestry.IComponent#getSpecification() specification}&nbsp;and
051     * {@link org.apache.tapestry.IComponent#getMessages() messages}&nbsp;properties of the page or
052     * component class are converted into read/write properties that can be set via reflection
053     * (including {@link #newInstance(Class, Map)}.
054     * 
055     * @author Howard Lewis Ship
056     * @since 4.0
057     */
058    public class Creator
059    {
060        /**
061         * Keyed on Class, value is an {@link ComponentConstructor}.
062         */
063        private final Map _constructors = new HashMap();
064    
065        private final ClassFactory _classFactory = new ClassFactoryImpl();
066    
067        private final ClassResolver _classResolver = new DefaultClassResolver();
068    
069        private final List _workers = new ArrayList();
070    
071        private final Resource _creatorResource = new ClasspathResource(_classResolver,
072                "/CreatorLocation");
073    
074        private final Location _creatorLocation = new DescribedLocation(_creatorResource,
075                "Creator Location");
076    
077        {
078            // Overrride AbstractComponent's implementations of
079            // these two properties (making them read/write).
080    
081            _workers.add(new CreatePropertyWorker("messages", _creatorLocation));
082            _workers.add(new CreatePropertyWorker("specification", _creatorLocation));
083    
084            // Implement any abstract properties.
085            // Note that we don't bother setting the errorLog property
086            // so failures may turn into NPEs.
087    
088            _workers.add(new AbstractPropertyWorker());
089        }
090    
091        private ComponentConstructor createComponentConstructor(Class inputClass)
092        {
093            if (inputClass.isInterface() || inputClass.isPrimitive() || inputClass.isArray())
094                throw new IllegalArgumentException(ScriptMessages.wrongTypeForEnhancement(inputClass));
095    
096            EnhancementOperationImpl op = new EnhancementOperationImpl(_classResolver,
097                    new ComponentSpecification(), inputClass, _classFactory, null);
098    
099            IComponentSpecification spec = new ComponentSpecification();
100            spec.setLocation(_creatorLocation);
101    
102            Iterator i = _workers.iterator();
103            while (i.hasNext())
104            {
105                EnhancementWorker worker = (EnhancementWorker) i.next();
106    
107                worker.performEnhancement(op, spec);
108            }
109    
110            return op.getConstructor();
111        }
112    
113        private ComponentConstructor getComponentConstructor(Class inputClass)
114        {
115            ComponentConstructor result = (ComponentConstructor) _constructors.get(inputClass);
116    
117            if (result == null)
118            {
119                result = createComponentConstructor(inputClass);
120    
121                _constructors.put(inputClass, result);
122            }
123    
124            return result;
125        }
126    
127        /**
128         * Given a particular abstract class; will create an instance of that class. A subclass is
129         * created with all abstract properties filled in with ordinary implementations.
130         */
131        public Object newInstance(Class abstractClass)
132        {
133            ComponentConstructor constructor = getComponentConstructor(abstractClass);
134    
135            try
136            {
137                return constructor.newInstance();
138            }
139            catch (Exception ex)
140            {
141                throw new ApplicationRuntimeException(ScriptMessages.unableToInstantiate(
142                        abstractClass,
143                        ex));
144            }
145        }
146    
147        /**
148         * Creates a new instance of a given class, and then initializes properties of the instance. The
149         * map contains string keys that are property names, and object values.
150         */
151        public Object newInstance(Class abstractClass, Map properties)
152        {
153            Object result = newInstance(abstractClass);
154    
155            if (properties != null)
156            {
157                Iterator i = properties.entrySet().iterator();
158    
159                while (i.hasNext())
160                {
161                    Map.Entry e = (Map.Entry) i.next();
162    
163                    String propertyName = (String) e.getKey();
164    
165                    PropertyUtils.write(result, propertyName, e.getValue());
166                }
167            }
168    
169            return result;
170        }
171    
172        /**
173         * A convienience (useful in test code) for invoking {@link #newInstance(Class, Map)}. The Map
174         * is constructed from the properties array, which consists of alternating keys and values.
175         */
176    
177        public Object newInstance(Class abstractClass, Object[] properties)
178        {
179            Map propertyMap = Tapestry.convertArrayToMap(properties);
180    
181            return newInstance(abstractClass, propertyMap);
182        }
183    }