View Javadoc

1   /**
2    * This file Copyright (c) 2011 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.HashMap;
63  import java.util.Iterator;
64  import java.util.List;
65  import java.util.Map;
66  
67  import javax.jcr.Node;
68  import javax.jcr.RepositoryException;
69  
70  import org.apache.commons.lang.StringUtils;
71  import org.slf4j.Logger;
72  import org.slf4j.LoggerFactory;
73  
74  
75  /**
76   * Renders an area and outputs a marker that instructs the page editor to place a bar at this location.
77   *
78   * @version $Id$
79   */
80  public class AreaElement extends AbstractContentTemplatingElement {
81  
82      private static final Logger log = LoggerFactory.getLogger(AreaElement.class);
83      public static final String CMS_AREA = "cms:area";
84  
85      public static final String ATTRIBUTE_COMPONENT = "component";
86      public static final String ATTRIBUTE_COMPONENTS = "components";
87  
88      private final RenderingEngine renderingEngine;
89  
90      private Node areaNode;
91      private TemplateDefinition templateDefinition;
92      private AreaDefinition areaDefinition;
93      private String name;
94      private String type;
95      private String dialog;
96      private String availableComponents;
97      private String label;
98      private String description;
99      private boolean inherit;
100     private Boolean editable;
101 
102     private Map<String, Object> contextAttributes = new HashMap<String, Object>();
103 
104     private String areaPath;
105 
106     private boolean isAreaDefinitionEnabled;
107 
108 
109     public AreaElement(ServerConfiguration server, RenderingContext renderingContext, RenderingEngine renderingEngine) {
110         super(server, renderingContext);
111         this.renderingEngine = renderingEngine;
112     }
113 
114     @Override
115     public void begin(Appendable out) throws IOException, RenderException {
116 
117         this.templateDefinition = resolveTemplateDefinition();
118         Messages messages = MessagesManager.getMessages(templateDefinition.getI18nBasename());
119 
120         this.areaDefinition = resolveAreaDefinition();
121 
122         this.isAreaDefinitionEnabled = areaDefinition != null && (areaDefinition.isEnabled() == null || areaDefinition.isEnabled());
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.editable = resolveEditable();
135 
136         this.description = templateDefinition.getDescription();
137 
138         // build an adhoc area definition if no area definition can be resolved
139         if(this.areaDefinition == null){
140             buildAdHocAreaDefinition();
141         }
142 
143         // read area node and calculate the area path
144         this.areaNode = getPassedContent();
145         if(this.areaNode != null){
146             this.areaPath = getNodePath(areaNode);
147         }
148         else{
149             // will be null if no area has been created (for instance for optional areas)
150             // current content is the parent node
151             Node parentNode = currentContent();
152             this.areaNode = tryToCreateAreaNode(parentNode);
153             this.areaPath = getNodePath(parentNode) + "/" + name;
154         }
155 
156         if (isAdmin()) {
157             MarkupHelper helper = new MarkupHelper(out);
158 
159             helper.openComment(CMS_AREA).attribute(AbstractDirective.CONTENT_ATTRIBUTE, this.areaPath);
160             helper.attribute("name", this.name);
161             helper.attribute("availableComponents", this.availableComponents);
162             helper.attribute("type", this.type);
163             helper.attribute("dialog", this.dialog);
164             helper.attribute("label", messages.getWithDefault(this.label, this.label));
165             helper.attribute("inherit", String.valueOf(this.inherit));
166             if (this.editable != null) {
167                 helper.attribute("editable", String.valueOf(this.editable));
168             }
169             helper.attribute("optional", String.valueOf(this.areaDefinition.isOptional()));
170             if(isOptionalAreaCreated()) {
171                 helper.attribute("created", "true");
172             }
173             helper.attribute("showAddButton", String.valueOf(shouldShowAddButton()));
174             if (StringUtils.isNotBlank(description)) {
175                 helper.attribute("description", messages.getWithDefault(description, description));
176             }
177 
178             helper.append(" -->\n");
179 
180         }
181     }
182 
183     private Node createNewAreaNode(Node parentNode) throws RepositoryException {
184         final String parentId = parentNode.getIdentifier();
185         final String workspaceName = parentNode.getSession().getWorkspace().getName();
186         try {
187             MgnlContext.doInSystemContext(new MgnlContext.Op<Void, RepositoryException>() {
188                 @Override
189                 public Void exec() throws RepositoryException {
190                     Node parentNodeInSystemSession = NodeUtil.getNodeByIdentifier(workspaceName, parentId);
191                     Node newAreaNode = NodeUtil.createPath(parentNodeInSystemSession, AreaElement.this.name, MgnlNodeType.NT_AREA);
192                     NodeUtil.createPath(newAreaNode, MetaData.DEFAULT_META_NODE, MgnlNodeType.NT_METADATA);
193                     newAreaNode.getSession().save();
194                     return null;
195                 }
196             });
197         } catch (RepositoryException e) {
198             log.error("ignoring problem w/ creating area in workspace {} for node {}", workspaceName, parentId);
199             // ignore, when working w/ versioned nodes ...
200             return null;
201         }
202         return parentNode.getNode(this.name);
203     }
204 
205     protected void buildAdHocAreaDefinition() {
206         ConfiguredAreaDefinition addHocAreaDefinition = new ConfiguredAreaDefinition();
207         addHocAreaDefinition.setName(this.name);
208         addHocAreaDefinition.setDialog(this.dialog);
209         addHocAreaDefinition.setType(this.type);
210         addHocAreaDefinition.setRenderType(this.templateDefinition.getRenderType());
211         areaDefinition = addHocAreaDefinition;
212     }
213 
214     @Override
215     public void end(Appendable out) throws RenderException {
216 
217         try {
218             if (canRenderAreaScript()) {
219                 if(isInherit() && areaNode != null) {
220                     try {
221                         areaNode = new DefaultInheritanceContentDecorator(areaNode, areaDefinition.getInheritance()).wrapNode(areaNode);
222                     } catch (RepositoryException e) {
223                         throw new RuntimeRepositoryException(e);
224                     }
225                 }
226                 Map<String, Object> contextObjects = new HashMap<String, Object>();
227 
228                 List<ContentMap> components = new ArrayList<ContentMap>();
229 
230                 if (areaNode != null) {
231                     for (Node node : NodeUtil.getNodes(areaNode, MgnlNodeType.NT_COMPONENT)) {
232                         components.add(new ContentMap(node));
233                     }
234                 }
235                 if(AreaDefinition.TYPE_SINGLE.equals(type)) {
236                     if(components.size() > 1) {
237                         throw new RenderException("Can't render single area [" + areaNode + "]: expected one component node but found more.");
238                     }
239                     if(components.size() == 1) {
240                         contextObjects.put(ATTRIBUTE_COMPONENT, components.get(0));
241                     } else {
242                         contextObjects.put(ATTRIBUTE_COMPONENT, null);
243                     }
244                 } else {
245                     contextObjects.put(ATTRIBUTE_COMPONENTS, components);
246                 }
247                 // FIXME we shouldn't manipulate the area definition directly
248                 // we should use merge with the proxy approach
249                 if(areaDefinition.getRenderType() == null && areaDefinition instanceof ConfiguredAreaDefinition){
250                     ((ConfiguredAreaDefinition)areaDefinition).setRenderType(this.templateDefinition.getRenderType());
251                 }
252 
253                 // FIXME we shouldn't manipulate the area definition directly
254                 // we should use merge with the proxy approach
255                 if(areaDefinition.getI18nBasename() == null && areaDefinition instanceof ConfiguredAreaDefinition){
256                     ((ConfiguredAreaDefinition)areaDefinition).setI18nBasename(this.templateDefinition.getI18nBasename());
257                 }
258                 WebContext webContext = MgnlContext.getWebContext();
259                 webContext.push(webContext.getRequest(), webContext.getResponse());
260                 setAttributesInWebContext(contextAttributes, WebContext.LOCAL_SCOPE);
261                 try {
262                     AppendableOnlyOutputProvider appendable = new AppendableOnlyOutputProvider(out);
263                     if(StringUtils.isNotEmpty(areaDefinition.getTemplateScript())){
264                         renderingEngine.render(areaNode, areaDefinition, contextObjects, appendable);
265                     }
266                     // no script
267                     else{
268                         for (ContentMap component : components) {
269                             ComponentElement componentElement = Components.newInstance(ComponentElement.class);
270                             componentElement.setContent(component.getJCRNode());
271                             componentElement.begin(out);
272                             componentElement.end(out);
273                         }
274                     }
275                 } finally {
276                     webContext.pop();
277                     webContext.setPageContext(null);
278                     restoreAttributesInWebContext(contextAttributes, WebContext.LOCAL_SCOPE);
279                 }
280 
281             }
282 
283             if (isAdmin() && this.isAreaDefinitionEnabled) {
284                 MarkupHelper helper = new MarkupHelper(out);
285                 helper.closeComment(CMS_AREA);
286             }
287         } catch (Exception e) {
288             throw new RenderException("Can't render area " + areaNode + " with name " + this.name, e);
289         }
290     }
291 
292     protected Node tryToCreateAreaNode(Node parentNode) throws RenderException {
293         Node area = null;
294         try {
295             if(parentNode.hasNode(name)){
296                 area = parentNode.getNode(name);
297             } else {
298                 //autocreate and save area only if it's not optional
299                 if(!areaDefinition.isOptional()) {
300                     area = createNewAreaNode(parentNode);
301                 }
302             }
303         }
304         catch (RepositoryException e) {
305             throw new RenderException("Can't access area node [" + name + "] on [" + parentNode + "]", e);
306         }
307         //at this stage we can be sure that the target area, unless optional, has been created.
308         if(area != null) {
309             //TODO fgrilli: what about other component types to be autogenerated (i.e. autogenerating an entire page)?
310             final AutoGenerationConfiguration autoGeneration = areaDefinition.getAutoGeneration();
311             if (autoGeneration != null && autoGeneration.getGeneratorClass() != null) {
312                 Components.newInstance(autoGeneration.getGeneratorClass(), area).generate(autoGeneration);
313             }
314         }
315         return area;
316     }
317 
318     protected AreaDefinition resolveAreaDefinition() {
319         if (areaDefinition != null) {
320             return areaDefinition;
321         }
322 
323         if (!StringUtils.isEmpty(name)) {
324             if (templateDefinition != null && templateDefinition.getAreas().containsKey(name)) {
325                 return templateDefinition.getAreas().get(name);
326             }
327         }
328         // happens if no area definition is passed or configured
329         // an ad-hoc area definition will be created
330         return null;
331     }
332 
333     protected TemplateDefinition resolveTemplateDefinition() throws RenderException {
334         final RenderableDefinition renderableDefinition = getRenderingContext().getRenderableDefinition();
335         if (renderableDefinition == null || renderableDefinition instanceof TemplateDefinition) {
336             return (TemplateDefinition) renderableDefinition;
337         }
338         throw new RenderException("Current RenderableDefinition [" + renderableDefinition + "] is not of type TemplateDefinition. Areas cannot be supported");
339     }
340 
341     /*
342      * An area script can be rendered when
343      * area is enabled
344      *
345      * AND
346      *
347      * If an area is optional:
348      *
349      * if not yet created the area bar has a create button and the script is
350      * - executed in the edit mode but the content object is null (otherwise we can't place the bar)
351      * - not executed otherwise (no place holder divs)
352      *
353      * If created, the bar has a remove button (other areas cannot be removed nor created)
354      *
355      * If an area is required:
356      *
357      * the area node gets created (always) the script is always executed.
358      */
359     private boolean canRenderAreaScript() {
360         // 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
361         return this.isAreaDefinitionEnabled && (areaNode != null || (areaNode == null && areaDefinition.isOptional() && !MgnlContext.getAggregationState().isPreviewMode()));
362     }
363 
364     private String resolveDialog() {
365         return dialog != null ? dialog : areaDefinition != null ? areaDefinition.getDialog() : null;
366     }
367 
368     private String resolveType() {
369         return type != null ? type : areaDefinition != null && areaDefinition.getType() != null ? areaDefinition.getType() : AreaDefinition.DEFAULT_TYPE;
370     }
371 
372     private String resolveName() {
373         return name != null ? name : (areaDefinition != null ? areaDefinition.getName() : null);
374     }
375 
376     private String resolveLabel() {
377         return label != null ? label : (areaDefinition != null && StringUtils.isNotBlank(areaDefinition.getTitle()) ? areaDefinition.getTitle() : StringUtils.capitalize(name));
378     }
379 
380     private Boolean resolveEditable() {
381         return editable != null ? editable : areaDefinition != null && areaDefinition.getEditable() != null ? areaDefinition.getEditable() : null;
382     }
383 
384     private boolean isInheritanceEnabled() {
385         return areaDefinition != null && areaDefinition.getInheritance() != null && areaDefinition.getInheritance().isEnabled() != null && areaDefinition.getInheritance().isEnabled();
386     }
387 
388     private boolean isOptionalAreaCreated() {
389         return this.areaDefinition.isOptional() && this.areaNode != null;
390     }
391 
392     private boolean hasComponents(Node parent) throws RenderException {
393         try {
394             return NodeUtil.getNodes(parent, MgnlNodeType.NT_COMPONENT).iterator().hasNext();
395         } catch (RepositoryException e) {
396             throw new RenderException(e);
397         }
398     }
399 
400     protected String resolveAvailableComponents() {
401         if (StringUtils.isNotEmpty(availableComponents)) {
402             return availableComponents;
403         }
404         if (areaDefinition != null && areaDefinition.getAvailableComponents().size() > 0) {
405             Iterator<ComponentAvailability> iterator = areaDefinition.getAvailableComponents().values().iterator();
406             List<String> componentIds = new ArrayList<String>();
407             while (iterator.hasNext()) {
408                 ComponentAvailability availableComponent = iterator.next();
409                 if(availableComponent.isEnabled()) {
410                     componentIds.add(availableComponent.getId());
411                 }
412             }
413             return StringUtils.join(componentIds, ',');
414         }
415         return "";
416     }
417 
418     private boolean shouldShowAddButton() throws RenderException {
419 
420         if(areaNode == null || type.equals(AreaDefinition.TYPE_NO_COMPONENT) || (type.equals(AreaDefinition.TYPE_SINGLE) && hasComponents(areaNode))) {
421             return false;
422         }
423 
424         return true;
425     }
426 
427     public String getName() {
428         return name;
429     }
430 
431     public void setName(String name) {
432         this.name = name;
433     }
434 
435     public AreaDefinition getArea() {
436         return areaDefinition;
437     }
438 
439     public void setArea(AreaDefinition area) {
440         this.areaDefinition = area;
441     }
442 
443     public String getAvailableComponents() {
444         return availableComponents;
445     }
446 
447     public void setAvailableComponents(String availableComponents) {
448         this.availableComponents = availableComponents;
449     }
450 
451     public String getType() {
452         return type;
453     }
454 
455     public void setType(String type) {
456         this.type = type;
457     }
458 
459     public String getDialog() {
460         return dialog;
461     }
462 
463     public void setDialog(String dialog) {
464         this.dialog = dialog;
465     }
466 
467     public String getLabel() {
468         return label;
469     }
470 
471     public void setLabel(String label) {
472         this.label = label;
473     }
474 
475     public String getDescription() {
476         return description;
477     }
478 
479     public void setDescription(String description) {
480         this.description = description;
481     }
482 
483     public boolean isInherit() {
484         return inherit;
485     }
486 
487     public void setInherit(boolean inherit) {
488         this.inherit = inherit;
489     }
490 
491     public Boolean getEditable() {
492         return editable;
493     }
494 
495     public void setEditable(Boolean editable) {
496         this.editable = editable;
497     }
498 
499     public Map<String, Object> getContextAttributes() {
500         return contextAttributes;
501     }
502 
503     public void setContextAttributes(Map<String, Object> contextAttributes) {
504         this.contextAttributes = contextAttributes;
505     }
506 }