/* * Copyright 2012 david gonzalez. * * 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. */ package com.activecq.samples.workflow.impl; import com.day.cq.dam.commons.util.DamUtil; import com.day.cq.tagging.Tag; import com.day.cq.tagging.TagManager; import com.day.cq.wcm.api.Page; import com.day.cq.workflow.WorkflowException; import com.day.cq.workflow.WorkflowSession; import com.day.cq.workflow.exec.WorkItem; import com.day.cq.workflow.exec.WorkflowData; import com.day.cq.workflow.exec.WorkflowProcess; import com.day.cq.workflow.metadata.MetaDataMap; import org.apache.commons.collections.IteratorUtils; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.LocaleUtils; import org.apache.commons.lang.StringUtils; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Properties; import org.apache.felix.scr.annotations.Property; import org.apache.felix.scr.annotations.Reference; import org.apache.felix.scr.annotations.Service; import org.apache.jackrabbit.JcrConstants; import org.apache.sling.api.resource.LoginException; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.resource.ResourceResolverFactory; import org.apache.sling.api.resource.ValueMap; import org.apache.sling.jcr.resource.JcrResourceConstants; import org.osgi.framework.Constants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.jcr.Node; import javax.jcr.PathNotFoundException; import javax.jcr.RepositoryException; import javax.jcr.Session; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; /** * @author david */ @Component( label = "Samples - Localized Tag Title Extractor Workflow", description = "Sample Workflow Process implementation", metatype = false, immediate = false ) @Properties({ @Property( name = Constants.SERVICE_DESCRIPTION, value = "Sample Workflow Process implementation - Writes tags titles to index-able property.", propertyPrivate = true ), @Property( label = "Vendor", name = Constants.SERVICE_VENDOR, value = "ActiveCQ", propertyPrivate = true ), @Property( label = "Workflow Label", name = "process.label", value = "Localized Tag Title Extractor", description = "Writes localized tag Titles to a index-able Page/Asset property (tag-titles)." ) }) @Service public class LocalizedTagTitleExtractorProcessWorkflow implements WorkflowProcess { public static final int MIN_TAG_DEPTH = 0; public static final String TYPE_CQ_PAGE_CONTENT = "cq:PageContent"; public static final String TYPE_CQ_PAGE = "cq:Page"; public static final String TYPE_DAM_ASSET = "dam:Asset"; public static final String TYPE_DAM_ASSET_METADATA = "metatdata"; public static final String REL_PATH_DAM_ASSET_METADATA = "jcr:content/metatdata"; public static final String PATH_DELIMITER = "/"; public static final String PROPERTY_TAG_TITLES = "tag-titles"; public static final String PROPERTY_CQ_TAGS = "cq:tags"; /** * OSGi Service References * */ @Reference private ResourceResolverFactory resourceResolverFactory; /** * Fields * */ private static final Logger log = LoggerFactory.getLogger(LocalizedTagTitleExtractorProcessWorkflow.class); /** * Work flow execute method * */ @Override public void execute(WorkItem workItem, WorkflowSession workflowSession, MetaDataMap args) throws WorkflowException { boolean useUpstream = true; boolean useDownstream = true; final WorkflowData workflowData = workItem.getWorkflowData(); final String type = workflowData.getPayloadType(); // Check if the payload is a path in the JCR if (!StringUtils.equals(type, "JCR_PATH")) { return; } Session session = workflowSession.getSession(); // Get the path to the JCR resource from the payload String path = workflowData.getPayload().toString(); // Get a ResourceResolver using the same permission set as the Workflow's executing Session ResourceResolver resolver = null; Map<String, Object> authInfo = new HashMap<String, Object>(); authInfo.put(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, session); // Initialize some variables List<String> newTagTitles = new ArrayList<String>(); List<String> newUpstreamTagTitles = new ArrayList<String>(); List<String> newDownstreamTagTitles = new ArrayList<String>(); Locale locale = null; try { // Get the Workflow Sessions' resource resolver using the authInfo created above resolver = resourceResolverFactory.getResourceResolver(authInfo); // Get the Resource representing the WF payload final Resource resource = resolver.getResource(path); // Get the TagManager (using the same permission level as the Workflow's Session) final TagManager tagManager = resolver.adaptTo(TagManager.class); // Use custom implementation to find the resource to look for cq:tags and write the // custom property "tag-titles" to final Resource contentResource = getContentResource(resource); if (contentResource == null) { log.error("Could not find a valid content resource node for payload: {}", resource.getPath()); return; } // Gain access to the content resournce's properties final ValueMap properties = contentResource.adaptTo(ValueMap.class); // Get the full tag paths (namespace:path/to/tag) from the content resource // This only works on the cq:tags property final Tag[] tags = tagManager.getTags(contentResource); // Get any previously applied Localized Tag Titles. // This is used to determine if changes if any updates are needed to this node. final String[] previousTagTitles = properties.get(PROPERTY_TAG_TITLES, new String[]{}); if (!ArrayUtils.isEmpty(tags)) { // Derive the locale if (DamUtil.isAsset(resource)) { // Dam assets use path segments to derive the locale (/content/dam/us/en/...) locale = getLocaleFromPath(resource); } else { // Page's use the jcr:language property accessed via the CQ Page API Page page = resource.adaptTo(Page.class); if (page != null) { locale = page.getLanguage(true); } } // Derive the Localized Tag Titles for all tags in the tag hierarchy from the Tags stored in the cq:tags property // This does not remove duplicate titles (different tag trees could repeat titles) if (useUpstream) { newUpstreamTagTitles = tagsToUpstreamLocalizedTagTitles(tags, locale, tagManager); newTagTitles.addAll(newUpstreamTagTitles); } if (useDownstream) { newDownstreamTagTitles = tagsToDownstreamLocalizedTagTitles(tags, locale, tagManager, new ArrayList<String>(), 0); newTagTitles.addAll(newDownstreamTagTitles); } if (!useUpstream && !useDownstream) { newTagTitles.addAll(tagsToLocalizedTagTitles(tags, locale)); } } try { // Get the node in the JCR the payload points to final Node node = session.getNode(contentResource.getPath()); // If the currently applied Tag Titles are the same as the derived Tag titles then skip! if (!isSame(newTagTitles.toArray(new String[]{}), previousTagTitles)) { // If changes have been made to the Tag Names, then apply to the tag-titles property // on the content resource. node.setProperty(PROPERTY_TAG_TITLES, newUpstreamTagTitles.toArray(new String[newUpstreamTagTitles.size()])); } else { log.debug("No change in Tag Titles. Do not update this content resource."); } } catch (PathNotFoundException ex) { log.error(ex.getMessage()); } catch (RepositoryException ex) { log.error(ex.getMessage()); } } catch (LoginException ex) { log.error(ex.getMessage()); } finally { // Clean up after yourself please!!! if (resolver != null) { resolver.close(); resolver = null; } } } /** Helper methods **/ /** * Checks if two String arrays are the same (same values and same order) * * @param a * @param b * @return */ private boolean isSame(String[] a, String[] b) { if (a.length != b.length) { return false; } for (int i = 0; i < a.length; i++) { if (!StringUtils.equals(a[i], b[i])) { return false; } } return true; } /** * Returns localized Tag Titles for all the ancestor tags to the tags supplied in "tagPaths" * <p/> * Tags in * * @param tags * @param locale * @param tagManager * @return */ private List<String> tagsToUpstreamLocalizedTagTitles(Tag[] tags, Locale locale, TagManager tagManager) { List<String> localizedTagTitles = new ArrayList<String>(); for (final Tag tag : tags) { String tagID = tag.getTagID(); boolean isLast = false; int count = StringUtils.countMatches(tagID, PATH_DELIMITER); while (count >= MIN_TAG_DEPTH && count >= 0) { final Tag ancestorTag = tagManager.resolve(tagID); if (ancestorTag != null) { localizedTagTitles.add(getLocalizedTagTitle(ancestorTag, locale)); } if (isLast) { break; } tagID = StringUtils.substringBeforeLast(tagID, PATH_DELIMITER); count = StringUtils.countMatches(tagID, PATH_DELIMITER); if (count <= 0) { isLast = true; } } } return localizedTagTitles; } /** * Returns localized Tag Titles for all the ancestor tags to the tags supplied in "tagPaths" * <p/> * Tags in * * @param tags * @param locale * @param tagManager * @param localizedTagTitles * @return */ private List<String> tagsToDownstreamLocalizedTagTitles(final Tag[] tags, final Locale locale, final TagManager tagManager, List<String> localizedTagTitles, int depth) { depth++; for (final Tag tag : tags) { if (tag.listChildren() != null && tag.listChildren().hasNext()) { final List<Tag> children = IteratorUtils.toList(tag.listChildren()); localizedTagTitles = tagsToDownstreamLocalizedTagTitles(children.toArray(new Tag[children.size()]), locale, tagManager, localizedTagTitles, depth); } if (depth > 1) { // Do not include the tags explicitly set on the resource localizedTagTitles.add(getLocalizedTagTitle(tag, locale)); } } return localizedTagTitles; } /** * @param tags * @param locale * @return */ private List<String> tagsToLocalizedTagTitles(final Tag[] tags, final Locale locale) { List<String> localizedTagTitles = new ArrayList<String>(); for (final Tag tag : tags) { localizedTagTitles.add(getLocalizedTagTitle(tag, locale)); } return localizedTagTitles; } /** * Derive the locale from the parent path segments (/content/us/en/..) * * @param resource * @return */ private Locale getLocaleFromPath(final Resource resource) { final String[] segments = StringUtils.split(resource.getPath(), PATH_DELIMITER); String country = ""; String language = ""; for (final String segment : segments) { if (ArrayUtils.contains(Locale.getISOCountries(), segment)) { country = segment; } else if (ArrayUtils.contains(Locale.getISOLanguages(), segment)) { language = segment; } } if (StringUtils.isNotBlank(country) && StringUtils.isNotBlank(language)) { return LocaleUtils.toLocale(country + "-" + language); } else if (StringUtils.isNotBlank(country)) { return LocaleUtils.toLocale(country); } else if (StringUtils.isNotBlank(language)) { return LocaleUtils.toLocale(language); } return null; } /** * @param tag * @param locale * @return */ private String getLocalizedTagTitle(Tag tag, Locale locale) { final String title = tag.getTitle(); final String localizeTitle = tag.getTitle(locale); if (StringUtils.isNotBlank(localizeTitle)) { return localizeTitle; } else if (StringUtils.isNotBlank(title)) { return title; } return null; } /** * Finds the proper "content" resource to read cq:tags from and write tag-titles to, based on * payload resource type. * <p/> * cq:Page * cq:PageContent * nt:unstructured acting as cq:PageContent * <p/> * dam:Asset * dam:Asset metadata * * @param payloadResource * @return */ private Resource getContentResource(final Resource payloadResource) { if (isPrimaryType(payloadResource, TYPE_CQ_PAGE)) { /** cq:Page **/ return payloadResource.getChild(JcrConstants.JCR_CONTENT); } else if (StringUtils.equals(payloadResource.getName(), JcrConstants.JCR_CONTENT) && isPrimaryType(payloadResource, TYPE_CQ_PAGE_CONTENT)) { /** cq:PageContent **/ return payloadResource; } else if (isPrimaryType(payloadResource, JcrConstants.NT_UNSTRUCTURED)) { /** nt:unstructured **/ final Resource parent = payloadResource.getParent(); if (parent != null && isPrimaryType(parent, TYPE_CQ_PAGE) && StringUtils.equals(payloadResource.getName(), JcrConstants.JCR_CONTENT)) { /** cq:Page / jcr:content(nt:unstructured) **/ return payloadResource; } else if (StringUtils.equals(payloadResource.getName(), TYPE_DAM_ASSET_METADATA)) { if (parent != null && StringUtils.equals(parent.getName(), JcrConstants.JCR_CONTENT)) { Resource grandParent = null; if (parent != null) { grandParent = parent.getParent(); } if (grandParent != null && isPrimaryType(grandParent, TYPE_DAM_ASSET)) { /** dam:Asset / jcr:content / metadata **/ return payloadResource; } } } } else if (isPrimaryType(payloadResource, TYPE_DAM_ASSET)) { /** dam:Asset **/ return payloadResource.getChild(REL_PATH_DAM_ASSET_METADATA); } /** Use the payload resource; Ex. a component resource that uses cq:tags **/ return payloadResource; } /** * Checks if the jcr:PrimaryType of a resource matches the type param * * @param resource * @param type * @return */ private boolean isPrimaryType(final Resource resource, final String type) { ValueMap properties = resource.adaptTo(ValueMap.class); String primaryType = properties.get(JcrConstants.JCR_PRIMARYTYPE, "__unknown__"); return StringUtils.equals(type, primaryType); } }