View Javadoc

1   /**
2    * This file Copyright (c) 2011-2012 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.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.slf4j.Logger;
75  import org.slf4j.LoggerFactory;
76  
77  
78  /**
79   * Renders an area and outputs a marker that instructs the page editor to place a bar at this location.
80   *
81   * @version $Id$
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 
113 
114     public AreaElement(ServerConfiguration server, RenderingContext renderingContext, RenderingEngine renderingEngine) {
115         super(server, renderingContext);
116         this.renderingEngine = renderingEngine;
117     }
118 
119     @Override
120     public void begin(Appendable out) throws IOException, RenderException {
121 
122         this.templateDefinition = resolveTemplateDefinition();
123         Messages messages = MessagesManager.getMessages(templateDefinition.getI18nBasename());
124 
125         this.areaDefinition = resolveAreaDefinition();
126 
127         this.isAreaDefinitionEnabled = areaDefinition != null && (areaDefinition.isEnabled() == null || areaDefinition.isEnabled());
128 
129         if (!this.isAreaDefinitionEnabled) {
130             return;
131         }
132         // set the values based on the area definition if not passed
133         this.name = resolveName();
134         this.dialog = resolveDialog();
135         this.type = resolveType();
136         this.label = resolveLabel();
137         this.availableComponents = resolveAvailableComponents();
138         this.inherit = isInheritanceEnabled();
139         this.optional = resolveOptional();
140         this.editable = resolveEditable();
141 
142         this.description = templateDefinition.getDescription();
143 
144         // build an adhoc area definition if no area definition can be resolved
145         if(this.areaDefinition == null){
146             buildAdHocAreaDefinition();
147         }
148 
149         this.maxComponents = resolveMaximumOfComponents();
150 
151         // read area node and calculate the area path
152         this.areaNode = getPassedContent();
153         if (this.areaNode != null) {
154             this.areaPath = getNodePath(areaNode);
155         } else {
156             // will be null if no area has been created (for instance for optional areas)
157             // current content is the parent node
158             Node parentNode = currentContent();
159             this.areaNode = tryToCreateAreaNode(parentNode);
160             this.areaPath = getNodePath(parentNode) + "/" + name;
161         }
162         if (isAdmin() && hasPermission(this.areaNode)) {
163             MarkupHelper helper = new MarkupHelper(out);
164 
165             helper.openComment(CMS_AREA).attribute(AbstractDirective.CONTENT_ATTRIBUTE, this.areaPath);
166             helper.attribute("name", this.name);
167             helper.attribute("availableComponents", this.availableComponents);
168             helper.attribute("type", this.type);
169             helper.attribute("dialog", this.dialog);
170             helper.attribute("label", messages.getWithDefault(this.label, this.label));
171             helper.attribute("inherit", String.valueOf(this.inherit));
172             if (this.editable != null) {
173                 helper.attribute("editable", String.valueOf(this.editable));
174             }
175             helper.attribute("optional", String.valueOf(this.optional));
176             if(isOptionalAreaCreated()) {
177                 helper.attribute("created", "true");
178             }
179             helper.attribute("showAddButton", String.valueOf(shouldShowAddButton()));
180             if (StringUtils.isNotBlank(description)) {
181                 helper.attribute("description", messages.getWithDefault(description, description));
182             }
183 
184             helper.append(" -->\n");
185 
186         }
187     }
188 
189     private boolean hasPermission(Node node) {
190         if (node == null) {
191             node = currentContent();
192         }
193         try {
194             return node.getSession().hasPermission(node.getPath(), Session.ACTION_SET_PROPERTY);
195         } catch (RepositoryException e) {
196             log.error("Could not determine permission for node {}", node);
197         }
198         return false;
199     }
200 
201     private Node createNewAreaNode(Node parentNode) throws RepositoryException {
202         final String parentId = parentNode.getIdentifier();
203         final String workspaceName = parentNode.getSession().getWorkspace().getName();
204         try {
205             MgnlContext.doInSystemContext(new MgnlContext.Op<Void, RepositoryException>() {
206                 @Override
207                 public Void exec() throws RepositoryException {
208                     Node parentNodeInSystemSession = NodeUtil.getNodeByIdentifier(workspaceName, parentId);
209                     Node newAreaNode = NodeUtil.createPath(parentNodeInSystemSession, AreaElement.this.name, MgnlNodeType.NT_AREA);
210                     NodeUtil.createPath(newAreaNode, MetaData.DEFAULT_META_NODE, MgnlNodeType.NT_METADATA);
211                     newAreaNode.getSession().save();
212                     return null;
213                 }
214             });
215         } catch (RepositoryException e) {
216             log.error("ignoring problem w/ creating area in workspace {} for node {}", workspaceName, parentId);
217             // ignore, when working w/ versioned nodes ...
218             return null;
219         }
220         parentNode.getSession().refresh(true);
221         if (!parentNode.hasNode(name)) {
222             boolean existUnwrapped = MgnlContext.getJCRSession(parentNode.getSession().getWorkspace().getName()).itemExists(parentNode.getPath() + "/" + name);
223             log.warn("Failed to find previously created child [" + name + "] of node [" + parentNode.getPath() + "]. This node should be visible or there should be an error logged earlier in the log files explaining the problem.");
224             log.warn("This node has " + (existUnwrapped ? "" : "not") + " been found in unwrapped call to the session."
225                     + (existUnwrapped ? "This means one of the wrappers is probably swallawing the changes on the node." : "This means concurrent node modifications by different sessions are not visible fast enough to other sessions even after forced refresh. This seems like a JR bug."));
226             // try to refresh again in case this was just item cache override (could this really happen? if so it would be most likely JR error).
227             parentNode.getSession().refresh(true);
228         }
229         return parentNode.getNode(this.name);
230     }
231 
232     protected void buildAdHocAreaDefinition() {
233         ConfiguredAreaDefinition addHocAreaDefinition = new ConfiguredAreaDefinition();
234         addHocAreaDefinition.setName(this.name);
235         addHocAreaDefinition.setDialog(this.dialog);
236         addHocAreaDefinition.setType(this.type);
237         addHocAreaDefinition.setRenderType(this.templateDefinition.getRenderType());
238         areaDefinition = addHocAreaDefinition;
239     }
240 
241     @Override
242     public void end(Appendable out) throws RenderException {
243 
244         try {
245             if (canRenderAreaScript()) {
246                 if(isInherit() && areaNode != null) {
247                     try {
248                         areaNode = new DefaultInheritanceContentDecorator(areaNode, areaDefinition.getInheritance()).wrapNode(areaNode);
249                     } catch (RepositoryException e) {
250                         throw new RuntimeRepositoryException(e);
251                     }
252                 }
253                 Map<String, Object> contextObjects = new HashMap<String, Object>();
254 
255                 List<ContentMap> components = new ArrayList<ContentMap>();
256 
257                 if (areaNode != null) {
258                     List<Node> listOfComponents = NodeUtil.asList(NodeUtil.getNodes(areaNode, MgnlNodeType.NT_COMPONENT));
259                     int numberOfComponents = listOfComponents.size();
260 
261                     if (numberOfComponents > maxComponents) {
262                         listOfComponents = listOfComponents.subList(0, maxComponents);
263                         log.warn("The area {} have maximum number of components set to {}, but has got " + numberOfComponents +
264                               " components. Exceeded components won't be added.", areaNode, maxComponents);
265                     }
266 
267                     for (Node node : listOfComponents) {
268                         components.add(new ContentMap(node));
269                     }
270                 }
271 
272                 if(AreaDefinition.TYPE_SINGLE.equals(type)) {
273                     if(components.size() > 1) {
274                         throw new RenderException("Can't render single area [" + areaNode + "]: expected one component node but found more.");
275                     }
276                     if(components.size() == 1) {
277                         contextObjects.put(ATTRIBUTE_COMPONENT, components.get(0));
278                     } else {
279                         contextObjects.put(ATTRIBUTE_COMPONENT, null);
280                     }
281                 } else {
282                     contextObjects.put(ATTRIBUTE_COMPONENTS, components);
283                 }
284                 // FIXME we shouldn't manipulate the area definition directly
285                 // we should use merge with the proxy approach
286                 if(areaDefinition.getRenderType() == null && areaDefinition instanceof ConfiguredAreaDefinition){
287                     ((ConfiguredAreaDefinition)areaDefinition).setRenderType(this.templateDefinition.getRenderType());
288                 }
289 
290                 // FIXME we shouldn't manipulate the area definition directly
291                 // we should use merge with the proxy approach
292                 if(areaDefinition.getI18nBasename() == null && areaDefinition instanceof ConfiguredAreaDefinition){
293                     ((ConfiguredAreaDefinition)areaDefinition).setI18nBasename(this.templateDefinition.getI18nBasename());
294                 }
295                 WebContext webContext = MgnlContext.getWebContext();
296                 webContext.push(webContext.getRequest(), webContext.getResponse());
297                 setAttributesInWebContext(contextAttributes, WebContext.LOCAL_SCOPE);
298                 try {
299                     AppendableOnlyOutputProvider appendable = new AppendableOnlyOutputProvider(out);
300                     if(StringUtils.isNotEmpty(areaDefinition.getTemplateScript())){
301                         renderingEngine.render(areaNode, areaDefinition, contextObjects, appendable);
302                     }
303                     // no script
304                     else{
305                         for (ContentMap component : components) {
306                             ComponentElement componentElement = Components.newInstance(ComponentElement.class);
307                             componentElement.setContent(component.getJCRNode());
308                             componentElement.begin(out);
309                             componentElement.end(out);
310                         }
311                     }
312                 } finally {
313                     webContext.pop();
314                     webContext.setPageContext(null);
315                     restoreAttributesInWebContext(contextAttributes, WebContext.LOCAL_SCOPE);
316                 }
317 
318             }
319 
320             if (isAdmin() && this.isAreaDefinitionEnabled) {
321                 MarkupHelper helper = new MarkupHelper(out);
322                 helper.closeComment(CMS_AREA);
323             }
324         } catch (Exception e) {
325             throw new RenderException("Can't render area " + areaNode + " with name " + this.name, e);
326         }
327     }
328 
329     protected Node tryToCreateAreaNode(Node parentNode) throws RenderException {
330         Node area = null;
331         try {
332             if(parentNode.hasNode(name)){
333                 area = parentNode.getNode(name);
334             } else {
335                 //autocreate and save area only if it's not optional
336                 if(!this.optional) {
337                     area = createNewAreaNode(parentNode);
338                 }
339             }
340         }
341         catch (RepositoryException e) {
342             throw new RenderException("Can't access or create area node [" + name + "] on [" + parentNode + "]: " + e.getMessage(), e);
343         }
344         //at this stage we can be sure that the target area, unless optional, has been created.
345         if(area != null) {
346             //TODO fgrilli: what about other component types to be autogenerated (i.e. autogenerating an entire page)?
347             final AutoGenerationConfiguration autoGeneration = areaDefinition.getAutoGeneration();
348             if (autoGeneration != null && autoGeneration.getGeneratorClass() != null) {
349                 Components.newInstance(autoGeneration.getGeneratorClass(), area).generate(autoGeneration);
350             }
351         }
352         return area;
353     }
354 
355     protected AreaDefinition resolveAreaDefinition() {
356         if (areaDefinition != null) {
357             return areaDefinition;
358         }
359 
360         if (!StringUtils.isEmpty(name)) {
361             if (templateDefinition != null && templateDefinition.getAreas().containsKey(name)) {
362                 return templateDefinition.getAreas().get(name);
363             }
364         }
365         // happens if no area definition is passed or configured
366         // an ad-hoc area definition will be created
367         return null;
368     }
369 
370     protected TemplateDefinition resolveTemplateDefinition() throws RenderException {
371         final RenderableDefinition renderableDefinition = getRenderingContext().getRenderableDefinition();
372         if (renderableDefinition == null || renderableDefinition instanceof TemplateDefinition) {
373             return (TemplateDefinition) renderableDefinition;
374         }
375         throw new RenderException("Current RenderableDefinition [" + renderableDefinition + "] is not of type TemplateDefinition. Areas cannot be supported");
376     }
377 
378     /*
379      * An area script can be rendered when
380      * area is enabled
381      *
382      * AND
383      *
384      * If an area is optional:
385      *
386      * if not yet created the area bar has a create button and the script is
387      * - executed in the edit mode but the content object is null (otherwise we can't place the bar)
388      * - not executed otherwise (no place holder divs)
389      *
390      * If created, the bar has a remove button (other areas cannot be removed nor created)
391      *
392      * If an area is required:
393      *
394      * the area node gets created (always) the script is always executed.
395      */
396     private boolean canRenderAreaScript() {
397         // FYI: areaDefinition == null when it is not set explicitly and can't be merged with the parent. In such case we will render it as if it was enabled
398         return this.isAreaDefinitionEnabled && (areaNode != null || (areaNode == null && areaDefinition.isOptional() && !MgnlContext.getAggregationState().isPreviewMode()));
399     }
400 
401     private String resolveDialog() {
402         return dialog != null ? dialog : areaDefinition != null ? areaDefinition.getDialog() : null;
403     }
404 
405     private String resolveType() {
406         return type != null ? type : areaDefinition != null && areaDefinition.getType() != null ? areaDefinition.getType() : AreaDefinition.DEFAULT_TYPE;
407     }
408 
409     private String resolveName() {
410         return name != null ? name : (areaDefinition != null ? areaDefinition.getName() : null);
411     }
412 
413     private String resolveLabel() {
414         return label != null ? label : (areaDefinition != null && StringUtils.isNotBlank(areaDefinition.getTitle()) ? areaDefinition.getTitle() : StringUtils.capitalize(name));
415     }
416 
417     private Boolean resolveOptional() {
418         return optional != null ? optional : areaDefinition != null && areaDefinition.isOptional() != null ? areaDefinition.isOptional() : Boolean.FALSE;
419     }
420 
421     private Boolean resolveEditable() {
422         return editable != null ? editable : areaDefinition != null && areaDefinition.getEditable() != null ? areaDefinition.getEditable() : null;
423     }
424 
425     private Integer resolveMaximumOfComponents() {
426         return maxComponents != null ? maxComponents : areaDefinition != null && areaDefinition.getMaxComponents() != null ? areaDefinition.getMaxComponents() : Integer.MAX_VALUE;
427     }
428 
429     private boolean isInheritanceEnabled() {
430         return areaDefinition != null && areaDefinition.getInheritance() != null && areaDefinition.getInheritance().isEnabled() != null && areaDefinition.getInheritance().isEnabled();
431     }
432 
433     private boolean isOptionalAreaCreated() {
434         return this.optional && this.areaNode != null;
435     }
436 
437     private boolean hasComponents(Node parent) throws RenderException {
438         try {
439             return NodeUtil.getNodes(parent, MgnlNodeType.NT_COMPONENT).iterator().hasNext();
440         } catch (RepositoryException e) {
441             throw new RenderException(e);
442         }
443     }
444 
445     private int numberOfComponents(Node parent) throws RenderException {
446             try {
447                 return NodeUtil.asList(NodeUtil.getNodes(parent, MgnlNodeType.NT_COMPONENT)).size();
448             } catch (RepositoryException e) {
449                 throw new RenderException(e);
450             }
451     }
452 
453     protected String resolveAvailableComponents() {
454         if (StringUtils.isNotEmpty(availableComponents)) {
455             return StringUtils.remove(availableComponents, " ");
456         }
457         if (areaDefinition != null && areaDefinition.getAvailableComponents().size() > 0) {
458             Iterator<ComponentAvailability> iterator = areaDefinition.getAvailableComponents().values().iterator();
459             List<String> componentIds = new ArrayList<String>();
460             final Collection<String> userRoles = MgnlContext.getUser().getAllRoles();
461             while (iterator.hasNext()) {
462                 ComponentAvailability availableComponent = iterator.next();
463                 if(availableComponent.isEnabled()) {
464                     // check roles
465                     final Collection<String> roles = availableComponent.getRoles();
466                     if (!roles.isEmpty()) {
467                          if (CollectionUtils.containsAny(userRoles, roles)) {
468                              componentIds.add(availableComponent.getId());
469                          }
470                     } else {
471                         componentIds.add(availableComponent.getId());
472                     }
473                 }
474             }
475             return StringUtils.join(componentIds, ',');
476         }
477         return "";
478     }
479 
480     private boolean shouldShowAddButton() throws RenderException {
481 
482         if(areaNode == null || type.equals(AreaDefinition.TYPE_NO_COMPONENT)) {
483             return false;
484         }
485 
486         int numberOfComponents = numberOfComponents(areaNode);
487         if (numberOfComponents >= maxComponents || numberOfComponents > 0 && type.equals(AreaDefinition.TYPE_SINGLE) ) {
488             return false;
489         }
490 
491         return true;
492     }
493 
494     public String getName() {
495         return name;
496     }
497 
498     public void setName(String name) {
499         this.name = name;
500     }
501 
502     public AreaDefinition getArea() {
503         return areaDefinition;
504     }
505 
506     public void setArea(AreaDefinition area) {
507         this.areaDefinition = area;
508     }
509 
510     public String getAvailableComponents() {
511         return availableComponents;
512     }
513 
514     public void setAvailableComponents(String availableComponents) {
515         this.availableComponents = availableComponents;
516     }
517 
518     public String getType() {
519         return type;
520     }
521 
522     public void setType(String type) {
523         this.type = type;
524     }
525 
526     public String getDialog() {
527         return dialog;
528     }
529 
530     public void setDialog(String dialog) {
531         this.dialog = dialog;
532     }
533 
534     public String getLabel() {
535         return label;
536     }
537 
538     public void setLabel(String label) {
539         this.label = label;
540     }
541 
542     public String getDescription() {
543         return description;
544     }
545 
546     public void setDescription(String description) {
547         this.description = description;
548     }
549 
550     public boolean isInherit() {
551         return inherit;
552     }
553 
554     public void setInherit(boolean inherit) {
555         this.inherit = inherit;
556     }
557 
558     public Boolean getEditable() {
559         return editable;
560     }
561 
562     public void setEditable(Boolean editable) {
563         this.editable = editable;
564     }
565 
566     public Map<String, Object> getContextAttributes() {
567         return contextAttributes;
568     }
569 
570     public void setContextAttributes(Map<String, Object> contextAttributes) {
571         this.contextAttributes = contextAttributes;
572     }
573 
574     public Integer getMaxComponents() {
575         return maxComponents;
576     }
577 
578     public void setMaxComponents(Integer maxComponents) {
579         this.maxComponents = maxComponents;
580     }
581 }