Parsys.java
/*
* #%L
* wcm.io
* %%
* Copyright (C) 2014 wcm.io
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package io.wcm.wcm.parsys.controller;
import static io.wcm.wcm.parsys.ParsysNameConstants.PN_PARSYS_GENERATE_DEAFULT_CSS;
import static io.wcm.wcm.parsys.ParsysNameConstants.PN_PARSYS_NEWAREA_CSS;
import static io.wcm.wcm.parsys.ParsysNameConstants.PN_PARSYS_PARAGRAPH_CSS;
import static io.wcm.wcm.parsys.ParsysNameConstants.PN_PARSYS_PARAGRAPH_ELEMENT;
import static io.wcm.wcm.parsys.ParsysNameConstants.PN_PARSYS_PARAGRAPH_NODECORATION_WCMMODE;
import static io.wcm.wcm.parsys.ParsysNameConstants.PN_PARSYS_PARAGRAPH_VALIDATE;
import static io.wcm.wcm.parsys.ParsysNameConstants.PN_PARSYS_WRAPPER_CSS;
import static io.wcm.wcm.parsys.ParsysNameConstants.PN_PARSYS_WRAPPER_ELEMENT;
import static org.apache.jackrabbit.JcrConstants.NT_UNSTRUCTURED;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.annotation.PostConstruct;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.models.annotations.Exporter;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy;
import org.apache.sling.models.annotations.injectorspecific.OSGiService;
import org.apache.sling.models.annotations.injectorspecific.RequestAttribute;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.apache.sling.models.factory.ModelClassException;
import org.apache.sling.models.factory.ModelFactory;
import org.jetbrains.annotations.NotNull;
import org.osgi.annotation.versioning.ProviderType;
import com.adobe.cq.export.json.ComponentExporter;
import com.adobe.cq.export.json.ContainerExporter;
import com.adobe.cq.export.json.ExporterConstants;
import com.adobe.cq.export.json.SlingModelFilter;
import com.day.cq.wcm.api.NameConstants;
import com.day.cq.wcm.api.WCMMode;
import com.day.cq.wcm.api.components.Component;
import com.day.cq.wcm.api.components.ComponentContext;
import com.day.cq.wcm.api.components.ComponentManager;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.wcm.sling.commons.adapter.AdaptTo;
import io.wcm.sling.models.annotations.AemObject;
import io.wcm.wcm.commons.component.ComponentPropertyResolution;
import io.wcm.wcm.commons.component.ComponentPropertyResolver;
import io.wcm.wcm.commons.component.ComponentPropertyResolverFactory;
import io.wcm.wcm.parsys.ParsysItem;
/**
* Controller for paragraph system.
* Unlike the AEM-builtin paragraph systems this parsys does not support column controls or iparsys inheritance,
* but is only a simple paragraph system which allows full control about the markup generated for the child resources
* and the new area.
*/
@Model(adaptables = SlingHttpServletRequest.class,
adapters = { Parsys.class, ContainerExporter.class, ComponentExporter.class },
resourceType = Parsys.RESOURCE_TYPE)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
@ProviderType
public final class Parsys implements ContainerExporter {
static final String RESOURCE_TYPE = "wcm-io/wcm/parsys/components/parsys";
static final String RA_PARSYS_PARENT_RESOURCE = "parsysParentResource";
static final String SECTION_DEFAULT_CLASS_NAME = "section";
static final String NEWAREA_RESOURCE_PATH = "./*";
static final String NEWAREA_STYLE = "clear:both";
static final String NEWAREA_CSS_CLASS_NAME = "new";
static final String NEWAREA_CHILD_NAME = "newpar";
static final String FALLBACK_NEWAREA_RESOURCE_TYPE = "wcm-io/wcm/parsys/components/parsys/newpar";
static final String DEFAULT_ELEMENT_NAME = "div";
/**
* Allows to override the resource which children are iterated to display the parsys.
*/
@RequestAttribute(name = RA_PARSYS_PARENT_RESOURCE, injectionStrategy = InjectionStrategy.OPTIONAL)
private Resource parsysParentResource;
@SlingObject
private SlingHttpServletRequest request;
@SlingObject
private Resource currentResource;
@SlingObject
private ResourceResolver resolver;
@AemObject
private WCMMode wcmMode;
@AemObject
private ComponentContext componentContext;
@OSGiService
private ModelFactory modelFactory;
@OSGiService
private ComponentPropertyResolverFactory componentPropertyResolverFactory;
@OSGiService
private SlingModelFilter slingModelFilter;
private ComponentManager componentManager;
private boolean generateDefaultCss;
private String paragraphCss;
private String newAreaCss;
private String paragraphElementName;
private boolean paragraphDecoration;
private boolean paragraphValidate;
private String wrapperElementName;
private String wrapperCss;
private List<Item> items;
private Map<String, ? extends ComponentExporter> childModels;
private String[] exportedItemsOrder;
@PostConstruct
private void activate() {
// read customize properties from parsys component
try (ComponentPropertyResolver componentPropertyResolver = componentPropertyResolverFactory.get(componentContext)
.componentPropertiesResolution(ComponentPropertyResolution.RESOLVE_INHERIT)) {
generateDefaultCss = componentPropertyResolver.get(PN_PARSYS_GENERATE_DEAFULT_CSS, true);
paragraphCss = componentPropertyResolver.get(PN_PARSYS_PARAGRAPH_CSS, String.class);
newAreaCss = componentPropertyResolver.get(PN_PARSYS_NEWAREA_CSS, String.class);
paragraphElementName = componentPropertyResolver.get(PN_PARSYS_PARAGRAPH_ELEMENT, String.class);
wrapperElementName = componentPropertyResolver.get(PN_PARSYS_WRAPPER_ELEMENT, String.class);
wrapperCss = componentPropertyResolver.get(PN_PARSYS_WRAPPER_CSS, String.class);
paragraphValidate = componentPropertyResolver.get(PN_PARSYS_PARAGRAPH_VALIDATE, false);
// check decoration
String[] paragraphNoDecorationWcmMode = componentPropertyResolver.get(PN_PARSYS_PARAGRAPH_NODECORATION_WCMMODE, String[].class);
paragraphDecoration = getDecoration(paragraphNoDecorationWcmMode, wcmMode);
}
// prepare paragraph items
items = new ArrayList<>();
if (parsysParentResource == null) {
parsysParentResource = currentResource;
}
for (Resource childResource : parsysParentResource.getChildren()) {
if (!acceptResource(childResource)) {
continue;
}
Item item = createResourceItem(childResource);
if (wcmMode != WCMMode.DISABLED || item.isValid()) {
items.add(item);
}
}
if (wcmMode != WCMMode.DISABLED) {
items.add(createNewAreaItem());
}
}
private static boolean acceptResource(Resource resource) {
// skip resources without assigned resource type
return !StringUtils.equals(resource.getResourceType(), NT_UNSTRUCTURED);
}
private static boolean getDecoration(String[] paragraphNoDecorationWcmMode, WCMMode wcmMode) {
if (paragraphNoDecorationWcmMode != null && paragraphNoDecorationWcmMode.length > 0) {
for (String wcmModeItem : paragraphNoDecorationWcmMode) {
if (StringUtils.equalsIgnoreCase(wcmMode.name(), wcmModeItem)) {
return false;
}
}
}
return true;
}
private Item createResourceItem(Resource resource) {
CssBuilder css = new CssBuilder();
if (generateDefaultCss) {
css.add(SECTION_DEFAULT_CLASS_NAME);
}
css.add(paragraphCss);
Map<String,String> htmlTagAttrs = getComponentHtmlTagAttributes(resource.getResourceType());
// apply html tag attributes from component definition
String itemElementName = paragraphElementName;
if (StringUtils.isEmpty(itemElementName)) {
itemElementName = StringUtils.defaultString(htmlTagAttrs.get(NameConstants.PN_TAG_NAME), DEFAULT_ELEMENT_NAME);
}
if (StringUtils.isEmpty(paragraphCss)) {
css.add(htmlTagAttrs.get("class"));
}
// try to check valid state of paragraph item
boolean valid = true;
if (paragraphValidate) {
Optional<Boolean> validStatus = isParagraphValid(resource);
if (validStatus.isPresent()) {
valid = validStatus.get();
}
}
return new Item(resource.getPath())
.elementName(itemElementName)
.cssClassName(css.build())
.decorate(paragraphDecoration)
.valid(valid);
}
/**
* Checks if the given paragraph is valid.
* @param resource Resource
* @return if the return value is empty there is no model associated with this resource, or
* it does not support validation via {@link ParsysItem} interface. Otherwise it contains the valid status.
*/
@SuppressWarnings({ "null", "unused" })
private Optional<@NotNull Boolean> isParagraphValid(Resource resource) {
// try to get model adapting from request associated with the resource implementing ParsysItem
ParsysItem parsysItem = modelFactory.getModelFromWrappedRequest(request, resource, ParsysItem.class);
if (parsysItem != null) {
return Optional.of(parsysItem.isValid());
}
else {
try {
// alternatively try to get model adapting from resource, and check if it implements the ParsysItem interface
Object model = modelFactory.getModelFromResource(resource);
if (model instanceof ParsysItem) {
return Optional.of(((ParsysItem)model).isValid());
}
}
catch (ModelClassException ex) {
// ignore if no model was registered for this resource type
}
}
return Optional.empty();
}
/**
* Get HTML tag attributes from component.
* @param resourceType Component path
* @return Map (never null)
*/
private Map<String, String> getComponentHtmlTagAttributes(String resourceType) {
if (StringUtils.isNotEmpty(resourceType)) {
Component component = componentManager().getComponent(resourceType);
if (component != null && component.getHtmlTagAttributes() != null) {
return component.getHtmlTagAttributes();
}
}
return Collections.emptyMap();
}
private ComponentManager componentManager() {
if (componentManager == null) {
componentManager = AdaptTo.notNull(this.resolver, ComponentManager.class);
}
return componentManager;
}
private Item createNewAreaItem() {
String style = null;
CssBuilder css = new CssBuilder();
css.add(NEWAREA_CSS_CLASS_NAME);
if (generateDefaultCss) {
style = NEWAREA_STYLE;
css.add(SECTION_DEFAULT_CLASS_NAME);
}
css.add(newAreaCss);
String newAreaElementName = StringUtils.defaultString(paragraphElementName, DEFAULT_ELEMENT_NAME);
String newAreaResourceType = getNewAreaResourceType(componentContext.getComponent().getPath());
return new Item(NEWAREA_RESOURCE_PATH)
.newArea(true)
.resourceType(newAreaResourceType)
.elementName(newAreaElementName)
.style(style)
.cssClassName(css.build())
.decorate(true);
}
/**
* Get resource type for new area - from current parsys component or from a supertype component.
* @param componentPath Component path
* @return Resource type (never null)
*/
private String getNewAreaResourceType(String componentPath) {
Resource componentResource = resolver.getResource(componentPath);
if (componentResource != null) {
if (componentResource.getChild(NEWAREA_CHILD_NAME) != null) {
return componentPath + "/" + NEWAREA_CHILD_NAME;
}
String resourceSuperType = componentResource.getResourceSuperType();
if (StringUtils.isNotEmpty(resourceSuperType)) {
return getNewAreaResourceType(resourceSuperType);
}
}
return FALLBACK_NEWAREA_RESOURCE_TYPE;
}
/**
* @return Paragraph system items
*/
@JsonIgnore
public List<Item> getItems() {
return items;
}
/**
* @return Element name for wrapper element
*/
@JsonIgnore
public String getWrapperElementName() {
return StringUtils.defaultString(wrapperElementName, DEFAULT_ELEMENT_NAME);
}
/**
* @return Wrapper element CSS
*/
@JsonIgnore
public String getWrapperCss() {
return this.wrapperCss;
}
/**
* @return True if the wrapper element should be rendered
*/
@JsonIgnore
public boolean isWrapperElement() {
return StringUtils.isNotBlank(wrapperElementName);
}
@Override
public @NotNull String getExportedType() {
return currentResource.getResourceType();
}
@Override
public @NotNull Map<String, ? extends ComponentExporter> getExportedItems() {
if (childModels == null) {
childModels = getChildModels(ComponentExporter.class);
}
return childModels;
}
@Override
public String @NotNull [] getExportedItemsOrder() {
if (exportedItemsOrder == null) {
Map<String, ? extends ComponentExporter> models = getExportedItems();
if (!models.isEmpty()) {
exportedItemsOrder = models.keySet().toArray(ArrayUtils.EMPTY_STRING_ARRAY);
}
else {
exportedItemsOrder = ArrayUtils.EMPTY_STRING_ARRAY;
}
}
return Arrays.copyOf(exportedItemsOrder, exportedItemsOrder.length);
}
@SuppressWarnings("null")
private <T> Map<String, T> getChildModels(@NotNull Class<T> modelClass) {
Map<String, T> models = new LinkedHashMap<>();
for (Resource child : slingModelFilter.filterChildResources(currentResource.getChildren())) {
if (!acceptResource(child)) {
continue;
}
T model = modelFactory.getModelFromWrappedRequest(request, child, modelClass);
if (model != null) {
models.put(child.getName(), model);
}
}
return models;
}
/**
* Paragraph system item.
*/
public static final class Item {
private final String resourcePath;
private String resourceType;
private String elementName;
private String style;
private String cssClassName;
private boolean decorate;
private boolean newArea;
private boolean valid;
Item(String resourcePath) {
this.resourcePath = resourcePath;
}
Item resourceType(String value) {
this.resourceType = value;
return this;
}
Item elementName(String value) {
this.elementName = value;
return this;
}
Item style(String value) {
this.style = value;
return this;
}
Item cssClassName(String value) {
this.cssClassName = value;
return this;
}
Item decorate(boolean value) {
this.decorate = value;
return this;
}
Item newArea(boolean value) {
this.newArea = value;
return this;
}
Item valid(boolean value) {
this.valid = value;
return this;
}
/**
* @return Resource path
*/
public String getResourcePath() {
return resourcePath;
}
/**
* @return Resource type
*/
public String getResourceType() {
return resourceType;
}
/**
* @return Name for item element
*/
public String getElementName() {
return elementName;
}
/**
* @return Style string
*/
public String getStyle() {
return style;
}
/**
* @return CSS classes
*/
public String getCssClassName() {
return cssClassName;
}
/**
* @return Render with decoration tag
*/
public boolean isDecorate() {
return this.decorate;
}
/**
* @return true if this is the new area
*/
public boolean isNewArea() {
return newArea;
}
/**
* @return true if content of this paragraph item is valid.
* If not it should be hidded when wcmmode=disabled.
*/
public boolean isValid() {
return this.valid;
}
}
}