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 }