
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:
*
*
*
*
* Name
* Type
* Description
*
*
*
*
* granite:id
* Property: StringEL
* The id attribute.
*
*
* granite:rel
* Property: StringEL
* The class attribute. This is used to indicate the semantic relationship of the component similar to rel
attribute.
*
*
* granite:class
* Property: StringEL
* The class attribute.
*
*
* granite:title
* Property: String
* The title attribute. This property is i18nable.
*
*
* granite:hidden
* Property: Boolean
* The hidden attribute.
*
*
* granite:itemscope
* Property: Boolean
* The itemscope attribute.
*
*
* granite:itemtype
* Property: String
* The itemtype attribute.
*
*
* granite:itemprop
* Property: String
* The itemprop attribute.
*
*
* granite:data
* Node
* Each 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:
*
* - layoutResource is not null, the resourceType is layoutResource's RT
* - layoutResource is null, the resourceType is {@link Config#LAYOUT}
* child node's RT
* - the resourceType is default layout as a catch-all fallback
*
* @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;
}
}
}