View Javadoc
1   /**
2    * This file Copyright (c) 2008-2017 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.rendering.renderer;
35  
36  import info.magnolia.cms.beans.config.MIMEMapping;
37  import info.magnolia.cms.core.AggregationState;
38  import info.magnolia.cms.core.HTMLEscapingAggregationState;
39  import info.magnolia.context.MgnlContext;
40  import info.magnolia.jcr.decoration.ContentDecoratorUtil;
41  import info.magnolia.jcr.util.ContentMap;
42  import info.magnolia.jcr.util.NodeUtil;
43  import info.magnolia.jcr.wrapper.ChannelVisibilityContentDecorator;
44  import info.magnolia.jcr.wrapper.HTMLEscapingNodeWrapper;
45  import info.magnolia.jcr.wrapper.I18nNodeWrapper;
46  import info.magnolia.objectfactory.Components;
47  import info.magnolia.objectfactory.MgnlInstantiationException;
48  import info.magnolia.objectfactory.ParameterInfo;
49  import info.magnolia.objectfactory.ParameterResolver;
50  import info.magnolia.rendering.context.RenderingContext;
51  import info.magnolia.rendering.engine.RenderException;
52  import info.magnolia.rendering.engine.RenderingEngine;
53  import info.magnolia.rendering.model.EarlyExecutionAware;
54  import info.magnolia.rendering.model.ModelExecutionFilter;
55  import info.magnolia.rendering.model.RenderingModel;
56  import info.magnolia.rendering.model.RenderingModelImpl;
57  import info.magnolia.rendering.template.RenderableDefinition;
58  
59  import java.lang.reflect.InvocationTargetException;
60  import java.util.HashMap;
61  import java.util.Map;
62  import java.util.Map.Entry;
63  
64  import javax.inject.Inject;
65  import javax.jcr.Node;
66  import javax.jcr.RepositoryException;
67  import javax.servlet.http.HttpServletResponse;
68  
69  import org.apache.commons.beanutils.BeanUtils;
70  import org.apache.commons.lang3.StringUtils;
71  import org.apache.commons.lang3.exception.ExceptionUtils;
72  import org.slf4j.Logger;
73  import org.slf4j.LoggerFactory;
74  
75  /**
76   * Abstract renderer with support for typical functionality such as setting context attributes and executing a
77   * rendering model. Sets up the context by providing the following objects:
78   * <ul>
79   * <li>content</li>
80   * <li>def</li>
81   * <li>state</li>
82   * <li>model</li>
83   * <li>actionResult</li>
84   * </ul>
85   */
86  public abstract class AbstractRenderer implements Renderer, RenderingModelBasedRenderer {
87  
88      private static final Logger log = LoggerFactory.getLogger(AbstractRenderer.class);
89  
90      protected static final String MODEL_ATTRIBUTE = RenderingModel.class.getName();
91  
92      private Map<String, ContextAttributeConfiguration> contextAttributes = new HashMap<String, ContextAttributeConfiguration>();
93  
94      private RenderingEngine renderingEngine = null;
95  
96      private String contentType;
97  
98      /**
99       * @deprecated since 5.0.2, use {@link #AbstractRenderer(RenderingEngine)}
100      */
101     public AbstractRenderer() {
102         this.renderingEngine = Components.getComponent(RenderingEngine.class);
103     }
104 
105     @Inject
106     public AbstractRenderer(RenderingEngine renderingEngine) {
107         this.renderingEngine = renderingEngine;
108     }
109 
110     @Override
111     public void render(RenderingContext renderingCtx, Map<String, Object> contextObjects) throws RenderException {
112 
113         final RenderingModel<?> parentModel = MgnlContext.getAttribute(MODEL_ATTRIBUTE);
114         Node content = renderingCtx.getCurrentContent();
115         RenderableDefinition definition = renderingCtx.getRenderableDefinition();
116 
117         RenderingModel<?> model = null;
118         String actionResult = null;
119 
120         if (content != null) {
121             String uuid;
122             try {
123                 uuid = content.getIdentifier();
124             } catch (RepositoryException e) {
125                 throw new RenderException(e);
126             }
127 
128             model = MgnlContext.getAttribute(ModelExecutionFilter.MODEL_ATTRIBUTE_PREFIX + uuid);
129             if (model != null) {
130                 actionResult = MgnlContext.getAttribute(ModelExecutionFilter.ACTION_RESULT_ATTRIBUTE_PREFIX + uuid);
131                 if (model instanceof EarlyExecutionAware) {
132                     ((EarlyExecutionAware) model).setParent(parentModel);
133                 }
134             }
135         }
136 
137         if (model == null) {
138             model = newModel(content, definition, parentModel);
139             if (model != null) {
140                 actionResult = model.execute();
141                 if (RenderingModel.SKIP_RENDERING.equals(actionResult)) {
142                     return;
143                 }
144             }
145         }
146 
147         String templatePath = resolveTemplateScript(content, definition, model, actionResult);
148         if (templatePath == null) {
149             throw new RenderException("No template script defined for the template definition [" + definition + "]");
150         }
151 
152         final Map<String, Object> ctx = newContext();
153         final Map<String, Object> savedContextState = saveContextState(ctx);
154         setupContext(ctx, content, definition, model, actionResult);
155         ctx.putAll(contextObjects);
156         MgnlContext.setAttribute(MODEL_ATTRIBUTE, model);
157         content = wrapNodeForModel(content);
158         renderingCtx.push(content, definition);
159         try {
160             final HttpServletResponse response = MgnlContext.getWebContext().getResponse();
161             if (!response.containsHeader("Content-Type")) {
162                 response.setContentType(StringUtils.defaultIfBlank(getContentType(definition), MIMEMapping.getMIMETypeOrDefault(MIMEMapping.DEFAULT_EXTENSION)));
163             }
164 
165             onRender(content, definition, renderingCtx, ctx, templatePath);
166         } finally {
167             renderingCtx.pop();
168         }
169         MgnlContext.setAttribute(MODEL_ATTRIBUTE, parentModel);
170 
171         restoreContext(ctx, savedContextState);
172     }
173 
174     /**
175      * It's currently not possible to determine the content type of the renderer from the {@link RenderableDefinition}.
176      * <code>info.magnolia.module.resources.renderers.ResourcesTextTemplateRenderer</code> is the only renderer making
177      * use of it atm.
178      */
179     protected String getContentType(RenderableDefinition definition) {
180         return this.getContentType();
181     }
182 
183     /**
184      * Hook-method to be overriden when required. Default implementation ignores all arguments except definition.
185      *
186      * @param definition reference templateScript is retrieved from
187      * @return the templateScript to use
188      */
189     protected String resolveTemplateScript(Node content, RenderableDefinition definition, RenderingModel<?> model, final String actionResult) {
190         return definition.getTemplateScript();
191     }
192 
193     /**
194      * Instantiates the model based on the class defined by the {@link info.magnolia.rendering.template.RenderableDefinition#getModelClass()} property. All the request
195      * parameters are then mapped to the model's properties.
196      */
197     @Override
198     public RenderingModel<?> newModel(final Node content, final RenderableDefinition definition, final RenderingModel<?> parentModel) throws RenderException {
199 
200         Class clazz = definition.getModelClass();
201 
202         // if none is set we default to RenderingModelImpl, so there will always be a model available in templates
203         if (clazz == null) {
204             clazz = RenderingModelImpl.class;
205         }
206 
207         final Node wrappedContent = wrapNodeForModel(content);
208 
209         return newModel(clazz, wrappedContent, definition, parentModel);
210     }
211 
212     protected <T extends RenderingModel<?>> T newModel(Class<T> modelClass, final Node content, final RenderableDefinition definition, final RenderingModel<?> parentModel) throws RenderException {
213 
214         try {
215 
216             T model = Components.getComponentProvider().newInstanceWithParameterResolvers(modelClass,
217                     new ParameterResolver() {
218                         @Override
219                         public Object resolveParameter(ParameterInfo parameter) {
220                             if (parameter.getParameterType().equals(Node.class)) {
221                                 return content;
222                             }
223                             if (parameter.getParameterType().isAssignableFrom(definition.getClass())) {
224                                 return definition;
225                             }
226                             if (parameter.getParameterType().equals(RenderingModel.class)) {
227                                 return parentModel;
228                             }
229                             return UNRESOLVED;
230                         }
231                     }
232             );
233 
234             // populate the instance with values given as request parameters
235             final boolean autoPopulateFromRequest = definition.getAutoPopulateFromRequest() != null ? definition.getAutoPopulateFromRequest() : renderingEngine.getAutoPopulateFromRequest();
236             if (!autoPopulateFromRequest) {
237                 return model;
238             }
239 
240             Map<String, String[]> params = MgnlContext.getWebContext().getRequest().getParameterMap();
241             if (params == null || params.isEmpty()) {
242                 return model;
243             }
244 
245             // needed workaround to not break rendering when there is no index between square brackets
246             // see https://issues.apache.org/jira/browse/BEANUTILS-419
247             Map<String, Object> filtered = new HashMap<String, Object>();
248             for (Entry<String, String[]> entry : params.entrySet()) {
249                 String key = entry.getKey();
250                 String[] value = entry.getValue();
251                 if (StringUtils.contains(key, "[")) {
252                     key = StringUtils.substringBefore(key, "[");
253                 }
254                 filtered.put(key, value);
255             }
256 
257             BeanUtils.populate(model, filtered);
258 
259             return model;
260 
261         } catch (MgnlInstantiationException e) {
262             throw new RenderException("Can't instantiate model: " + modelClass, e);
263         } catch (InvocationTargetException e) {
264             throw new RenderException("Can't populate rendering model: " + ExceptionUtils.getRootCauseMessage(e), e);
265         } catch (IllegalAccessException e) {
266             throw new RenderException("Can't populate rendering model: " + ExceptionUtils.getRootCauseMessage(e), e);
267         }
268     }
269 
270     protected Map<String, Object> saveContextState(final Map<String, Object> ctx) {
271         Map<String, Object> state = new HashMap<String, Object>();
272         // save former values
273         saveAttribute(ctx, state, "content");
274         saveAttribute(ctx, state, "def");
275         saveAttribute(ctx, state, "state");
276         saveAttribute(ctx, state, "model");
277         saveAttribute(ctx, state, "actionResult");
278 
279         return state;
280     }
281 
282     protected void saveAttribute(final Map<String, Object> ctx, Map<String, Object> state, String name) {
283         state.put(name, ctx.get(name));
284     }
285 
286     protected void restoreContext(final Map<String, Object> ctx, Map<String, Object> state) {
287         for (Entry<String, Object> entry : state.entrySet()) {
288             setContextAttribute(ctx, entry.getKey(), entry.getValue());
289         }
290     }
291 
292     protected void setupContext(final Map<String, Object> ctx, Node content, RenderableDefinition definition, RenderingModel<?> model, Object actionResult) {
293         setContextAttribute(ctx, "content", content != null ? new ContentMap(wrapNodeForTemplate(content)) : null);
294         setContextAttribute(ctx, "def", definition);
295         setContextAttribute(ctx, "state", getAggregationStateSafely());
296         setContextAttribute(ctx, "model", model);
297         setContextAttribute(ctx, "actionResult", actionResult);
298 
299         for (Entry<String, ContextAttributeConfiguration> entry : contextAttributes.entrySet()) {
300             setContextAttribute(ctx, entry.getValue());
301         }
302     }
303 
304     /**
305      * Gets the current main content or returns null if aggregation state is not set.
306      */
307     protected Node getMainContentSafely(Node content) {
308         AggregationState state = getAggregationStateSafely();
309         return state == null ? content : state.getMainContentNode();
310     }
311 
312     /**
313      * This gets the aggregation state without throwing an exception if the current context is not a WebContext.
314      */
315     protected AggregationState getAggregationStateSafely() {
316         if (MgnlContext.isWebContext()) {
317             if (renderingEngine.getEscapeHtml()) {
318                 return Components.getComponent(HTMLEscapingAggregationState.class);
319             } else {
320                 return MgnlContext.getAggregationState();
321             }
322         }
323         return null;
324     }
325 
326     /**
327      * Wraps the current content node before passing it to the model.
328      *
329      * @param content the actual content
330      * @return the wrapped content
331      */
332     protected Node wrapNodeForModel(Node content) {
333         NodeUtil.deepUnwrap(content, HTMLEscapingNodeWrapper.class);
334         content = wrapWithChannelVisibilityWrapper(content);
335         content = wrapWithI18NWrapper(content);
336         return content;
337     }
338 
339     /**
340      * Wraps the current content node for exposing it to the template script as a context attribute.
341      *
342      * @param content the actual content
343      * @return the wrapped content
344      */
345     protected Node wrapNodeForTemplate(Node content) {
346         content = wrapWithChannelVisibilityWrapper(content);
347         content = wrapWithI18NWrapper(content);
348         content = wrapWithHTMLEscapingWrapper(content);
349         return content;
350     }
351 
352     private Node wrapWithHTMLEscapingWrapper(Node content) {
353         if (!NodeUtil.isWrappedWith(content, HTMLEscapingNodeWrapper.class)) {
354             content = new HTMLEscapingNodeWrapper(content, true);
355         }
356         return content;
357     }
358 
359     private Node wrapWithI18NWrapper(Node content) {
360         if (!NodeUtil.isWrappedWith(content, I18nNodeWrapper.class)) {
361             content = new I18nNodeWrapper(content);
362         }
363         return content;
364     }
365 
366     private Node wrapWithChannelVisibilityWrapper(Node content) {
367         // If it's already wrapped then we don't need to add a new one
368         if (ContentDecoratorUtil.isDecoratedWith(content, ChannelVisibilityContentDecorator.class)) {
369             return content;
370         }
371         AggregationState aggregationState = getAggregationStateSafely();
372         if (aggregationState == null) {
373             return content;
374         }
375         String channel = aggregationState.getChannel().getName();
376         if (StringUtils.isEmpty(channel) || channel.equalsIgnoreCase("all")) {
377             return content;
378         }
379         return new ChannelVisibilityContentDecorator(channel).wrapNode(content);
380     }
381 
382     /**
383      * Adds a {@link info.magnolia.rendering.renderer.ContextAttributeConfiguration} to the given context map.
384      */
385     private void setContextAttribute(Map<String, Object> ctx, ContextAttributeConfiguration contextAttributeConfiguration) {
386         final String ctxAttributeName = contextAttributeConfiguration.getName();
387         final Class ctxAttributeComponentClass = contextAttributeConfiguration.getComponentClass();
388         if (ctxAttributeName != null && ctxAttributeComponentClass != null) {
389             setContextAttribute(ctx, ctxAttributeName, Components.getComponent(ctxAttributeComponentClass));
390         } else {
391             log.error("Could not add contextAttribute with name '{}' and componentClass '{}'. Please make sure that name and componentClass are not null.", ctxAttributeName, ctxAttributeComponentClass);
392         }
393     }
394 
395     protected Object setContextAttribute(final Map<String, Object> ctx, final String name, Object value) {
396         return ctx.put(name, value);
397     }
398 
399     public Map<String, ContextAttributeConfiguration> getContextAttributes() {
400         return contextAttributes;
401     }
402 
403     public void setContextAttributes(Map<String, ContextAttributeConfiguration> contextAttributes) {
404         if (this.contextAttributes != null) {
405             this.contextAttributes.putAll(contextAttributes);
406         } else {
407             this.contextAttributes = contextAttributes;
408         }
409     }
410 
411     public void addContextAttribute(String name, ContextAttributeConfiguration contextAttributeConfiguration) {
412         this.contextAttributes.put(name, contextAttributeConfiguration);
413     }
414 
415     /**
416      * Create a new context object which is a map.
417      */
418     protected abstract Map<String, Object> newContext();
419 
420     /**
421      * Finally execute the rendering.
422      */
423     protected abstract void onRender(Node content, RenderableDefinition definition, RenderingContext renderingCtx, Map<String, Object> ctx, String templateScript) throws RenderException;
424 
425     public String getContentType() {
426         return contentType;
427     }
428 
429     public void setContentType(String contentType) {
430         this.contentType = contentType;
431     }
432 
433     public RenderingEngine getRenderingEngine() {
434         return renderingEngine;
435     }
436 }