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,
467                        _classFab.toString(), _specification.getLocation());
468            }
469            catch (Throwable t)
470            {
471                throw new ApplicationRuntimeException(EnhanceMessages.classEnhancementFailure(_baseClass, t), _classFab, null, t);
472            }
473        }
474    
475        void finalizeEnhancedClass()
476        {
477            finalizeIncompleteMethods();
478    
479            if (_constructorBuilder != null)
480            {
481                _constructorBuilder.end();
482                
483                Class[] types = (Class[]) _constructorTypes.toArray(new Class[_constructorTypes.size()]);
484                
485                _classFab.addConstructor(types, null, _constructorBuilder.toString());
486            }
487    
488            if (_log != null && _log.isDebugEnabled()) 
489                _log.debug("Creating class:\n\n" + _classFab);
490        }
491    
492        private void finalizeIncompleteMethods()
493        {
494            Iterator i = _incompleteMethods.entrySet().iterator();
495            while(i.hasNext())
496            {
497                Map.Entry e = (Map.Entry) i.next();
498                MethodSignature sig = (MethodSignature) e.getKey();
499                BodyBuilder builder = (BodyBuilder) e.getValue();
500    
501                // Each BodyBuilder is created and given a begin(), this is
502                // the matching end()
503    
504                builder.end();
505    
506                _classFab.addMethod(Modifier.PUBLIC, sig, builder.toString());
507            }
508        }
509    
510        private Constructor findConstructor()
511        {
512            Class componentClass = _classFab.createClass();
513            
514            // The fabricated base class always has exactly one constructor
515    
516            return componentClass.getConstructors()[0];
517        }
518    
519        private String newClassName()
520        {
521            String baseName = _baseClass.getName();
522            int dotx = baseName.lastIndexOf('.');
523    
524            return "$" + baseName.substring(dotx + 1) + "_" + _uid++;
525        }
526    
527        public void extendMethodImplementation(Class interfaceClass, MethodSignature methodSignature, String code)
528        {
529            addInterfaceIfNeeded(interfaceClass);
530    
531            BodyBuilder builder = (BodyBuilder) _incompleteMethods.get(methodSignature);
532    
533            if (builder == null)
534            {
535                builder = createIncompleteMethod(methodSignature);
536    
537                _incompleteMethods.put(methodSignature, builder);
538            }
539    
540            builder.addln(code);
541        }
542    
543        private void addInterfaceIfNeeded(Class interfaceClass)
544        {
545            if (implementsInterface(interfaceClass)) 
546                return;
547    
548            _classFab.addInterface(interfaceClass);
549            _addedInterfaces.add(interfaceClass);
550        }
551    
552        public boolean implementsInterface(Class interfaceClass)
553        {
554            if (interfaceClass.isAssignableFrom(_baseClass)) 
555                return true;
556    
557            Iterator i = _addedInterfaces.iterator();
558            while(i.hasNext())
559            {
560                Class addedInterface = (Class) i.next();
561    
562                if (interfaceClass.isAssignableFrom(addedInterface)) 
563                    return true;
564            }
565    
566            return false;
567        }
568    
569        private BodyBuilder createIncompleteMethod(MethodSignature sig)
570        {
571            BodyBuilder result = new BodyBuilder();
572    
573            // Matched inside finalizeIncompleteMethods()
574    
575            result.begin();
576    
577            if (existingImplementation(sig))
578                result.addln("super.{0}($$);", sig.getName());
579    
580            return result;
581        }
582    
583        /**
584         * Returns true if the base class implements the provided method as either a
585         * public or a protected method.
586         */
587    
588        private boolean existingImplementation(MethodSignature sig)
589        {
590            Method m = findMethod(sig);
591    
592            return m != null && !Modifier.isAbstract(m.getModifiers());
593        }
594    
595        /**
596         * Finds a public or protected method in the base class.
597         */
598        private Method findMethod(MethodSignature sig)
599        {
600            // Finding a public method is easy:
601    
602            try
603            {
604                return _baseClass.getMethod(sig.getName(), sig.getParameterTypes());
605    
606            }
607            catch (NoSuchMethodException ex)
608            {
609                // Good; no super-implementation to invoke.
610            }
611    
612            Class c = _baseClass;
613    
614            while(c != Object.class)
615            {
616                try
617                {
618                    return c.getDeclaredMethod(sig.getName(), sig
619                            .getParameterTypes());
620                }
621                catch (NoSuchMethodException ex)
622                {
623                    // Ok, continue loop up to next base class.
624                }
625    
626                c = c.getSuperclass();
627            }
628    
629            return null;
630        }
631    
632        public List findUnclaimedAbstractProperties()
633        {
634            List result = new ArrayList();
635    
636            Iterator i = _properties.values().iterator();
637    
638            while(i.hasNext())
639            {
640                PropertyDescriptor pd = (PropertyDescriptor) i.next();
641    
642                String name = pd.getName();
643    
644                if (_claimedProperties.contains(name)) 
645                    continue;
646    
647                if (isAbstractProperty(pd)) 
648                    result.add(name);
649            }
650    
651            return result;
652        }
653    
654        /**
655         * A property is abstract if either its read method or it write method is
656         * abstract. We could do some additional checking to ensure that both are
657         * abstract if either is. Note that in many cases, there will only be one
658         * accessor (a reader or a writer).
659         */
660        private boolean isAbstractProperty(PropertyDescriptor pd)
661        {
662            return isExistingAbstractMethod(pd.getReadMethod())
663                    || isExistingAbstractMethod(pd.getWriteMethod());
664        }
665    
666        private boolean isExistingAbstractMethod(Method m)
667        {
668            return m != null && Modifier.isAbstract(m.getModifiers());
669        }
670    }