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