/* * #%L * ACS AEM Commons Bundle * %% * Copyright (C) 2015 Adobe * %% * 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 com.adobe.acs.commons.wcm.impl; import com.adobe.acs.commons.json.AbstractJSONObjectVisitor; import com.adobe.acs.commons.util.BufferingResponse; import com.adobe.acs.commons.util.InfoWriter; import com.adobe.acs.commons.util.PathInfoUtil; import com.day.cq.commons.jcr.JcrConstants; import org.apache.commons.lang.StringUtils; import org.apache.felix.scr.annotations.Activate; import org.apache.felix.scr.annotations.Property; import org.apache.felix.scr.annotations.sling.SlingServlet; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.SlingHttpServletResponse; import org.apache.sling.api.request.RequestDispatcherOptions; import org.apache.sling.api.request.RequestUtil; import org.apache.sling.api.servlets.SlingSafeMethodsServlet; import org.apache.sling.commons.json.JSONException; import org.apache.sling.commons.json.JSONObject; import org.apache.sling.commons.osgi.PropertiesUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.ServletException; import java.io.IOException; import java.net.URLDecoder; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * ACS AEM Commons - CQInclude Property Namespace. */ //@formatter:off @SuppressWarnings("serial") @SlingServlet( label = "ACS AEM Commons - CQInclude Property Namespace", metatype = true, selectors = "overlay.cqinclude.namespace", extensions = "json", resourceTypes = "sling/servlet/default") //@formatter:on public final class CQIncludePropertyNamespaceServlet extends SlingSafeMethodsServlet { //@formatter:off private static final Logger log = LoggerFactory.getLogger(CQIncludePropertyNamespaceServlet.class); private static final String REQ_ATTR = CQIncludePropertyNamespaceServlet.class.getName() + ".processed"; private static final String AEM_CQ_INCLUDE_SELECTORS = "overlay.infinity"; private static final String CQINCLUDE_NAMESPACE_URL_REGEX = "(.+\\.cqinclude\\.namespace\\.)(.+)(\\.json)"; private static final int NAME_PROPERTY_SELECTOR_INDEX = 3; private static final String PN_NAME = "name"; private static final String PN_XTYPE = "xtype"; private static final String PN_PATH = "path"; private static final String NT_CQ_WIDGET = "cq:Widget"; private static final String ESCAPED_SLASH = "%252F"; private static final String[] DEFAULT_NAMESPACEABLE_PROPERTY_NAMES = new String[]{ PN_NAME, "cropParameter", "fileNameParameter", "fileReferenceParameter", "mapParameter", "rotateParameter", "widthParameter", "heightParameter" }; private String[] namespaceablePropertyNames = null; @Property(label = "Property Names", description = "Namespace properties defined in this list. Leave empty for on 'name'. " + " Defaults to [ name, cropParameter, fileNameParameter, fileReferenceParameter, " + "mapParameter, rotateParameter, widthParameter, heightParameter] ", value = { PN_NAME, "cropParameter", "fileNameParameter", "fileReferenceParameter", "mapParameter", "rotateParameter", "widthParameter", "heightParameter" } ) public static final String PROP_NAMESPACEABLE_PROPERTY_NAMES = "namespace.property-names"; private static final String[] DEFAULT_NAMESPACEABLE_PROPERTY_VALUE_PATTERNS = new String[]{"^\\./.*"}; private List<Pattern> namespaceablePropertyValuePatterns = new ArrayList<Pattern>(); @Property(label = "Property Value Patterns", description = "Namespace properties whose values match a regex in this list. " + "Defaults to [ \"^\\\\./.*\" ]", value = {"^\\./.*"}) public static final String PROP_NAMESPACEABLE_PROPERTY_VALUE_PATTERNS = "namespace.property-value-patterns"; private static final boolean DEFAULT_SUPPORT_MULTI_LEVEL = false; private boolean supportMultiLevel = DEFAULT_SUPPORT_MULTI_LEVEL; @Property(label = "Support Multi-level", description = "When set to true, cqinclude servlet will support multi-level path-ing if nested cqinclude namespaces. Defaults to false", boolValue = false) public static final String PROP_SUPPORT_MULTI_LEVEL = "namespace.multi-level"; //@formatter:on @Override protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException { response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); if (!this.accepts(request)) { response.setStatus(SlingHttpServletResponse.SC_NOT_FOUND); response.getWriter().write(new JSONObject().toString()); } /* Servlet accepts this request */ RequestUtil.setRequestAttribute(request, REQ_ATTR, true); final String namespace = URLDecoder.decode(PathInfoUtil.getSelector(request, NAME_PROPERTY_SELECTOR_INDEX), "UTF-8"); final RequestDispatcherOptions options = new RequestDispatcherOptions(); options.setReplaceSelectors(AEM_CQ_INCLUDE_SELECTORS); final BufferingResponse bufferingResponse = new BufferingResponse(response); request.getRequestDispatcher(request.getResource(), options).forward(request, bufferingResponse); try { final JSONObject json = new JSONObject(bufferingResponse.getContents()); final PropertyNamespaceUpdater propertyNamespaceUpdater = new PropertyNamespaceUpdater(namespace); propertyNamespaceUpdater.accept(json); response.getWriter().write(json.toString()); } catch (JSONException e) { log.error("Error composing the cqinclude JSON representation of the widget overlay for [ {} ]", request.getRequestURI(), e); response.setStatus(SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR); response.getWriter().write(new JSONObject().toString()); } } protected boolean accepts(SlingHttpServletRequest request) { if (request.getAttribute(REQ_ATTR) != null) { // Cyclic loop log.warn("Identified a cyclic loop in the ACS Commons CQ Include Namespace prefix Servlet for [ {} ]", request.getRequestURI()); return false; } for (int i = 0; i <= NAME_PROPERTY_SELECTOR_INDEX; i++) { if (StringUtils.isBlank(PathInfoUtil.getSelector(request, i))) { // Missing necessary selectors; the first N - 1 should be redundant since the selectors are specified // in the Servlet registration return false; } } return true; } @Activate protected void activate(final Map<String, Object> config) { supportMultiLevel = PropertiesUtil.toBoolean(config.get(PROP_SUPPORT_MULTI_LEVEL), DEFAULT_SUPPORT_MULTI_LEVEL); // Property Names namespaceablePropertyNames = PropertiesUtil.toStringArray(config.get(PROP_NAMESPACEABLE_PROPERTY_NAMES), DEFAULT_NAMESPACEABLE_PROPERTY_NAMES); // Property Value Patterns namespaceablePropertyValuePatterns = new ArrayList<Pattern>(); String[] regexes = PropertiesUtil.toStringArray(config.get(PROP_NAMESPACEABLE_PROPERTY_VALUE_PATTERNS), DEFAULT_NAMESPACEABLE_PROPERTY_VALUE_PATTERNS); for (final String regex : regexes) { namespaceablePropertyValuePatterns.add(Pattern.compile(regex)); } final InfoWriter iw = new InfoWriter(); iw.title("ACS AEM Commons - CQInclude Property Namespace Servlet"); iw.message("Namespace-able Property Names: {}", Arrays.asList(namespaceablePropertyNames)); iw.message("Namespace-able Property Value Patterns: {}", namespaceablePropertyValuePatterns); iw.end(); log.info(iw.toString()); } public final class PropertyNamespaceUpdater extends AbstractJSONObjectVisitor { private final String namespace; private static final String DOT_SLASH = "./"; public PropertyNamespaceUpdater(final String namespace) { this.namespace = namespace; } private boolean accept(String propertyName, String propertyValue) { // Check if the property name denotes namespaceability if (namespaceablePropertyNames != null) { for (final String name : namespaceablePropertyNames) { if (StringUtils.equals(name, propertyName)) { return true; } } } // Check if the property value denotes namespaceability if (namespaceablePropertyValuePatterns != null) { for (final Pattern pattern : namespaceablePropertyValuePatterns) { final Matcher matcher = pattern.matcher(propertyValue); if (matcher.matches()) { return true; } } } return false; } protected boolean isCqincludeNamespaceWidget(JSONObject jsonObject) { if (StringUtils.equals(jsonObject.optString(JcrConstants.JCR_PRIMARYTYPE), NT_CQ_WIDGET) && (StringUtils.equals(jsonObject.optString(PN_XTYPE), "cqinclude"))) { String path = jsonObject.optString(PN_PATH); if (StringUtils.isNotBlank(path) && path.matches(CQINCLUDE_NAMESPACE_URL_REGEX)) { return true; } } return false; } protected JSONObject makeMultiLevel(JSONObject jsonObject) { String path = jsonObject.optString(PN_PATH); if (StringUtils.isNotBlank(path)) { Pattern pattern = Pattern.compile(CQINCLUDE_NAMESPACE_URL_REGEX); Matcher m = pattern.matcher(path); if (m.matches()) { path = m.group(1) + this.namespace + ESCAPED_SLASH + m.group(2) + m.group(3); try { jsonObject.put(PN_PATH, path); } catch (JSONException e) { log.error("Could not update cqinclude namespace with path [ {} ]", path); } } } return jsonObject; } @SuppressWarnings("PMD.CollapsibleIfStatements") @Override protected void visit(JSONObject jsonObject) { if (StringUtils.equals(jsonObject.optString(JcrConstants.JCR_PRIMARYTYPE), NT_CQ_WIDGET)) { if (supportMultiLevel) { if (isCqincludeNamespaceWidget(jsonObject)) { jsonObject = makeMultiLevel(jsonObject); } } final Iterator<String> keys = jsonObject.keys(); while (keys.hasNext()) { final String propertyName = keys.next(); if (!this.accept(propertyName, jsonObject.optString(propertyName))) { log.debug("Property [ {} ~> {} ] is not a namespace-able property name/value", propertyName, jsonObject.optString(propertyName)); continue; } String value = jsonObject.optString(propertyName); if (value != null) { String prefix = ""; if (StringUtils.startsWith(value, DOT_SLASH)) { value = StringUtils.removeStart(value, DOT_SLASH); prefix = DOT_SLASH; } if (StringUtils.isNotBlank(value)) { try { jsonObject.put(propertyName, prefix + namespace + "/" + value); } catch (final JSONException e) { log.error("Error updating the Name property of the JSON object", e); } } } } } } } }