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