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