![JAR search and dependency download from the Maven repository](/logo.png)
com.day.cq.wcm.designimporter.DesignPackageImporter Maven / Gradle / Ivy
Show all versions of aem-sdk-api Show documentation
/*************************************************************************
*
* 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:
*
* - The design package is unloaded at a unique location under /etc/designs. This design path is translated from the CQ page initiating the request
* - 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
* - The {@link CanvasBuilder} builds the CQ page by parsing the HTML file as following:
*
* - It parses the HTML document and culls out CQ components, scripts, styles and other relevant meta information
* - It builds the page component nodes for the CQ components extracted
* - It aggregates the extracted scripts and styles into CQ clientlibs
* - It generates a top level canvas component that contains the reference to all the extracted CQ components as well as the clientlibs
*
*
*
* 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();
}
}