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 }