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