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.enhance;
016
017import java.beans.BeanInfo;
018import java.beans.IntrospectionException;
019import java.beans.Introspector;
020import java.beans.PropertyDescriptor;
021import java.lang.reflect.Constructor;
022import java.lang.reflect.Method;
023import java.lang.reflect.Modifier;
024import java.util.ArrayList;
025import java.util.HashMap;
026import java.util.HashSet;
027import java.util.Iterator;
028import java.util.List;
029import java.util.Map;
030import java.util.Set;
031
032import org.apache.commons.logging.Log;
033import org.apache.hivemind.ApplicationRuntimeException;
034import org.apache.hivemind.ClassResolver;
035import org.apache.hivemind.HiveMind;
036import org.apache.hivemind.Location;
037import org.apache.hivemind.service.BodyBuilder;
038import org.apache.hivemind.service.ClassFab;
039import org.apache.hivemind.service.ClassFactory;
040import org.apache.hivemind.service.MethodSignature;
041import org.apache.hivemind.util.Defense;
042import org.apache.hivemind.util.ToStringBuilder;
043import org.apache.tapestry.services.ComponentConstructor;
044import org.apache.tapestry.spec.IComponentSpecification;
045import org.apache.tapestry.util.IdAllocator;
046import org.apache.tapestry.util.ObjectIdentityMap;
047
048/**
049 * Implementation of {@link org.apache.tapestry.enhance.EnhancementOperation}that knows how to
050 * collect class changes from enhancements. The method {@link #getConstructor()} finalizes the
051 * enhancement into a {@link org.apache.tapestry.services.ComponentConstructor}.
052 * 
053 * @author Howard M. Lewis Ship
054 * @since 4.0
055 */
056public class EnhancementOperationImpl implements EnhancementOperation
057{
058    private ClassResolver _resolver;
059
060    private IComponentSpecification _specification;
061
062    private Class _baseClass;
063
064    private ClassFab _classFab;
065
066    private final Set _claimedProperties = new HashSet();
067
068    private final JavaClassMapping _javaClassMapping = new JavaClassMapping();
069
070    private final List _constructorTypes = new ArrayList();
071
072    private final List _constructorArguments = new ArrayList();
073
074    private final ObjectIdentityMap _finalFields = new ObjectIdentityMap();
075
076    /**
077     * Set of interfaces added to the enhanced class.
078     */
079
080    private Set _addedInterfaces = new HashSet();
081
082    /**
083     * Map of {@link BodyBuilder}, keyed on {@link MethodSignature}.
084     */
085
086    private Map _incompleteMethods = new HashMap();
087
088    /**
089     * Map of property names to {@link PropertyDescriptor}.
090     */
091
092    private Map _properties = new HashMap();
093
094    /**
095     * Used to incrementally assemble the constructor for the enhanced class.
096     */
097
098    private BodyBuilder _constructorBuilder;
099
100    /**
101     * Makes sure that names created by {@link #addInjectedField(String, Object)} have unique names.
102     */
103
104    private final IdAllocator _idAllocator = new IdAllocator();
105
106    /**
107     * Map keyed on MethodSignature, value is Location. Used to track which methods have been
108     * created, based on which location data (identified conflicts).
109     */
110
111    private final Map _methods = new HashMap();
112
113    // May be null
114
115    private final Log _log;
116
117    public EnhancementOperationImpl(ClassResolver classResolver,
118            IComponentSpecification specification, Class baseClass, ClassFactory classFactory,
119            Log log)
120    {
121        Defense.notNull(classResolver, "classResolver");
122        Defense.notNull(specification, "specification");
123        Defense.notNull(baseClass, "baseClass");
124        Defense.notNull(classFactory, "classFactory");
125
126        _resolver = classResolver;
127        _specification = specification;
128        _baseClass = baseClass;
129
130        introspectBaseClass();
131
132        String name = newClassName();
133
134        _classFab = classFactory.newClass(name, _baseClass);
135        _log = log;
136    }
137
138    public String toString()
139    {
140        ToStringBuilder builder = new ToStringBuilder(this);
141
142        builder.append("baseClass", _baseClass.getName());
143        builder.append("claimedProperties", _claimedProperties);
144        builder.append("classFab", _classFab);
145
146        return builder.toString();
147    }
148
149    /**
150     * We want to find the properties of the class, but in many cases, the class is abstract. Some
151     * JDK's (Sun) will include public methods from interfaces implemented by the class in the
152     * public declared methods for the class (which is used by the Introspector). Eclipse's built-in
153     * compiler does not appear to (this may have to do with compiler options I've been unable to
154     * track down). The solution is to augment the information provided directly by the Introspector
155     * with additional information compiled by Introspecting the interfaces directly or indirectly
156     * implemented by the class.
157     */
158    private void introspectBaseClass()
159    {
160        try
161        {
162            synchronized (HiveMind.INTROSPECTOR_MUTEX)
163            {
164                addPropertiesDeclaredInBaseClass();
165            }
166        }
167        catch (IntrospectionException ex)
168        {
169            throw new ApplicationRuntimeException(EnhanceMessages.unabelToIntrospectClass(
170                    _baseClass,
171                    ex), ex);
172        }
173
174    }
175
176    private void addPropertiesDeclaredInBaseClass() throws IntrospectionException
177    {
178        Class introspectClass = _baseClass;
179
180        addPropertiesDeclaredInClass(introspectClass);
181
182        List interfaceQueue = new ArrayList();
183
184        while (introspectClass != null)
185        {
186            addInterfacesToQueue(introspectClass, interfaceQueue);
187
188            introspectClass = introspectClass.getSuperclass();
189        }
190
191        while (!interfaceQueue.isEmpty())
192        {
193            Class interfaceClass = (Class) interfaceQueue.remove(0);
194
195            addPropertiesDeclaredInClass(interfaceClass);
196
197            addInterfacesToQueue(interfaceClass, interfaceQueue);
198        }
199    }
200
201    private void addInterfacesToQueue(Class introspectClass, List interfaceQueue)
202    {
203        Class[] interfaces = introspectClass.getInterfaces();
204
205        for (int i = 0; i < interfaces.length; i++)
206            interfaceQueue.add(interfaces[i]);
207    }
208
209    private void addPropertiesDeclaredInClass(Class introspectClass) throws IntrospectionException
210    {
211        BeanInfo bi = Introspector.getBeanInfo(introspectClass);
212
213        PropertyDescriptor[] pds = bi.getPropertyDescriptors();
214
215        for (int i = 0; i < pds.length; i++)
216        {
217            PropertyDescriptor pd = pds[i];
218
219            String name = pd.getName();
220
221            if (!_properties.containsKey(name))
222                _properties.put(name, pd);
223        }
224    }
225
226    /**
227     * Alternate package private constructor used by the test suite, to bypass the defense checks
228     * above.
229     */
230
231    EnhancementOperationImpl()
232    {
233        _log = null;
234    }
235
236    public void claimProperty(String propertyName)
237    {
238        Defense.notNull(propertyName, "propertyName");
239
240        if (_claimedProperties.contains(propertyName))
241            throw new ApplicationRuntimeException(EnhanceMessages.claimedProperty(propertyName));
242
243        _claimedProperties.add(propertyName);
244    }
245
246    public void claimReadonlyProperty(String propertyName)
247    {
248        claimProperty(propertyName);
249
250        PropertyDescriptor pd = getPropertyDescriptor(propertyName);
251
252        if (pd != null && pd.getWriteMethod() != null)
253            throw new ApplicationRuntimeException(EnhanceMessages.readonlyProperty(propertyName, pd
254                    .getWriteMethod()));
255    }
256
257    public void addField(String name, Class type)
258    {
259        _classFab.addField(name, type);
260    }
261
262    public String addInjectedField(String fieldName, Class fieldType, Object value)
263    {
264        Defense.notNull(fieldName, "fieldName");
265        Defense.notNull(fieldType, "fieldType");
266        Defense.notNull(value, "value");
267
268        String existing = (String) _finalFields.get(value);
269
270        // See if this object has been previously added.
271
272        if (existing != null)
273            return existing;
274
275        // TODO: Should be ensure that the name is unique?
276
277        // Make sure that the field has a unique name (at least, among anything added
278        // via addFinalField().
279
280        String uniqueName = _idAllocator.allocateId(fieldName);
281
282        // ClassFab doesn't have an option for saying the field should be final, just private.
283        // Doesn't make a huge difference.
284
285        _classFab.addField(uniqueName, fieldType);
286
287        int parameterIndex = addConstructorParameter(fieldType, value);
288
289        constructorBuilder().addln("{0} = ${1};", uniqueName, Integer.toString(parameterIndex));
290
291        // Remember the mapping from the value to the field name.
292
293        _finalFields.put(value, uniqueName);
294
295        return uniqueName;
296    }
297
298    public Class convertTypeName(String type)
299    {
300        Defense.notNull(type, "type");
301
302        Class result = _javaClassMapping.getType(type);
303
304        if (result == null)
305        {
306            result = _resolver.findClass(type);
307
308            _javaClassMapping.recordType(type, result);
309        }
310
311        return result;
312    }
313
314    public Class getPropertyType(String name)
315    {
316        Defense.notNull(name, "name");
317
318        PropertyDescriptor pd = getPropertyDescriptor(name);
319
320        return pd == null ? null : pd.getPropertyType();
321    }
322
323    public void validateProperty(String name, Class expectedType)
324    {
325        Defense.notNull(name, "name");
326        Defense.notNull(expectedType, "expectedType");
327
328        PropertyDescriptor pd = getPropertyDescriptor(name);
329
330        if (pd == null)
331            return;
332
333        Class propertyType = pd.getPropertyType();
334
335        if (propertyType.equals(expectedType))
336            return;
337
338        throw new ApplicationRuntimeException(EnhanceMessages.propertyTypeMismatch(
339                _baseClass,
340                name,
341                propertyType,
342                expectedType));
343    }
344
345    private PropertyDescriptor getPropertyDescriptor(String name)
346    {
347        return (PropertyDescriptor) _properties.get(name);
348    }
349
350    public String getAccessorMethodName(String propertyName)
351    {
352        Defense.notNull(propertyName, "propertyName");
353
354        PropertyDescriptor pd = getPropertyDescriptor(propertyName);
355
356        if (pd != null && pd.getReadMethod() != null)
357            return pd.getReadMethod().getName();
358
359        return EnhanceUtils.createAccessorMethodName(propertyName);
360    }
361
362    public void addMethod(int modifier, MethodSignature sig, String methodBody, Location location)
363    {
364        Defense.notNull(sig, "sig");
365        Defense.notNull(methodBody, "methodBody");
366        Defense.notNull(location, "location");
367
368        Location existing = (Location) _methods.get(sig);
369        if (existing != null)
370            throw new ApplicationRuntimeException(EnhanceMessages.methodConflict(sig, existing),
371                    location, null);
372
373        _methods.put(sig, location);
374
375        _classFab.addMethod(modifier, sig, methodBody);
376    }
377
378    public Class getBaseClass()
379    {
380        return _baseClass;
381    }
382
383    public String getClassReference(Class clazz)
384    {
385        Defense.notNull(clazz, "clazz");
386
387        String result = (String) _finalFields.get(clazz);
388
389        if (result == null)
390            result = addClassReference(clazz);
391
392        return result;
393    }
394
395    private String addClassReference(Class clazz)
396    {
397        StringBuffer buffer = new StringBuffer("_class$");
398
399        Class c = clazz;
400
401        while (c.isArray())
402        {
403            buffer.append("array$");
404            c = c.getComponentType();
405        }
406
407        buffer.append(c.getName().replace('.', '$'));
408
409        String fieldName = buffer.toString();
410
411        return addInjectedField(fieldName, Class.class, clazz);
412    }
413
414    /**
415     * Adds a new constructor parameter, returning the new count. This is convienient, because the
416     * first element added is accessed as $1, etc.
417     */
418
419    private int addConstructorParameter(Class type, Object value)
420    {
421        _constructorTypes.add(type);
422        _constructorArguments.add(value);
423
424        return _constructorArguments.size();
425    }
426
427    private BodyBuilder constructorBuilder()
428    {
429        if (_constructorBuilder == null)
430        {
431            _constructorBuilder = new BodyBuilder();
432            _constructorBuilder.begin();
433        }
434
435        return _constructorBuilder;
436    }
437
438    /**
439     * Returns an object that can be used to construct instances of the enhanced component subclass.
440     * This should only be called once.
441     */
442
443    public ComponentConstructor getConstructor()
444    {
445        try
446        {
447            finalizeEnhancedClass();
448
449            Constructor c = findConstructor();
450
451            Object[] params = _constructorArguments.toArray();
452
453            return new ComponentConstructorImpl(c, params, _classFab.toString(), _specification
454                    .getLocation());
455        }
456        catch (Throwable t)
457        {
458            throw new ApplicationRuntimeException(EnhanceMessages.classEnhancementFailure(
459                    _baseClass,
460                    t), _classFab, null, t);
461        }
462    }
463
464    void finalizeEnhancedClass()
465    {
466        finalizeIncompleteMethods();
467
468        if (_constructorBuilder != null)
469        {
470            _constructorBuilder.end();
471
472            Class[] types = (Class[]) _constructorTypes
473                    .toArray(new Class[_constructorTypes.size()]);
474
475            _classFab.addConstructor(types, null, _constructorBuilder.toString());
476        }
477
478        if (_log != null)
479            _log.debug("Creating class:\n\n" + _classFab);
480    }
481
482    private void finalizeIncompleteMethods()
483    {
484        Iterator i = _incompleteMethods.entrySet().iterator();
485        while (i.hasNext())
486        {
487            Map.Entry e = (Map.Entry) i.next();
488            MethodSignature sig = (MethodSignature) e.getKey();
489            BodyBuilder builder = (BodyBuilder) e.getValue();
490
491            // Each BodyBuilder is created and given a begin(), this is
492            // the matching end()
493
494            builder.end();
495
496            _classFab.addMethod(Modifier.PUBLIC, sig, builder.toString());
497        }
498    }
499
500    private Constructor findConstructor()
501    {
502        Class componentClass = _classFab.createClass();
503
504        // The fabricated base class always has exactly one constructor
505
506        return componentClass.getConstructors()[0];
507    }
508
509    static int _uid = 0;
510
511    private String newClassName()
512    {
513        String baseName = _baseClass.getName();
514        int dotx = baseName.lastIndexOf('.');
515
516        return "$" + baseName.substring(dotx + 1) + "_" + _uid++;
517    }
518
519    public void extendMethodImplementation(Class interfaceClass, MethodSignature methodSignature,
520            String code)
521    {
522        addInterfaceIfNeeded(interfaceClass);
523
524        BodyBuilder builder = (BodyBuilder) _incompleteMethods.get(methodSignature);
525
526        if (builder == null)
527        {
528            builder = createIncompleteMethod(methodSignature);
529
530            _incompleteMethods.put(methodSignature, builder);
531        }
532
533        builder.addln(code);
534    }
535
536    private void addInterfaceIfNeeded(Class interfaceClass)
537    {
538        if (implementsInterface(interfaceClass))
539            return;
540
541        _classFab.addInterface(interfaceClass);
542        _addedInterfaces.add(interfaceClass);
543    }
544
545    public boolean implementsInterface(Class interfaceClass)
546    {
547        if (interfaceClass.isAssignableFrom(_baseClass))
548            return true;
549
550        Iterator i = _addedInterfaces.iterator();
551        while (i.hasNext())
552        {
553            Class addedInterface = (Class) i.next();
554
555            if (interfaceClass.isAssignableFrom(addedInterface))
556                return true;
557        }
558
559        return false;
560    }
561
562    private BodyBuilder createIncompleteMethod(MethodSignature sig)
563    {
564        BodyBuilder result = new BodyBuilder();
565
566        // Matched inside finalizeIncompleteMethods()
567
568        result.begin();
569
570        if (existingImplementation(sig))
571            result.addln("super.{0}($$);", sig.getName());
572
573        return result;
574    }
575
576    /**
577     * Returns true if the base class implements the provided method as either a public or a
578     * protected method.
579     */
580
581    private boolean existingImplementation(MethodSignature sig)
582    {
583        Method m = findMethod(sig);
584
585        return m != null && !Modifier.isAbstract(m.getModifiers());
586    }
587
588    /**
589     * Finds a public or protected method in the base class.
590     */
591    private Method findMethod(MethodSignature sig)
592    {
593        // Finding a public method is easy:
594
595        try
596        {
597            return _baseClass.getMethod(sig.getName(), sig.getParameterTypes());
598
599        }
600        catch (NoSuchMethodException ex)
601        {
602            // Good; no super-implementation to invoke.
603        }
604
605        Class c = _baseClass;
606
607        while (c != Object.class)
608        {
609            try
610            {
611                return c.getDeclaredMethod(sig.getName(), sig.getParameterTypes());
612            }
613            catch (NoSuchMethodException ex)
614            {
615                // Ok, continue loop up to next base class.
616            }
617
618            c = c.getSuperclass();
619        }
620
621        return null;
622    }
623
624    public List findUnclaimedAbstractProperties()
625    {
626        List result = new ArrayList();
627
628        Iterator i = _properties.values().iterator();
629
630        while (i.hasNext())
631        {
632            PropertyDescriptor pd = (PropertyDescriptor) i.next();
633
634            String name = pd.getName();
635
636            if (_claimedProperties.contains(name))
637                continue;
638
639            if (isAbstractProperty(pd))
640                result.add(name);
641        }
642
643        return result;
644    }
645
646    /**
647     * A property is abstract if either its read method or it write method is abstract. We could do
648     * some additional checking to ensure that both are abstract if either is. Note that in many
649     * cases, there will only be one accessor (a reader or a writer).
650     */
651    private boolean isAbstractProperty(PropertyDescriptor pd)
652    {
653        return isExistingAbstractMethod(pd.getReadMethod())
654                || isExistingAbstractMethod(pd.getWriteMethod());
655    }
656
657    private boolean isExistingAbstractMethod(Method m)
658    {
659        return m != null && Modifier.isAbstract(m.getModifiers());
660    }
661}