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