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.enhance.InjectRenderWorker;
037    import org.apache.tapestry.services.ComponentConstructor;
038    import org.apache.tapestry.services.ComponentRenderWorker;
039    import org.apache.tapestry.spec.ComponentSpecification;
040    import org.apache.tapestry.spec.IComponentSpecification;
041    import org.apache.tapestry.util.DescribedLocation;
042    
043    /**
044     * A utility class that is used to instantiate abstract Tapestry pages and components. It creates,
045     * at runtime, a subclass where all abstract properties are filled in (each property complete with
046     * an instance variable, an accessor method and a mutator method). This isn't quite the same as how
047     * the class is enhanced at runtime (though it does use a subset of the same
048     * {@link org.apache.tapestry.enhance.EnhancementWorker code}), but is sufficient to unit test the
049     * class, especially listener methods.
050     * <p>
051     * One part of the enhancement is that the
052     * {@link org.apache.tapestry.IComponent#getSpecification() specification}&nbsp;and
053     * {@link org.apache.tapestry.IComponent#getMessages() messages}&nbsp;properties of the page or
054     * component class are converted into read/write properties that can be set via reflection
055     * (including {@link #newInstance(Class, Map)}.
056     * 
057     * @author Howard Lewis Ship
058     * @since 4.0
059     */
060    public class Creator
061    {
062        /**
063         * Keyed on Class, value is an {@link ComponentConstructor}.
064         */
065        private final Map _constructors = new HashMap();
066    
067        private final ClassFactory _classFactory = new ClassFactoryImpl();
068    
069        private final ClassResolver _classResolver = new DefaultClassResolver();
070    
071        private final List _workers = new ArrayList();
072    
073        private final Resource _creatorResource = new ClasspathResource(_classResolver,
074                "/CreatorLocation");
075    
076        private final Location _creatorLocation = new DescribedLocation(_creatorResource,
077                "Creator Location");
078        
079        private final ComponentRenderWorker _renderWorker = new MockComponentRenderWorker();
080        
081        private final InjectRenderWorker _injectRender = new InjectRenderWorker();
082        
083        {
084            
085            _injectRender.setRenderWorker(_renderWorker);
086            
087            // Overrride AbstractComponent's implementations of
088            // these two properties (making them read/write).
089    
090            _workers.add(new CreatePropertyWorker("messages", _creatorLocation));
091            _workers.add(new CreatePropertyWorker("specification", _creatorLocation));
092            _workers.add(_injectRender);
093            
094            // Implement any abstract properties.
095            // Note that we don't bother setting the errorLog property
096            // so failures may turn into NPEs.
097            
098            _workers.add(new AbstractPropertyWorker());
099        }
100    
101        private ComponentConstructor createComponentConstructor(Class inputClass)
102        {
103            if (inputClass.isInterface() || inputClass.isPrimitive() || inputClass.isArray())
104                throw new IllegalArgumentException(ScriptMessages.wrongTypeForEnhancement(inputClass));
105    
106            EnhancementOperationImpl op = new EnhancementOperationImpl(_classResolver,
107                    new ComponentSpecification(), inputClass, _classFactory, null);
108    
109            IComponentSpecification spec = new ComponentSpecification();
110            spec.setLocation(_creatorLocation);
111    
112            Iterator i = _workers.iterator();
113            while (i.hasNext())
114            {
115                EnhancementWorker worker = (EnhancementWorker) i.next();
116    
117                worker.performEnhancement(op, spec);
118            }
119    
120            return op.getConstructor();
121        }
122    
123        private ComponentConstructor getComponentConstructor(Class inputClass)
124        {
125            ComponentConstructor result = (ComponentConstructor) _constructors.get(inputClass);
126    
127            if (result == null)
128            {
129                result = createComponentConstructor(inputClass);
130    
131                _constructors.put(inputClass, result);
132            }
133    
134            return result;
135        }
136    
137        /**
138         * Given a particular abstract class; will create an instance of that class. A subclass is
139         * created with all abstract properties filled in with ordinary implementations.
140         */
141        public Object newInstance(Class abstractClass)
142        {
143            ComponentConstructor constructor = getComponentConstructor(abstractClass);
144    
145            try
146            {
147                return constructor.newInstance();
148            }
149            catch (Exception ex)
150            {
151                throw new ApplicationRuntimeException(ScriptMessages.unableToInstantiate(
152                        abstractClass,
153                        ex));
154            }
155        }
156    
157        /**
158         * Creates a new instance of a given class, and then initializes properties of the instance. The
159         * map contains string keys that are property names, and object values.
160         */
161        public Object newInstance(Class abstractClass, Map properties)
162        {
163            Object result = newInstance(abstractClass);
164    
165            if (properties != null)
166            {
167                Iterator i = properties.entrySet().iterator();
168    
169                while (i.hasNext())
170                {
171                    Map.Entry e = (Map.Entry) i.next();
172    
173                    String propertyName = (String) e.getKey();
174    
175                    PropertyUtils.write(result, propertyName, e.getValue());
176                }
177            }
178    
179            return result;
180        }
181    
182        /**
183         * A convienience (useful in test code) for invoking {@link #newInstance(Class, Map)}. The Map
184         * is constructed from the properties array, which consists of alternating keys and values.
185         */
186    
187        public Object newInstance(Class abstractClass, Object[] properties)
188        {
189            Map propertyMap = Tapestry.convertArrayToMap(properties);
190    
191            return newInstance(abstractClass, propertyMap);
192        }
193    }