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