001 // Copyright 2006, 2007, 2008, 2009, 2010, 2011 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.tapestry5.ioc.internal;
016
017 import org.apache.tapestry5.func.F;
018 import org.apache.tapestry5.func.Mapper;
019 import org.apache.tapestry5.func.Predicate;
020 import org.apache.tapestry5.ioc.*;
021 import org.apache.tapestry5.ioc.annotations.*;
022 import org.apache.tapestry5.ioc.def.*;
023 import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
024 import org.apache.tapestry5.ioc.internal.util.InternalUtils;
025 import org.apache.tapestry5.ioc.services.PlasticProxyFactory;
026 import org.slf4j.Logger;
027
028 import java.lang.annotation.Annotation;
029 import java.lang.reflect.InvocationTargetException;
030 import java.lang.reflect.Method;
031 import java.lang.reflect.Modifier;
032 import java.util.*;
033
034 /**
035 * Starting from the Class for a module, identifies all the services (service builder methods),
036 * decorators (service
037 * decorator methods) and (not yet implemented) contributions (service contributor methods).
038 */
039 public class DefaultModuleDefImpl implements ModuleDef2, ServiceDefAccumulator
040 {
041 /**
042 * The prefix used to identify service builder methods.
043 */
044 private static final String BUILD_METHOD_NAME_PREFIX = "build";
045
046 /**
047 * The prefix used to identify service decorator methods.
048 */
049 private static final String DECORATE_METHOD_NAME_PREFIX = "decorate";
050
051 /**
052 * The prefix used to identify service contribution methods.
053 */
054 private static final String CONTRIBUTE_METHOD_NAME_PREFIX = "contribute";
055
056 private static final String ADVISE_METHOD_NAME_PREFIX = "advise";
057
058 private final static Map<Class, ConfigurationType> PARAMETER_TYPE_TO_CONFIGURATION_TYPE = CollectionFactory
059 .newMap();
060
061 private final Class moduleClass;
062
063 private final Logger logger;
064
065 private final PlasticProxyFactory proxyFactory;
066
067 /**
068 * Keyed on service id.
069 */
070 private final Map<String, ServiceDef> serviceDefs = CollectionFactory.newCaseInsensitiveMap();
071
072 /**
073 * Keyed on decorator id.
074 */
075 private final Map<String, DecoratorDef> decoratorDefs = CollectionFactory.newCaseInsensitiveMap();
076
077 private final Map<String, AdvisorDef> advisorDefs = CollectionFactory.newCaseInsensitiveMap();
078
079 private final Set<ContributionDef> contributionDefs = CollectionFactory.newSet();
080
081 private final Set<Class> defaultMarkers = CollectionFactory.newSet();
082
083 private final static Set<Method> OBJECT_METHODS = CollectionFactory.newSet(Object.class.getMethods());
084
085 static
086 {
087 PARAMETER_TYPE_TO_CONFIGURATION_TYPE.put(Configuration.class, ConfigurationType.UNORDERED);
088 PARAMETER_TYPE_TO_CONFIGURATION_TYPE.put(OrderedConfiguration.class, ConfigurationType.ORDERED);
089 PARAMETER_TYPE_TO_CONFIGURATION_TYPE.put(MappedConfiguration.class, ConfigurationType.MAPPED);
090 }
091
092 /**
093 * @param moduleClass the class that is responsible for building services, etc.
094 * @param logger based on the class name of the module
095 * @param proxyFactory factory used to create proxy classes at runtime
096 */
097 public DefaultModuleDefImpl(Class<?> moduleClass, Logger logger, PlasticProxyFactory proxyFactory)
098 {
099 this.moduleClass = moduleClass;
100 this.logger = logger;
101 this.proxyFactory = proxyFactory;
102
103 Marker annotation = moduleClass.getAnnotation(Marker.class);
104
105 if (annotation != null)
106 {
107 InternalUtils.validateMarkerAnnotations(annotation.value());
108 defaultMarkers.addAll(Arrays.asList(annotation.value()));
109 }
110
111 // Want to verify that every public method is meaningful to Tapestry IoC. Remaining methods
112 // might
113 // have typos, i.e., "createFoo" that should be "buildFoo".
114
115 Set<Method> methods = CollectionFactory.newSet(moduleClass.getMethods());
116
117 methods.removeAll(OBJECT_METHODS);
118 removeSyntheticMethods(methods);
119
120 boolean modulePreventsServiceDecoration = moduleClass.getAnnotation(PreventServiceDecoration.class) != null;
121
122 grind(methods, modulePreventsServiceDecoration);
123 bind(methods, modulePreventsServiceDecoration);
124
125 if (methods.isEmpty())
126 return;
127
128 throw new RuntimeException(String.format("Module class %s contains unrecognized public methods: %s.",
129 moduleClass.getName(), InternalUtils.joinSorted(methods)));
130 }
131
132 /**
133 * Identifies the module class and a list of service ids within the module.
134 */
135 @Override
136 public String toString()
137 {
138 return String.format("ModuleDef[%s %s]", moduleClass.getName(), InternalUtils.joinSorted(serviceDefs.keySet()));
139 }
140
141 public Class getBuilderClass()
142 {
143 return moduleClass;
144 }
145
146 public Set<String> getServiceIds()
147 {
148 return serviceDefs.keySet();
149 }
150
151 public ServiceDef getServiceDef(String serviceId)
152 {
153 return serviceDefs.get(serviceId);
154 }
155
156 private void removeSyntheticMethods(Set<Method> methods)
157 {
158 Iterator<Method> iterator = methods.iterator();
159
160 while (iterator.hasNext())
161 {
162 Method m = iterator.next();
163
164 if (m.isSynthetic() || m.getName().startsWith("$"))
165 {
166 iterator.remove();
167 }
168 }
169 }
170
171 private void grind(Set<Method> remainingMethods, boolean modulePreventsServiceDecoration)
172 {
173 Method[] methods = moduleClass.getMethods();
174
175 Comparator<Method> c = new Comparator<Method>()
176 {
177 // By name, ascending, then by parameter count, descending.
178
179 public int compare(Method o1, Method o2)
180 {
181 int result = o1.getName().compareTo(o2.getName());
182
183 if (result == 0)
184 result = o2.getParameterTypes().length - o1.getParameterTypes().length;
185
186 return result;
187 }
188 };
189
190 Arrays.sort(methods, c);
191
192 for (Method m : methods)
193 {
194 String name = m.getName();
195
196 if (name.startsWith(BUILD_METHOD_NAME_PREFIX))
197 {
198 addServiceDef(m, modulePreventsServiceDecoration);
199 remainingMethods.remove(m);
200 continue;
201 }
202
203 if (name.startsWith(DECORATE_METHOD_NAME_PREFIX) || m.isAnnotationPresent(Decorate.class))
204 {
205 addDecoratorDef(m);
206 remainingMethods.remove(m);
207 continue;
208 }
209
210 if (name.startsWith(CONTRIBUTE_METHOD_NAME_PREFIX) || m.isAnnotationPresent(Contribute.class))
211 {
212 addContributionDef(m);
213 remainingMethods.remove(m);
214 continue;
215 }
216
217 if (name.startsWith(ADVISE_METHOD_NAME_PREFIX) || m.isAnnotationPresent(Advise.class))
218 {
219 addAdvisorDef(m);
220 remainingMethods.remove(m);
221 continue;
222 }
223
224 if (m.isAnnotationPresent(Startup.class))
225 {
226 addStartupDef(m);
227 remainingMethods.remove(m);
228 continue;
229 }
230 }
231 }
232
233 private void addStartupDef(Method method)
234 {
235 Set<Class> markers = Collections.emptySet();
236
237 ContributionDef2 def = new ContributionDefImpl("RegistryStartup", method, false, proxyFactory, Runnable.class, markers);
238
239 contributionDefs.add(def);
240 }
241
242 private void addContributionDef(Method method)
243 {
244 Contribute annotation = method.getAnnotation(Contribute.class);
245
246 Class serviceInterface = annotation == null ? null : annotation.value();
247
248 String serviceId = annotation != null ? null : stripMethodPrefix(method, CONTRIBUTE_METHOD_NAME_PREFIX);
249
250 Class returnType = method.getReturnType();
251 if (!returnType.equals(void.class))
252 logger.warn(IOCMessages.contributionWrongReturnType(method));
253
254 ConfigurationType type = null;
255
256 for (Class parameterType : method.getParameterTypes())
257 {
258 ConfigurationType thisParameter = PARAMETER_TYPE_TO_CONFIGURATION_TYPE.get(parameterType);
259
260 if (thisParameter != null)
261 {
262 if (type != null)
263 throw new RuntimeException(IOCMessages.tooManyContributionParameters(method));
264
265 type = thisParameter;
266 }
267 }
268
269 if (type == null)
270 throw new RuntimeException(IOCMessages.noContributionParameter(method));
271
272 Set<Class> markers = extractMarkers(method, Contribute.class, Optional.class);
273
274 boolean optional = method.getAnnotation(Optional.class) != null;
275
276 ContributionDef3 def = new ContributionDefImpl(serviceId, method, optional, proxyFactory, serviceInterface, markers);
277
278 contributionDefs.add(def);
279 }
280
281 private void addDecoratorDef(Method method)
282 {
283 Decorate annotation = method.getAnnotation(Decorate.class);
284
285 Class serviceInterface = annotation == null ? null : annotation.serviceInterface();
286
287 // TODO: methods just named "decorate"
288
289 String decoratorId = annotation == null ? stripMethodPrefix(method, DECORATE_METHOD_NAME_PREFIX) : extractId(
290 serviceInterface, annotation.id());
291
292 // TODO: Check for duplicates
293
294 Class returnType = method.getReturnType();
295
296 if (returnType.isPrimitive() || returnType.isArray())
297 throw new RuntimeException(IOCMessages.decoratorMethodWrongReturnType(method));
298
299 Set<Class> markers = extractMarkers(method, Decorate.class);
300
301 DecoratorDef def = new DecoratorDefImpl(method, extractPatterns(decoratorId, method),
302 extractConstraints(method), proxyFactory, decoratorId, serviceInterface, markers);
303
304 decoratorDefs.put(decoratorId, def);
305 }
306
307 private <T extends Annotation> String[] extractPatterns(String id, Method method)
308 {
309 Match match = method.getAnnotation(Match.class);
310
311 if (match == null)
312 {
313 return new String[]{id};
314 }
315
316 return match.value();
317 }
318
319 private String[] extractConstraints(Method method)
320 {
321 Order order = method.getAnnotation(Order.class);
322
323 if (order == null)
324 return null;
325
326 return order.value();
327 }
328
329 private void addAdvisorDef(Method method)
330 {
331 Advise annotation = method.getAnnotation(Advise.class);
332
333 Class serviceInterface = annotation == null ? null : annotation.serviceInterface();
334
335 // TODO: methods just named "decorate"
336
337 String advisorId = annotation == null ? stripMethodPrefix(method, ADVISE_METHOD_NAME_PREFIX) : extractId(
338 serviceInterface, annotation.id());
339
340 // TODO: Check for duplicates
341
342 Class returnType = method.getReturnType();
343
344 if (!returnType.equals(void.class))
345 throw new RuntimeException(String.format("Advise method %s does not return void.", toString(method)));
346
347 boolean found = false;
348
349 for (Class pt : method.getParameterTypes())
350 {
351 if (pt.equals(MethodAdviceReceiver.class))
352 {
353 found = true;
354
355 break;
356 }
357 }
358
359 if (!found)
360 throw new RuntimeException(String.format("Advise method %s must take a parameter of type %s.",
361 toString(method), MethodAdviceReceiver.class.getName()));
362
363 Set<Class> markers = extractMarkers(method, Advise.class);
364
365 AdvisorDef def = new AdvisorDefImpl(method, extractPatterns(advisorId, method),
366 extractConstraints(method), proxyFactory, advisorId, serviceInterface, markers);
367
368 advisorDefs.put(advisorId, def);
369
370 }
371
372 private String extractId(Class serviceInterface, String id)
373 {
374 return InternalUtils.isBlank(id) ? serviceInterface.getSimpleName() : id;
375 }
376
377 private String toString(Method method)
378 {
379 return InternalUtils.asString(method, proxyFactory);
380 }
381
382 private String stripMethodPrefix(Method method, String prefix)
383 {
384 return method.getName().substring(prefix.length());
385 }
386
387 /**
388 * Invoked for public methods that have the proper prefix.
389 */
390 private void addServiceDef(final Method method, boolean modulePreventsServiceDecoration)
391 {
392 String serviceId = InternalUtils.getServiceId(method);
393
394 if (serviceId == null)
395 {
396 serviceId = stripMethodPrefix(method, BUILD_METHOD_NAME_PREFIX);
397 }
398
399 // If the method name was just "build()", then work from the return type.
400
401 if (serviceId.equals(""))
402 serviceId = method.getReturnType().getSimpleName();
403
404 // Any number of parameters is fine, we'll adapt. Eventually we have to check
405 // that we can satisfy the parameters requested. Thrown exceptions of the method
406 // will be caught and wrapped, so we don't need to check those. But we do need a proper
407 // return type.
408
409 Class returnType = method.getReturnType();
410
411 if (returnType.isPrimitive() || returnType.isArray())
412 throw new RuntimeException(IOCMessages.buildMethodWrongReturnType(method));
413
414 String scope = extractServiceScope(method);
415 boolean eagerLoad = method.isAnnotationPresent(EagerLoad.class);
416
417 boolean preventDecoration = modulePreventsServiceDecoration
418 || method.getAnnotation(PreventServiceDecoration.class) != null;
419
420 ObjectCreatorSource source = new ObjectCreatorSource()
421 {
422 public ObjectCreator constructCreator(ServiceBuilderResources resources)
423 {
424 return new ServiceBuilderMethodInvoker(resources, getDescription(), method);
425 }
426
427 public String getDescription()
428 {
429 return DefaultModuleDefImpl.this.toString(method);
430 }
431 };
432
433 Set<Class> markers = CollectionFactory.newSet(defaultMarkers);
434 markers.addAll(extractServiceDefMarkers(method));
435
436 ServiceDefImpl serviceDef = new ServiceDefImpl(returnType, null, serviceId, markers, scope, eagerLoad,
437 preventDecoration, source);
438
439 addServiceDef(serviceDef);
440 }
441
442 private Collection<Class> extractServiceDefMarkers(Method method)
443 {
444 Marker annotation = method.getAnnotation(Marker.class);
445
446 if (annotation == null)
447 return Collections.emptyList();
448
449 return CollectionFactory.newList(annotation.value());
450 }
451
452 @SuppressWarnings("rawtypes")
453 private Set<Class> extractMarkers(Method method, final Class... annotationClassesToSkip)
454 {
455 return F.flow(method.getAnnotations()).map(new Mapper<Annotation, Class>()
456 {
457 public Class map(Annotation value)
458 {
459 return value.annotationType();
460 }
461 }).filter(new Predicate<Class>()
462 {
463 public boolean accept(Class element)
464 {
465 for (Class skip : annotationClassesToSkip)
466 {
467 if (skip.equals(element))
468 {
469 return false;
470 }
471 }
472
473 return true;
474 }
475 }).toSet();
476 }
477
478 public void addServiceDef(ServiceDef serviceDef)
479 {
480 String serviceId = serviceDef.getServiceId();
481
482 ServiceDef existing = serviceDefs.get(serviceId);
483
484 if (existing != null)
485 throw new RuntimeException(IOCMessages.buildMethodConflict(serviceId, serviceDef.toString(),
486 existing.toString()));
487
488 serviceDefs.put(serviceId, serviceDef);
489 }
490
491 private String extractServiceScope(Method method)
492 {
493 Scope scope = method.getAnnotation(Scope.class);
494
495 return scope != null ? scope.value() : ScopeConstants.DEFAULT;
496 }
497
498 public Set<DecoratorDef> getDecoratorDefs()
499 {
500 return toSet(decoratorDefs);
501 }
502
503 public Set<ContributionDef> getContributionDefs()
504 {
505 return contributionDefs;
506 }
507
508 public String getLoggerName()
509 {
510 return moduleClass.getName();
511 }
512
513 /**
514 * See if the build class defined a bind method and invoke it.
515 *
516 * @param remainingMethods set of methods as yet unaccounted for
517 * @param modulePreventsServiceDecoration
518 * true if {@link org.apache.tapestry5.ioc.annotations.PreventServiceDecoration} on
519 * module
520 * class
521 */
522 private void bind(Set<Method> remainingMethods, boolean modulePreventsServiceDecoration)
523 {
524 Throwable failure;
525 Method bindMethod = null;
526
527 try
528 {
529 bindMethod = moduleClass.getMethod("bind", ServiceBinder.class);
530
531 if (!Modifier.isStatic(bindMethod.getModifiers()))
532 throw new RuntimeException(IOCMessages.bindMethodMustBeStatic(toString(bindMethod)));
533
534 ServiceBinderImpl binder = new ServiceBinderImpl(this, bindMethod, proxyFactory, defaultMarkers,
535 modulePreventsServiceDecoration);
536
537 bindMethod.invoke(null, binder);
538
539 binder.finish();
540
541 remainingMethods.remove(bindMethod);
542
543 return;
544 } catch (NoSuchMethodException ex)
545 {
546 // No problem! Many modules will not have such a method.
547
548 return;
549 } catch (IllegalArgumentException ex)
550 {
551 failure = ex;
552 } catch (IllegalAccessException ex)
553 {
554 failure = ex;
555 } catch (InvocationTargetException ex)
556 {
557 failure = ex.getTargetException();
558 }
559
560 String methodId = toString(bindMethod);
561
562 throw new RuntimeException(IOCMessages.errorInBindMethod(methodId, failure), failure);
563 }
564
565 public Set<AdvisorDef> getAdvisorDefs()
566 {
567 return toSet(advisorDefs);
568 }
569
570 private <K, V> Set<V> toSet(Map<K, V> map)
571 {
572 return CollectionFactory.newSet(map.values());
573 }
574 }