View Javadoc
1   /**
2    * This file Copyright (c) 2010-2015 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.util.ArrayList;
39  import java.util.HashMap;
40  import java.util.List;
41  import java.util.Map;
42  import javax.jcr.Node;
43  
44  import org.apache.commons.lang.StringUtils;
45  import org.slf4j.Logger;
46  import org.slf4j.LoggerFactory;
47  import org.springframework.util.ClassUtils;
48  import org.springframework.util.ReflectionUtils;
49  
50  import info.magnolia.cms.core.Content;
51  import info.magnolia.cms.util.ContentUtil;
52  import info.magnolia.module.blossom.annotation.Area;
53  import info.magnolia.module.blossom.annotation.AutoGenerator;
54  import info.magnolia.module.blossom.annotation.Available;
55  import info.magnolia.module.blossom.annotation.AvailableComponentClasses;
56  import info.magnolia.module.blossom.annotation.AvailableComponents;
57  import info.magnolia.module.blossom.annotation.ComponentCategory;
58  import info.magnolia.module.blossom.annotation.I18nBasename;
59  import info.magnolia.module.blossom.annotation.Inherits;
60  import info.magnolia.module.blossom.annotation.Template;
61  import info.magnolia.module.blossom.annotation.TemplateDescription;
62  import info.magnolia.module.blossom.annotation.TernaryBoolean;
63  import info.magnolia.module.blossom.dispatcher.BlossomDispatcher;
64  import info.magnolia.module.blossom.support.MethodInvocationUtils;
65  import info.magnolia.module.blossom.support.ParameterResolver;
66  import info.magnolia.rendering.engine.RenderException;
67  import info.magnolia.rendering.generator.Generator;
68  import info.magnolia.rendering.template.AreaDefinition;
69  import info.magnolia.rendering.template.AutoGenerationConfiguration;
70  import info.magnolia.rendering.template.ComponentAvailability;
71  import info.magnolia.rendering.template.InheritanceConfiguration;
72  import info.magnolia.rendering.template.TemplateAvailability;
73  import info.magnolia.rendering.template.TemplateDefinition;
74  import info.magnolia.rendering.template.configured.ConfiguredComponentAvailability;
75  import info.magnolia.rendering.template.configured.ConfiguredInheritance;
76  
77  /**
78   * Builds template descriptions from annotations.
79   *
80   * @since 1.0
81   */
82  public class TemplateDefinitionBuilder {
83  
84      private final Logger logger = LoggerFactory.getLogger(getClass());
85  
86      public BlossomTemplateDefinition buildTemplateDefinition(BlossomDispatcher dispatcher, DetectedHandlersMetaData detectedHandlers, HandlerMetaData template) {
87  
88          Class<?> handlerClass = template.getHandlerClass();
89          Object handler = template.getHandler();
90          String handlerPath = template.getHandlerPath();
91          Template annotation = handlerClass.getAnnotation(Template.class);
92  
93          BlossomTemplateDefinition definition = new BlossomTemplateDefinition();
94          definition.setId(resolveTemplateId(handlerClass));
95          definition.setName(definition.getId());
96          definition.setTitle(resolveTemplateTitle(template));
97          definition.setDescription(resolveDescription(template));
98          definition.setI18nBasename(getI18nBasename(template));
99          definition.setHandlerPath(handlerPath);
100         definition.setDialog(StringUtils.trimToNull(annotation.dialog()));
101         definition.setVisible(annotation.visible());
102         definition.setDispatcher(dispatcher);
103         definition.setHandler(handler);
104         TemplateAvailability templateAvailability = resolveTemplateAvailability(template);
105         if (templateAvailability != null) {
106             definition.setTemplateAvailability(templateAvailability);
107         }
108         definition.setRenderType("blossom");
109 
110         definition.setAreas(buildAreaDefinitionsForTemplate(dispatcher, detectedHandlers, template));
111 
112         return definition;
113     }
114 
115     protected String resolveTemplateId(Class<?> handlerClass) {
116         Template annotation = handlerClass.getAnnotation(Template.class);
117         if (annotation == null) {
118             throw new IllegalArgumentException("Could not resolve template id, @Template is not present on class [" + handlerClass.getName() + "]");
119         }
120         return annotation.id();
121     }
122 
123     protected String resolveTemplateTitle(HandlerMetaData template) {
124         Template annotation = template.getHandlerClass().getAnnotation(Template.class);
125         return annotation.title();
126     }
127 
128     protected String resolveDescription(HandlerMetaData template) {
129         TemplateDescription templateDescription = template.getHandlerClass().getAnnotation(TemplateDescription.class);
130         if (templateDescription != null && StringUtils.isNotEmpty(templateDescription.value())) {
131             return templateDescription.value();
132         }
133         return "";
134     }
135 
136     protected String getI18nBasename(HandlerMetaData handler) {
137         I18nBasename i18nBasename = handler.getHandlerClass().getAnnotation(I18nBasename.class);
138         return i18nBasename != null ? i18nBasename.value() : null;
139     }
140 
141     protected Map<String, AreaDefinition> buildAreaDefinitionsForTemplate(BlossomDispatcher dispatcher, DetectedHandlersMetaData detectedHandlers, HandlerMetaData template) {
142 
143         Map<String, AreaDefinition> areas = new HashMap<String, AreaDefinition>();
144 
145         Class<?> handlerClass = template.getHandlerClass();
146         while (handlerClass != null) {
147 
148             List<HandlerMetaData> list = detectedHandlers.getAreasByEnclosingClass(handlerClass);
149             if (list != null) {
150                 for (HandlerMetaData area : list) {
151                     BlossomAreaDefinition areaDefinition = buildAreaDefinition(dispatcher, detectedHandlers, area);
152                     areas.put(areaDefinition.getId(), areaDefinition);
153                 }
154             }
155 
156             handlerClass = handlerClass.getSuperclass();
157         }
158 
159         return areas;
160     }
161 
162     protected BlossomAreaDefinition buildAreaDefinition(BlossomDispatcher dispatcher, DetectedHandlersMetaData detectedHandlers, HandlerMetaData area) {
163 
164         Area annotation = area.getHandlerClass().getAnnotation(Area.class);
165 
166         BlossomAreaDefinition definition = new BlossomAreaDefinition();
167         definition.setId(annotation.value());
168         definition.setName(annotation.value());
169         definition.setTitle(StringUtils.isNotEmpty(annotation.title()) ? annotation.title() : StringUtils.capitalize(annotation.value()));
170         definition.setRenderType("blossom");
171         definition.setHandlerPath(area.getHandlerPath());
172         definition.setHandler(area.getHandler());
173         definition.setDispatcher(dispatcher);
174         definition.setDialog(StringUtils.trimToNull(annotation.dialog()));
175         definition.setI18nBasename(getI18nBasename(area));
176         definition.setType(annotation.type().getDefinitionFormat());
177         if (annotation.maxComponents() != Integer.MAX_VALUE) {
178             definition.setMaxComponents(annotation.maxComponents());
179         }
180         if (annotation.optional() != TernaryBoolean.UNSPECIFIED) {
181             definition.setOptional(TernaryBoolean.toBoolean(annotation.optional()));
182         }
183         if (annotation.createAreaNode() != TernaryBoolean.UNSPECIFIED) {
184             Boolean createAreaNode = TernaryBoolean.toBoolean(annotation.createAreaNode());
185             // This feature was introduced in Magnolia 5.2.4 (MAGNOLIA-4711) to remain compatible with earlier
186             // versions this flag is set using reflection.
187             Method method = ReflectionUtils.findMethod(definition.getClass(), "setCreateAreaNode", Boolean.class);
188             if (method != null)
189                 ReflectionUtils.invokeMethod(method, definition, createAreaNode);
190         }
191 
192         // If the templateScript is null the area is rendered simply by looping the components, to prevent this we
193         // need to set it to something. The actual template script used later when rendering will be the one returned
194         // by the controller
195         definition.setTemplateScript("<area-script-placeholder>");
196 
197         definition.setInheritance(resolveInheritanceConfiguration(area));
198         definition.setAvailableComponents(resolveAvailableComponents(detectedHandlers, area));
199         definition.setAreas(buildAreaDefinitionsForTemplate(dispatcher, detectedHandlers, area));
200         definition.setAutoGeneration(resolveAutoGenerationConfiguration(definition, area));
201 
202         return definition;
203     }
204 
205     protected Map<String, ComponentAvailability> resolveAvailableComponents(DetectedHandlersMetaData detectedHandlers, HandlerMetaData area) {
206         Map<String, ComponentAvailability> map = new HashMap<String, ComponentAvailability>();
207         AvailableComponents availableComponents = area.getHandlerClass().getAnnotation(AvailableComponents.class);
208         if (availableComponents != null) {
209             for (String componentId : availableComponents.value()) {
210                 ConfiguredComponentAvailability availability = new ConfiguredComponentAvailability();
211                 availability.setId(componentId);
212                 map.put(componentId, availability);
213             }
214         }
215         AvailableComponentClasses availableComponentClasses = area.getHandlerClass().getAnnotation(AvailableComponentClasses.class);
216         if (availableComponentClasses != null) {
217             for (Class<?> clazz : availableComponentClasses.value()) {
218                 if (clazz.isAnnotation()) {
219                     if (!clazz.isAnnotationPresent(ComponentCategory.class)) {
220                         throw new IllegalArgumentException("Annotation [" + clazz.getName() + "] specified on area [" + area.getHandlerClass().getName() + "] is not a @ComponentCategory");
221                     }
222 
223                     List<String> templatesInCategory = detectedHandlers.getTemplatesInCategory((Class<? extends Annotation>) clazz);
224                     for (String componentId : templatesInCategory) {
225                         ConfiguredComponentAvailability availability = new ConfiguredComponentAvailability();
226                         availability.setId(componentId);
227                         map.put(componentId, availability);
228                     }
229                 } else {
230                     String componentId = resolveTemplateId(clazz);
231                     ConfiguredComponentAvailability availability = new ConfiguredComponentAvailability();
232                     availability.setId(componentId);
233                     map.put(componentId, availability);
234                 }
235             }
236         }
237         return map;
238     }
239 
240     protected InheritanceConfiguration resolveInheritanceConfiguration(HandlerMetaData area) {
241         Inherits inherits = area.getHandlerClass().getAnnotation(Inherits.class);
242         ConfiguredInheritance inheritance = new ConfiguredInheritance();
243         if (inherits != null) {
244             inheritance.setEnabled(true);
245             inheritance.setComponents(inherits.components().getConfigurationFormat());
246             inheritance.setProperties(inherits.properties().getConfigurationFormat());
247         }
248         return inheritance;
249     }
250 
251     protected TemplateAvailability resolveTemplateAvailability(HandlerMetaData template) {
252         Method method = resolveTemplateAvailabilityMethod(template);
253         if (method != null) {
254             return new DefaultTemplateAvailability(template.getHandler(), method);
255         }
256         return null;
257     }
258 
259     protected Method resolveTemplateAvailabilityMethod(HandlerMetaData template) {
260 
261         final List<Method> matchingMethods = findMethodsAnnotatedWith(template.getHandlerClass(), Available.class);
262 
263         if (matchingMethods.size() == 0) {
264             return null;
265         }
266         if (matchingMethods.size() != 1) {
267             throw new IllegalStateException("Multiple @Available annotated methods found for handler [" + template.getHandlerClass() + "]");
268         }
269         Method method = matchingMethods.get(0);
270         if (!method.getReturnType().equals(Boolean.TYPE)) {
271             if (logger.isWarnEnabled()) {
272                 logger.error("Method annotated with @Available has wrong return type [" + method.getClass() + "] should be boolean.");
273             }
274             return null;
275         }
276         return method;
277     }
278 
279     protected ParameterResolver getTemplateAvailabilityParameters(final Node node, final TemplateDefinition templateDefinition) {
280         return new ParameterResolver() {
281 
282             @Override
283             public Object resolveParameter(Class<?> parameterType) {
284                 if (parameterType.equals(Node.class)) {
285                     return node;
286                 }
287                 if (parameterType.equals(Content.class)) {
288                     return ContentUtil.asContent(node);
289                 }
290                 if (parameterType.equals(TemplateDefinition.class)) {
291                     return templateDefinition;
292                 }
293                 return super.resolveParameter(parameterType);
294             }
295         };
296     }
297 
298     private class DefaultTemplateAvailability implements TemplateAvailability {
299 
300         private final Object handler;
301         private final Method method;
302 
303         public DefaultTemplateAvailability(Object handler, Method method) {
304             this.handler = handler;
305             this.method = method;
306         }
307 
308         @Override
309         public boolean isAvailable(Node node, TemplateDefinition templateDefinition) {
310             ParameterResolver parameters = getTemplateAvailabilityParameters(node, templateDefinition);
311             return (Boolean) MethodInvocationUtils.invoke(method, handler, parameters);
312         }
313     }
314 
315     private AutoGenerationConfiguration resolveAutoGenerationConfiguration(BlossomAreaDefinition definition, HandlerMetaData template) {
316         final List<Method> matchingMethods = findMethodsAnnotatedWith(template.getHandlerClass(), AutoGenerator.class);
317 
318         if (matchingMethods.size() == 0) {
319             return null;
320         }
321         if (matchingMethods.size() != 1) {
322             throw new IllegalStateException("Multiple @AutoGenerator annotated methods found for handler [" + template.getHandlerClass() + "]");
323         }
324         return new BlossomAutoGenerationConfiguration(this, definition, template.getHandler(), matchingMethods.get(0));
325     }
326 
327     /**
328      * Generator configuration that keeps references needed to perform a method invocation on the handler.
329      *
330      * @see AutoGenerationConfiguration
331      * @see BlossomGenerator
332      * @since 3.0.2
333      */
334     public static class BlossomAutoGenerationConfiguration implements AutoGenerationConfiguration {
335 
336         private TemplateDefinitionBuilder templateDefinitionBuilder;
337         private final BlossomAreaDefinition definition;
338         private final Object handler;
339         private final Method method;
340 
341         public BlossomAutoGenerationConfiguration(TemplateDefinitionBuilder templateDefinitionBuilder, BlossomAreaDefinition definition, Object handler, Method method) {
342             this.templateDefinitionBuilder = templateDefinitionBuilder;
343             this.definition = definition;
344             this.handler = handler;
345             this.method = method;
346         }
347 
348         public TemplateDefinitionBuilder getTemplateDefinitionBuilder() {
349             return templateDefinitionBuilder;
350         }
351 
352         public Object getHandler() {
353             return handler;
354         }
355 
356         public Method getMethod() {
357             return method;
358         }
359 
360         public BlossomAreaDefinition getDefinition() {
361             return definition;
362         }
363 
364         @Override
365         public Map<String, Object> getContent() {
366             return null;
367         }
368 
369         @Override
370         public Class getGeneratorClass() {
371             return (Class) BlossomGenerator.class;
372         }
373     }
374 
375     /**
376      * Invokes a method used to auto generate content.
377      *
378      * @see Generator
379      * @see BlossomAutoGenerationConfiguration
380      * @since 3.0.2
381      */
382     public static class BlossomGenerator implements Generator<BlossomAutoGenerationConfiguration> {
383 
384         private final Node node;
385 
386         public BlossomGenerator(Node node) {
387             this.node = node;
388         }
389 
390         public Node getNode() {
391             return node;
392         }
393 
394         @Override
395         public void generate(final BlossomAutoGenerationConfiguration configuration) throws RenderException {
396             ParameterResolver parameters = configuration.getTemplateDefinitionBuilder().getAutoGenerationParameters(configuration, node);
397             MethodInvocationUtils.invoke(configuration.getMethod(), configuration.getHandler(), parameters);
398         }
399     }
400 
401     protected ParameterResolver getAutoGenerationParameters(final BlossomAutoGenerationConfiguration configuration, final Node node) {
402         return new ParameterResolver() {
403 
404             @Override
405             public Object resolveParameter(Class<?> parameterType) {
406                 if (parameterType.equals(Node.class)) {
407                     return node;
408                 }
409                 if (parameterType.isAssignableFrom(BlossomAreaDefinition.class)) {
410                     return configuration.getDefinition();
411                 }
412                 return super.resolveParameter(parameterType);
413             }
414         };
415     }
416 
417     /**
418      * Finds methods with a particular annotation by inspecting the class collecting all matching methods and considering
419      * the super class only if none are found. This has the effect that methods in superclasses are ignored if any are
420      * present deeper in the hierarchy.
421      *
422      * @param clazz class to inspect
423      * @param annotationClass annotation to look for
424      * @return all matching methods on the class deepest in the hierarchy having at least one
425      */
426     private static List<Method> findMethodsAnnotatedWith(Class<?> clazz, Class<? extends Annotation> annotationClass) {
427         List<Method> matchingMethods = new ArrayList<Method>();
428         Class<?> currentClass = clazz;
429         while (matchingMethods.isEmpty() && currentClass != null) {
430             Method[] methods = currentClass.getDeclaredMethods();
431             for (final Method method : methods) {
432 
433                 // The method must have the annotation
434                 if (!method.isAnnotationPresent(annotationClass)) {
435                     continue;
436                 }
437 
438                 // The method must not be overridden
439                 if (!method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
440                     continue;
441                 }
442 
443                 matchingMethods.add(method);
444             }
445             currentClass = currentClass.getSuperclass();
446         }
447         return matchingMethods;
448     }
449 }