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