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