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 }