View Javadoc

1   /**
2    * This file Copyright (c) 2011-2014 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.core.MetaData;
38  import info.magnolia.cms.core.MgnlNodeType;
39  import info.magnolia.cms.i18n.Messages;
40  import info.magnolia.cms.i18n.MessagesManager;
41  import info.magnolia.context.MgnlContext;
42  import info.magnolia.context.WebContext;
43  import info.magnolia.jcr.RuntimeRepositoryException;
44  import info.magnolia.jcr.util.ContentMap;
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.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  import info.magnolia.templating.renderers.NoScriptRenderer;
61  
62  import java.io.IOException;
63  import java.util.ArrayList;
64  import java.util.Collection;
65  import java.util.HashMap;
66  import java.util.Iterator;
67  import java.util.List;
68  import java.util.Map;
69  
70  import javax.jcr.Node;
71  import javax.jcr.RepositoryException;
72  import javax.jcr.Session;
73  
74  import org.apache.commons.collections.CollectionUtils;
75  import org.apache.commons.lang.StringUtils;
76  import org.apache.jackrabbit.JcrConstants;
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      public 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      public static final String SHOW_NEW_COMPONENT_AREA = "showNewComponentArea";
92      public static final String SHOW_ADD_BUTTON = "showAddButton";
93  
94      private final RenderingEngine renderingEngine;
95      private final RenderableVariationResolver variationResolver;
96  
97      private Node areaNode;
98      private TemplateDefinition templateDefinition;
99      private AreaDefinition areaDefinition;
100     private String name;
101     private String type;
102     private String dialog;
103     private String availableComponents;
104     private String label;
105     private String description;
106     private Boolean inherit;
107     private Boolean optional;
108     private Boolean editable;
109     private Integer maxComponents;
110 
111     private Map<String, Object> contextAttributes = new HashMap<String, Object>();
112 
113     private String areaPath;
114 
115     private boolean isAreaDefinitionEnabled;
116 
117     public AreaElement(ServerConfiguration server, RenderingContext renderingContext, RenderingEngine renderingEngine) {
118         super(server, renderingContext);
119         this.renderingEngine = renderingEngine;
120         this.variationResolver = Components.getComponent(RenderableVariationResolver.class);
121     }
122 
123     @Override
124     public void begin(Appendable out) throws IOException, RenderException {
125 
126         this.templateDefinition = resolveTemplateDefinition();
127         this.areaDefinition = resolveAreaDefinition();
128         this.isAreaDefinitionEnabled = areaDefinition != null && (areaDefinition.getEnabled() == null || areaDefinition.getEnabled());
129 
130         if (!this.isAreaDefinitionEnabled) {
131             return;
132         }
133         // set the values based on the area definition if not passed
134         this.name = resolveName();
135         this.dialog = resolveDialog();
136         this.type = resolveType();
137         this.label = resolveLabel();
138         this.availableComponents = resolveAvailableComponents();
139         this.inherit = isInheritanceEnabled();
140         this.optional = resolveOptional();
141         this.editable = resolveEditable();
142 
143         this.description = templateDefinition.getDescription();
144 
145         // build an adhoc area definition if no area definition can be resolved
146         if (this.areaDefinition == null) {
147             buildAdHocAreaDefinition();
148         }
149 
150         this.maxComponents = resolveMaximumOfComponents();
151 
152         // read area node and calculate the area path
153         this.areaNode = getPassedContent();
154         if (this.areaNode != null) {
155             this.areaPath = getNodePath(areaNode);
156         } else {
157             // will be null if no area has been created (for instance for optional areas)
158             // current content is the parent node
159             Node parentNode = currentContent();
160             this.areaNode = tryToCreateAreaNode(parentNode);
161             this.areaPath = getNodePath(parentNode) + "/" + name;
162         }
163 
164         if (renderComments()) { // add condition into renderComments() method when adding extra condition to make sure it's in sync with adding comments in end() method
165             MarkupHelper helper = new MarkupHelper(out);
166 
167             helper.openComment(CMS_AREA).attribute(AbstractDirective.CONTENT_ATTRIBUTE, this.areaPath);
168             helper.attribute("name", this.name);
169             helper.attribute("availableComponents", this.availableComponents);
170             helper.attribute("type", this.type);
171             helper.attribute("dialog", this.dialog);
172 
173             final String i18nBasename = StringUtils.isNotEmpty(areaDefinition.getI18nBasename()) ? areaDefinition.getI18nBasename() : templateDefinition.getI18nBasename();
174             Messages messages = MessagesManager.getMessages(i18nBasename);
175             helper.attribute("label", messages.getWithDefault(this.label, this.label));
176 
177             helper.attribute("inherit", String.valueOf(this.inherit));
178             if (this.editable != null) {
179                 helper.attribute("editable", String.valueOf(this.editable));
180             }
181             helper.attribute("optional", String.valueOf(this.optional));
182             if (isOptionalAreaCreated()) {
183                 helper.attribute("created", "true");
184             }
185             helper.attribute(SHOW_ADD_BUTTON, String.valueOf(shouldShowAddButton()));
186             helper.attribute(SHOW_NEW_COMPONENT_AREA, String.valueOf(shouldShowNewComponentArea()));
187 
188             if (StringUtils.isNotBlank(description)) {
189                 helper.attribute("description", messages.getWithDefault(description, description));
190             }
191 
192             helper.append(" -->\n");
193 
194         }
195     }
196 
197     private boolean hasPermission(Node node) {
198         if (node == null) {
199             node = currentContent();
200         }
201         try {
202             return node.getSession().hasPermission(node.getPath(), Session.ACTION_SET_PROPERTY);
203         } catch (RepositoryException e) {
204             log.error("Could not determine permission for node {}", node);
205         }
206         return false;
207     }
208 
209     private Node createNewAreaNode(Node parentNode) throws RepositoryException {
210         final String parentId = parentNode.getIdentifier();
211         final String workspaceName = parentNode.getSession().getWorkspace().getName();
212         try {
213             MgnlContext.doInSystemContext(new MgnlContext.Op<Void, RepositoryException>() {
214                 @Override
215                 public Void exec() throws RepositoryException {
216                     Node parentNodeInSystemSession = NodeUtil.getNodeByIdentifier(workspaceName, parentId);
217                     Node newAreaNode = NodeUtil.createPath(parentNodeInSystemSession, AreaElement.this.name, MgnlNodeType.NT_AREA);
218                     NodeUtil.createPath(newAreaNode, MetaData.DEFAULT_META_NODE, MgnlNodeType.NT_METADATA);
219                     newAreaNode.getSession().save();
220                     return null;
221                 }
222             });
223         } catch (RepositoryException e) {
224             final String parentPath = parentNode.getPath();
225             if (parentPath.startsWith("/" + JcrConstants.JCR_SYSTEM)) {
226                 log.warn("Could not autogenerate area in workspace {} for node {} because it is read-only. Pls preview page before versioning to avoid this issue", workspaceName, parentPath);
227             } else {
228                 log.error("Could not autogenerate area in workspace " + workspaceName + " for node " + parentPath, e);
229             }
230             return null;
231         }
232         // new node has been created in SystemContext - if it's now not around in user session, then just because JCR repo did not refresh yet.
233         // We try few times then we give up
234         for (int i = 0; i < 5; i++) {
235             if (parentNode.hasNode(this.name)) {
236                 return parentNode.getNode(this.name);
237             }
238             log.debug("New node {} is not yet visible in session for user {} - will try again after short sleep.", NodeUtil.combinePathAndName(parentNode.getPath(), this.name), MgnlContext.getUser().getName());
239             try {
240                 Thread.sleep(100);
241             } catch (InterruptedException e) {
242                 log.debug("Exception when waiting before next session refresh", e);
243                 Thread.interrupted();
244             }
245             parentNode.refresh(true);
246         }
247         log.warn("New node {} is still not visible in session for user {}. It seems your server is busy - you might want to increase assigned resources", NodeUtil.combinePathAndName(parentNode.getPath(), this.name), MgnlContext.getUser().getName());
248         return null;
249     }
250 
251     protected void buildAdHocAreaDefinition() {
252         ConfiguredAreaDefinition addHocAreaDefinition = new ConfiguredAreaDefinition();
253         addHocAreaDefinition.setName(this.name);
254         addHocAreaDefinition.setDialog(this.dialog);
255         addHocAreaDefinition.setType(this.type);
256         addHocAreaDefinition.setRenderType(this.templateDefinition.getRenderType());
257         areaDefinition = addHocAreaDefinition;
258     }
259 
260     @Override
261     public void end(Appendable out) throws RenderException {
262 
263         try {
264             if (canRenderAreaScript()) {
265                 if (isInherit() && areaNode != null && areaDefinition.getInheritance() != null) {
266                     try {
267                         areaNode = new DefaultInheritanceContentDecorator(areaNode, areaDefinition.getInheritance()).wrapNode(areaNode);
268                     } catch (RepositoryException e) {
269                         throw new RuntimeRepositoryException(e);
270                     }
271                 }
272                 List<Node> listOfComponents = null;
273                 int numberOfComponents = 0;
274                 if (areaNode != null) {
275                     listOfComponents = NodeUtil.asList(NodeUtil.getNodes(areaNode, MgnlNodeType.NT_COMPONENT));
276                     numberOfComponents = listOfComponents.size();
277                 }
278                 if (renderingEngine.getRenderEmptyAreas() || numberOfComponents > 0 || !(AreaDefinition.TYPE_LIST.equals(areaDefinition.getType()) || AreaDefinition.TYPE_SINGLE.equals(areaDefinition.getType()))) {
279 
280                     Map<String, Object> contextObjects = new HashMap<String, Object>();
281 
282                     List<ContentMap> components = new ArrayList<ContentMap>();
283 
284                     if (areaNode != null) {
285                         if (numberOfComponents > maxComponents) {
286                             listOfComponents = listOfComponents.subList(0, maxComponents);
287                             log.warn("The area {} have maximum number of components set to {}, but has got " + numberOfComponents +
288                                     " components. Exceeded components won't be added.", areaNode, maxComponents);
289                         }
290 
291                         for (Node node : listOfComponents) {
292                             components.add(new ContentMap(node));
293                         }
294                     }
295 
296                     if (AreaDefinition.TYPE_SINGLE.equals(type)) {
297                         if (components.size() > 1) {
298                             throw new RenderException("Can't render single area [" + areaNode + "]: expected one component node but found more.");
299                         }
300                         if (components.size() == 1) {
301                             contextObjects.put(ATTRIBUTE_COMPONENT, components.get(0));
302                         } else {
303                             contextObjects.put(ATTRIBUTE_COMPONENT, null);
304                         }
305                     } else {
306                         contextObjects.put(ATTRIBUTE_COMPONENTS, components);
307                     }
308                     // FIXME we shouldn't manipulate the area definition directly
309                     // we should use merge with the proxy approach
310                     if (areaDefinition instanceof ConfiguredAreaDefinition) {
311                         if (areaDefinition.getTemplateScript() == null) {
312                             ((ConfiguredAreaDefinition) areaDefinition).setRenderType(NoScriptRenderer.NO_SCRIPT_RENDERER);
313 
314                         } else if (areaDefinition.getRenderType() == null) {
315                             ((ConfiguredAreaDefinition) areaDefinition).setRenderType(this.templateDefinition.getRenderType());
316                         }
317 
318                         if (areaDefinition.getI18nBasename() == null) {
319                             ((ConfiguredAreaDefinition) areaDefinition).setI18nBasename(this.templateDefinition.getI18nBasename());
320                         }
321                     }
322 
323                     WebContext webContext = MgnlContext.getWebContext();
324                     webContext.push(webContext.getRequest(), webContext.getResponse());
325                     setAttributesInWebContext(contextAttributes, WebContext.LOCAL_SCOPE);
326                     try {
327                         AppendableOnlyOutputProvider appendable = new AppendableOnlyOutputProvider(out);
328                         renderingEngine.render(areaNode, areaDefinition, contextObjects, appendable);
329                     } finally {
330                         webContext.pop();
331                         webContext.setPageContext(null);
332                         restoreAttributesInWebContext(contextAttributes, WebContext.LOCAL_SCOPE);
333                     }
334                 }
335             }
336             if (renderComments()) { // add condition into renderComments() method when adding extra condition to make sure it's in sync with adding comments in begin() method
337                 MarkupHelper helper = new MarkupHelper(out);
338                 helper.closeComment(CMS_AREA);
339             }
340 
341         } catch (Exception e) {
342             throw new RenderException("Can't render area " + areaNode + " with name " + this.name, e);
343         }
344     }
345 
346     protected Node tryToCreateAreaNode(Node parentNode) throws RenderException {
347         Node area = null;
348         try {
349             if (parentNode.hasNode(name)) {
350                 area = parentNode.getNode(name);
351             } else {
352                 // autocreate and save area only if it's not optional
353                 if (!this.optional) {
354                     area = createNewAreaNode(parentNode);
355                 }
356             }
357         } catch (RepositoryException e) {
358             throw new RenderException("Can't access or create area node [" + name + "] on [" + parentNode + "]: " + e.getMessage(), e);
359         }
360         // at this stage we can be sure that the target area, unless optional, has been created.
361         if (area != null) {
362             // TODO fgrilli: what about other component types to be autogenerated (i.e. autogenerating an entire page)?
363             final AutoGenerationConfiguration autoGeneration = areaDefinition.getAutoGeneration();
364             if (autoGeneration != null && autoGeneration.getGeneratorClass() != null) {
365                 try {
366                     final String areaId = area.getIdentifier();
367                     final String workspaceName = area.getSession().getWorkspace().getName();
368                     MgnlContext.doInSystemContext(new MgnlContext.Op<Void, RepositoryException>() {
369                         @Override
370                         public Void exec() throws RepositoryException {
371                             Node areaNodeInSystemSession = NodeUtil.getNodeByIdentifier(workspaceName, areaId);
372                             try {
373                                 Components.newInstance(autoGeneration.getGeneratorClass(), areaNodeInSystemSession).generate(autoGeneration);
374                             } catch (RenderException e) {
375                                 log.error("Can't render autogenerated area '{}'.", areaNodeInSystemSession);
376                             }
377                             return null;
378                         }
379                     });
380                 } catch (RepositoryException e) {
381                     log.error("Can't autocreate area '{}'.", area);
382                 }
383             }
384         }
385         return area;
386     }
387 
388     protected AreaDefinition resolveAreaDefinition() {
389         if (areaDefinition != null) {
390             return areaDefinition;
391         }
392 
393         if (!StringUtils.isEmpty(name)) {
394             if (templateDefinition != null && templateDefinition.getAreas().containsKey(name)) {
395                 return templateDefinition.getAreas().get(name);
396             }
397         }
398         // happens if no area definition is passed or configured
399         // an ad-hoc area definition will be created
400         return null;
401     }
402 
403     protected TemplateDefinition resolveTemplateDefinition() throws RenderException {
404 
405         RenderableDefinition renderableDefinition = getRenderingContext().getRenderableDefinition();
406         RenderableDefinition variation = variationResolver.resolveVariation(renderableDefinition);
407         renderableDefinition = variation == null ? renderableDefinition : variation;
408 
409         if (renderableDefinition == null || renderableDefinition instanceof TemplateDefinition) {
410             return (TemplateDefinition) renderableDefinition;
411         }
412         throw new RenderException("Current RenderableDefinition [" + renderableDefinition + "] is not of type TemplateDefinition. Areas cannot be supported");
413     }
414 
415     /*
416      * An area script can be rendered when
417      * area is enabled
418      * 
419      * AND
420      * 
421      * If an area is optional:
422      * 
423      * if not yet created the area bar has a create button and the script is
424      * - executed in the edit mode but the content object is null (otherwise we can't place the bar)
425      * - not executed otherwise (no place holder divs)
426      * 
427      * If created, the bar has a remove button (other areas cannot be removed nor created)
428      * 
429      * If an area is required:
430      * 
431      * the area node gets created (always) the script is always executed.
432      */
433     private boolean canRenderAreaScript() {
434         if (!this.isAreaDefinitionEnabled) { // area script can be rendered only when area is enabled
435             return false;
436         }
437         if (this.areaNode != null) {
438             return true;
439         }
440         if (this.optional && this.getServer().isAdmin() && !MgnlContext.getAggregationState().isPreviewMode()) { // render script for optional areas when being in edit mode on author instance
441             return true;
442         }
443         return false;
444     }
445 
446     private String resolveDialog() {
447         return dialog != null ? dialog : areaDefinition != null ? areaDefinition.getDialog() : null;
448     }
449 
450     private String resolveType() {
451         return type != null ? type : areaDefinition != null && areaDefinition.getType() != null ? areaDefinition.getType() : AreaDefinition.DEFAULT_TYPE;
452     }
453 
454     private String resolveName() {
455         return name != null ? name : areaDefinition != null ? areaDefinition.getName() : null;
456     }
457 
458     private String resolveLabel() {
459         return label != null ? label : areaDefinition != null && StringUtils.isNotBlank(areaDefinition.getTitle()) ? areaDefinition.getTitle() : StringUtils.capitalize(name);
460     }
461 
462     private Boolean resolveOptional() {
463         return optional != null ? optional : areaDefinition != null && areaDefinition.getOptional() != null ? areaDefinition.getOptional() : Boolean.FALSE;
464     }
465 
466     private Boolean resolveEditable() {
467         return editable != null ? editable : areaDefinition != null && areaDefinition.getEditable() != null ? areaDefinition.getEditable() : null;
468     }
469 
470     private Integer resolveMaximumOfComponents() {
471         return maxComponents != null ? maxComponents : areaDefinition != null && areaDefinition.getMaxComponents() != null ? areaDefinition.getMaxComponents() : Integer.MAX_VALUE;
472     }
473 
474     private boolean isInheritanceEnabled() {
475         return areaDefinition != null && areaDefinition.getInheritance() != null && areaDefinition.getInheritance().isEnabled() != null && areaDefinition.getInheritance().isEnabled();
476     }
477 
478     private boolean isOptionalAreaCreated() {
479         return this.optional && this.areaNode != null;
480     }
481 
482     private boolean hasComponents(Node parent) throws RenderException {
483         try {
484             return NodeUtil.getNodes(parent, MgnlNodeType.NT_COMPONENT).iterator().hasNext();
485         } catch (RepositoryException e) {
486             throw new RenderException(e);
487         }
488     }
489 
490     private int numberOfComponents(Node parent) throws RenderException {
491         try {
492             return NodeUtil.asList(NodeUtil.getNodes(parent, MgnlNodeType.NT_COMPONENT)).size();
493         } catch (RepositoryException e) {
494             throw new RenderException(e);
495         }
496     }
497 
498     protected String resolveAvailableComponents() {
499         if (StringUtils.isNotEmpty(availableComponents)) {
500             return StringUtils.remove(availableComponents, " ");
501         }
502         if (areaDefinition != null && areaDefinition.getAvailableComponents().size() > 0) {
503             Iterator<ComponentAvailability> iterator = areaDefinition.getAvailableComponents().values().iterator();
504             List<String> componentIds = new ArrayList<String>();
505             final Collection<String> userRoles = MgnlContext.getUser().getAllRoles();
506             while (iterator.hasNext()) {
507                 ComponentAvailability availableComponent = iterator.next();
508                 if (availableComponent.isEnabled()) {
509                     // check roles
510                     final Collection<String> roles = availableComponent.getRoles();
511                     if (!roles.isEmpty()) {
512                         if (CollectionUtils.containsAny(userRoles, roles)) {
513                             componentIds.add(availableComponent.getId());
514                         }
515                     } else {
516                         componentIds.add(availableComponent.getId());
517                     }
518                 }
519             }
520             return StringUtils.join(componentIds, ',');
521         }
522         return "";
523     }
524 
525     private boolean shouldShowAddButton() throws RenderException {
526 
527         if (areaNode == null || type.equals(AreaDefinition.TYPE_NO_COMPONENT)) {
528             return false;
529         }
530 
531         int numberOfComponents = numberOfComponents(areaNode);
532         if (numberOfComponents >= maxComponents || numberOfComponents > 0 && type.equals(AreaDefinition.TYPE_SINGLE)) {
533             return false;
534         }
535 
536         return true;
537     }
538 
539     private boolean shouldShowNewComponentArea() 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     @Override
636     protected boolean renderComments() {
637         return this.isAreaDefinitionEnabled && isAdmin() && !MgnlContext.getAggregationState().isPreviewMode() && hasPermission(this.areaNode);
638     }
639 }