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