View Javadoc
1   /**
2    * This file Copyright (c) 2011-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.templating.elements;
35  
36  import info.magnolia.beanmerger.BeanMergerUtil;
37  import info.magnolia.cms.beans.config.ServerConfiguration;
38  import info.magnolia.context.MgnlContext;
39  import info.magnolia.context.WebContext;
40  import info.magnolia.i18nsystem.I18nizer;
41  import info.magnolia.jcr.RuntimeRepositoryException;
42  import info.magnolia.jcr.util.ContentMap;
43  import info.magnolia.jcr.util.NodeTypes;
44  import info.magnolia.jcr.util.NodeUtil;
45  import info.magnolia.objectfactory.Components;
46  import info.magnolia.rendering.context.RenderingContext;
47  import info.magnolia.rendering.engine.AppendableOnlyOutputProvider;
48  import info.magnolia.rendering.engine.RenderException;
49  import info.magnolia.rendering.engine.RenderingEngine;
50  import info.magnolia.rendering.generator.Generator;
51  import info.magnolia.rendering.template.AreaDefinition;
52  import info.magnolia.rendering.template.AutoGenerationConfiguration;
53  import info.magnolia.rendering.template.RenderableDefinition;
54  import info.magnolia.rendering.template.TemplateDefinition;
55  import info.magnolia.rendering.template.configured.ConfiguredAreaDefinition;
56  import info.magnolia.rendering.template.variation.RenderableVariationResolver;
57  import info.magnolia.templating.elements.attribute.AvailableComponents;
58  import info.magnolia.templating.inheritance.DefaultInheritanceContentDecorator;
59  import info.magnolia.templating.module.TemplatingModule;
60  import info.magnolia.templating.renderers.NoScriptRenderer;
61  
62  import java.io.IOException;
63  import java.util.ArrayList;
64  import java.util.HashMap;
65  import java.util.List;
66  import java.util.Map;
67  
68  import javax.inject.Inject;
69  import javax.inject.Provider;
70  import javax.jcr.Node;
71  import javax.jcr.PathNotFoundException;
72  import javax.jcr.RepositoryException;
73  import javax.jcr.Session;
74  import javax.jcr.lock.LockException;
75  
76  import org.apache.commons.lang3.StringUtils;
77  import org.slf4j.Logger;
78  import org.slf4j.LoggerFactory;
79  
80  /**
81   * Renders an area and outputs a marker that instructs the page editor to place a bar at this location.
82   */
83  public class AreaElement extends AbstractContentTemplatingElement {
84  
85      private static final Logger log = LoggerFactory.getLogger(AreaElement.class);
86      private static final String CMS_AREA = "cms:area";
87  
88      public static final String ATTRIBUTE_COMPONENT = "component";
89      public static final String ATTRIBUTE_COMPONENTS = "components";
90  
91      private final RenderingEngine renderingEngine;
92  
93      private AreaDefinition areaDefinition;
94      private String name;
95      private String type;
96      private String dialog;
97      private String availableComponents;
98      private String label;
99      private String description;
100     private Boolean optional;
101     private Integer maxComponents;
102     private Boolean createAreaNode;
103 
104     private Map<String, Object> contextAttributes = new HashMap<>();
105 
106     private boolean isAreaDefinitionEnabled;
107 
108     private final RenderableVariationResolver variationResolver;
109     private boolean inherit;
110 
111     @Inject
112     public AreaElement(ServerConfiguration server, RenderingContext renderingContext, RenderingEngine renderingEngine, RenderableVariationResolver variationResolver, Provider<TemplatingModule> templatingModuleProvider, WebContext webContext) {
113         super(server, renderingContext, templatingModuleProvider, webContext);
114         this.renderingEngine = renderingEngine;
115         this.variationResolver = variationResolver;
116     }
117 
118     /**
119      * @deprecated since 6.1, use {@link #AreaElement(info.magnolia.cms.beans.config.ServerConfiguration, info.magnolia.rendering.context.RenderingContext, info.magnolia.rendering.engine.RenderingEngine, info.magnolia.rendering.template.variation.RenderableVariationResolver, javax.inject.Provider, info.magnolia.context.WebContext)} instead.
120      */
121     @Deprecated
122     public AreaElement(ServerConfiguration server, RenderingContext renderingContext, RenderingEngine renderingEngine, RenderableVariationResolver variationResolver, I18nizer i18nizer) {
123         this(server, renderingContext, renderingEngine, variationResolver, () -> Components.getComponent(TemplatingModule.class), Components.getComponent(WebContext.class));
124     }
125 
126     @Override
127     public void begin(Appendable out) throws IOException, RenderException {
128 
129         setTemplateDefinition(resolveTemplateDefinition());
130         this.areaDefinition = resolveAreaDefinition();
131 
132         this.isAreaDefinitionEnabled = areaDefinition != null && (areaDefinition.getEnabled() == null || areaDefinition.getEnabled());
133 
134         if (!this.isAreaDefinitionEnabled) {
135             return;
136         }
137         // set the values based on the area definition if not passed
138         this.name = resolveName();
139         this.dialog = resolveDialog();
140         this.type = resolveType();
141         this.availableComponents = resolveAvailableComponents();
142         this.inherit = isInheritanceEnabled();
143         this.optional = resolveOptional();
144         this.createAreaNode = resolveCreateAreaNode();
145 
146         // build an adhoc area definition if no area definition can be resolved
147         if (this.areaDefinition == null) {
148             buildAdHocAreaDefinition();
149         }
150 
151         this.maxComponents = resolveMaximumOfComponents();
152 
153         // read area node and calculate the area path
154         setContent(getPassedContent());
155         if (getContent() == null) {
156             // will be null if no area has been created (for instance for optional areas)
157             // current content is the parent node
158             Node parentNode = currentContent();
159             if (createAreaNode) {
160                 setContent(tryToCreateAreaNode(parentNode));
161             } else {
162                 setContent(parentNode);
163             }
164         }
165 
166         if (renderComments()) { // add condition into renderComments() method when adding extra condition to make sure it's in sync with adding comments in end() method
167             MarkupHelper helper = new MarkupHelper(out);
168             helper.openComment(CMS_AREA);
169             setPageEditorAttributes(helper, "area");
170             helper.append(" -->\n");
171         }
172     }
173 
174     private boolean hasPermission(Node node) {
175         if (node == null) {
176             node = currentContent();
177         }
178         try {
179             return node.getSession().hasPermission(node.getPath(), Session.ACTION_SET_PROPERTY);
180         } catch (RepositoryException e) {
181             log.error("Could not determine permission for node {}", node);
182         }
183         return false;
184     }
185 
186     private Node createNewAreaNode(Node parentNode) throws RepositoryException {
187         final String parentId = parentNode.getIdentifier();
188         final String workspaceName = parentNode.getSession().getWorkspace().getName();
189         try {
190             // we need to lock the page to prevent concurrent editing, not just the parent of area node we are about to create (think of nested areas)
191             MgnlContext.doInSystemContext(new MgnlContext.LockingOp(workspaceName, parentNode.getPath(), NodeTypes.Page.NAME) {
192 
193                 @Override
194                 public void doExec() throws RepositoryException {
195                     Node parentNodeInSystemSession = NodeUtil.getNodeByIdentifier(workspaceName, parentId);
196                     Node newAreaNode = NodeUtil.createPath(parentNodeInSystemSession, AreaElement.this.name, NodeTypes.Area.NAME);
197                     newAreaNode.getSession().save();
198                 }
199             });
200         } catch (LockException e) {
201             // maybe server is too busy, but that could also mean that someone else created our are in the mean time.
202             log.warn("Failed to create area due to locking problem. {}", e.getMessage(), e);
203         } catch (PathNotFoundException e) {
204             // page or the area was deleted while we attempted to obtain a lock on it
205             log.warn("Failed to create area due to concurrent deletion of page or the parent area. {}", e.getMessage(), e);
206         }
207         // JR will force refresh, some other JCRs might not
208         parentNode.getSession().refresh(true);
209         return parentNode.getNode(this.name);
210     }
211 
212     protected void buildAdHocAreaDefinition() {
213         ConfiguredAreaDefinition addHocAreaDefinition = new ConfiguredAreaDefinition();
214         addHocAreaDefinition.setName(this.name);
215         addHocAreaDefinition.setDialog(this.dialog);
216         addHocAreaDefinition.setType(this.type);
217         addHocAreaDefinition.setRenderType(getTemplateDefinition().getRenderType());
218         areaDefinition = addHocAreaDefinition;
219     }
220 
221     @Override
222     public void end(Appendable out) throws RenderException {
223 
224         try {
225             if (canRenderAreaScript()) {
226                 if (isInherit() && getContent() != null && areaDefinition.getInheritance() != null) {
227                     try {
228                         setContent(new DefaultInheritanceContentDecorator(getContent(), areaDefinition.getInheritance()).wrapNode(getContent()));
229                     } catch (RepositoryException e) {
230                         throw new RuntimeRepositoryException(e);
231                     }
232                 }
233                 List<Node> listOfComponents = null;
234                 int numberOfComponents = 0;
235                 if (getContent() != null) {
236                     listOfComponents = NodeUtil.asList(NodeUtil.getNodes(getContent(), NodeTypes.Component.NAME));
237                     numberOfComponents = listOfComponents.size();
238                 }
239                 if (renderingEngine.getRenderEmptyAreas() || numberOfComponents > 0 || !(AreaDefinition.TYPE_LIST.equals(areaDefinition.getType()) || AreaDefinition.TYPE_SINGLE.equals(areaDefinition.getType()))) {
240 
241                     Map<String, Object> contextObjects = new HashMap<String, Object>();
242 
243                     List<ContentMap> components = new ArrayList<ContentMap>();
244 
245                     if (getContent() != null) {
246                         if (numberOfComponents > maxComponents) {
247                             listOfComponents = listOfComponents.subList(0, maxComponents);
248                             log.warn("The area {} have maximum number of components set to {}, but has got {} components. Exceeded components won't be added.", getContent(), maxComponents, numberOfComponents);
249                         }
250 
251                         for (Node node : listOfComponents) {
252                             components.add(new ContentMap(node));
253                         }
254                     }
255 
256                     if (AreaDefinition.TYPE_SINGLE.equals(type)) {
257                         if (components.size() > 1) {
258                             log.warn("Single area [{}]: expected one component node but found [{}].", getContent(), components.size());
259                         }
260                         if (components.size() >= 1) {
261                             contextObjects.put(ATTRIBUTE_COMPONENT, components.get(0));
262                         } else {
263                             contextObjects.put(ATTRIBUTE_COMPONENT, null);
264                         }
265                     } else {
266                         contextObjects.put(ATTRIBUTE_COMPONENTS, components);
267                     }
268 
269                     // We set fallbacks for when the area definition's properties area incomplete, providing the most
270                     // basic fallback values for rendering.
271                     // Note that we override the area definition only when necessary, otherwise proxying to the original
272                     final ConfiguredAreaDefinition override = new ConfiguredAreaDefinition(areaDefinition.getTemplateAvailability());
273                     if (areaDefinition.getTemplateScript() == null) {
274                         override.setRenderType(NoScriptRenderer.NO_SCRIPT_RENDERER);
275                     } else if (areaDefinition.getRenderType() == null) {
276                         override.setRenderType(getTemplateDefinition().getRenderType());
277                     }
278                     if (areaDefinition.getI18nBasename() == null) {
279                         override.setI18nBasename(getTemplateDefinition().getI18nBasename());
280                     }
281 
282                     final ConfiguredAreaDefinition mergedAreaDefinition = BeanMergerUtil.merge(override, areaDefinition);
283 
284                     getWebContext().push(getWebContext().getRequest(), getWebContext().getResponse());
285                     setAttributesInWebContext(contextAttributes, WebContext.LOCAL_SCOPE);
286                     try {
287                         AppendableOnlyOutputProvider appendable = new AppendableOnlyOutputProvider(out);
288                         renderingEngine.render(getContent(), mergedAreaDefinition, contextObjects, appendable);
289                     } finally {
290                         getWebContext().pop();
291                         restoreAttributesInWebContext(contextAttributes, WebContext.LOCAL_SCOPE);
292                     }
293                 }
294             }
295             if (renderComments()) { // add condition into renderComments() method when adding extra condition to make sure it's in sync with adding comments in begin() method
296                 MarkupHelper helper = new MarkupHelper(out);
297                 helper.closeComment(CMS_AREA);
298             }
299         } catch (Exception e) {
300             throw new RenderException("Can't render area " + getContent() + " with name " + this.name, e);
301         }
302     }
303 
304     protected Node tryToCreateAreaNode(Node parentNode) throws RenderException {
305         Node area = null;
306         try {
307             if (parentNode.hasNode(name)) {
308                 area = parentNode.getNode(name);
309             } else {
310                 if (parentNode.getDefinition().isProtected()) {
311                     log.debug("Not auto-creating area '{}', node is protected.");
312                     return null;
313                 } else if (!this.optional) {
314                     // autocreate and save area only if it's not optional
315                     area = createNewAreaNode(parentNode);
316                 }
317             }
318         } catch (RepositoryException e) {
319             // yes, we now throw this consistently when area can't be created
320             log.error("Can't autocreate area '{}'.", area, e);
321         }
322         // at this stage we can be sure that the target area, unless optional, has been created.
323         if (area != null) {
324             // TODO fgrilli: what about other component types to be autogenerated (i.e. autogenerating an entire page)?
325             final AutoGenerationConfiguration autoGeneration = areaDefinition.getAutoGeneration();
326             if (autoGeneration != null && autoGeneration.getGeneratorClass() != null) {
327                 try {
328                     final String areaId = area.getIdentifier();
329                     final String workspaceName = area.getSession().getWorkspace().getName();
330                     MgnlContext.doInSystemContext(new MgnlContext.RepositoryOp() {
331                         @Override
332                         public void doExec() throws RepositoryException {
333                             Node areaNodeInSystemSession = NodeUtil.getNodeByIdentifier(workspaceName, areaId);
334                             try {
335                                 ((Generator<AutoGenerationConfiguration>) Components.newInstance(autoGeneration.getGeneratorClass(), areaNodeInSystemSession)).generate(autoGeneration);
336                             } catch (RenderException e) {
337                                 log.error("Can't render autogenerated area '{}'.", areaNodeInSystemSession);
338                             }
339                             return;
340                         }
341                     });
342                 } catch (RepositoryException e) {
343                     log.error("Can't autocreate area '{}'.", area, e);
344                 }
345             }
346         }
347         return area;
348     }
349 
350     protected AreaDefinition resolveAreaDefinition() {
351         if (areaDefinition != null) {
352             return areaDefinition;
353         }
354 
355         if (!StringUtils.isEmpty(name)) {
356             if (getTemplateDefinition() != null && getTemplateDefinition().getAreas().containsKey(name)) {
357                 return getTemplateDefinition().getAreas().get(name);
358             }
359         }
360         // happens if no area definition is passed or configured
361         // an ad-hoc area definition will be created
362         return null;
363     }
364 
365     protected TemplateDefinition resolveTemplateDefinition() throws RenderException {
366 
367         RenderableDefinition renderableDefinition = getRenderingContext().getRenderableDefinition();
368         RenderableDefinition variation = variationResolver.resolveVariation(renderableDefinition);
369         renderableDefinition = variation == null ? renderableDefinition : variation;
370 
371         if (renderableDefinition == null || renderableDefinition instanceof TemplateDefinition) {
372             return (TemplateDefinition) renderableDefinition;
373         }
374         throw new RenderException("Current RenderableDefinition [" + renderableDefinition + "] is not of type TemplateDefinition. Areas cannot be supported");
375     }
376 
377     /*
378      * An area script can be rendered when
379      * area is enabled
380      *
381      * AND
382      *
383      * If an area is optional:
384      *
385      * if not yet created the area bar has a create button and the script is
386      * - executed in the edit mode but the content object is null (otherwise we can't place the bar)
387      * - not executed otherwise (no place holder divs)
388      *
389      * If created, the bar has a remove button (other areas cannot be removed nor created)
390      *
391      * If an area is required:
392      *
393      * the area node gets created (always) the script is always executed.
394      */
395     private boolean canRenderAreaScript() {
396         if (!this.isAreaDefinitionEnabled) { // area script can be rendered only when area is enabled
397             return false;
398         }
399         if (getContent() != null) {
400             return true;
401         }
402         if (this.optional && this.getServer().isAdmin() && !MgnlContext.getAggregationState().isPreviewMode()) { // render script for optional areas when being in edit mode on author instance
403             return true;
404         }
405         return false;
406     }
407 
408     private String resolveDialog() {
409         return dialog != null ? dialog : areaDefinition != null ? areaDefinition.getDialog() : null;
410     }
411 
412     private String resolveType() {
413         return type != null ? type : areaDefinition != null && areaDefinition.getType() != null ? areaDefinition.getType() : AreaDefinition.DEFAULT_TYPE;
414     }
415 
416     private String resolveName() {
417         return name != null ? name : areaDefinition != null ? areaDefinition.getName() : null;
418     }
419 
420     private Boolean resolveOptional() {
421         return optional != null ? optional : areaDefinition != null && areaDefinition.getOptional() != null ? areaDefinition.getOptional() : Boolean.FALSE;
422     }
423 
424     private Integer resolveMaximumOfComponents() {
425         return maxComponents != null ? maxComponents : areaDefinition != null && areaDefinition.getMaxComponents() != null ? areaDefinition.getMaxComponents() : Integer.MAX_VALUE;
426     }
427 
428     private Boolean resolveCreateAreaNode() {
429         return createAreaNode != null ? createAreaNode : areaDefinition != null && areaDefinition.getCreateAreaNode() != null ? areaDefinition.getCreateAreaNode() : Boolean.TRUE;
430     }
431 
432     private boolean isInheritanceEnabled() {
433         return areaDefinition != null && areaDefinition.getInheritance() != null && areaDefinition.getInheritance().isEnabled() != null && areaDefinition.getInheritance().isEnabled();
434     }
435 
436     /**
437      * @deprecated since 6.1. Use {@link info.magnolia.templating.elements.attribute.AvailableComponents} instead.
438      */
439     @Deprecated
440     protected String resolveAvailableComponents() {
441         return new AvailableComponents(this::getWebContext).getValue(this).orElse(null);
442     }
443 
444     public String getName() {
445         return name;
446     }
447 
448     public void setName(String name) {
449         this.name = name;
450     }
451 
452     public AreaDefinition getArea() {
453         return areaDefinition;
454     }
455 
456     public void setArea(AreaDefinition area) {
457         this.areaDefinition = area;
458     }
459 
460     public String getAvailableComponents() {
461         return availableComponents;
462     }
463 
464     public void setAvailableComponents(String availableComponents) {
465         this.availableComponents = availableComponents;
466     }
467 
468     public String getType() {
469         return type;
470     }
471 
472     public void setType(String type) {
473         this.type = type;
474     }
475 
476     public String getDialog() {
477         return dialog;
478     }
479 
480     public void setDialog(String dialog) {
481         this.dialog = dialog;
482     }
483 
484     public String getLabel() {
485         return label;
486     }
487 
488     public void setLabel(String label) {
489         this.label = label;
490     }
491 
492     public String getDescription() {
493         return description;
494     }
495 
496     public void setDescription(String description) {
497         this.description = description;
498     }
499 
500     public boolean isInherit() {
501         return inherit;
502     }
503 
504     public void setInherit(boolean inherit) {
505         this.inherit = inherit;
506     }
507 
508     public Map<String, Object> getContextAttributes() {
509         return contextAttributes;
510     }
511 
512     public void setContextAttributes(Map<String, Object> contextAttributes) {
513         this.contextAttributes = contextAttributes;
514     }
515 
516     public Integer getMaxComponents() {
517         return maxComponents;
518     }
519 
520     public void setMaxComponents(Integer maxComponents) {
521         this.maxComponents = maxComponents;
522     }
523 
524     public Boolean getCreateAreaNode() {
525         return createAreaNode;
526     }
527 
528     public void setCreateAreaNode(Boolean createAreaNode) {
529         this.createAreaNode = createAreaNode;
530     }
531 
532     public AreaDefinition getAreaDefinition() {
533         return areaDefinition;
534     }
535 
536     public Boolean getOptional() {
537         return optional;
538     }
539 
540     @Override
541     protected boolean renderComments() {
542         return this.isAreaDefinitionEnabled && isAdmin() && !MgnlContext.getAggregationState().isPreviewMode() && hasPermission(getContent());
543     }
544 }