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.enhance;
016    
017    import java.beans.BeanInfo;
018    import java.beans.IntrospectionException;
019    import java.beans.Introspector;
020    import java.beans.PropertyDescriptor;
021    import java.lang.reflect.Constructor;
022    import java.lang.reflect.Method;
023    import java.lang.reflect.Modifier;
024    import java.util.ArrayList;
025    import java.util.HashMap;
026    import java.util.HashSet;
027    import java.util.Iterator;
028    import java.util.List;
029    import java.util.Map;
030    import java.util.Set;
031    
032    import org.apache.commons.logging.Log;
033    import org.apache.hivemind.ApplicationRuntimeException;
034    import org.apache.hivemind.ClassResolver;
035    import org.apache.hivemind.HiveMind;
036    import org.apache.hivemind.Location;
037    import org.apache.hivemind.service.BodyBuilder;
038    import org.apache.hivemind.service.ClassFab;
039    import org.apache.hivemind.service.ClassFactory;
040    import org.apache.hivemind.service.MethodSignature;
041    import org.apache.hivemind.util.Defense;
042    import org.apache.hivemind.util.ToStringBuilder;
043    import org.apache.tapestry.services.ComponentConstructor;
044    import org.apache.tapestry.spec.IComponentSpecification;
045    import org.apache.tapestry.util.IdAllocator;
046    import 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     */
056    public 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    }