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.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  import info.magnolia.templating.renderers.NoScriptRenderer;
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.jcr.Node;
70  import javax.jcr.RepositoryException;
71  import javax.jcr.Session;
72  
73  import org.apache.commons.collections.CollectionUtils;
74  import org.apache.commons.lang.StringUtils;
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   * @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      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  
96      private Node areaNode;
97      private TemplateDefinition templateDefinition;
98      private AreaDefinition areaDefinition;
99      private String name;
100     private String type;
101     private String dialog;
102     private String availableComponents;
103     private String label;
104     private String description;
105     private Boolean inherit;
106     private Boolean optional;
107     private Boolean editable;
108     private Integer maxComponents;
109 
110     private Map<String, Object> contextAttributes = new HashMap<String, Object>();
111 
112     private String areaPath;
113 
114     private boolean isAreaDefinitionEnabled;
115 
116     public AreaElement(ServerConfiguration server, RenderingContext renderingContext, RenderingEngine renderingEngine) {
117         super(server, renderingContext);
118         this.renderingEngine = renderingEngine;
119     }
120 
121     @Override
122     public void begin(Appendable out) throws IOException, RenderException {
123 
124         this.templateDefinition = resolveTemplateDefinition();
125         Messages messages = MessagesManager.getMessages(templateDefinition.getI18nBasename());
126 
127         this.areaDefinition = resolveAreaDefinition();
128 
129         this.isAreaDefinitionEnabled = areaDefinition != null && (areaDefinition.getEnabled() == null || areaDefinition.getEnabled());
130 
131         if (!this.isAreaDefinitionEnabled) {
132             return;
133         }
134         // set the values based on the area definition if not passed
135         this.name = resolveName();
136         this.dialog = resolveDialog();
137         this.type = resolveType();
138         this.label = resolveLabel();
139         this.availableComponents = resolveAvailableComponents();
140         this.inherit = isInheritanceEnabled();
141         this.optional = resolveOptional();
142         this.editable = resolveEditable();
143 
144         this.description = templateDefinition.getDescription();
145 
146         // build an adhoc area definition if no area definition can be resolved
147         if (this.areaDefinition == null) {
148             buildAdHocAreaDefinition();
149         }
150 
151         this.maxComponents = resolveMaximumOfComponents();
152 
153         // read area node and calculate the area path
154         this.areaNode = getPassedContent();
155         if (this.areaNode != null) {
156             this.areaPath = getNodePath(areaNode);
157         } else {
158             // will be null if no area has been created (for instance for optional areas)
159             // current content is the parent node
160             Node parentNode = currentContent();
161             this.areaNode = tryToCreateAreaNode(parentNode);
162             this.areaPath = getNodePath(parentNode) + "/" + name;
163         }
164 
165         if (isAdmin() && hasPermission(this.areaNode)) {
166             MarkupHelper helper = new MarkupHelper(out);
167 
168             helper.openComment(CMS_AREA).attribute(AbstractDirective.CONTENT_ATTRIBUTE, this.areaPath);
169             helper.attribute("name", this.name);
170             helper.attribute("availableComponents", this.availableComponents);
171             helper.attribute("type", this.type);
172             helper.attribute("dialog", this.dialog);
173             helper.attribute("label", messages.getWithDefault(this.label, this.label));
174             helper.attribute("inherit", String.valueOf(this.inherit));
175             if (this.editable != null) {
176                 helper.attribute("editable", String.valueOf(this.editable));
177             }
178             helper.attribute("optional", String.valueOf(this.optional));
179             if (isOptionalAreaCreated()) {
180                 helper.attribute("created", "true");
181             }
182             helper.attribute(SHOW_ADD_BUTTON, String.valueOf(shouldShowAddButton()));
183             helper.attribute(SHOW_NEW_COMPONENT_AREA, String.valueOf(shouldShowNewComponentArea()));
184 
185             if (StringUtils.isNotBlank(description)) {
186                 helper.attribute("description", messages.getWithDefault(description, description));
187             }
188 
189             helper.append(" -->\n");
190 
191         }
192     }
193 
194     private boolean hasPermission(Node node) {
195         if (node == null) {
196             node = currentContent();
197         }
198         try {
199             return node.getSession().hasPermission(node.getPath(), Session.ACTION_SET_PROPERTY);
200         } catch (RepositoryException e) {
201             log.error("Could not determine permission for node {}", node);
202         }
203         return false;
204     }
205 
206     private Node createNewAreaNode(Node parentNode) throws RepositoryException {
207         final String parentId = parentNode.getIdentifier();
208         final String workspaceName = parentNode.getSession().getWorkspace().getName();
209         try {
210             MgnlContext.doInSystemContext(new MgnlContext.Op<Void, RepositoryException>() {
211                 @Override
212                 public Void exec() throws RepositoryException {
213                     Node parentNodeInSystemSession = NodeUtil.getNodeByIdentifier(workspaceName, parentId);
214                     Node newAreaNode = NodeUtil.createPath(parentNodeInSystemSession, AreaElement.this.name, MgnlNodeType.NT_AREA);
215                     NodeUtil.createPath(newAreaNode, MetaData.DEFAULT_META_NODE, MgnlNodeType.NT_METADATA);
216                     newAreaNode.getSession().save();
217                     return null;
218                 }
219             });
220         } catch (RepositoryException e) {
221             log.error("ignoring problem w/ creating area in workspace {} for node {}", workspaceName, parentId);
222             // ignore, when working w/ versioned nodes ...
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, MgnlNodeType.NT_COMPONENT));
269                     numberOfComponents = listOfComponents.size();
270                 }
271                 if (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(NoScriptRenderer.NO_SCRIPT_RENDERER);
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() && this.isAreaDefinitionEnabled) {
329                 MarkupHelper helper = new MarkupHelper(out);
330                 helper.closeComment(CMS_AREA);
331             }
332 
333         } catch (Exception e) {
334             throw new RenderException("Can't render area " + areaNode + " with name " + this.name, e);
335         }
336     }
337 
338     protected Node tryToCreateAreaNode(Node parentNode) throws RenderException {
339         Node area = null;
340         try {
341             if (parentNode.hasNode(name)) {
342                 area = parentNode.getNode(name);
343             } else {
344                 // autocreate and save area only if it's not optional
345                 if (!this.optional) {
346                     area = createNewAreaNode(parentNode);
347                 }
348             }
349         } catch (RepositoryException e) {
350             throw new RenderException("Can't access or create area node [" + name + "] on [" + parentNode + "]: " + e.getMessage(), e);
351         }
352         // at this stage we can be sure that the target area, unless optional, has been created.
353         if (area != null) {
354             // TODO fgrilli: what about other component types to be autogenerated (i.e. autogenerating an entire page)?
355             final AutoGenerationConfiguration autoGeneration = areaDefinition.getAutoGeneration();
356             if (autoGeneration != null && autoGeneration.getGeneratorClass() != null) {
357                 Components.newInstance(autoGeneration.getGeneratorClass(), area).generate(autoGeneration);
358             }
359         }
360         return area;
361     }
362 
363     protected AreaDefinition resolveAreaDefinition() {
364         if (areaDefinition != null) {
365             return areaDefinition;
366         }
367 
368         if (!StringUtils.isEmpty(name)) {
369             if (templateDefinition != null && templateDefinition.getAreas().containsKey(name)) {
370                 return templateDefinition.getAreas().get(name);
371             }
372         }
373         // happens if no area definition is passed or configured
374         // an ad-hoc area definition will be created
375         return null;
376     }
377 
378     protected TemplateDefinition resolveTemplateDefinition() throws RenderException {
379         final RenderableDefinition renderableDefinition = getRenderingContext().getRenderableDefinition();
380         if (renderableDefinition == null || renderableDefinition instanceof TemplateDefinition) {
381             return (TemplateDefinition) renderableDefinition;
382         }
383         throw new RenderException("Current RenderableDefinition [" + renderableDefinition + "] is not of type TemplateDefinition. Areas cannot be supported");
384     }
385 
386     /*
387      * An area script can be rendered when
388      * area is enabled
389      * 
390      * AND
391      * 
392      * If an area is optional:
393      * 
394      * if not yet created the area bar has a create button and the script is
395      * - executed in the edit mode but the content object is null (otherwise we can't place the bar)
396      * - not executed otherwise (no place holder divs)
397      * 
398      * If created, the bar has a remove button (other areas cannot be removed nor created)
399      * 
400      * If an area is required:
401      * 
402      * the area node gets created (always) the script is always executed.
403      */
404     private boolean canRenderAreaScript() {
405         // 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
406         return this.isAreaDefinitionEnabled && (areaNode != null || areaNode == null && this.optional && !MgnlContext.getAggregationState().isPreviewMode());
407     }
408 
409     private String resolveDialog() {
410         return dialog != null ? dialog : areaDefinition != null ? areaDefinition.getDialog() : null;
411     }
412 
413     private String resolveType() {
414         return type != null ? type : areaDefinition != null && areaDefinition.getType() != null ? areaDefinition.getType() : AreaDefinition.DEFAULT_TYPE;
415     }
416 
417     private String resolveName() {
418         return name != null ? name : areaDefinition != null ? areaDefinition.getName() : null;
419     }
420 
421     private String resolveLabel() {
422         return label != null ? label : areaDefinition != null && StringUtils.isNotBlank(areaDefinition.getTitle()) ? areaDefinition.getTitle() : StringUtils.capitalize(name);
423     }
424 
425     private Boolean resolveOptional() {
426         return optional != null ? optional : areaDefinition != null && areaDefinition.getOptional() != null ? areaDefinition.getOptional() : Boolean.FALSE;
427     }
428 
429     private Boolean resolveEditable() {
430         return editable != null ? editable : areaDefinition != null && areaDefinition.getEditable() != null ? areaDefinition.getEditable() : null;
431     }
432 
433     private Integer resolveMaximumOfComponents() {
434         return maxComponents != null ? maxComponents : areaDefinition != null && areaDefinition.getMaxComponents() != null ? areaDefinition.getMaxComponents() : Integer.MAX_VALUE;
435     }
436 
437     private boolean isInheritanceEnabled() {
438         return areaDefinition != null && areaDefinition.getInheritance() != null && areaDefinition.getInheritance().isEnabled() != null && areaDefinition.getInheritance().isEnabled();
439     }
440 
441     private boolean isOptionalAreaCreated() {
442         return this.optional && this.areaNode != null;
443     }
444 
445     private boolean hasComponents(Node parent) throws RenderException {
446         try {
447             return NodeUtil.getNodes(parent, MgnlNodeType.NT_COMPONENT).iterator().hasNext();
448         } catch (RepositoryException e) {
449             throw new RenderException(e);
450         }
451     }
452 
453     private int numberOfComponents(Node parent) throws RenderException {
454         try {
455             return NodeUtil.asList(NodeUtil.getNodes(parent, MgnlNodeType.NT_COMPONENT)).size();
456         } catch (RepositoryException e) {
457             throw new RenderException(e);
458         }
459     }
460 
461     protected String resolveAvailableComponents() {
462         if (StringUtils.isNotEmpty(availableComponents)) {
463             return StringUtils.remove(availableComponents, " ");
464         }
465         if (areaDefinition != null && areaDefinition.getAvailableComponents().size() > 0) {
466             Iterator<ComponentAvailability> iterator = areaDefinition.getAvailableComponents().values().iterator();
467             List<String> componentIds = new ArrayList<String>();
468             final Collection<String> userRoles = MgnlContext.getUser().getAllRoles();
469             while (iterator.hasNext()) {
470                 ComponentAvailability availableComponent = iterator.next();
471                 if (availableComponent.isEnabled()) {
472                     // check roles
473                     final Collection<String> roles = availableComponent.getRoles();
474                     if (!roles.isEmpty()) {
475                         if (CollectionUtils.containsAny(userRoles, roles)) {
476                             componentIds.add(availableComponent.getId());
477                         }
478                     } else {
479                         componentIds.add(availableComponent.getId());
480                     }
481                 }
482             }
483             return StringUtils.join(componentIds, ',');
484         }
485         return "";
486     }
487 
488     private boolean shouldShowAddButton() throws RenderException {
489 
490         if (areaNode == null || type.equals(AreaDefinition.TYPE_NO_COMPONENT)) {
491             return false;
492         }
493 
494         int numberOfComponents = numberOfComponents(areaNode);
495         if (numberOfComponents >= maxComponents || numberOfComponents > 0 && type.equals(AreaDefinition.TYPE_SINGLE)) {
496             return false;
497         }
498 
499         return true;
500     }
501 
502     private boolean shouldShowNewComponentArea() throws RenderException {
503 
504         if (areaNode == null || type.equals(AreaDefinition.TYPE_NO_COMPONENT) || type.equals(AreaDefinition.TYPE_SINGLE) && hasComponents(areaNode)) {
505             return false;
506         }
507         return true;
508     }
509 
510     public String getName() {
511         return name;
512     }
513 
514     public void setName(String name) {
515         this.name = name;
516     }
517 
518     public AreaDefinition getArea() {
519         return areaDefinition;
520     }
521 
522     public void setArea(AreaDefinition area) {
523         this.areaDefinition = area;
524     }
525 
526     public String getAvailableComponents() {
527         return availableComponents;
528     }
529 
530     public void setAvailableComponents(String availableComponents) {
531         this.availableComponents = availableComponents;
532     }
533 
534     public String getType() {
535         return type;
536     }
537 
538     public void setType(String type) {
539         this.type = type;
540     }
541 
542     public String getDialog() {
543         return dialog;
544     }
545 
546     public void setDialog(String dialog) {
547         this.dialog = dialog;
548     }
549 
550     public String getLabel() {
551         return label;
552     }
553 
554     public void setLabel(String label) {
555         this.label = label;
556     }
557 
558     public String getDescription() {
559         return description;
560     }
561 
562     public void setDescription(String description) {
563         this.description = description;
564     }
565 
566     public boolean isInherit() {
567         return inherit;
568     }
569 
570     public void setInherit(boolean inherit) {
571         this.inherit = inherit;
572     }
573 
574     public Boolean getEditable() {
575         return editable;
576     }
577 
578     public void setEditable(Boolean editable) {
579         this.editable = editable;
580     }
581 
582     public Map<String, Object> getContextAttributes() {
583         return contextAttributes;
584     }
585 
586     public void setContextAttributes(Map<String, Object> contextAttributes) {
587         this.contextAttributes = contextAttributes;
588     }
589 
590     public Integer getMaxComponents() {
591         return maxComponents;
592     }
593 
594     public void setMaxComponents(Integer maxComponents) {
595         this.maxComponents = maxComponents;
596     }
597 }