All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.adobe.granite.ui.components.ComponentHelper Maven / Gradle / Ivy

/*************************************************************************
 *
 * ADOBE CONFIDENTIAL
 * __________________
 *
 *  Copyright 2013 Adobe Systems Incorporated
 *  All Rights Reserved.
 *
 * NOTICE:  All information contained herein is, and remains
 * the property of Adobe Systems Incorporated and its suppliers,
 * if any.  The intellectual and technical concepts contained
 * herein are proprietary to Adobe Systems Incorporated and its
 * suppliers and are protected by trade secret or copyright law.
 * Dissemination of this information or reproduction of this material
 * is strictly forbidden unless prior written permission is obtained
 * from Adobe Systems Incorporated.
 **************************************************************************/
package com.adobe.granite.ui.components;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import javax.servlet.RequestDispatcher;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.jsp.PageContext;

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.request.RequestDispatcherOptions;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.scripting.SlingBindings;
import org.apache.sling.api.scripting.SlingScriptHelper;
import org.apache.sling.api.servlets.ServletResolver;
import org.apache.sling.jcr.resource.JcrResourceConstants;
import org.apache.sling.scripting.jsp.util.JspSlingHttpServletResponseWrapper;

import com.adobe.granite.ui.components.ds.DataSource;
import com.adobe.granite.ui.components.ds.EmptyDataSource;
import com.adobe.granite.ui.components.ds.ResourceDataSource;
import com.adobe.granite.ui.components.rendercondition.RenderCondition;
import com.adobe.granite.ui.components.rendercondition.SimpleRenderCondition;
import com.adobe.granite.xss.XSSAPI;
import com.day.cq.i18n.I18n;

/**
 * A convenient helper for development of Granite UI component.
 */
public class ComponentHelper {
    private static final String DEFAULT_LAYOUT_RT = "granite/ui/components/foundation/layouts/container";
    private static final String ATTRIBUTE_CACHE_RC = ComponentHelper.class.getName() + ".cache.rc";

    private PageContext pageContext;
    
    private SlingScriptHelper sling;
    
    private SlingHttpServletRequest request;

    private I18n i18n;

    private XSSAPI xss;

    private Config config;

    private Value value;

    private ExpressionHelper ex;

    private State state;

    private OptionsHolder optionsHolder;

    public ComponentHelper(PageContext pageContext) {
        this.pageContext = pageContext;

        SlingBindings bindings = (SlingBindings) pageContext.getRequest().getAttribute(SlingBindings.class.getName());
        request = bindings.getRequest();
        sling = bindings.getSling();
        
        optionsHolder = (OptionsHolder) request.getAttribute(OptionsHolder.class.getName());
        request.removeAttribute(OptionsHolder.class.getName());
    }

    /**
     * Returns I18n appropriate for the current page.
     * @return I18n appropriate for the current page
     */
    public I18n getI18n() {
        if (i18n == null) {
            i18n = new I18n(request);
        }
        return i18n;
    }

    /**
     * Returns XSSAPI based on the current request.
     * @return XSSAPI based on the current request
     */
    public XSSAPI getXss() {
        if (xss == null) {
            xss = sling.getService(XSSAPI.class).getRequestSpecificAPI(request);
        }
        return xss;
    }

    /**
     * Returns the config of current resource of the page.
     * @return the config of current resource of the page
     */
    public Config getConfig() {
        if (config == null) {
            config = new Config(request.getResource());
        }
        return config;
    }

    /**
     * Returns the values that is applicable for the current page.
     * @return the values that is applicable for the current page
     */
    public Value getValue() {
        if (value == null) {
            value = new Value(request, getConfig());
        }
        return value;
    }

    /**
     * Returns the ExpressionHelper appropriate for the current page.
     * @return the ExpressionHelper appropriate for the current page
     */
    public ExpressionHelper getExpressionHelper() {
        if (ex == null) {
            ex = new ExpressionHelper(sling.getService(ExpressionResolver.class), pageContext);
        }
        return ex;
    }

