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