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