View Javadoc

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