All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.day.cq.wcm.designimporter.DesignPackageImporter Maven / Gradle / Ivy

There is a newer version: 2024.11.18751.20241128T090041Z-241100
Show newest version
/*************************************************************************
 *
 * ADOBE CONFIDENTIAL
 * ___________________
 *
 *  Copyright 2012 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.day.cq.wcm.designimporter;

import com.day.cq.commons.jcr.JcrConstants;
import com.day.cq.commons.jcr.JcrUtil;
import com.day.cq.dam.api.AssetManager;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.PageManager;
import com.day.cq.wcm.api.PageManagerFactory;
import com.day.cq.wcm.api.designer.Designer;
import com.day.cq.wcm.designimporter.api.CanvasBuilder;
import com.day.cq.wcm.designimporter.api.EntryPreprocessor;
import com.day.cq.wcm.designimporter.api.ImporterConstants;
import com.day.cq.wcm.designimporter.util.ImporterUtil;
import com.day.cq.wcm.designimporter.util.StreamUtil;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
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.commons.JcrUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.request.RequestParameter;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.commons.mime.MimeTypeService;
import org.apache.sling.commons.osgi.OsgiUtil;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.nodetype.NodeType;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipInputStream;

/*
AdobePatentID="2613US01"
*/

/**
 * Provides API for importing a design package. The entry service for the design importer functionality.
 *
 * 

* A design package is an archived HTML project containing an HTML file along with several (optional) referenced * scripts and styles. This class provides API for importing that design package under a CQ page or component. *

* * @see CanvasBuilder */ @Component(label = "%design.importer.name", description = "%design.importer.description", metatype = true) @Service(value = DesignPackageImporter.class) public class DesignPackageImporter { private static final String DEFAULT_EXTRACT_FILTER_GITIGNORE = "[^.]*\\.gitignore"; private static final String DEFAULT_EXTRACT_FILTER_DS_STORE = "[^.]*\\.DS_Store"; private static final String DEFAULT_EXTRACT_FILTER_MACOSX = "__MACOSX.*"; @Property(value = { DEFAULT_EXTRACT_FILTER_MACOSX, DEFAULT_EXTRACT_FILTER_DS_STORE, DEFAULT_EXTRACT_FILTER_GITIGNORE }) static private final String PN_EXTRACT_FILTER = "extract.filter"; private Logger logger = LoggerFactory.getLogger(DesignPackageImporter.class); @Reference protected MimeTypeService mimeTypeService; @Reference protected EntryPreprocessor entryPreprocessor; @Reference protected PageManagerFactory pageManagerFactory; /** * The list of nodes temporarily holding the extracted HTML files. */ private ArrayList extractedHtmlNodes = new ArrayList(); private ArrayList extractedResources = new ArrayList(); private BundleContext bundleContext; private String[] extractFilters; /** * Asset manager used to create assets for images. */ private AssetManager assetManager; /** * Imports a design package from a sling request. *

* A design package is typically imported by dropping a design package onto the designimporter component, which in turn POSTs * the file to CQ server. This API serves as the starting point for import of the design package from the POST sling request. * The POST request contains the uploaded file stream as a request parameter named "designfile". *

* *

* The design package is expected to be a conforming HTML project with a "defined" structure. At the minimum, the project must contain * the HTML file with the name index.html or index.htm at the root (This rule is configurable via {@link CanvasBuilder} OSGi component configuration). * This is the only conformance required. Authors are free to choose any project structure or file/directory naming conventions as per their wishes. The * importer works by fully parsing the HTML document and not by relying upon certain naming conventions. *

* *

* The import process involves the following steps: *

    *
  1. The design package is unloaded at a unique location under /etc/designs. This design path is translated from the CQ page initiating the request
  2. *
  3. The unloaded files are looked up for HTML files. Appropriate {@link CanvasBuilder} service implementation that is registered to handle the HTML of that name pattern is executed
  4. *
  5. The {@link CanvasBuilder} builds the CQ page by parsing the HTML file as following: *
      *
    1. It parses the HTML document and culls out CQ components, scripts, styles and other relevant meta information
    2. *
    3. It builds the page component nodes for the CQ components extracted
    4. *
    5. It aggregates the extracted scripts and styles into CQ clientlibs
    6. *
    7. It generates a top level canvas component that contains the reference to all the extracted CQ components as well as the clientlibs
    8. *
    *
  6. *
* If a more controlled building is required, consider using the {@link #importDesignPackage(org.apache.sling.api.SlingHttpServletRequest, CanvasBuildOptions)} API instead. *

* * @param slingHttpServletRequest The sling request for importing the design package. A design package is an archived HTML project containing the main HTML file * along with all the referenced scripts, styles and assets. By default, the main HTML file should be named * index.html and appear at the root level of the zip archive. The rule can however be modified via {@link CanvasBuilder} * configuration. * * * @return DesignImportResult The object encapsulating the warnings ,if they arose, during the import process * @throws MalformedArchiveException if there is an error reading the input archive stream * @throws MissingHTMLException if there is no index.html or index.htm entry in the zip archive * @throws UnsupportedTagContentException if unsupported content is encountered within a tag * @throws MissingCanvasException if the input HTML stream is missing the canvas boundary * @throws DesignImportException if there is an exception writing to the CRX repository */ public DesignImportResult importDesignPackage(SlingHttpServletRequest slingHttpServletRequest) throws DesignImportException { return importDesignPackage(slingHttpServletRequest, null); } /** * Imports a design package from a sling request similar to how {@link #importDesignPackage(org.apache.sling.api.SlingHttpServletRequest)} imports, the difference being * the amount of control you've over various building options. * *

This api lets you have control over what you want to build by providing build flags specified via {@link CanvasBuildOptions}. You can choose to switch on or off, * the building of canvas nodes, canvas component and clientlibs. With clientlibs, you can further choose to switch on or off, the building of head scripts, head styles, * body scripts, body styles or a combination of some of those

* * @param slingHttpServletRequest Contains http request and input stream of the design package. Design package is a zipped HTML project containing the main HTML file * along with all the referenced scripts, styles and assets. By default, the main HTML file should be named * index.html and appear at the root level of the zip archive. The rule can however be modified via {@link CanvasBuilder} OSGi configuration. * @param buildOptions The {@link CanvasBuildOptions} object that contains build flags for switching on or off certain build options * * @return DesignImportResult The object encapsulating the warnings ,if they arose, during the import process * @throws MalformedArchiveException if there is an error reading the input archive stream * @throws MissingHTMLException if there is no index.html or index.htm entry in the zip archive * @throws UnsupportedTagContentException if unsupported content is encountered within a tag * @throws MissingCanvasException if the input HTML stream is missing the canvas boundary * @throws DesignImportException if there is an exception writing to the CRX repository */ public DesignImportResult importDesignPackage(SlingHttpServletRequest slingHttpServletRequest, CanvasBuildOptions buildOptions) throws DesignImportException { try { if(buildOptions == null){ buildOptions = new CanvasBuildOptions(); buildOptions.setBuildPageNodes(true); buildOptions.setBuildClientLibs(true); buildOptions.setBuildCanvasComponent(true); } // get importer resource and archive from request Resource importer = slingHttpServletRequest.getResource(); InputStream designPackage = getArchiveStreamFromRequest(slingHttpServletRequest); return importDesignPackageInternal(importer, designPackage, buildOptions); } catch (ZipException e) { throw new MalformedArchiveException(); } catch (IOException e) { throw new DesignImportException(e); } catch (RepositoryException e) { throw new DesignImportException(e); } } /** * Imports a design package, similarly to {@link #importDesignPackage(org.apache.sling.api.SlingHttpServletRequest, com.day.cq.wcm.designimporter.CanvasBuildOptions)}, but with a different set of parameters. * * @param importerPage An existing page of template type "wcm/designimporter/templates/importerpage" * @param designPackagePath The absolute path to a design package zip file in the repository * @param buildOptions The {@link CanvasBuildOptions} object that contains build flags for switching on or off certain build options * * @return DesignImportResult The object encapsulating the warnings ,if they arose, during the import process * @throws MalformedArchiveException if there is an error reading the input archive stream * @throws MissingHTMLException if there is no index.html or index.htm entry in the zip archive * @throws UnsupportedTagContentException if unsupported content is encountered within a tag * @throws MissingCanvasException if the input HTML stream is missing the canvas boundary * @throws DesignImportException if there is an exception writing to the CRX repository */ public DesignImportResult importDesignPackage(Page importerPage, String designPackagePath, CanvasBuildOptions buildOptions) throws DesignImportException { try { if(buildOptions == null){ buildOptions = new CanvasBuildOptions(); buildOptions.setBuildPageNodes(true); buildOptions.setBuildClientLibs(true); buildOptions.setBuildCanvasComponent(true); } // get importer resource of the importer page String importerPath = JcrConstants.JCR_CONTENT + "/importer"; Resource importer = importerPage.adaptTo(Resource.class).getChild(importerPath); // copy design package file to importer page Node importerPageNode = importerPage.adaptTo(Node.class); Node dst = importerPageNode.getNode(importerPath).addNode(ImporterConstants.NN_DESIGNPACKAGE, JcrConstants.NT_UNSTRUCTURED); Node src = importerPageNode.getSession().getNode(designPackagePath); JcrUtil.copy(src, dst, ImporterConstants.NN_DESIGNPACKAGEFILE); // create archive input stream Resource jcrContent = importer.getChild(ImporterConstants.NN_DESIGNPACKAGE + "/" + ImporterConstants.NN_DESIGNPACKAGEFILE + "/" + JcrConstants.JCR_CONTENT); InputStream designPackage = (InputStream) jcrContent.adaptTo(ValueMap.class).get(JcrConstants.JCR_DATA); return importDesignPackageInternal(importer, designPackage, buildOptions); } catch (ZipException e) { throw new MalformedArchiveException(); } catch (IOException e) { throw new DesignImportException(e); } catch (RepositoryException e) { throw new DesignImportException(e); } } /** * Extracts the zip entry under the parent node according to the mapped path. * * @param entry The zip entry * @param parent The node under which the zip entry must be unloaded * @param zipInputStream The zip input stream * @param designImporterContext * @return The extracted nt:file {@link Node} or null if the entry wasn't extracted. * @throws RepositoryException * @throws IOException */ protected Node extractEntry(ZipEntry entry, Node parent, ZipInputStream zipInputStream, DesignImporterContext designImporterContext) throws RepositoryException, IOException { if (!entry.isDirectory()) { String destPath = mapPath(entry.getName()); int lastIndexOfSlash = destPath.lastIndexOf('/'); String fileName = destPath.substring(lastIndexOfSlash + 1); Node fileParent = parent; if (lastIndexOfSlash > 0) { String folder = destPath.substring(0, lastIndexOfSlash); fileParent = JcrUtil.createPath(parent, folder, false, NodeType.NT_FOLDER, NodeType.NT_FOLDER, parent.getSession(), true); } String encoding = "utf-8"; InputStream stream = zipInputStream; if(entry.getName().matches("(?i)[^.]*\\.html")){ if (!stream.markSupported()) { stream = new BufferedInputStream(zipInputStream); } encoding = StreamUtil.getEncoding(stream); } String mimeType = getMimeType(fileName); InputStream entryStream = stream; if(entryPreprocessor != null) entryStream = entryPreprocessor.getProcessedStream(entry.getName(), stream, designImporterContext); // CQ5-34699: for images.. if (assetManager != null && mimeType.startsWith("image/")) { // ...create assets, in order for the image editing to work correctly assetManager.createAsset(fileParent.getPath() + "/" + fileName, entryStream, mimeType, true); } else { mimeType = mimeType + ";charset=" + encoding; return JcrUtils.putFile(fileParent, fileName, mimeType, entryStream); } } return null; } /** * Creates the design path. * * @param importer The importer resource * @return The design {@link Node} * @throws RepositoryException * */ protected Node getOrCreateDesignPath(Resource importer) throws RepositoryException { ResourceResolver resourceResolver = importer.getResourceResolver(); PageManager pageManager = pageManagerFactory.getPageManager(resourceResolver); Page page = pageManager.getContainingPage(importer); Session session = page.adaptTo(Node.class).getSession(); String pagePath = page.getPath(); String path = null; if(ImporterUtil.isImporter(importer)) { Designer designer = resourceResolver.adaptTo(Designer.class); String pageDesignPath = designer.getDesign(page).getPath(); path = pageDesignPath + "/canvas" + importer.getPath(); } else { path = "/etc/designs/canvaspage" + pagePath; } Node designNode = JcrUtil.createPath(path, NodeType.NT_FOLDER, "cq:Page", session, true); Node jcrContent = JcrUtils.getOrAddNode(designNode, JcrConstants.JCR_CONTENT, NodeType.NT_UNSTRUCTURED); JcrUtil.setProperty(jcrContent, "cq:doctype", "html_5"); JcrUtil.setProperty(jcrContent, "sling:resourceType", "wcm/core/components/designer"); session.save(); return designNode; } /** * Creates the design path if it does not exists. * * @param page The Page for which the design path needs to be generated. * @return The design path {@link Node} * @throws RepositoryException * * @deprecated Use {@link #getOrCreateDesignPath(org.apache.sling.api.resource.Resource)} instead */ protected Node getOrCreateDesignPath(Page page) throws RepositoryException { Session session = page.adaptTo(Node.class).getSession(); String pagePath = page.getPath(); String path = "/etc/designs/canvaspage" + pagePath; /*pagePath.replace("/content/campaigns", "/etc/designs/canvaspage")*/ Node designNode = JcrUtil.createPath(path, NodeType.NT_FOLDER, "cq:Page", session, true); Node jcrContent = JcrUtils.getOrAddNode(designNode, JcrConstants.JCR_CONTENT, NodeType.NT_UNSTRUCTURED); JcrUtil.setProperty(jcrContent, "cq:doctype", "html_5"); JcrUtil.setProperty(jcrContent, "sling:resourceType", "wcm/core/components/designer"); session.save(); return designNode; } /** * Maps the path of a zip entry to the destination path. For example, a zip entry scripts/myscript.js could be unzipped at * clientlibs/scripts/myscript.js *

* Subclasses could override this method to provide alternate unzip locations for zip entries. * * @param zipEntry The name of the zip entry * @return The mapped path onto which the zip entry must be unloaded */ protected String mapPath(String zipEntry) { return zipEntry; } /** * Convenience method for mapping zipEntry path. See {@link #mapPath(String)} * * @param entry The ZipEntry which needs to be mapped to a path * @return The mapped path onto which the zip entry must be unloaded */ protected String mapPath(ZipEntry entry) { return mapPath(entry.getName()); } /** * Decides if the passed zip entry must be unloaded or not. Can be overridden * for controlling the zip unload behavior. * @param entry * @return */ protected boolean shouldExtractEntry(ZipEntry entry) { for (String extractFilter : extractFilters) { if (entry.getName().matches(extractFilter)) return false; } return true; } private void cleanup(Node designNode, Set resourcesToCleanup) { // Delete the temporarily extracted HTML file from the design node. try { for (Node node : extractedHtmlNodes) { node.remove(); } } catch (RepositoryException e) { logger.error("A repository exception occured while cleaning up the temporary HTML file nodes from the design path", e); } // remove resources in below the design node that were copied into client library folders try { for (String resource : resourcesToCleanup) { Node source = designNode.getNode(resource); Node parent = source.getParent(); source.remove(); // remove empty parent folders to cleanup while ( !parent.getNodes().hasNext() ) { Node p = parent; parent = parent.getParent(); p.remove(); } } designNode.getSession().save(); } catch (RepositoryException e) { logger.warn("Caught exception while cleaning up resources", e); } } /** * @param designNode * @param archiveStream * @param designImporterContext * @throws IOException * @throws RepositoryException */ private void extractArchive(Node designNode, InputStream archiveStream, DesignImporterContext designImporterContext) throws IOException, RepositoryException { try{ ZipInputStream zipInputStream = new ZipInputStream(archiveStream); ZipEntry entry = zipInputStream.getNextEntry(); // If there is not even a single zip entry, consider it corrupt if (entry == null) throw new ZipException(); while (entry != null) { if (shouldExtractEntry(entry)) { Node node = extractEntry(entry, designNode, zipInputStream, designImporterContext); if (isHtmlEntry(entry)) { extractedHtmlNodes.add(node); } else if (!entry.isDirectory()) { extractedResources.add(entry.getName()); } } entry = zipInputStream.getNextEntry(); } }catch(IllegalArgumentException ex){ throw new IOException("Archived file is not in a valid format"); } } private String getMimeType(String name) { String mimeType = mimeTypeService.getMimeType(name); return mimeType != null ? mimeType : ""; } private DesignImportResult importDesignPackageInternal(Resource importer, InputStream designPackage, CanvasBuildOptions buildOptions) throws DesignImportException, RepositoryException, IOException { initialize(); // get asset manager assetManager = importer.getResourceResolver().adaptTo(AssetManager.class); // get landing page PageManager pageManager = pageManagerFactory.getPageManager(importer.getResourceResolver()); Page page = pageManager.getContainingPage(importer); Node designNode = getOrCreateDesignPath(importer); DesignImporterContext importerContext = new DesignImporterContext(page, designNode, null); importerContext.setImporter(importer); extractArchive(designNode, designPackage, importerContext); if (extractedHtmlNodes.size() == 0) throw new MissingHTMLException(); // Sort the html nodes to make sure that index.html is picked before mobile.index.html. // Note: This is a temporary respite to a problem which we need to fix after the grey areas // around multiple page conversions get resolved. Comparator comparator = new Comparator() { public int compare(Node n1, Node n2) { String n1Name = ""; String n2Name = ""; try { n1Name = n1.getName(); n2Name = n2.getName(); } catch (RepositoryException e) { } return n2Name.compareTo(n1Name); } }; Set resourcesToRemove = new HashSet(); Collections.sort(extractedHtmlNodes, comparator); List warnings = new ArrayList(); boolean built = false; for (int i = extractedHtmlNodes.size() - 1; i >= 0; i--) { Node htmlNode = extractedHtmlNodes.get(i); String htmlName = htmlNode.getName(); InputStream htmlStream = htmlNode.getNode(JcrConstants.JCR_CONTENT).getProperty(JcrConstants.JCR_DATA).getBinary().getStream(); CanvasBuilder canvasBuilder = getCanvasBuilder(htmlNode, importer); if (canvasBuilder != null) { DesignImporterContext designImporterContext = new DesignImporterContext(page, designNode, htmlName, canvasBuilder, bundleContext, extractedResources); designImporterContext.setImporter(importer); canvasBuilder.build(htmlStream, designImporterContext, buildOptions); warnings.addAll(designImporterContext.importWarnings); resourcesToRemove.addAll(designImporterContext.getResourcesToRemove()); built = true; } else { extractedHtmlNodes.remove(i); } } if (!built) throw new MissingHTMLException(); cleanup(designNode, resourcesToRemove); designNode.getSession().save(); return new DesignImportResult(warnings); } /** * Returns an input stream for design package file attached to the request. * * @param request the request object * @throws IOException * @throws DesignImportException */ private InputStream getArchiveStreamFromRequest(SlingHttpServletRequest request) throws IOException, DesignImportException { RequestParameter designfile = request.getRequestParameter(ImporterConstants.PARAM_DESIGNFILE); InputStream archiveStream = null; if(designfile != null) { archiveStream = designfile.getInputStream(); } else { Resource importer = request.getResource(); if(ImporterUtil.isImporter(importer)) { Resource jcrContent = importer.getChild(ImporterConstants.NN_DESIGNPACKAGE + "/" + ImporterConstants.NN_DESIGNPACKAGEFILE + "/" + JcrConstants.JCR_CONTENT); if(jcrContent == null) throw new DesignImportException("Design Package not found"); archiveStream = (InputStream) jcrContent.adaptTo(ValueMap.class).get(JcrConstants.JCR_DATA); } } return archiveStream; } private void initialize() { extractedHtmlNodes = new ArrayList(); extractedResources = new ArrayList(); } private boolean isHtmlEntry(ZipEntry entry) { return entry.getName().matches("(?i)[^/\\\\]*\\.html?"); } protected CanvasBuilder getCanvasBuilder(Node htmlNode, Resource importer) throws RepositoryException { try { ServiceReference[] references = bundleContext.getServiceReferences(CanvasBuilder.class.getName(), null); Arrays.sort(references, new Comparator() { public int compare(ServiceReference o1, ServiceReference o2) { int o1Priority = OsgiUtil.toInteger(o1.getProperty(Constants.SERVICE_RANKING), 0); int o2Priority = OsgiUtil.toInteger(o2.getProperty(Constants.SERVICE_RANKING), 0); return o2Priority - o1Priority; } }); for (ServiceReference reference : references) { String filepattern = (String) reference.getProperty(CanvasBuilder.PN_FILEPATTERN); if (htmlNode.getName().matches(filepattern)) return (CanvasBuilder) bundleContext.getService(reference); } } catch (InvalidSyntaxException e) { logger.error("An error occurred while obtaining CanvasPageBuilder ServiceReference", e); } return null; } /** * The bundle activator method. For internal use. * @param context */ @Activate protected void activate(ComponentContext context) { extractFilters = OsgiUtil.toStringArray(context.getProperties().get(PN_EXTRACT_FILTER)); this.bundleContext = context.getBundleContext(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy