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