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
015package org.apache.tapestry.test;
016
017import java.util.ArrayList;
018import java.util.HashMap;
019import java.util.Iterator;
020import java.util.List;
021import java.util.Map;
022
023import org.apache.hivemind.ApplicationRuntimeException;
024import org.apache.hivemind.ClassResolver;
025import org.apache.hivemind.Location;
026import org.apache.hivemind.Resource;
027import org.apache.hivemind.impl.DefaultClassResolver;
028import org.apache.hivemind.service.ClassFactory;
029import org.apache.hivemind.service.impl.ClassFactoryImpl;
030import org.apache.hivemind.util.ClasspathResource;
031import org.apache.hivemind.util.PropertyUtils;
032import org.apache.tapestry.Tapestry;
033import org.apache.tapestry.enhance.AbstractPropertyWorker;
034import org.apache.tapestry.enhance.EnhancementOperationImpl;
035import org.apache.tapestry.enhance.EnhancementWorker;
036import org.apache.tapestry.services.ComponentConstructor;
037import org.apache.tapestry.spec.ComponentSpecification;
038import org.apache.tapestry.spec.IComponentSpecification;
039import 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 */
058public 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}