io.wcm.tooling.commons.contentpackagebuilder.ContentPackage.java Source code

Java tutorial

Introduction

Here is the source code for io.wcm.tooling.commons.contentpackagebuilder.ContentPackage.java

Source

/*
 * #%L
 * wcm.io
 * %%
 * Copyright (C) 2015 wcm.io
 * %%
 * 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 io.wcm.tooling.commons.contentpackagebuilder;

import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.w3c.dom.Document;

import com.google.common.base.Charsets;

/**
 * Represents an AEM content package.
 * Content like structured JCR data and binary files can be added.
 * This class is not thread-safe.
 */
public final class ContentPackage implements Closeable {

    private final PackageMetadata metadata;
    private final ZipOutputStream zip;
    private final TransformerFactory transformerFactory;
    private final Transformer transformer;
    private final XmlContentBuilder xmlContentBuilder;

    private static final String CONTENT_TYPE_CHARSET_EXTENSION = ";charset=";

    private static final Pattern NAMESPACE_PATH_PART = Pattern.compile("^([^/\\:]+)\\:([^/]+)$");

    /**
     * @param os Output stream
     * @throws IOException
     */
    ContentPackage(PackageMetadata metadata, OutputStream os) throws IOException {
        this.metadata = metadata;
        this.zip = new ZipOutputStream(os);

        this.transformerFactory = TransformerFactory.newInstance();
        this.transformerFactory.setAttribute("indent-number", 2);
        try {
            this.transformer = transformerFactory.newTransformer();
            this.transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            this.transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
        } catch (TransformerException ex) {
            throw new RuntimeException("Failed to set up XML transformer: " + ex.getMessage(), ex);
        }

        this.xmlContentBuilder = new XmlContentBuilder(metadata.getXmlNamespaces());

        buildPackageMetadata();
    }

    /**
     * Adds a page with given content. The "cq:Page/cq:PageContent envelope" is added automatically.
     * @param path Full content path of page.
     * @param content Map with page properties. If the map contains nested maps this builds a tree of JCR nodes.
     *          The key of the nested map in its parent map is the node name,
     *          the nested map contain the properties of the child node.
     * @throws IOException
     */
    public void addPage(String path, Map<String, Object> content) throws IOException {
        String fullPath = buildJcrPathForZip(path) + "/.content.xml";
        Document doc = xmlContentBuilder.buildPage(content);
        writeXmlDocument(fullPath, doc);
    }

    /**
     * Add some JCR content structure directly to the package.
     * @param path Full content path of content root node.
     * @param content Map with node properties. If the map contains nested maps this builds a tree of JCR nodes.
     *          The key of the nested map in its parent map is the node name,
     *          the nested map contain the properties of the child node.
     * @throws IOException
     */
    public void addContent(String path, Map<String, Object> content) throws IOException {
        String fullPath = buildJcrPathForZip(path) + "/.content.xml";
        Document doc = xmlContentBuilder.buildContent(content);
        writeXmlDocument(fullPath, doc);
    }

    /**
     * Adds a binary file.
     * @param path Full content path and file name of file
     * @param inputStream Input stream with binary dta
     * @throws IOException
     */
    public void addFile(String path, InputStream inputStream) throws IOException {
        addFile(path, inputStream, null);
    }

    /**
     * Adds a binary file with explicit mime type.
     * @param path Full content path and file name of file
     * @param inputStream Input stream with binary data
     * @param contentType Mime type, optionally with ";charset=XYZ" extension
     * @throws IOException
     */
    public void addFile(String path, InputStream inputStream, String contentType) throws IOException {
        String fullPath = buildJcrPathForZip(path);
        writeBinaryFile(fullPath, inputStream);

        if (StringUtils.isNotEmpty(contentType)) {
            String mimeType = StringUtils.substringBefore(contentType, CONTENT_TYPE_CHARSET_EXTENSION);
            String encoding = StringUtils.substringAfter(contentType, CONTENT_TYPE_CHARSET_EXTENSION);

            String fullPathMetadata = fullPath + ".dir/.content.xml";
            Document doc = xmlContentBuilder.buildNtFile(mimeType, encoding);
            writeXmlDocument(fullPathMetadata, doc);
        }
    }

    /**
     * If path parts contain namespace definitions they need to be escaped for the ZIP file.
     * Example: oak:index -> _oak_index
     * @param path Path
     * @return Safe path
     */
    private String buildJcrPathForZip(String path) {
        String[] pathParts = StringUtils.split(path, "/");
        for (int i = 0; i < pathParts.length; i++) {
            Matcher matcher = NAMESPACE_PATH_PART.matcher(pathParts[i]);
            if (matcher.matches()) {
                pathParts[i] = "_" + matcher.group(1) + "_" + matcher.group(2);
            }
        }
        if (pathParts.length == 0) {
            return "jcr_root";
        } else {
            return "jcr_root/" + StringUtils.join(pathParts, "/");
        }
    }

