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