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