    /**
     * Adds a binary file.
     * @param path Full content path and file name of file
     * @param file File with binary data
     * @throws IOException
     */
    public void addFile(String path, File file) throws IOException {
        addFile(path, file, null);
    }

    /**
     * Adds a binary file with explicit mime type.
     * @param path Full content path and file name of file
     * @param file File with binary data
     * @param contentType Mime type, optionally with ";charset=XYZ" extension
     * @throws IOException
     */
    public void addFile(String path, File file, String contentType) throws IOException {
        try (InputStream is = new FileInputStream(file)) {
            addFile(path, is, contentType);
        }
    }

    /**
     * Close the underlying ZIP stream of the package.
     * @throws IOException
     */
    @Override
    public void close() throws IOException {
        zip.flush();
        zip.close();
    }

    /**
     * Get root path of the package. This does only work if there is only one filter of the package.
     * If they are more filters use {@link #getFilters()} instead.
     * @return Root path of package
     */
    public String getRootPath() {
        if (metadata.getFilters().size() == 1) {
            return metadata.getFilters().get(0).getRootPath();
        } else {
            throw new IllegalStateException(
                    "Content package has more than one package filter - please use getFilters().");
        }
    }

    /**
     * Get filters defined for this package.
     * @return List of package filters, optionally with include/exclude rules.
     */
    public List<PackageFilter> getFilters() {
        return metadata.getFilters();
    }

    /**
     * Build all package metadata files based on templates.
     * @throws IOException
     */
    private void buildPackageMetadata() throws IOException {
        metadata.validate();
        buildTemplatedMetadataFile("META-INF/vault/config.xml");
        buildPropertiesFile("META-INF/vault/properties.xml");
        buildTemplatedMetadataFile("META-INF/vault/settings.xml");
        writeXmlDocument("META-INF/vault/filter.xml", xmlContentBuilder.buildFilter(metadata.getFilters()));
    }

    /**
     * Read template file from classpath, replace variables and store it in the zip stream.
     * @param path Path
     * @throws IOException
     */
    private void buildTemplatedMetadataFile(String path) throws IOException {
        try (InputStream is = getClass().getResourceAsStream("/content-package-template/" + path)) {
            String xmlContent = IOUtils.toString(is);
            for (Map.Entry<String, Object> entry : metadata.getVars().entrySet()) {
                xmlContent = StringUtils.replace(xmlContent, "{{" + entry.getKey() + "}}",
                        StringEscapeUtils.escapeXml(entry.getValue().toString()));
            }
            zip.putNextEntry(new ZipEntry(path));
            try {
                zip.write(xmlContent.getBytes(Charsets.UTF_8));
            } finally {
                zip.closeEntry();
            }
        }
    }

    /**
     * Build java Properties XML file.
     * @param path Path
     * @throws IOException
     */
    private void buildPropertiesFile(String path) throws IOException {
        Properties properties = new Properties();
        properties.put("packageFormatVersion", "2");
        properties.put("requiresRoot", "false");

        for (Map.Entry<String, Object> entry : metadata.getVars().entrySet()) {
            String value = ObjectUtils.toString(entry.getValue());
            if (StringUtils.isNotEmpty(value)) {
                properties.put(entry.getKey(), value);
            }
        }

        zip.putNextEntry(new ZipEntry(path));
        try {
            properties.storeToXML(zip, null);
        } finally {
            zip.closeEntry();
        }
    }

    /**
     * Writes an XML document as binary file entry to the ZIP output stream.
     * @param path Content path
     * @param doc XML conent
     * @throws IOException
     */
    private void writeXmlDocument(String path, Document doc) throws IOException {
        zip.putNextEntry(new ZipEntry(path));
        try {
            DOMSource source = new DOMSource(doc);
            StreamResult result = new StreamResult(zip);
            transformer.transform(source, result);
        } catch (TransformerException ex) {
            throw new IOException("Failed to generate XML: " + ex.getMessage(), ex);
        } finally {
            zip.closeEntry();
        }
    }

    /**
     * Writes an binary file entry to the ZIP output stream.
     * @param path Content path
     * @param is Input stream with binary data
     * @throws IOException
     */
    private void writeBinaryFile(String path, InputStream is) throws IOException {
        zip.putNextEntry(new ZipEntry(path));
        try {
            IOUtils.copy(is, zip);
        } finally {
            zip.closeEntry();
        }
    }

}