/* * #%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.components.longformtext.impl; import com.adobe.acs.commons.components.longformtext.LongFormTextComponent; import com.day.cq.commons.jcr.JcrConstants; import com.day.cq.commons.jcr.JcrUtil; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Reference; import org.apache.felix.scr.annotations.Service; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceUtil; import org.apache.sling.commons.html.HtmlParser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.NodeList; import javax.jcr.Node; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import java.io.StringWriter; import java.util.ArrayList; import java.util.List; /** * ACS AEM Commons - Components - Long-Form Text * Provides support for the ACS AEM Commons Long-form Text Component. */ @Component @Service public class LongFormTextComponentImpl implements LongFormTextComponent { private static final Logger log = LoggerFactory.getLogger(LongFormTextComponentImpl.class); @Reference private HtmlParser htmlParser; @Override public final String[] getTextParagraphs(final String text) { List<String> paragraphs = new ArrayList<String>(); try { final Document doc = htmlParser.parse(null, IOUtils.toInputStream(text), "UTF-8"); doc.getDocumentElement().normalize(); TransformerFactory tf = TransformerFactory.newInstance(); Transformer transformer = tf.newTransformer(); transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); final NodeList bodies = doc.getElementsByTagName("body"); if (bodies != null && bodies.getLength() == 1) { final org.w3c.dom.Node body = bodies.item(0); final NodeList children = body.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { StringWriter writer = new StringWriter(); StreamResult result = new StreamResult(writer); final org.w3c.dom.Node child = children.item(i); if (child == null) { log.warn("Found a null dom node."); continue; } else if (child.getNodeType() != org.w3c.dom.Node.ELEMENT_NODE) { log.warn("Found a dom node is not an element; skipping"); continue; } stripNamespaces(child); transformer.transform(new DOMSource(child), result); writer.flush(); final String outerHTML = writer.toString(); if (StringUtils.isNotBlank(outerHTML)) { paragraphs.add(outerHTML); } } } else { log.debug("HTML does not have a single body tag. Cannot parse as expected."); } } catch (Exception e) { log.warn("Long Form Text encountered a parser error: {}", e); } return paragraphs.toArray(new String[paragraphs.size()]); } @Override public final void mergeParagraphSystems(final Resource resource, final int textParagraphSize) throws RepositoryException { if (resource == null || ResourceUtil.isNonExistingResource(resource) || !this.isModifiable(resource)) { // Nothing to merge, or user does not have access to merge return; } final Node targetNode = this.getOrCreateLastParagraphSystemResource(resource, textParagraphSize); if (targetNode == null) { log.info("Could not find last target node to merge long-form-text text inline par resources: {}", textParagraphSize); return; } for (final Resource child : resource.getChildren()) { int index = this.getResourceIndex(child); if (index > textParagraphSize) { this.moveChildrenToNode(child, targetNode); } } } @Override public boolean hasContents(final Resource resource, final int index) { final Resource parResource = resource.getChild(LONG_FORM_TEXT_PAR + index); return parResource != null && parResource.listChildren().hasNext(); } private void moveChildrenToNode(Resource resource, Node targetNode) throws RepositoryException { for (Resource child : resource.getChildren()) { // Use this to create a unique node name; else existing components might get overwritten. final Node uniqueNode = JcrUtil.createUniqueNode(targetNode, child.getName(), JcrConstants.NT_UNSTRUCTURED, targetNode.getSession()); // Once we have a unique node we made as a place holder, we can copy over it w the real component content JcrUtil.copy(child.adaptTo(Node.class), targetNode, uniqueNode.getName(), true); } // Remove the old long-form-text-par- node resource.adaptTo(Node.class).remove(); // Save all changes targetNode.getSession().save(); } private Node getOrCreateLastParagraphSystemResource(final Resource resource, final int lastIndex) throws RepositoryException { final String resourceName = LONG_FORM_TEXT_PAR + lastIndex; final Resource lastResource = resource.getChild(resourceName); if (lastResource != null) { return lastResource.adaptTo(Node.class); } final Node parentNode = resource.adaptTo(Node.class); if (parentNode == null) { return null; } final Session session = parentNode.getSession(); final Node node = JcrUtil.createPath(parentNode, resourceName, false, JcrConstants.NT_UNSTRUCTURED, JcrConstants.NT_UNSTRUCTURED, session, true); return node; } private int getResourceIndex(final Resource resource) { final String resourceName = resource.getName(); if (!StringUtils.startsWith(resourceName, LONG_FORM_TEXT_PAR)) { return -1; } final String indexStr = StringUtils.removeStart(resourceName, LONG_FORM_TEXT_PAR); try { return Integer.parseInt(indexStr); } catch (NumberFormatException ex) { return -1; } } private boolean isModifiable(final Resource resource) throws RepositoryException { final String writePermissions = "add_node,set_property,remove"; final Session userSession = resource.getResourceResolver().adaptTo(Session.class); final String path = resource.getPath(); try { userSession.checkPermission(path, writePermissions); } catch (java.security.AccessControlException e) { log.debug("User does not have modify permissions [ {} ] on [ {} ]", writePermissions, resource.getPath()); return false; } return true; } /** * Method borrowed from: https://blog.avisi.nl/2013/07/24/java-stripping-namespaces-from-xml-using-dom/ * * Recursively renames the namespace of a node. * @param node the starting node. */ private void stripNamespaces(org.w3c.dom.Node node) { Document document = node.getOwnerDocument(); if (node.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE) { document.renameNode(node, null, node.getNodeName()); } NodeList list = node.getChildNodes(); for (int i = 0; i < list.getLength(); ++i) { stripNamespaces(list.item(i)); } } }