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