View Javadoc
1   /**
2    * This file Copyright (c) 2011-2015 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.cms.beans.config.ServerConfiguration;
37  import info.magnolia.cms.i18n.Messages;
38  import info.magnolia.cms.i18n.MessagesManager;
39  import info.magnolia.context.MgnlContext;
40  import info.magnolia.context.WebContext;
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.ComponentAvailability;
54  import info.magnolia.rendering.template.RenderableDefinition;
55  import info.magnolia.rendering.template.TemplateDefinition;
56  import info.magnolia.rendering.template.configured.ConfiguredAreaDefinition;
57  import info.magnolia.rendering.template.variation.RenderableVariationResolver;
58  import info.magnolia.templating.freemarker.AbstractDirective;
59  import info.magnolia.templating.inheritance.DefaultInheritanceContentDecorator;
60  
61  import java.io.IOException;
62  import java.util.ArrayList;
63  import java.util.Collection;
64  import java.util.HashMap;
65  import java.util.Iterator;
66  import java.util.List;
67  import java.util.Map;
68  
69  import javax.inject.Inject;
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.collections.CollectionUtils;
77  import org.apache.commons.lang3.StringUtils;
78  import org.slf4j.Logger;
79  import org.slf4j.LoggerFactory;
80  
81  /**
82   * Renders an area and outputs a marker that instructs the page editor to place a bar at this location.
83   */
84  public class AreaElement extends AbstractContentTemplatingElement {
85  
86      private static final Logger log = LoggerFactory.getLogger(AreaElement.class);
87      public static final String CMS_AREA = "cms:area";
88  
89      public static final String ATTRIBUTE_COMPONENT = "component";
90      public static final String ATTRIBUTE_COMPONENTS = "components";
91  
92      private final RenderingEngine renderingEngine;
93  
94      private Node areaNode;
95      private TemplateDefinition templateDefinition;
96      private AreaDefinition areaDefinition;
97      private String name;
98      private String type;
99      private String dialog;
100     private String availableComponents;
101     private String label;
102     private String description;
103     private Boolean inherit;
104     private Boolean optional;
105     private Boolean editable;
106     private Integer maxComponents;
107     private Boolean createAreaNode;
108 
109     private Map<String, Object> contextAttributes = new HashMap<String, Object>();
110 
111     private String areaPath;
112 
113     private boolean isAreaDefinitionEnabled;
114     private final RenderableVariationResolver variationResolver;
115 
116     @Inject
117     public AreaElement(ServerConfiguration server, RenderingContext renderingContext, RenderingEngine renderingEngine, RenderableVariationResolver variationResolver) {
118         super(server, renderingContext);
119         this.renderingEngine = renderingEngine;
120         this.variationResolver = variationResolver;
121     }
122 
123     /**
124      * @deprecated since 5.0.5, use {@link #AreaElement(ServerConfiguration, RenderingContext, RenderingEngine, RenderableVariationResolver)} instead.
125      */
126     public AreaElement(ServerConfiguration server, RenderingContext renderingContext, RenderingEngine renderingEngine) {
127         this(server, renderingContext, renderingEngine, Components.getComponent(RenderableVariationResolver.class));
128     }
129 
130     @Override
131     public void begin(Appendable out) throws IOException, RenderException {
132 
133         this.templateDefinition = resolveTemplateDefinition();
134         this.areaDefinition = resolveAreaDefinition();
135 
136         this.isAreaDefinitionEnabled = areaDefinition != null && (areaDefinition.getEnabled() == null || areaDefinition.getEnabled());
137 
138         if (!this.isAreaDefinitionEnabled) {
139             return;
140         }
141         // set the values based on the area definition if not passed
142         this.name = resolveName();
143         this.dialog = resolveDialog();
144         this.type = resolveType();
145         this.label = resolveLabel();
146         this.availableComponents = resolveAvailableComponents();
147         this.inherit = isInheritanceEnabled();
148         this.optional = resolveOptional();
149         this.editable = resolveEditable();
150         this.createAreaNode = resolveCreateAreaNode();
151 
152         this.description = templateDefinition.getDescription();
153 
154         // build an adhoc area definition if no area definition can be resolved
155         if (this.areaDefinition == null) {
156             buildAdHocAreaDefinition();
157         }
158 
159         this.maxComponents = resolveMaximumOfComponents();
160 
161         // read area node and calculate the area path
162         this.areaNode = getPassedContent();
163         if (this.areaNode != null) {
164             this.areaPath = getNodePath(areaNode);
165         }
166         else {
167             // will be null if no area has been created (for instance for optional areas)
168             // current content is the parent node
169             Node parentNode = currentContent();
170             if(createAreaNode){
171                 this.areaNode = tryToCreateAreaNode(parentNode);
172                 this.areaPath = getNodePath(parentNode) + "/" + name;
173             } else {
174                 this.areaNode = parentNode;
175                 this.areaPath = getNodePath(parentNode);
176             }
177         }
178 
179         if (renderComments()) { // add condition into renderComments() method when adding extra condition to make sure it's in sync with adding comments in end() method
180             MarkupHelper helper = new MarkupHelper(out);
181 
182             helper.openComment(CMS_AREA).attribute(AbstractDirective.CONTENT_ATTRIBUTE, this.areaPath);
183             helper.attribute("name", this.name);
184             helper.attribute("availableComponents", this.availableComponents);
185             helper.attribute("type", this.type);
186             helper.attribute("dialog", this.dialog);
187 
188             final String i18nBasename = StringUtils.isNotEmpty(areaDefinition.getI18nBasename()) ? areaDefinition.getI18nBasename() : templateDefinition.getI18nBasename();
189             Messages messages = MessagesManager.getMessages(i18nBasename);
190             helper.attribute("label", messages.getWithDefault(this.label, this.label));
191 
192             helper.attribute("inherit", String.valueOf(this.inherit));
193             if (this.editable != null) {
194                 helper.attribute("editable", String.valueOf(this.editable));
195             }
196             helper.attribute("optional", String.valueOf(this.optional));
197             if (isOptionalAreaCreated()) {
198                 helper.attribute("created", "true");
199             }
200             helper.attribute("createAreaNode", String.valueOf(this.createAreaNode));
201             helper.attribute("showAddButton", String.valueOf(shouldShowAddButton()));
202             helper.attribute("showNewComponentArea", String.valueOf(this.shouldShowAddNewComponent()));
203             if (StringUtils.isNotBlank(description)) {
204                 helper.attribute("description", messages.getWithDefault(description, description));
205             }
206 
207             helper.append(" -->\n");
208 
209         }
210     }
211 
212     private boolean hasPermission(Node node) {
213         if (node == null) {
214             node = currentContent();
215         }
216         try {
217             return node.getSession().hasPermission(node.getPath(), Session.ACTION_SET_PROPERTY);
218         } catch (RepositoryException e) {
219             log.error("Could not determine permission for node {}", node);
220         }
221         return false;
222     }
223 
224     private Node createNewAreaNode(Node parentNode) throws RepositoryException {
225         final String parentId = parentNode.getIdentifier();
226         final String workspaceName = parentNode.getSession().getWorkspace().getName();
227         try {
228         // 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)
229         MgnlContext.doInSystemContext(new MgnlContext.LockingOp(workspaceName, parentNode.getPath(), NodeTypes.Page.NAME) {
230 
231             @Override
232             public void doExec() throws RepositoryException {
233                 Node parentNodeInSystemSession = NodeUtil.getNodeByIdentifier(workspaceName, parentId);
234                 Node newAreaNode = NodeUtil.createPath(parentNodeInSystemSession, AreaElement.this.name, NodeTypes.Area.NAME);
235                 newAreaNode.getSession().save();
236             }
237         });
238         } catch (LockException e) {
239             // maybe server is too busy, but that could also mean that someone else created our are in the mean time.
240             log.warn("Failed to create area due to locking problem. {}", e.getMessage(), e);
241         } catch (PathNotFoundException e) {
242             // page or the area was deleted while we attempted to obtain a lock on it
243             log.warn("Failed to create area due to concurrent deletion of page or the parent area. {}", e.getMessage(), e);
244         }
245         // JR will force refresh, some other JCRs might not
246         parentNode.getSession().refresh(true);
247         return parentNode.getNode(this.name);
248     }
249 
250     protected void buildAdHocAreaDefinition() {
251         ConfiguredAreaDefinition addHocAreaDefinition = new ConfiguredAreaDefinition();
252         addHocAreaDefinition.setName(this.name);
253         addHocAreaDefinition.setDialog(this.dialog);
254         addHocAreaDefinition.setType(this.type);
255         addHocAreaDefinition.setRenderType(this.templateDefinition.getRenderType());
256         areaDefinition = addHocAreaDefinition;
257     }
258 
259     @Override
260     public void end(Appendable out) throws RenderException {
261 
262         try {
263             if (canRenderAreaScript()) {
264                 if (isInherit() && areaNode != null && areaDefinition.getInheritance() != null) {
265                     try {
266                         areaNode = new DefaultInheritanceContentDecorator(areaNode, areaDefinition.getInheritance()).wrapNode(areaNode);
267                     } catch (RepositoryException e) {
268                         throw new RuntimeRepositoryException(e);
269                     }
270                 }
271                 List<Node> listOfComponents = null;
272                 int numberOfComponents = 0;
273                 if (areaNode != null) {
274                     listOfComponents = NodeUtil.asList(NodeUtil.getNodes(areaNode, NodeTypes.Component.NAME));
275                     numberOfComponents = listOfComponents.size();
276                 }
277                 if (renderingEngine.getRenderEmptyAreas() || numberOfComponents > 0 || !(AreaDefinition.TYPE_LIST.equals(areaDefinition.getType()) || AreaDefinition.TYPE_SINGLE.equals(areaDefinition.getType()))) {
278 
279                     Map<String, Object> contextObjects = new HashMap<String, Object>();
280 
281                     List<ContentMap> components = new ArrayList<ContentMap>();
282 
283                     if (areaNode != null) {
284                         if (numberOfComponents > maxComponents) {
285                             listOfComponents = listOfComponents.subList(0, maxComponents);
286                             log.warn("The area {} have maximum number of components set to {}, but has got {} components. Exceeded components won't be added.", areaNode, maxComponents, numberOfComponents);
287                         }
288 
289                         for (Node node : listOfComponents) {
290                             components.add(new ContentMap(node));
291                         }
292                     }
293 
294                     if (AreaDefinition.TYPE_SINGLE.equals(type)) {
295                         if (components.size() > 1) {
296                             log.warn("Single area [{}]: expected one component node but found [{}].", areaNode, components.size());
297                         }
298                         if (components.size() >= 1) {
299                             contextObjects.put(ATTRIBUTE_COMPONENT, components.get(0));
300                         } else {
301                             contextObjects.put(ATTRIBUTE_COMPONENT, null);
302                         }
303                     } else {
304                         contextObjects.put(ATTRIBUTE_COMPONENTS, components);
305                     }
306                     // FIXME we shouldn't manipulate the area definition directly
307                     // we should use merge with the proxy approach
308                     if (areaDefinition instanceof ConfiguredAreaDefinition) {
309                         if (areaDefinition.getTemplateScript() == null) {
310                             ((ConfiguredAreaDefinition) areaDefinition).setRenderType("noscript");
311                         } else if (areaDefinition.getRenderType() == null) {
312                             ((ConfiguredAreaDefinition) areaDefinition).setRenderType(this.templateDefinition.getRenderType());
313                         }
314 
315                         if (areaDefinition.getI18nBasename() == null) {
316                             ((ConfiguredAreaDefinition) areaDefinition).setI18nBasename(this.templateDefinition.getI18nBasename());
317                         }
318                     }
319 
320                     WebContext webContext = MgnlContext.getWebContext();
321                     webContext.push(webContext.getRequest(), webContext.getResponse());
322                     setAttributesInWebContext(contextAttributes, WebContext.LOCAL_SCOPE);
323                     try {
324                         AppendableOnlyOutputProvider appendable = new AppendableOnlyOutputProvider(out);
325                         renderingEngine.render(areaNode, areaDefinition, contextObjects, appendable);
326                     } finally {
327                         webContext.pop();
328                         webContext.setPageContext(null);
329                         restoreAttributesInWebContext(contextAttributes, WebContext.LOCAL_SCOPE);
330                     }
331                 }
332             }
333             if (renderComments()) { // add condition into renderComments() method when adding extra condition to make sure it's in sync with adding comments in begin() method
334                 MarkupHelper helper = new MarkupHelper(out);
335                 helper.closeComment(CMS_AREA);
336             }
337         } catch (Exception e) {
338             throw new RenderException("Can't render area " + areaNode + " with name " + this.name, e);
339         }
340     }
341 
342     protected Node tryToCreateAreaNode(Node parentNode) throws RenderException {
343         Node area = null;
344         try {
345             if (parentNode.hasNode(name)) {
346                 area = parentNode.getNode(name);
347             } else {
348                 // autocreate and save area only if it's not optional
349                 if (!this.optional) {
350                     area = createNewAreaNode(parentNode);
351                 }
352             }
353         } catch (RepositoryException e) {
354             // yes, we now throw this consistently when area can't be created
355             log.error("Can't autocreate area '{}'.", area, e);
356         }
357         // at this stage we can be sure that the target area, unless optional, has been created.
358         if (area != null) {
359             // TODO fgrilli: what about other component types to be autogenerated (i.e. autogenerating an entire page)?
360             final AutoGenerationConfiguration autoGeneration = areaDefinition.getAutoGeneration();
361             if (autoGeneration != null && autoGeneration.getGeneratorClass() != null) {
362                 try {
363                     final String areaId = area.getIdentifier();
364                     final String workspaceName = area.getSession().getWorkspace().getName();
365                     MgnlContext.doInSystemContext(new MgnlContext.RepositoryOp() {
366                         @Override
367                         public void doExec() throws RepositoryException {
368                             Node areaNodeInSystemSession = NodeUtil.getNodeByIdentifier(workspaceName, areaId);
369                             try {
370                                 ((Generator<AutoGenerationConfiguration>) Components.newInstance(autoGeneration.getGeneratorClass(), areaNodeInSystemSession)).generate(autoGeneration);
371                             } catch (RenderException e) {
372                                 log.error("Can't render autogenerated area '{}'.", areaNodeInSystemSession);
373                             }
374                             return;
375                         }
376                     });
377                 } catch (RepositoryException e) {
378                     log.error("Can't autocreate area '{}'.", area, e);
379                 }
380             }
381         }
382         return area;
383     }
384 
385     protected AreaDefinition resolveAreaDefinition() {
386         if (areaDefinition != null) {
387             return areaDefinition;
388         }
389 
390         if (!StringUtils.isEmpty(name)) {
391             if (templateDefinition != null && templateDefinition.getAreas().containsKey(name)) {
392                 return templateDefinition.getAreas().get(name);
393             }
394         }
395         // happens if no area definition is passed or configured
396         // an ad-hoc area definition will be created
397         return null;
398     }
399 
400     protected TemplateDefinition resolveTemplateDefinition() throws RenderException {
401 
402         RenderableDefinition renderableDefinition = getRenderingContext().getRenderableDefinition();
403         RenderableDefinition variation = variationResolver.resolveVariation(renderableDefinition);
404         renderableDefinition = variation == null ? renderableDefinition : variation;
405 
406         if (renderableDefinition == null || renderableDefinition instanceof TemplateDefinition) {
407             return (TemplateDefinition) renderableDefinition;
408         }
409         throw new RenderException("Current RenderableDefinition [" + renderableDefinition + "] is not of type TemplateDefinition. Areas cannot be supported");
410     }
411 
412     /*
413      * An area script can be rendered when
414      * area is enabled
415      *
416      * AND
417      *
418      * If an area is optional:
419      *
420      * if not yet created the area bar has a create button and the script is
421      * - executed in the edit mode but the content object is null (otherwise we can't place the bar)
422      * - not executed otherwise (no place holder divs)
423      *
424      * If created, the bar has a remove button (other areas cannot be removed nor created)
425      *
426      * If an area is required:
427      *
428      * the area node gets created (always) the script is always executed.
429      */
430     private boolean canRenderAreaScript() {
431         if (!this.isAreaDefinitionEnabled) { // area script can be rendered only when area is enabled
432             return false;
433         }
434         if (this.areaNode != null) {
435             return true;
436         }
437         if (this.optional && this.getServer().isAdmin() && !MgnlContext.getAggregationState().isPreviewMode()) { // render script for optional areas when being in edit mode on author instance
438             return true;
439         }
440         return false;
441     }
442 
443     private String resolveDialog() {
444         return dialog != null ? dialog : areaDefinition != null ? areaDefinition.getDialog() : null;
445     }
446 
447     private String resolveType() {
448         return type != null ? type : areaDefinition != null && areaDefinition.getType() != null ? areaDefinition.getType() : AreaDefinition.DEFAULT_TYPE;
449     }
450 
451     private String resolveName() {
452         return name != null ? name : areaDefinition != null ? areaDefinition.getName() : null;
453     }
454 
455     private String resolveLabel() {
456         return label != null ? label : areaDefinition != null && StringUtils.isNotBlank(areaDefinition.getTitle()) ? areaDefinition.getTitle() : StringUtils.capitalize(name);
457     }
458 
459     private Boolean resolveOptional() {
460         return optional != null ? optional : areaDefinition != null && areaDefinition.getOptional() != null ? areaDefinition.getOptional() : Boolean.FALSE;
461     }
462 
463     private Boolean resolveEditable() {
464         return editable != null ? editable : areaDefinition != null && areaDefinition.getEditable() != null ? areaDefinition.getEditable() : null;
465     }
466 
467     private Integer resolveMaximumOfComponents() {
468         return maxComponents != null ? maxComponents : areaDefinition != null && areaDefinition.getMaxComponents() != null ? areaDefinition.getMaxComponents() : Integer.MAX_VALUE;
469     }
470 
471     private Boolean resolveCreateAreaNode() {
472         return createAreaNode != null ? createAreaNode : areaDefinition != null && areaDefinition.getCreateAreaNode() != null ? areaDefinition.getCreateAreaNode() : Boolean.TRUE;
473     }
474 
475     private boolean isInheritanceEnabled() {
476         return areaDefinition != null && areaDefinition.getInheritance() != null && areaDefinition.getInheritance().isEnabled() != null && areaDefinition.getInheritance().isEnabled();
477     }
478 
479     private boolean isOptionalAreaCreated() {
480         return this.optional && this.areaNode != null;
481     }
482 
483     private boolean hasComponents(Node parent) throws RenderException {
484         try {
485             return NodeUtil.getNodes(parent, NodeTypes.Component.NAME).iterator().hasNext();
486         } catch (RepositoryException e) {
487             throw new RenderException(e);
488         }
489     }
490 
491     private int numberOfComponents(Node parent) throws RenderException {
492         try {
493             return NodeUtil.asList(NodeUtil.getNodes(parent, NodeTypes.Component.NAME)).size();
494         } catch (RepositoryException e) {
495             throw new RenderException(e);
496         }
497     }
498 
499     protected String resolveAvailableComponents() {
500         if (StringUtils.isNotEmpty(availableComponents)) {
501             return StringUtils.remove(availableComponents, " ");
502         }
503         if (areaDefinition != null && areaDefinition.getAvailableComponents().size() > 0) {
504             Iterator<ComponentAvailability> iterator = areaDefinition.getAvailableComponents().values().iterator();
505             List<String> componentIds = new ArrayList<String>();
506             final Collection<String> userRoles = MgnlContext.getUser().getAllRoles();
507             while (iterator.hasNext()) {
508                 ComponentAvailability availableComponent = iterator.next();
509                 if (availableComponent.isEnabled()) {
510                     // check roles
511                     final Collection<String> roles = availableComponent.getRoles();
512                     if (!roles.isEmpty()) {
513                         if (CollectionUtils.containsAny(userRoles, roles)) {
514                             componentIds.add(availableComponent.getId());
515                         }
516                     } else {
517                         componentIds.add(availableComponent.getId());
518                     }
519                 }
520             }
521             return StringUtils.join(componentIds, ',');
522         }
523         return "";
524     }
525 
526     private boolean shouldShowAddButton() throws RenderException {
527 
528         if (areaNode == null || type.equals(AreaDefinition.TYPE_NO_COMPONENT) || type.equals(AreaDefinition.TYPE_SINGLE) && hasComponents(areaNode) || numberOfComponents(areaNode) >= maxComponents) {
529             return false;
530         }
531         return true;
532     }
533 
534     private boolean shouldShowAddNewComponent() throws RenderException {
535 
536         if (areaNode == null || type.equals(AreaDefinition.TYPE_NO_COMPONENT) || type.equals(AreaDefinition.TYPE_SINGLE) && hasComponents(areaNode)) {
537             return false;
538         }
539         return true;
540     }
541 
542     public String getName() {
543         return name;
544     }
545 
546     public void setName(String name) {
547         this.name = name;
548     }
549 
550     public AreaDefinition getArea() {
551         return areaDefinition;
552     }
553 
554     public void setArea(AreaDefinition area) {
555         this.areaDefinition = area;
556     }
557 
558     public String getAvailableComponents() {
559         return availableComponents;
560     }
561 
562     public void setAvailableComponents(String availableComponents) {
563         this.availableComponents = availableComponents;
564     }
565 
566     public String getType() {
567         return type;
568     }
569 
570     public void setType(String type) {
571         this.type = type;
572     }
573 
574     public String getDialog() {
575         return dialog;
576     }
577 
578     public void setDialog(String dialog) {
579         this.dialog = dialog;
580     }
581 
582     public String getLabel() {
583         return label;
584     }
585 
586     public void setLabel(String label) {
587         this.label = label;
588     }
589 
590     public String getDescription() {
591         return description;
592     }
593 
594     public void setDescription(String description) {
595         this.description = description;
596     }
597 
598     public boolean isInherit() {
599         return inherit;
600     }
601 
602     public void setInherit(boolean inherit) {
603         this.inherit = inherit;
604     }
605 
606     public Boolean getEditable() {
607         return editable;
608     }
609 
610     public void setEditable(Boolean editable) {
611         this.editable = editable;
612     }
613 
614     public Map<String, Object> getContextAttributes() {
615         return contextAttributes;
616     }
617 
618     public void setContextAttributes(Map<String, Object> contextAttributes) {
619         this.contextAttributes = contextAttributes;
620     }
621 
622     public Integer getMaxComponents() {
623         return maxComponents;
624     }
625 
626     public void setMaxComponents(Integer maxComponents) {
627         this.maxComponents = maxComponents;
628     }
629 
630     public Boolean getCreateAreaNode() {
631         return createAreaNode;
632     }
633 
634     public void setCreateAreaNode(Boolean createAreaNode) {
635         this.createAreaNode = createAreaNode;
636     }
637 
638     @Override
639     protected boolean renderComments() {
640         return this.isAreaDefinitionEnabled && isAdmin() && !MgnlContext.getAggregationState().isPreviewMode() && hasPermission(this.areaNode);
641     }
642 }