    /**
     * Returns the client state.
     * @return the client state
     */
    public State getState() {
        if (state == null) {
            state = new State(request);
        }
        return state;
    }

    /**
     * Consumes the current available tag for current page. If the request
     * doesn't have the tag applicable to the page, a new tag is created.
     * 

* There is a mechanism such that a tag can be passed to another page when * including that page. This method is intended as a way to consume the tag * passed by other page. Component developer can make use this method to get * the main tag of the component regardless if there is a tag passed by * other page or not. * * @return the tag * @see #include(Resource, Tag) * @see #include(Resource, String, Tag) */ public Tag consumeTag() { Tag tag = getOptions().tag(); return tag == null ? new Tag(new AttrBuilder(request, getXss())) : tag; } /** * Consumes the current available layout resource for current page. This * method first attempts to return the layout resource from the options, * second it will attempt to return {@link Config#LAYOUT} child node, * otherwise null is returned. *

* This method is usually called by layout implementation to get the layout * resource that can be passed by the caller. * * @return the resource * @see #includeForLayout(Resource, Options) * @see #includeForLayout(Resource, Resource, Options) */ public Resource consumeLayoutResource() { Resource layout = getOptions().layoutResource(); return layout == null ? request.getResource().getChild(Config.LAYOUT) : layout; } /** * The overload of {@link #populateCommonAttrs(AttrBuilder, Resource)} using the current request's resource as the source. * * @param attrs the attribute builder */ public void populateCommonAttrs(AttrBuilder attrs) { populateCommonAttrs(attrs, request.getResource()); } /** * Populates the common attributes to the given {@link AttrBuilder}. * *

Currently the following common properties and nodes are read and processed from the given resource: * *

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
NameTypeDescription
granite:idProperty: StringELThe id attribute.
granite:relProperty: StringELThe class attribute. This is used to indicate the semantic relationship of the component similar to rel attribute.
granite:classProperty: StringELThe class attribute.
granite:titleProperty: StringThe title attribute. This property is i18nable.
granite:hiddenProperty: BooleanThe hidden attribute.
granite:itemscopeProperty: BooleanThe itemscope attribute.
granite:itemtypeProperty: StringThe itemtype attribute.
granite:itempropProperty: StringThe itemprop attribute.
granite:dataNodeEach property of this node is converted into a data-* attribute. * If the property value is an instance of a String, it will be interpreted as StringEL. * The property having a prefixed name is ignored.
* * @param attrs The attribute builder to populate to * @param src The resource of the source of the config */ public void populateCommonAttrs(AttrBuilder attrs, Resource src) { I18n i18n = getI18n(); Config config = new Config(src); ExpressionHelper ex = getExpressionHelper(); attrs.add("id", ex.getString(config.get("granite:id", String.class))); attrs.addRel(ex.getString(config.get("granite:rel", String.class))); attrs.addClass(ex.getString(config.get("granite:class", String.class))); attrs.add("title", i18n.getVar(config.get("granite:title", String.class))); attrs.addBoolean("hidden", config.get("granite:hidden", false)); attrs.addBoolean("itemscope", config.get("granite:itemscope", false)); attrs.add("itemtype", config.get("granite:itemtype", String.class)); attrs.add("itemprop", config.get("granite:itemprop", String.class)); Resource data = src.getChild("granite:data"); if (data == null) return; for (Entry e : data.getValueMap().entrySet()) { String key = e.getKey(); if (key.indexOf(":") >= 0) continue; Object v = e.getValue(); if (v instanceof String) { v = ex.getString(v.toString()); } attrs.addOther(key, v.toString()); } } /** * Returns the options passed from another page. If no options is passed, * empty options is returned. *

* There is a mechanism such that options can be passed to another page when * including that page. * * @return the options passed from another page. if no options is passed, * empty options is returned. * @see #include(Resource, Options) * @see #include(Resource, String, Options) */ public Options getOptions() { // When the options is passed to another page. Only that other page is // allowed to consume it. i.e. we try to // emulate page scope. // But as we are using request attribute to pass data between pages, // we have to safe-guard against the wrong // scope (mainly due to nesting). // Currently we use resource's path to check against nesting. if (optionsHolder != null && optionsHolder.getPath().equals(request.getResource().getPath())) { return optionsHolder.getOptions(); } return new Options(); } /** * Returns the layout config of current resource of the page. This method is * setting the default resource type of the layout. * * @return the layout config of current resource of the page * @see LayoutBuilder#getResourceType() */ public LayoutBuilder getLayout() { return LayoutBuilder.from(consumeLayoutResource(), DEFAULT_LAYOUT_RT); } /** * Returns the associated resource type of current resource for the purpose rendering read only version. * First the granite:readOnlyResourceType property of the resource type of the current resource (the RT) is used. * Otherwise it is defaulted to readonly child resource of the RT. * * @return the associated resource type of current resource */ public String getReadOnlyResourceType() { return getReadOnlyResourceType(request.getResource()); } /** * Returns the associated resource type of the given content resource for the purpose rendering read only version. * First the granite:readOnlyResourceType property of the resource type of the content resource (the RT) is used. * Otherwise it is defaulted to readonly child resource of the RT. * * @param resource the resource * @return the associated resource type of the given content resource */ public String getReadOnlyResourceType(Resource resource) { String resourceType = getResourceType(resource); if (resourceType == null) { return null; } Resource r = request.getResourceResolver().getResource(resourceType); if (r == null) { return null; } Resource ro = r.getChild("readonly"); if (ro != null) { return ro.getPath(); } return new Config(r).get("granite:readOnlyResourceType", String.class); } /** * Returns the datasource for items of the current resource. This is an * overload of {@link #getItemDataSource(Resource)} with resource is the * current request resource. * * @return the data source for items of the current resource * @throws ServletException in case there's a servlet error while fetching data * @throws IOException in case there's an i/o error while fetching data */ public DataSource getItemDataSource() throws ServletException, IOException { return getItemDataSource(request.getResource()); } /** * Returns the datasource for items of the given resource. * This method can be used to fetch the items that are specified literally * using {@link Config#ITEMS} subresource; or specified as datasource using {@link Config#DATASOURCE} subresource. * * If there is no {@link Config#ITEMS} or {@link Config#DATASOURCE} subresource, then {@link EmptyDataSource} is returned. * * In contrast with {@link #asDataSource(Resource, Resource)}, this method * looks for the datasource resource of the given resource. i.e. The given * resource is the parent of the items, not the datasource resource itself. * The given resource is also used as the context resource when calling * {@link #asDataSource(Resource, Resource)} internally. * * @param resource the resource * @return the data source for items of the given resource * @throws ServletException in case there's a servlet error while fetching data * @throws IOException in case there's an i/o error while fetching data */ public DataSource getItemDataSource(Resource resource) throws ServletException, IOException { Resource items = resource.getChild(Config.ITEMS); if (items != null) { return new ResourceDataSource(items); } Resource datasource = resource.getChild(Config.DATASOURCE); if (datasource != null) { return asDataSource(datasource, resource); } return EmptyDataSource.instance(); } /** * Returns the datasource given its datasource resource. This method is an * overload of {@link #asDataSource(Resource, Resource)} with context is * null. * @param datasource the resource representing the datasource * @return the datasource given its datasource resource * @throws ServletException in case there's a servlet error while fetching data * @throws IOException in case there's an i/o error while fetching data */ public DataSource asDataSource(Resource datasource) throws ServletException, IOException { return asDataSource(datasource, null); } /** * Returns the datasource given its datasource resource. * * @param datasource The resource representing the datasource * @param context The context resource that is returned when calling * {@link SlingHttpServletRequest#getResource()} at the * datasource implementation. If this is null, the * given datasource is used. * @return null if the given datasource is null. * @throws ServletException in case there's a servlet error while fetching data * @throws IOException in case there's an i/o error while fetching data */ public DataSource asDataSource(Resource datasource, Resource context) throws ServletException, IOException { if (datasource == null) return null; if (context == null) { context = datasource; } DataSource ds = fetchData(context, getResourceType(datasource), DataSource.class); return ds != null ? ds : EmptyDataSource.instance(); } /** * Returns the render condition of the current resource. This method is an * overload of {@link #getRenderCondition(Resource)} using the current * resource. * * The render condition is specified by granite:rendercondition * or rendercondition subresource. * * Contrast this with {@link #getRenderCondition(Resource, boolean)}, where * only granite:rendercondition is checked. This method is * meant for backward compatibility; otherwise it is better to use * {@link #getRenderCondition(Resource, boolean)} for performance. Once the * transition is over, this method will have the same behaviour as * {@link #getRenderCondition(Resource, boolean)} with cache = * false. * * @return the render condition of the current resource * @throws ServletException * in case there's a servlet error * @throws IOException * in case there's an i/o error */ public RenderCondition getRenderCondition() throws ServletException, IOException { return getRenderCondition(request.getResource()); } /** * Returns the render condition of the given resource. * * The render condition is specified by granite:rendercondition * or rendercondition subresource. * * Contrast this with {@link #getRenderCondition(Resource, boolean)}, where * only granite:rendercondition is checked. This method is * meant for backward compatibility; otherwise it is better to use * {@link #getRenderCondition(Resource, boolean)} for performance. Once the * transition is over, this method will have the same behaviour as * {@link #getRenderCondition(Resource, boolean)} with cache = * false. * * @param resource * the resource * @return the render condition of the given resource * @throws ServletException * in case there's a servlet error * @throws IOException * in case there's an i/o error */ public RenderCondition getRenderCondition(Resource resource) throws ServletException, IOException { RenderCondition rc = null; Resource condition = resource.getChild("granite:rendercondition"); if (condition == null) { condition = resource.getChild(Config.RENDERCONDITION); } if (condition != null) { String resourceType = getResourceType(condition, "granite/ui/components/foundation/renderconditions/simple"); rc = fetchData(condition, resourceType, RenderCondition.class); } if (rc == null) { rc = SimpleRenderCondition.TRUE; } return rc; } /** * Returns the render condition of the given resource. * * The render condition is specified by granite:rendercondition * subresource, unlike {@link #getRenderCondition(Resource)}. * * @param resource * The resource * @param cache * true to cache the result; Use it when checking * render condition of other resource (typically the item * resource) so that the render condition is only resolved once. * * @return The render condition of the given resource; never null. * * @throws ServletException * in case there's a servlet error * @throws IOException * in case there's an i/o error */ public RenderCondition getRenderCondition(Resource resource, boolean cache) throws ServletException, IOException { Map cacheMap = getRenderConditionCache(); final String key = resource.getPath(); RenderCondition rc = cacheMap.get(key); if (rc != null) { return rc; } Resource condition = resource.getChild("granite:rendercondition"); if (condition != null) { rc = fetchData(condition, getResourceType(condition), RenderCondition.class); } if (rc == null) { rc = SimpleRenderCondition.TRUE; } if (cache) { cacheMap.put(key, rc); } return rc; } /** * Returns the cache for {@link RenderCondition}. * * @return The cache */ private Map getRenderConditionCache() { @SuppressWarnings("unchecked") Map cache = (Map) request.getAttribute(ATTRIBUTE_CACHE_RC); if (cache == null) { cache = new HashMap(); request.setAttribute(ATTRIBUTE_CACHE_RC, cache); } return cache; } /** * Fetches data via include of the given type. * @param resource the resource * @param resourceType the resource type * @param type the type * @param the class * @return the data in the form of the given type * @throws ServletException in case there's a servlet error while fetching data * @throws IOException in case there's an i/o error while fetching data */ @SuppressWarnings("unchecked") private T fetchData(Resource resource, String resourceType, Class type) throws ServletException, IOException { if (resourceType == null) return null; try { RequestDispatcher dispatcher = request.getRequestDispatcher(resource, new RequestDispatcherOptions(resourceType)); if (dispatcher != null) { dispatcher.include(request, new JspSlingHttpServletResponseWrapper(pageContext)); return (T) request.getAttribute(type.getName()); } return null; } finally { request.removeAttribute(type.getName()); } } /** * Returns the icon class(es) for the given icon string from the content property. * @param icon the icon string * @return the icon class(es) for the given icon string from the content property */ public String getIconClass(String icon) { // In the future we can make it pluggable using OSGi so that others can provide their own icons based on certain icon class pattern. if (icon == null) return null; if (!icon.startsWith("icon-")) return icon; return "coral-Icon--" + toCamel(icon.substring(5)); } private static String toCamel(String s) { String[] parts = s.split("-"); StringBuilder b = new StringBuilder(); b.append(parts[0]); for (int i = 1; i < parts.length; i++) { b.append(parts[i].substring(0, 1).toUpperCase()); b.append(parts[i].substring(1)); } return b.toString(); } private static String getResourceType(Resource resource) { return new Config(resource).get(JcrResourceConstants.SLING_RESOURCE_TYPE_PROPERTY, String.class); } private static String getResourceType(Resource resource, String defaultValue) { return new Config(resource).get(JcrResourceConstants.SLING_RESOURCE_TYPE_PROPERTY, defaultValue); } /** * Includes the given resource and passes the given tag to its renderer. * This method performs similarly to <sling:include resource="" />. * @param resource the resource to include * @param tag the tag * @throws ServletException in case there's a servlet error * @throws IOException in case there's an i/o error */ public void include(Resource resource, Tag tag) throws ServletException, IOException { include(resource, null, tag); } /** * Includes the given resource and passes the given options to its renderer. * This method performs similarly to <sling:include resource="" />. * @param resource the resource to include * @param options the options * @throws ServletException in case there's a servlet error * @throws IOException in case there's an i/o error */ public void include(Resource resource, Options options) throws ServletException, IOException { include(resource, null, options); } /** * Includes the given resource with the given resourceType and passes the * given tag to its renderer. This method performs similarly to * <sling:include resource="" resourceType="" />. * @param resource the resource to include * @param resourceType the resource type * @param tag the tag * @throws ServletException in case there's a servlet error * @throws IOException in case there's an i/o error */ public void include(Resource resource, String resourceType, Tag tag) throws ServletException, IOException { include(resource, resourceType, new Options().tag(tag)); } /** * Includes the given resource with the given resourceType and passes the * given options to its renderer. This method performs similarly to * <sling:include resource="" resourceType="" />. * @param resource the resource to include * @param resourceType the resource type * @param options the options * @throws ServletException in case there's a servlet error * @throws IOException in case there's an i/o error */ public void include(Resource resource, String resourceType, Options options) throws ServletException, IOException { include(resource, resourceType, null, options); } /** * Includes the given resource with the given resourceType and passes the * given options to its renderer. This method performs similarly to * <sling:include resource="" replaceSelectors="" resourceType="" />. * @param resource the resource to include * @param resourceType the resource type * @param selectors the selectors to be included as part of the request. * @param options the options * @throws ServletException in case there's a servlet error * @throws IOException in case there's an i/o error */ public void include(Resource resource, String resourceType, String selectors, Options options) throws ServletException, IOException { try { OptionsHolder holder = new OptionsHolder(options, resource.getPath()); request.setAttribute(OptionsHolder.class.getName(), holder); RequestDispatcherOptions dispatcherOptions = new RequestDispatcherOptions(resourceType); if (selectors != null && selectors.length() > 0) { dispatcherOptions.setReplaceSelectors(selectors); } RequestDispatcher dispatcher = request.getRequestDispatcher(resource, dispatcherOptions); if (dispatcher != null) { dispatcher.include(request, new JspSlingHttpServletResponseWrapper(pageContext)); } } finally { request.removeAttribute(OptionsHolder.class.getName()); } } /** * A convenient overload to * {@link #includeForLayout(Resource, Resource, Options)}, with * layoutResource as null. * @param resource the resource to include * @param options the options * @throws ServletException in case there's a servlet error * @throws IOException in case there's an i/o error */ public void includeForLayout(Resource resource, Options options) throws ServletException, IOException { includeForLayout(resource, null, options); } /** * Includes the given resource to be rendered by the given layoutResource. * This method is used by a component to delegate the rendering process to a * layout. *

* If layoutResource is not null, the * {@link Options#layoutResource(Resource)} is set. *

* This method will attempt to derive the resourceType to be passed to * {@link #include(Resource, String, Options)} based the following * priorities: *

    *
  1. layoutResource is not null, the resourceType is layoutResource's RT
  2. *
  3. layoutResource is null, the resourceType is {@link Config#LAYOUT} * child node's RT
  4. *
  5. the resourceType is default layout as a catch-all fallback
  6. *
* @param resource the resource to include * @param layoutResource the layout resource to render the given resource with * @param options the options * @throws ServletException in case there's a servlet error * @throws IOException in case there's an i/o error */ public void includeForLayout(Resource resource, Resource layoutResource, Options options) throws ServletException, IOException { String resourceType = null; if (layoutResource == null) { Resource r = resource.getChild(Config.LAYOUT); if (r != null) { resourceType = getResourceType(r); } } else { resourceType = getResourceType(layoutResource); options.layoutResource(layoutResource); } if (resourceType == null) { resourceType = DEFAULT_LAYOUT_RT; } include(resource, resourceType, options); } /** * Calls the given script and passes the given options to its renderer. This * method performs similarly to <sling:call script="" />. * @param script the script to be called * @param options the options * @throws ServletException in case there's a servlet error * @throws IOException in case there's an i/o error */ public void call(String script, Options options) throws ServletException, IOException { try { OptionsHolder holder = new OptionsHolder(options, request.getResource().getPath()); request.setAttribute(OptionsHolder.class.getName(), holder); ServletResolver servletResolver = sling.getService(ServletResolver.class); Servlet servlet = servletResolver.resolveServlet(request.getResource(), script); if (servlet == null) { throw new ServletException("Could not find script " + script); } servlet.service(request, new JspSlingHttpServletResponseWrapper(pageContext)); } finally { request.removeAttribute(OptionsHolder.class.getName()); } } private class OptionsHolder { private Options options; private String path; public OptionsHolder(Options options, String path) { this.options = options; this.path = path; } public Options getOptions() { return options; } public String getPath() { return path; } } /** * An options to be passed to the included resource's renderer. */ public static class Options { private Tag tag; private boolean rootField = true; private Resource layout; /** * Creates a new instance. */ public Options() { } /** * Returns the tag. * @return the tag */ public Tag tag() { return tag; } /** * Sets the tag. * @param tag the tag * @return the options */ public Options tag(Tag tag) { this.tag = tag; return this; } /** * Returns true if the renderer (the field) should render * itself as root field. See {@link #rootField(boolean)} for details. * @return {@code true} if the renderer (the field) should render * itself as root field. */ public boolean rootField() { return rootField; } /** * Sets true to make the renderer (the field) should render * itself as root field; false otherwise. *

* A root field is a field that acts in its own context, instead of as * part of a composite field. For example, sizing field consists of * weight and height fields. So sizing field is a composite field and * wants to leverage the existing number field for width and height. In * this case when sizing field is including ( * {@link ComponentHelper#include(Resource, Options)}) the number field, * it should set this option as false. *

* The field implementation is free to interpret the exact behaviour of * root/non-root field. Most likely scenario, the root field will handle * it own sizing state (e.g. inline-block/block state), while non root * field will not, where the parent composite field is managing it. * * @param flag the flag * @return this instance */ public Options rootField(boolean flag) { rootField = flag; return this; } /** * Returns the layout resource. * @return the layout resource */ public Resource layoutResource() { return layout; } /** * Sets the layout resource. * @param r the layout resource to set * @return this instance */ public Options layoutResource(Resource r) { this.layout = r; return this; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy