info.magnolia.templating.elements.AreaElement.java Source code

Java tutorial

Introduction

Here is the source code for info.magnolia.templating.elements.AreaElement.java

Source

/**
 * 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;
    }
}