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         } 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() && hasPermission(this.areaNode)) {
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 boolean hasPermission(Node node) {
186         if (node == null) {
187             node = currentContent();
188         }
189         try {
190             return node.getSession().hasPermission(node.getPath(), Session.ACTION_SET_PROPERTY);
191         } catch (RepositoryException e) {
192             log.error("Could not determine permission for node {}", node);
193         }
194         return false;
195     }
196 
197     private Node createNewAreaNode(Node parentNode) throws RepositoryException {
198         final String parentId = parentNode.getIdentifier();
199         final String workspaceName = parentNode.getSession().getWorkspace().getName();
200         try {
201             MgnlContext.doInSystemContext(new MgnlContext.Op<Void, RepositoryException>() {
202                 @Override
203                 public Void exec() throws RepositoryException {
204                     Node parentNodeInSystemSession = NodeUtil.getNodeByIdentifier(workspaceName, parentId);
205                     Node newAreaNode = NodeUtil.createPath(parentNodeInSystemSession, AreaElement.this.name, MgnlNodeType.NT_AREA);
206                     NodeUtil.createPath(newAreaNode, MetaData.DEFAULT_META_NODE, MgnlNodeType.NT_METADATA);
207                     newAreaNode.getSession().save();
208                     return null;
209                 }
210             });
211         } catch (RepositoryException e) {
212             log.error("ignoring problem w/ creating area in workspace {} for node {}", workspaceName, parentId);
213             // ignore, when working w/ versioned nodes ...
214             return null;
215         }
216         parentNode.getSession().refresh(true);
217         if (!parentNode.hasNode(name)) {
218             boolean existUnwrapped = MgnlContext.getJCRSession(parentNode.getSession().getWorkspace().getName()).itemExists(parentNode.getPath() + "/" + name);
219             log.warn("Failed to find previously created child [" + name + "] of node [" + parentNode.getPath() + "]. This node should be visible or there should be an error logged earlier in the log files explaining the problem.");
220             log.warn("This node has " + (existUnwrapped ? "" : "not") + " been found in unwrapped call to the session."
221                     + (existUnwrapped ? "This means one of the wrappers is probably swallawing the changes on the node." : "This means concurrent node modifications by different sessions are not visible fast enough to other sessions even after forced refresh. This seems like a JR bug."));
222             // try to refresh again in case this was just item cache override (could this really happen? if so it would be most likely JR error).
223             parentNode.getSession().refresh(true);
224         }
225         return parentNode.getNode(this.name);
226     }
227 
228     protected void buildAdHocAreaDefinition() {
229         ConfiguredAreaDefinition addHocAreaDefinition = new ConfiguredAreaDefinition();
230         addHocAreaDefinition.setName(this.name);
231         addHocAreaDefinition.setDialog(this.dialog);
232         addHocAreaDefinition.setType(this.type);
233         addHocAreaDefinition.setRenderType(this.templateDefinition.getRenderType());
234         areaDefinition = addHocAreaDefinition;
235     }
236 
237     @Override
238     public void end(Appendable out) throws RenderException {
239 
240         try {
241             if (canRenderAreaScript()) {
242                 if(isInherit() && areaNode != null) {
243                     try {
244                         areaNode = new DefaultInheritanceContentDecorator(areaNode, areaDefinition.getInheritance()).wrapNode(areaNode);
245                     } catch (RepositoryException e) {
246                         throw new RuntimeRepositoryException(e);
247                     }
248                 }
249                 Map<String, Object> contextObjects = new HashMap<String, Object>();
250 
251                 List<ContentMap> components = new ArrayList<ContentMap>();
252 
253                 if (areaNode != null) {
254                     for (Node node : NodeUtil.getNodes(areaNode, MgnlNodeType.NT_COMPONENT)) {
255                         components.add(new ContentMap(node));
256                     }
257                 }
258                 if(AreaDefinition.TYPE_SINGLE.equals(type)) {
259                     if(components.size() > 1) {
260                         throw new RenderException("Can't render single area [" + areaNode + "]: expected one component node but found more.");
261                     }
262                     if(components.size() == 1) {
263                         contextObjects.put(ATTRIBUTE_COMPONENT, components.get(0));
264                     } else {
265                         contextObjects.put(ATTRIBUTE_COMPONENT, null);
266                     }
267                 } else {
268                     contextObjects.put(ATTRIBUTE_COMPONENTS, components);
269                 }
270                 // FIXME we shouldn't manipulate the area definition directly
271                 // we should use merge with the proxy approach
272                 if(areaDefinition.getRenderType() == null && areaDefinition instanceof ConfiguredAreaDefinition){
273                     ((ConfiguredAreaDefinition)areaDefinition).setRenderType(this.templateDefinition.getRenderType());
274                 }
275 
276                 // FIXME we shouldn't manipulate the area definition directly
277                 // we should use merge with the proxy approach
278                 if(areaDefinition.getI18nBasename() == null && areaDefinition instanceof ConfiguredAreaDefinition){
279                     ((ConfiguredAreaDefinition)areaDefinition).setI18nBasename(this.templateDefinition.getI18nBasename());
280                 }
281                 WebContext webContext = MgnlContext.getWebContext();
282                 webContext.push(webContext.getRequest(), webContext.getResponse());
283                 setAttributesInWebContext(contextAttributes, WebContext.LOCAL_SCOPE);
284                 try {
285                     AppendableOnlyOutputProvider appendable = new AppendableOnlyOutputProvider(out);
286                     if(StringUtils.isNotEmpty(areaDefinition.getTemplateScript())){
287                         renderingEngine.render(areaNode, areaDefinition, contextObjects, appendable);
288                     }
289                     // no script
290                     else{
291                         for (ContentMap component : components) {
292                             ComponentElement componentElement = Components.newInstance(ComponentElement.class);
293                             componentElement.setContent(component.getJCRNode());
294                             componentElement.begin(out);
295                             componentElement.end(out);
296                         }
297                     }
298                 } finally {
299                     webContext.pop();
300                     webContext.setPageContext(null);
301                     restoreAttributesInWebContext(contextAttributes, WebContext.LOCAL_SCOPE);
302                 }
303 
304             }
305 
306             if (isAdmin() && this.isAreaDefinitionEnabled) {
307                 MarkupHelper helper = new MarkupHelper(out);
308                 helper.closeComment(CMS_AREA);
309             }
310         } catch (Exception e) {
311             throw new RenderException("Can't render area " + areaNode + " with name " + this.name, e);
312         }
313     }
314 
315     protected Node tryToCreateAreaNode(Node parentNode) throws RenderException {
316         Node area = null;
317         try {
318             if(parentNode.hasNode(name)){
319                 area = parentNode.getNode(name);
320             } else {
321                 //autocreate and save area only if it's not optional
322                 if(!this.optional) {
323                     area = createNewAreaNode(parentNode);
324                 }
325             }
326         }
327         catch (RepositoryException e) {
328             throw new RenderException("Can't access or create area node [" + name + "] on [" + parentNode + "]: " + e.getMessage(), e);
329         }
330         //at this stage we can be sure that the target area, unless optional, has been created.
331         if(area != null) {
332             //TODO fgrilli: what about other component types to be autogenerated (i.e. autogenerating an entire page)?
333             final AutoGenerationConfiguration autoGeneration = areaDefinition.getAutoGeneration();
334             if (autoGeneration != null && autoGeneration.getGeneratorClass() != null) {
335                 Components.newInstance(autoGeneration.getGeneratorClass(), area).generate(autoGeneration);
336             }
337         }
338         return area;
339     }
340 
341     protected AreaDefinition resolveAreaDefinition() {
342         if (areaDefinition != null) {
343             return areaDefinition;
344         }
345 
346         if (!StringUtils.isEmpty(name)) {
347             if (templateDefinition != null && templateDefinition.getAreas().containsKey(name)) {
348                 return templateDefinition.getAreas().get(name);
349             }
350         }
351         // happens if no area definition is passed or configured
352         // an ad-hoc area definition will be created
353         return null;
354     }
355 
356     protected TemplateDefinition resolveTemplateDefinition() throws RenderException {
357         final RenderableDefinition renderableDefinition = getRenderingContext().getRenderableDefinition();
358         if (renderableDefinition == null || renderableDefinition instanceof TemplateDefinition) {
359             return (TemplateDefinition) renderableDefinition;
360         }
361         throw new RenderException("Current RenderableDefinition [" + renderableDefinition + "] is not of type TemplateDefinition. Areas cannot be supported");
362     }
363 
364     /*
365      * An area script can be rendered when
366      * area is enabled
367      *
368      * AND
369      *
370      * If an area is optional:
371      *
372      * if not yet created the area bar has a create button and the script is
373      * - executed in the edit mode but the content object is null (otherwise we can't place the bar)
374      * - not executed otherwise (no place holder divs)
375      *
376      * If created, the bar has a remove button (other areas cannot be removed nor created)
377      *
378      * If an area is required:
379      *
380      * the area node gets created (always) the script is always executed.
381      */
382     private boolean canRenderAreaScript() {
383         // 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
384         return this.isAreaDefinitionEnabled && (areaNode != null || (areaNode == null && areaDefinition.isOptional() && !MgnlContext.getAggregationState().isPreviewMode()));
385     }
386 
387     private String resolveDialog() {
388         return dialog != null ? dialog : areaDefinition != null ? areaDefinition.getDialog() : null;
389     }
390 
391     private String resolveType() {
392         return type != null ? type : areaDefinition != null && areaDefinition.getType() != null ? areaDefinition.getType() : AreaDefinition.DEFAULT_TYPE;
393     }
394 
395     private String resolveName() {
396         return name != null ? name : (areaDefinition != null ? areaDefinition.getName() : null);
397     }
398 
399     private String resolveLabel() {
400         return label != null ? label : (areaDefinition != null && StringUtils.isNotBlank(areaDefinition.getTitle()) ? areaDefinition.getTitle() : StringUtils.capitalize(name));
401     }
402 
403     private Boolean resolveOptional() {
404         return optional != null ? optional : areaDefinition != null && areaDefinition.isOptional() != null ? areaDefinition.isOptional() : Boolean.FALSE;
405     }
406 
407     private Boolean resolveEditable() {
408         return editable != null ? editable : areaDefinition != null && areaDefinition.getEditable() != null ? areaDefinition.getEditable() : null;
409     }
410 
411     private boolean isInheritanceEnabled() {
412         return areaDefinition != null && areaDefinition.getInheritance() != null && areaDefinition.getInheritance().isEnabled() != null && areaDefinition.getInheritance().isEnabled();
413     }
414 
415     private boolean isOptionalAreaCreated() {
416         return this.optional && this.areaNode != null;
417     }
418 
419     private boolean hasComponents(Node parent) throws RenderException {
420         try {
421             return NodeUtil.getNodes(parent, MgnlNodeType.NT_COMPONENT).iterator().hasNext();
422         } catch (RepositoryException e) {
423             throw new RenderException(e);
424         }
425     }
426 
427     protected String resolveAvailableComponents() {
428         if (StringUtils.isNotEmpty(availableComponents)) {
429             return StringUtils.remove(availableComponents, " ");
430         }
431         if (areaDefinition != null && areaDefinition.getAvailableComponents().size() > 0) {
432             Iterator<ComponentAvailability> iterator = areaDefinition.getAvailableComponents().values().iterator();
433             List<String> componentIds = new ArrayList<String>();
434             while (iterator.hasNext()) {
435                 ComponentAvailability availableComponent = iterator.next();
436                 if(availableComponent.isEnabled()) {
437                     componentIds.add(availableComponent.getId());
438                 }
439             }
440             return StringUtils.join(componentIds, ',');
441         }
442         return "";
443     }
444 
445     private boolean shouldShowAddButton() throws RenderException {
446 
447         if(areaNode == null || type.equals(AreaDefinition.TYPE_NO_COMPONENT) || (type.equals(AreaDefinition.TYPE_SINGLE) && hasComponents(areaNode))) {
448             return false;
449         }
450 
451         return true;
452     }
453 
454     public String getName() {
455         return name;
456     }
457 
458     public void setName(String name) {
459         this.name = name;
460     }
461 
462     public AreaDefinition getArea() {
463         return areaDefinition;
464     }
465 
466     public void setArea(AreaDefinition area) {
467         this.areaDefinition = area;
468     }
469 
470     public String getAvailableComponents() {
471         return availableComponents;
472     }
473 
474     public void setAvailableComponents(String availableComponents) {
475         this.availableComponents = availableComponents;
476     }
477 
478     public String getType() {
479         return type;
480     }
481 
482     public void setType(String type) {
483         this.type = type;
484     }
485 
486     public String getDialog() {
487         return dialog;
488     }
489 
490     public void setDialog(String dialog) {
491         this.dialog = dialog;
492     }
493 
494     public String getLabel() {
495         return label;
496     }
497 
498     public void setLabel(String label) {
499         this.label = label;
500     }
501 
502     public String getDescription() {
503         return description;
504     }
505 
506     public void setDescription(String description) {
507         this.description = description;
508     }
509 
510     public boolean isInherit() {
511         return inherit;
512     }
513 
514     public void setInherit(boolean inherit) {
515         this.inherit = inherit;
516     }
517 
518     public Boolean getEditable() {
519         return editable;
520     }
521 
522     public void setEditable(Boolean editable) {
523         this.editable = editable;
524     }
525 
526     public Map<String, Object> getContextAttributes() {
527         return contextAttributes;
528     }
529 
530     public void setContextAttributes(Map<String, Object> contextAttributes) {
531         this.contextAttributes = contextAttributes;
532     }
533 }