View Javadoc
1   /**
2    * This file Copyright (c) 2010-2016 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.module.blossom.template;
35  
36  import java.lang.annotation.Annotation;
37  import java.lang.reflect.Method;
38  import java.lang.reflect.Modifier;
39  import java.util.ArrayList;
40  import java.util.Collection;
41  import java.util.Collections;
42  import java.util.HashSet;
43  import java.util.List;
44  import java.util.Map;
45  import java.util.Set;
46  import java.util.concurrent.atomic.AtomicReference;
47  
48  import org.apache.commons.lang.StringUtils;
49  import org.slf4j.Logger;
50  import org.slf4j.LoggerFactory;
51  import org.springframework.aop.support.AopUtils;
52  import org.springframework.beans.BeansException;
53  import org.springframework.beans.factory.InitializingBean;
54  import org.springframework.beans.factory.config.BeanPostProcessor;
55  import org.springframework.context.ApplicationEvent;
56  import org.springframework.context.ApplicationListener;
57  import org.springframework.util.ClassUtils;
58  import org.springframework.util.ReflectionUtils;
59  import org.springframework.web.method.HandlerMethod;
60  import org.springframework.web.servlet.handler.AbstractUrlHandlerMapping;
61  import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
62  import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
63  
64  import info.magnolia.config.registry.DefinitionProvider;
65  import info.magnolia.module.blossom.annotation.Area;
66  import info.magnolia.module.blossom.annotation.ComponentCategory;
67  import info.magnolia.module.blossom.annotation.PreRegister;
68  import info.magnolia.module.blossom.annotation.Template;
69  import info.magnolia.module.blossom.dialog.BlossomDialogDefinitionProvider;
70  import info.magnolia.module.blossom.dialog.BlossomDialogDescription;
71  import info.magnolia.module.blossom.dialog.DialogDescriptionBuilder;
72  import info.magnolia.module.blossom.dispatcher.BlossomDispatcher;
73  import info.magnolia.module.blossom.dispatcher.BlossomDispatcherAware;
74  import info.magnolia.module.blossom.dispatcher.BlossomDispatcherInitializedEvent;
75  import info.magnolia.module.blossom.support.MethodInvocationUtils;
76  import info.magnolia.module.blossom.support.ParameterResolver;
77  import info.magnolia.objectfactory.Components;
78  import info.magnolia.rendering.template.AreaDefinition;
79  import info.magnolia.rendering.template.TemplateDefinition;
80  import info.magnolia.rendering.template.registry.TemplateDefinitionRegistry;
81  import info.magnolia.ui.dialog.definition.DialogDefinition;
82  import info.magnolia.ui.dialog.registry.DialogDefinitionRegistry;
83  
84  /**
85   * Detects templates by inspecting HandlerMappings.
86   *
87   * @since 1.1.1
88   */
89  public class TemplateExporter implements BeanPostProcessor, InitializingBean, ApplicationListener, BlossomDispatcherAware {
90  
91      private static final String TEMPLATE_DIALOG_PREFIX = "blossom-template-dialog:";
92      private static final String AREA_DIALOG_PREFIX = "blossom-area-dialog:";
93  
94      private final Logger logger = LoggerFactory.getLogger(getClass());
95  
96      private BlossomDispatcher dispatcher;
97      private TemplateDefinitionBuilder templateDefinitionBuilder;
98      private DialogDescriptionBuilder dialogDescriptionBuilder;
99  
100     private DetectedHandlersMetaData detectedHandlers = new DetectedHandlersMetaData();
101 
102     public void setTemplateDefinitionBuilder(TemplateDefinitionBuilder templateDefinitionBuilder) {
103         this.templateDefinitionBuilder = templateDefinitionBuilder;
104     }
105 
106     public void setDialogDescriptionBuilder(DialogDescriptionBuilder dialogDescriptionBuilder) {
107         this.dialogDescriptionBuilder = dialogDescriptionBuilder;
108     }
109 
110     @Override
111     public void setBlossomDispatcher(BlossomDispatcher dispatcher) {
112         this.dispatcher = dispatcher;
113     }
114 
115     @Override
116     public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
117         return bean;
118     }
119 
120     @Override
121     public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
122         if (bean instanceof AbstractUrlHandlerMapping) {
123             scanHandlerMapping((AbstractUrlHandlerMapping) bean);
124         }
125         if (bean instanceof RequestMappingInfoHandlerMapping) {
126             scanHandlerMapping((RequestMappingInfoHandlerMapping) bean);
127         }
128         return bean;
129     }
130 
131     protected void scanHandlerMapping(AbstractUrlHandlerMapping handlerMapping) {
132         for (Object object : handlerMapping.getHandlerMap().entrySet()) {
133 
134             Map.Entry entry = (Map.Entry) object;
135             String handlerPath = (String) entry.getKey();
136             Object handler = entry.getValue();
137 
138             postProcessHandler(handler, handlerPath);
139         }
140     }
141 
142     private void scanHandlerMapping(RequestMappingInfoHandlerMapping handlerMapping) {
143 
144         Map<RequestMappingInfo,HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();
145         for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethods.entrySet()) {
146 
147             RequestMappingInfo requestMappingInfo = entry.getKey();
148             HandlerMethod handlerMethod = entry.getValue();
149 
150             Set<String> patterns = requestMappingInfo.getPatternsCondition().getPatterns();
151             if (!patterns.isEmpty()) {
152                 String pattern = patterns.iterator().next();
153                 postProcessHandler(handlerMethod.createWithResolvedBean().getBean(), pattern);
154             }
155         }
156     }
157 
158     protected void postProcessHandler(Object handler, String handlerPath) {
159         Class<?> handlerClass = AopUtils.getTargetClass(handler);
160         if (handlerClass.isAnnotationPresent(Area.class)) {
161             detectedHandlers.addArea(new HandlerMetaData(handler, handlerPath, handlerClass));
162         } else if (handlerClass.isAnnotationPresent(Template.class)) {
163             detectedHandlers.addTemplate(new HandlerMetaData(handler, handlerPath, handlerClass));
164             String templateId = resolveTemplateId(handlerClass);
165             if (isComponent(templateId)) {
166                 List<Class<? extends Annotation>> categories = findAllAnnotationsMetaAnnotatedWith(handlerClass, ComponentCategory.class);
167                 detectedHandlers.addCategories(templateId, categories);
168             }
169         }
170     }
171 
172     @Override
173     public void onApplicationEvent(ApplicationEvent event) {
174         if (event instanceof BlossomDispatcherInitializedEvent && event.getSource() == dispatcher) {
175             exportTemplates();
176         }
177     }
178 
179     protected void exportTemplates() {
180 
181         Set<String> registeredIds = new HashSet<>();
182         for (HandlerMetaData template : detectedHandlers.getTemplates()) {
183 
184             BlossomTemplateDefinition definition = templateDefinitionBuilder.buildTemplateDefinition(dispatcher, detectedHandlers, template);
185 
186             // Avoid registering the same template again, happens when a controller has more than one @RequestMapping
187             if (registeredIds.contains(definition.getId())) {
188                 continue;
189             }
190             registeredIds.add(definition.getId());
191 
192             definition = postProcessTemplateDefinition(definition);
193             if (definition == null) {
194                 continue;
195             }
196 
197             Components.getComponent(TemplateDefinitionRegistry.class).register(createTemplateDefinitionProvider(definition));
198 
199             if (logger.isDebugEnabled()) {
200                 logger.debug("Registered template [" + template.getHandlerClass() + "] with id [" + definition.getId() + "]");
201             }
202 
203             template.setTemplateDefinition(definition);
204 
205             if (StringUtils.isEmpty(definition.getDialog())) {
206                 registerTemplateDialog(definition);
207             }
208 
209             registerDialogFactories(definition);
210 
211             registerAreaDialogs(definition.getAreas().values());
212         }
213 
214         if (logger.isInfoEnabled()) {
215             StringBuilder sb = new StringBuilder();
216             for (HandlerMetaData template : detectedHandlers.getTemplates()) {
217                 if (template.getTemplateDefinition() != null) {
218                     if (sb.length() != 0) {
219                         sb.append(",");
220                     }
221                     sb.append(template.getTemplateDefinition().getId());
222                 }
223             }
224             logger.info("Registered templates [" + sb.toString() + "]");
225         }
226 
227         // We're done with this structure so there's no need to keep it around
228         detectedHandlers = null;
229     }
230 
231     protected void registerDialogFactories(BlossomTemplateDefinition templateDefinition) {
232 
233         List<BlossomDialogDescription> dialogDescriptions = dialogDescriptionBuilder.buildDescriptions(templateDefinition.getHandler());
234 
235         for (BlossomDialogDescription dialogDescription : dialogDescriptions) {
236 
237             dialogDescription = postProcessDialogFactoryDialog(templateDefinition, dialogDescription);
238             if (dialogDescription == null) {
239                 continue;
240             }
241 
242             Components.getComponent(DialogDefinitionRegistry.class).register(createDialogDefinitionProvider(dialogDescription));
243 
244             if (logger.isDebugEnabled()) {
245                 logger.debug("Registered dialog factory within template [" + templateDefinition.getId() + "] with id [" + dialogDescription.getId() + "]");
246             }
247         }
248     }
249 
250     protected void registerTemplateDialog(BlossomTemplateDefinition templateDefinition) {
251 
252         String templateId = templateDefinition.getId();
253 
254         String dialogId = TEMPLATE_DIALOG_PREFIX + AopUtils.getTargetClass(templateDefinition.getHandler()).getName();
255 
256         BlossomDialogDescription dialogDescription = dialogDescriptionBuilder.buildDescription(dialogId, templateDefinition.getTitle(), templateDefinition.getHandler());
257 
258         if (dialogDescription.getFactoryMetaData().isEmpty()) {
259             return;
260         }
261 
262         dialogDescription = postProcessTemplateDialog(templateDefinition, dialogDescription);
263         if (dialogDescription == null) {
264             return;
265         }
266 
267         Components.getComponent(DialogDefinitionRegistry.class).register(createDialogDefinitionProvider(dialogDescription));
268 
269         templateDefinition.setDialog(dialogId);
270 
271         if (logger.isDebugEnabled()) {
272             logger.debug("Registered dialog for template [" + templateId + "] with id [" + dialogId + "]");
273         }
274     }
275 
276     protected void registerAreaDialogs(Collection<AreaDefinition> areas) {
277         for (AreaDefinition areaDefinition : areas) {
278             if (StringUtils.isEmpty(areaDefinition.getDialog())) {
279                 registerAreaDialog((BlossomAreaDefinition) areaDefinition);
280             }
281             registerAreaDialogs(areaDefinition.getAreas().values());
282         }
283     }
284 
285     protected void registerAreaDialog(BlossomAreaDefinition areaDefinition) {
286 
287         String areaName = areaDefinition.getName();
288 
289         String dialogId = AREA_DIALOG_PREFIX + AopUtils.getTargetClass(areaDefinition.getHandler()).getName();
290 
291         BlossomDialogDescription dialogDescription = dialogDescriptionBuilder.buildDescription(dialogId, areaDefinition.getTitle(), areaDefinition.getHandler());
292 
293         if (dialogDescription.getFactoryMetaData().isEmpty()) {
294             return;
295         }
296 
297         dialogDescription = postProcessAreaDialog(areaDefinition, dialogDescription);
298         if (dialogDescription == null) {
299             return;
300         }
301 
302         Components.getComponent(DialogDefinitionRegistry.class).register(createDialogDefinitionProvider(dialogDescription));
303 
304         areaDefinition.setDialog(dialogId);
305 
306         if (logger.isDebugEnabled()) {
307             logger.debug("Registered dialog for area [" + areaName + "] with id [" + dialogId + "]");
308         }
309     }
310 
311     @Override
312     public void afterPropertiesSet() throws Exception {
313         if (templateDefinitionBuilder == null) {
314             templateDefinitionBuilder = new TemplateDefinitionBuilder();
315         }
316         if (dialogDescriptionBuilder == null) {
317             dialogDescriptionBuilder = new DialogDescriptionBuilder();
318         }
319     }
320 
321     protected DefinitionProvider<TemplateDefinition> createTemplateDefinitionProvider(BlossomTemplateDefinition definition) {
322         return new BlossomTemplateDefinitionProvider(definition);
323     }
324 
325     protected DefinitionProvider<DialogDefinition> createDialogDefinitionProvider(BlossomDialogDescription dialogDescription) {
326         return new BlossomDialogDefinitionProvider(dialogDescription);
327     }
328 
329     /**
330      * Allows sub-classes to do post-processing of the template definition before it is registered. If it returns null
331      * the template will not be registered and neither will its dialog or any &#64;DialogFactory methods within it.
332      */
333     protected BlossomTemplateDefinition postProcessTemplateDefinition(final BlossomTemplateDefinition templateDefinition) {
334 
335         templateDefinition.setAreas(postProcessAreaDefinitions(templateDefinition, templateDefinition.getAreas()));
336 
337         Object handler = templateDefinition.getHandler();
338 
339         final Class<?> factoryClass = AopUtils.getTargetClass(handler);
340 
341         final List<Method> matchingMethods = new ArrayList<Method>();
342 
343         ReflectionUtils.doWithMethods(factoryClass, new ReflectionUtils.MethodCallback() {
344 
345             @Override
346             public void doWith(Method method) {
347                 if (method.isAnnotationPresent(PreRegister.class) && method.equals(ClassUtils.getMostSpecificMethod(method, factoryClass))) {
348                     if (Modifier.isStatic(method.getModifiers())) {
349                         throw new IllegalStateException("PreRegister annotation is not supported on static methods");
350                     }
351                     if (method.getParameterTypes().length == 1 && method.getParameterTypes()[0].isAssignableFrom(BlossomTemplateDefinition.class)) {
352                         matchingMethods.add(method);
353                     }
354                 }
355             }
356         });
357         Collections.reverse(matchingMethods);
358 
359         final AtomicReference<BlossomTemplateDefinition> reference = new AtomicReference<BlossomTemplateDefinition>(templateDefinition);
360         for (Method matchingMethod : matchingMethods) {
361             Object returnValue = MethodInvocationUtils.invoke(matchingMethod, handler, new ParameterResolver() {
362                 @Override
363                 public Object resolveParameter(Class<?> parameterType) {
364                     if (parameterType.isAssignableFrom(BlossomTemplateDefinition.class)) {
365                         return reference.get();
366                     }
367                     return super.resolveParameter(parameterType);
368                 }
369             });
370             if (returnValue == null && !matchingMethod.getReturnType().equals(Void.TYPE)) {
371                 return null;
372             }
373             if (returnValue instanceof BlossomTemplateDefinition) {
374                 reference.set((BlossomTemplateDefinition) returnValue);
375             }
376         }
377 
378         return reference.get();
379     }
380 
381     /**
382      * Recursively post-process area definitions starting in bottom up order.
383      */
384     protected Map<String, AreaDefinition> postProcessAreaDefinitions(BlossomTemplateDefinition templateDefinition, Map<String, AreaDefinition> areas) {
385 
386         for (String areaName : areas.keySet()) {
387 
388             BlossomAreaDefinition areaDefinition = (BlossomAreaDefinition) areas.get(areaName);
389             areaDefinition.setAreas(postProcessAreaDefinitions(templateDefinition, areaDefinition.getAreas()));
390 
391             areaDefinition = postProcessAreaDefinition(templateDefinition, areaDefinition);
392             if (areaDefinition == null) {
393                 areas.remove(areaName);
394             } else {
395                 areas.put(areaName, areaDefinition);
396             }
397         }
398 
399         return areas;
400     }
401 
402     protected BlossomAreaDefinition postProcessAreaDefinition(final BlossomTemplateDefinition templateDefinition, BlossomAreaDefinition areaDefinition) {
403 
404         Object handler = areaDefinition.getHandler();
405 
406         final Class<?> factoryClass = AopUtils.getTargetClass(handler);
407 
408         final List<Method> matchingMethods = new ArrayList<Method>();
409 
410         ReflectionUtils.doWithMethods(factoryClass, new ReflectionUtils.MethodCallback() {
411 
412             @Override
413             public void doWith(Method method) {
414                 if (method.isAnnotationPresent(PreRegister.class) && method.equals(ClassUtils.getMostSpecificMethod(method, factoryClass))) {
415                     if (Modifier.isStatic(method.getModifiers())) {
416                         throw new IllegalStateException("PreRegister annotation is not supported on static methods");
417                     }
418                     if (method.getParameterTypes().length == 1 && method.getParameterTypes()[0].isAssignableFrom(BlossomAreaDefinition.class)) {
419                         matchingMethods.add(method);
420                     }
421                 }
422             }
423         });
424         Collections.reverse(matchingMethods);
425 
426         final AtomicReference<BlossomAreaDefinition> reference = new AtomicReference<BlossomAreaDefinition>(areaDefinition);
427         for (Method matchingMethod : matchingMethods) {
428             Object returnValue = MethodInvocationUtils.invoke(matchingMethod, handler, new ParameterResolver() {
429                 @Override
430                 public Object resolveParameter(Class<?> parameterType) {
431                     if (parameterType.isAssignableFrom(BlossomAreaDefinition.class)) {
432                         return reference.get();
433                     }
434                     if (parameterType.isAssignableFrom(BlossomTemplateDefinition.class)) {
435                         return templateDefinition;
436                     }
437                     return super.resolveParameter(parameterType);
438                 }
439             });
440             if (returnValue == null && !matchingMethod.getReturnType().equals(Void.TYPE)) {
441                 return null;
442             }
443             if (returnValue instanceof BlossomAreaDefinition) {
444                 reference.set((BlossomAreaDefinition) returnValue);
445             }
446         }
447 
448         return reference.get();
449     }
450 
451     /**
452      * Allows sub-classes to do post-processing on dialogs for &#64;DialogFactory annotated methods before they're
453      * registered. If it returns null the dialog will not be registered.
454      */
455     protected BlossomDialogDescription postProcessDialogFactoryDialog(BlossomTemplateDefinition templateDefinition, BlossomDialogDescription dialogDescription) {
456         return dialogDescription;
457     }
458 
459     /**
460      * Allows sub-classes to do post-processing on dialogs for templates before they're registered. If it returns null
461      * the dialog will not be registered and the template will not have a dialog.
462      */
463     protected BlossomDialogDescription postProcessTemplateDialog(final BlossomTemplateDefinition templateDefinition, final BlossomDialogDescription dialogDescription) {
464         Object factoryObject = dialogDescription.getFactoryMetaData().getFactoryObject();
465 
466         final Class<?> factoryClass = AopUtils.getTargetClass(factoryObject);
467 
468         final List<Method> matchingMethods = new ArrayList<Method>();
469 
470         ReflectionUtils.doWithMethods(factoryClass, new ReflectionUtils.MethodCallback() {
471 
472             @Override
473             public void doWith(Method method) {
474                 if (method.isAnnotationPresent(PreRegister.class) && method.equals(ClassUtils.getMostSpecificMethod(method, factoryClass))) {
475                     if (Modifier.isStatic(method.getModifiers())) {
476                         throw new IllegalStateException("PreRegister annotation is not supported on static methods");
477                     }
478                     if (method.getParameterTypes().length == 1 && method.getParameterTypes()[0].isAssignableFrom(BlossomDialogDescription.class)) {
479                         matchingMethods.add(method);
480                     }
481                 }
482             }
483         });
484         Collections.reverse(matchingMethods);
485 
486         final AtomicReference<BlossomDialogDescription> reference = new AtomicReference<BlossomDialogDescription>(dialogDescription);
487         for (Method matchingMethod : matchingMethods) {
488             Object returnValue = MethodInvocationUtils.invoke(matchingMethod, factoryObject, new ParameterResolver() {
489                 @Override
490                 public Object resolveParameter(Class<?> parameterType) {
491                     if (parameterType.isAssignableFrom(BlossomDialogDescription.class)) {
492                         return reference.get();
493                     }
494                     if (parameterType.isAssignableFrom(BlossomTemplateDefinition.class)) {
495                         return templateDefinition;
496                     }
497                     return super.resolveParameter(parameterType);
498                 }
499             });
500             if (returnValue == null && !matchingMethod.getReturnType().equals(Void.TYPE)) {
501                 return null;
502             }
503             if (returnValue instanceof BlossomDialogDescription) {
504                 reference.set((BlossomDialogDescription) returnValue);
505             }
506         }
507 
508         return reference.get();
509     }
510 
511     /**
512      * Allows sub-classes to do post-processing on dialogs for areas before they're registered. If it returns null the
513      * dialog will not be registered and the area will not have a dialog.
514      */
515     protected BlossomDialogDescription postProcessAreaDialog(final BlossomAreaDefinition areaDefinition, final BlossomDialogDescription dialogDescription) {
516 
517         Object factoryObject = dialogDescription.getFactoryMetaData().getFactoryObject();
518 
519         final Class<?> factoryClass = AopUtils.getTargetClass(factoryObject);
520 
521         final List<Method> matchingMethods = new ArrayList<Method>();
522 
523         ReflectionUtils.doWithMethods(factoryClass, new ReflectionUtils.MethodCallback() {
524 
525             @Override
526             public void doWith(Method method) {
527                 if (method.isAnnotationPresent(PreRegister.class) && method.equals(ClassUtils.getMostSpecificMethod(method, factoryClass))) {
528                     if (Modifier.isStatic(method.getModifiers())) {
529                         throw new IllegalStateException("PreRegister annotation is not supported on static methods");
530                     }
531                     if (method.getParameterTypes().length >= 1 && method.getParameterTypes()[0].isAssignableFrom(BlossomDialogDescription.class)) {
532                         matchingMethods.add(method);
533                     }
534                 }
535             }
536         });
537         Collections.reverse(matchingMethods);
538 
539         final AtomicReference<BlossomDialogDescription> reference = new AtomicReference<BlossomDialogDescription>(dialogDescription);
540         for (Method matchingMethod : matchingMethods) {
541             Object returnValue = MethodInvocationUtils.invoke(matchingMethod, factoryObject, new ParameterResolver() {
542                 @Override
543                 public Object resolveParameter(Class<?> parameterType) {
544                     if (parameterType.isAssignableFrom(BlossomDialogDescription.class)) {
545                         return reference.get();
546                     }
547                     if (parameterType.isAssignableFrom(BlossomAreaDefinition.class)) {
548                         return areaDefinition;
549                     }
550                     return super.resolveParameter(parameterType);
551                 }
552             });
553             if (returnValue == null && !matchingMethod.getReturnType().equals(Void.TYPE)) {
554                 return null;
555             }
556             if (returnValue instanceof BlossomDialogDescription) {
557                 reference.set((BlossomDialogDescription) returnValue);
558             }
559         }
560 
561         return reference.get();
562     }
563 
564     /**
565      * Resolves the template id for a handler class as specified in the &#64;Template annotation.
566      *
567      * @param handlerClass handler class to find the template id for
568      * @return the template id specified
569      * @throws IllegalArgumentException if the handler class is not annotated with @Template
570      */
571     private String resolveTemplateId(Class<?> handlerClass) {
572         Template annotation = handlerClass.getAnnotation(Template.class);
573         if (annotation == null) {
574             throw new IllegalArgumentException("Could not resolve template id, @Template is not present on class [" + handlerClass.getName() + "]");
575         }
576         return annotation.id();
577     }
578 
579     /**
580      * Returns all annotations on a class that have themselves been annotated with a certain annotation, i.e. meta annotated.
581      *
582      * @param clazz the class to introspect
583      * @param targetAnnotationType the annotation to look for
584      * @return a list of the found annotations, never null
585      */
586     private List<Class<? extends Annotation>> findAllAnnotationsMetaAnnotatedWith(Class<?> clazz, Class<? extends Annotation> targetAnnotationType) {
587         List<Class<? extends Annotation>> annotationsWithMetaAnnotations = new ArrayList<Class<? extends Annotation>>();
588         Annotation[] annotations = clazz.getAnnotations();
589         for (Annotation annotation : annotations) {
590             Class<? extends Annotation> annotationType = annotation.annotationType();
591             Annotation[] metaAnnotations = annotationType.getAnnotations();
592             for (Annotation metaAnnotation : metaAnnotations) {
593                 Class<? extends Annotation> metaAnnotationType = metaAnnotation.annotationType();
594                 if (metaAnnotationType.equals(targetAnnotationType)) {
595                     annotationsWithMetaAnnotations.add(annotationType);
596                 }
597             }
598         }
599         return annotationsWithMetaAnnotations;
600     }
601 
602     /**
603      * Tests if a template id is a component.
604      *
605      * @param templateId the template id to evaluate
606      * @return true if the template id is a component
607      */
608     private boolean isComponent(String templateId) {
609         String path = StringUtils.substringAfter(templateId, ":");
610         path = StringUtils.removeStart(path, "/");
611         return path.startsWith("components/");
612     }
613 }