/**
* This file Copyright (c) 2011-2012 Magnolia International
* Ltd. (http://www.magnolia-cms.com). All rights reserved.
*
*
* This file is dual-licensed under both the Magnolia
* Network Agreement and the GNU General Public License.
* You may elect to use one or the other of these licenses.
*
* This file is distributed in the hope that it will be
* useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
* implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
* Redistribution, except as permitted by whichever of the GPL
* or MNA you select, is prohibited.
*
* 1. For the GPL license (GPL), you can redistribute and/or
* modify this file under the terms of the GNU General
* Public License, Version 3, as published by the Free Software
* Foundation. You should have received a copy of the GNU
* General Public License, Version 3 along with this program;
* if not, write to the Free Software Foundation, Inc., 51
* Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* 2. For the Magnolia Network Agreement (MNA), this file
* and the accompanying materials are made available under the
* terms of the MNA which accompanies this distribution, and
* is available at http://www.magnolia-cms.com/mna.html
*
* Any modifications to this file must keep this entire header
* intact.
*
*/
package info.magnolia.templating.elements;
import info.magnolia.cms.beans.config.ServerConfiguration;
import info.magnolia.cms.i18n.Messages;
import info.magnolia.cms.i18n.MessagesManager;
import info.magnolia.context.MgnlContext;
import info.magnolia.context.WebContext;
import info.magnolia.jcr.RuntimeRepositoryException;
import info.magnolia.jcr.util.ContentMap;
import info.magnolia.jcr.util.NodeTypes;
import info.magnolia.jcr.util.NodeUtil;
import info.magnolia.objectfactory.Components;
import info.magnolia.rendering.context.RenderingContext;
import info.magnolia.rendering.engine.AppendableOnlyOutputProvider;
import info.magnolia.rendering.engine.RenderException;
import info.magnolia.rendering.engine.RenderingEngine;
import info.magnolia.rendering.generator.Generator;
import info.magnolia.rendering.template.AreaDefinition;
import info.magnolia.rendering.template.AutoGenerationConfiguration;
import info.magnolia.rendering.template.ComponentAvailability;
import info.magnolia.rendering.template.RenderableDefinition;
import info.magnolia.rendering.template.TemplateDefinition;
import info.magnolia.rendering.template.configured.ConfiguredAreaDefinition;
import info.magnolia.templating.freemarker.AbstractDirective;
import info.magnolia.templating.inheritance.DefaultInheritanceContentDecorator;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Renders an area and outputs a marker that instructs the page editor to place a bar at this location.
*/
public class AreaElement extends AbstractContentTemplatingElement {
private static final Logger log = LoggerFactory.getLogger(AreaElement.class);
public static final String CMS_AREA = "cms:area";
public static final String ATTRIBUTE_COMPONENT = "component";
public static final String ATTRIBUTE_COMPONENTS = "components";
private final RenderingEngine renderingEngine;
private Node areaNode;
private TemplateDefinition templateDefinition;
private AreaDefinition areaDefinition;
private String name;
private String type;
private String dialog;
private String availableComponents;
private String label;
private String description;
private Boolean inherit;
private Boolean optional;
private Boolean editable;
private Integer maxComponents;
private Map<String, Object> contextAttributes = new HashMap<String, Object>();
private String areaPath;
private boolean isAreaDefinitionEnabled;
public AreaElement(ServerConfiguration server, RenderingContext renderingContext, RenderingEngine renderingEngine) {
super(server, renderingContext);
this.renderingEngine = renderingEngine;
}
@Override
public void begin(Appendable out) throws IOException, RenderException {
this.templateDefinition = resolveTemplateDefinition();
Messages messages = MessagesManager.getMessages(templateDefinition.getI18nBasename());
this.areaDefinition = resolveAreaDefinition();
this.isAreaDefinitionEnabled = areaDefinition != null && (areaDefinition.isEnabled() == null || areaDefinition.isEnabled());
if (!this.isAreaDefinitionEnabled) {
return;
}
// set the values based on the area definition if not passed
this.name = resolveName();
this.dialog = resolveDialog();
this.type = resolveType();
this.label = resolveLabel();
this.availableComponents = resolveAvailableComponents();
this.inherit = isInheritanceEnabled();
this.optional = resolveOptional();
this.editable = resolveEditable();
this.description = templateDefinition.getDescription();
// build an adhoc area definition if no area definition can be resolved
if(this.areaDefinition == null){
buildAdHocAreaDefinition();
}
this.maxComponents = resolveMaximumOfComponents();
// read area node and calculate the area path
this.areaNode = getPassedContent();
if(this.areaNode != null){
this.areaPath = getNodePath(areaNode);
}
else {
// will be null if no area has been created (for instance for optional areas)
// current content is the parent node
Node parentNode = currentContent();
this.areaNode = tryToCreateAreaNode(parentNode);
this.areaPath = getNodePath(parentNode) + "/" + name;
}
if (isAdmin() && hasPermission(this.areaNode)) {
MarkupHelper helper = new MarkupHelper(out);
helper.openComment(CMS_AREA).attribute(AbstractDirective.CONTENT_ATTRIBUTE, this.areaPath);
helper.attribute("name", this.name);
helper.attribute("availableComponents", this.availableComponents);
helper.attribute("type", this.type);
helper.attribute("dialog", this.dialog);
helper.attribute("label", messages.getWithDefault(this.label, this.label));
helper.attribute("inherit", String.valueOf(this.inherit));
if (this.editable != null) {
helper.attribute("editable", String.valueOf(this.editable));
}
helper.attribute("optional", String.valueOf(this.optional));
if(isOptionalAreaCreated()) {
helper.attribute("created", "true");
}
helper.attribute("showAddButton", String.valueOf(shouldShowAddButton()));
if (StringUtils.isNotBlank(description)) {
helper.attribute("description", messages.getWithDefault(description, description));
}
helper.append(" -->\n");
}
}
private boolean hasPermission(Node node) {
if (node == null) {
node = currentContent();
}
try {
return node.getSession().hasPermission(node.getPath(), Session.ACTION_SET_PROPERTY);
} catch (RepositoryException e) {
log.error("Could not determine permission for node {}", node);
}
return false;
}
private Node createNewAreaNode(Node parentNode) throws RepositoryException {
final String parentId = parentNode.getIdentifier();
final String workspaceName = parentNode.getSession().getWorkspace().getName();
try {
MgnlContext.doInSystemContext(new MgnlContext.Op<Void, RepositoryException>() {
@Override
public Void exec() throws RepositoryException {
Node parentNodeInSystemSession = NodeUtil.getNodeByIdentifier(workspaceName, parentId);
Node newAreaNode = NodeUtil.createPath(parentNodeInSystemSession, AreaElement.this.name, NodeTypes.Area.NAME);
newAreaNode.getSession().save();
return null;
}
});
} catch (RepositoryException e) {
log.error("ignoring problem w/ creating area in workspace {} for node {}", workspaceName, parentId);
// ignore, when working w/ versioned nodes ...
return null;
}
return parentNode.getNode(this.name);
}
protected void buildAdHocAreaDefinition() {
ConfiguredAreaDefinition addHocAreaDefinition = new ConfiguredAreaDefinition();
addHocAreaDefinition.setName(this.name);
addHocAreaDefinition.setDialog(this.dialog);
addHocAreaDefinition.setType(this.type);
addHocAreaDefinition.setRenderType(this.templateDefinition.getRenderType());
areaDefinition = addHocAreaDefinition;
}
@Override
public void end(Appendable out) throws RenderException {
try {
if (canRenderAreaScript()) {
if(isInherit() && areaNode != null) {
try {
areaNode = new DefaultInheritanceContentDecorator(areaNode, areaDefinition.getInheritance()).wrapNode(areaNode);
} catch (RepositoryException e) {
throw new RuntimeRepositoryException(e);
}
}
Map<String, Object> contextObjects = new HashMap<String, Object>();
List<ContentMap> components = new ArrayList<ContentMap>();
if (areaNode != null) {
List<Node> listOfComponents = NodeUtil.asList(NodeUtil.getNodes(areaNode, NodeTypes.Component.NAME));
int numberOfComponents = listOfComponents.size();
if (numberOfComponents > maxComponents) {
listOfComponents = listOfComponents.subList(0, maxComponents);
log.warn("The area {} have maximum number of components set to {}, but has got " + numberOfComponents +
" components. Exceeded components won't be added.", areaNode, maxComponents);
}
for (Node node : listOfComponents) {
components.add(new ContentMap(node));
}
}
if(AreaDefinition.TYPE_SINGLE.equals(type)) {
if(components.size() > 1) {
throw new RenderException("Can't render single area [" + areaNode + "]: expected one component node but found more.");
}
if(components.size() == 1) {
contextObjects.put(ATTRIBUTE_COMPONENT, components.get(0));
} else {
contextObjects.put(ATTRIBUTE_COMPONENT, null);
}
} else {
contextObjects.put(ATTRIBUTE_COMPONENTS, components);
}
// FIXME we shouldn't manipulate the area definition directly
// we should use merge with the proxy approach
if(areaDefinition.getRenderType() == null && areaDefinition instanceof ConfiguredAreaDefinition){
((ConfiguredAreaDefinition)areaDefinition).setRenderType(this.templateDefinition.getRenderType());
}
// FIXME we shouldn't manipulate the area definition directly
// we should use merge with the proxy approach
if(areaDefinition.getI18nBasename() == null && areaDefinition instanceof ConfiguredAreaDefinition){
((ConfiguredAreaDefinition)areaDefinition).setI18nBasename(this.templateDefinition.getI18nBasename());
}
WebContext webContext = MgnlContext.getWebContext();
webContext.push(webContext.getRequest(), webContext.getResponse());
setAttributesInWebContext(contextAttributes, WebContext.LOCAL_SCOPE);
try {
AppendableOnlyOutputProvider appendable = new AppendableOnlyOutputProvider(out);
if(StringUtils.isNotEmpty(areaDefinition.getTemplateScript())){
renderingEngine.render(areaNode, areaDefinition, contextObjects, appendable);
}
// no script
else{
for (ContentMap component : components) {
ComponentElement componentElement = Components.newInstance(ComponentElement.class);
componentElement.setContent(component.getJCRNode());
componentElement.begin(out);
componentElement.end(out);
}
}
} finally {
webContext.pop();
webContext.setPageContext(null);
restoreAttributesInWebContext(contextAttributes, WebContext.LOCAL_SCOPE);
}
}
if (isAdmin() && this.isAreaDefinitionEnabled) {
MarkupHelper helper = new MarkupHelper(out);
helper.closeComment(CMS_AREA);
}
} catch (Exception e) {
throw new RenderException("Can't render area " + areaNode + " with name " + this.name, e);
}
}
protected Node tryToCreateAreaNode(Node parentNode) throws RenderException {
Node area = null;
try {
if(parentNode.hasNode(name)){
area = parentNode.getNode(name);
} else {
//autocreate and save area only if it's not optional
if(!this.optional) {
area = createNewAreaNode(parentNode);
}
}
}
catch (RepositoryException e) {
throw new RenderException("Can't access area node [" + name + "] on [" + parentNode + "]", e);
}
//at this stage we can be sure that the target area, unless optional, has been created.
if(area != null) {
//TODO fgrilli: what about other component types to be autogenerated (i.e. autogenerating an entire page)?
final AutoGenerationConfiguration autoGeneration = areaDefinition.getAutoGeneration();
if (autoGeneration != null && autoGeneration.getGeneratorClass() != null) {
((Generator<AutoGenerationConfiguration>) Components.newInstance(autoGeneration.getGeneratorClass(), area)).generate(autoGeneration);
}
}
return area;
}
protected AreaDefinition resolveAreaDefinition() {
if (areaDefinition != null) {
return areaDefinition;
}
if (!StringUtils.isEmpty(name)) {
if (templateDefinition != null && templateDefinition.getAreas().containsKey(name)) {
return templateDefinition.getAreas().get(name);
}
}
// happens if no area definition is passed or configured
// an ad-hoc area definition will be created
return null;
}
protected TemplateDefinition resolveTemplateDefinition() throws RenderException {
final RenderableDefinition renderableDefinition = getRenderingContext().getRenderableDefinition();
if (renderableDefinition == null || renderableDefinition instanceof TemplateDefinition) {
return (TemplateDefinition) renderableDefinition;
}
throw new RenderException("Current RenderableDefinition [" + renderableDefinition + "] is not of type TemplateDefinition. Areas cannot be supported");
}
/*
* An area script can be rendered when
* area is enabled
*
* AND
*
* If an area is optional:
*
* if not yet created the area bar has a create button and the script is
* - executed in the edit mode but the content object is null (otherwise we can't place the bar)
* - not executed otherwise (no place holder divs)
*
* If created, the bar has a remove button (other areas cannot be removed nor created)
*
* If an area is required:
*
* the area node gets created (always) the script is always executed.
*/
private boolean canRenderAreaScript() {
// 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
return this.isAreaDefinitionEnabled && (areaNode != null || (areaNode == null && areaDefinition.isOptional() && !MgnlContext.getAggregationState().isPreviewMode()));
}
private String resolveDialog() {
return dialog != null ? dialog : areaDefinition != null ? areaDefinition.getDialog() : null;
}
private String resolveType() {
return type != null ? type : areaDefinition != null && areaDefinition.getType() != null ? areaDefinition.getType() : AreaDefinition.DEFAULT_TYPE;
}
private String resolveName() {
return name != null ? name : (areaDefinition != null ? areaDefinition.getName() : null);
}
private String resolveLabel() {
return label != null ? label : (areaDefinition != null && StringUtils.isNotBlank(areaDefinition.getTitle()) ? areaDefinition.getTitle() : StringUtils.capitalize(name));
}
private Boolean resolveOptional() {
return optional != null ? optional : areaDefinition != null && areaDefinition.isOptional() != null ? areaDefinition.isOptional() : Boolean.FALSE;
}
private Boolean resolveEditable() {
return editable != null ? editable : areaDefinition != null && areaDefinition.getEditable() != null ? areaDefinition.getEditable() : null;
}
private Integer resolveMaximumOfComponents() {
return maxComponents != null ? maxComponents : areaDefinition != null && areaDefinition.getMaxComponents() != null ? areaDefinition.getMaxComponents() : Integer.MAX_VALUE;
}
private boolean isInheritanceEnabled() {
return areaDefinition != null && areaDefinition.getInheritance() != null && areaDefinition.getInheritance().isEnabled() != null && areaDefinition.getInheritance().isEnabled();
}
private boolean isOptionalAreaCreated() {
return this.optional && this.areaNode != null;
}
private boolean hasComponents(Node parent) throws RenderException {
try {
return NodeUtil.getNodes(parent, NodeTypes.Component.NAME).iterator().hasNext();
} catch (RepositoryException e) {
throw new RenderException(e);
}
}
private int numberOfComponents(Node parent) throws RenderException {
try {
return NodeUtil.asList(NodeUtil.getNodes(parent, NodeTypes.Component.NAME)).size();
} catch (RepositoryException e) {
throw new RenderException(e);
}
}
protected String resolveAvailableComponents() {
if (StringUtils.isNotEmpty(availableComponents)) {
return StringUtils.remove(availableComponents, " ");
}
if (areaDefinition != null && areaDefinition.getAvailableComponents().size() > 0) {
Iterator<ComponentAvailability> iterator = areaDefinition.getAvailableComponents().values().iterator();
List<String> componentIds = new ArrayList<String>();
final Collection<String> userRoles = MgnlContext.getUser().getAllRoles();
while (iterator.hasNext()) {
ComponentAvailability availableComponent = iterator.next();
if(availableComponent.isEnabled()) {
// check roles
final Collection<String> roles = availableComponent.getRoles();
if (!roles.isEmpty()) {
if (CollectionUtils.containsAny(userRoles, roles)) {
componentIds.add(availableComponent.getId());
}
} else {
componentIds.add(availableComponent.getId());
}
}
}
return StringUtils.join(componentIds, ',');
}
return "";
}
private boolean shouldShowAddButton() throws RenderException {
if(areaNode == null || type.equals(AreaDefinition.TYPE_NO_COMPONENT) || (type.equals(AreaDefinition.TYPE_SINGLE) && hasComponents(areaNode)) || numberOfComponents(areaNode) >= maxComponents) {
return false;
}
return true;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public AreaDefinition getArea() {
return areaDefinition;
}
public void setArea(AreaDefinition area) {
this.areaDefinition = area;
}
public String getAvailableComponents() {
return availableComponents;
}
public void setAvailableComponents(String availableComponents) {
this.availableComponents = availableComponents;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getDialog() {
return dialog;
}
public void setDialog(String dialog) {
this.dialog = dialog;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public boolean isInherit() {
return inherit;
}
public void setInherit(boolean inherit) {
this.inherit = inherit;
}
public Boolean getEditable() {
return editable;
}
public void setEditable(Boolean editable) {
this.editable = editable;
}
public Map<String, Object> getContextAttributes() {
return contextAttributes;
}
public void setContextAttributes(Map<String, Object> contextAttributes) {
this.contextAttributes = contextAttributes;
}
public Integer getMaxComponents() {
return maxComponents;
}
public void setMaxComponents(Integer maxComponents) {
this.maxComponents = maxComponents;
}
}