blob: 8fabc9c7fa567f2dd1f2a1d095749c0587b435e0 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
package org.apache.jackrabbit.vault.fs.io;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.nodetype.NodeDefinition;
import javax.jcr.nodetype.NodeType;
import javax.jcr.nodetype.NodeTypeManager;
import org.apache.jackrabbit.util.ISO8601;
import org.apache.jackrabbit.vault.fs.api.AggregateManager;
import org.apache.jackrabbit.vault.fs.api.PathFilterSet;
import org.apache.jackrabbit.vault.fs.api.ProgressTrackerListener;
import org.apache.jackrabbit.vault.fs.api.SimplePathMapping;
import org.apache.jackrabbit.vault.fs.api.VaultFile;
import org.apache.jackrabbit.vault.fs.api.WorkspaceFilter;
import org.apache.jackrabbit.vault.fs.config.MetaInf;
import org.apache.jackrabbit.vault.fs.spi.CNDWriter;
import org.apache.jackrabbit.vault.fs.spi.ProgressTracker;
import org.apache.jackrabbit.vault.fs.spi.ServiceProviderFactory;
import org.apache.jackrabbit.vault.packaging.PackageId;
import org.apache.jackrabbit.vault.packaging.PackageType;
import org.apache.jackrabbit.vault.util.Constants;
import org.apache.jackrabbit.vault.util.Text;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.jackrabbit.vault.packaging.PackageProperties.NAME_DEPENDENCIES;
import static org.apache.jackrabbit.vault.packaging.PackageProperties.NAME_DESCRIPTION;
import static org.apache.jackrabbit.vault.packaging.PackageProperties.NAME_GROUP;
import static org.apache.jackrabbit.vault.packaging.PackageProperties.NAME_NAME;
import static org.apache.jackrabbit.vault.packaging.PackageProperties.NAME_PACKAGE_TYPE;
import static org.apache.jackrabbit.vault.packaging.PackageProperties.NAME_VERSION;
/**
* Generic context for exporters
*
*/
public abstract class AbstractExporter implements AutoCloseable {
/**
* default logger
*/
private static final Logger log = LoggerFactory.getLogger(AbstractExporter.class);
/**
* name of the manifest property for the package id
*/
private static final String MF_PACKAGE_ID = "Content-Package-Id";
/**
* name of the manifest property for the package dependencies
*/
private static final String MF_PACKAGE_DEPENDENCIES = "Content-Package-Dependencies";
/**
* name of the manifest property for the package roots
*/
private static final String MF_PACKAGE_ROOTS = "Content-Package-Roots";
/**
* name of the manifest property for the package description
*/
private static final String MF_PACKAGE_DESC = "Content-Package-Description";
/**
* name of the manifest property for the package type
*/
private static final String MF_PACKAGE_TYPE = "Content-Package-Type";
private ProgressTracker tracker;
private boolean relativePaths;
private String rootPath = Constants.ROOT_DIR;
private Properties properties = new Properties();
private boolean noMetaInf;
protected ExportInfo exportInfo = new ExportInfo();
public boolean isVerbose() {
return tracker != null;
}
public void setVerbose(ProgressTrackerListener out) {
if (out == null) {
tracker = null;
} else {
if (tracker == null) {
tracker = new ProgressTracker();
}
tracker.setListener(out);
}
}
public boolean isRelativePaths() {
return relativePaths;
}
public void setProperty(String name, String value) {
properties.put(name, value);
}
public void setProperty(String name, Calendar value) {
properties.put(name, ISO8601.format(value));
}
public void setProperties(Properties properties) {
if (properties != null) {
this.properties.putAll(properties);
}
}
public String getRootPath() {
return rootPath;
}
public void setRootPath(String rootPath) {
this.rootPath = rootPath;
}
public boolean isNoMetaInf() {
return noMetaInf;
}
public void setNoMetaInf(boolean noMetaInf) {
this.noMetaInf = noMetaInf;
}
public ExportInfo getExportInfo() {
return exportInfo;
}
/**
* Defines if the exported files should include their entire path or just
* be relative to the export root. eg.: exporting /apps/components relative
* would not include /apps in the path.
*
* @param relativePaths relative flag
*/
public void setRelativePaths(boolean relativePaths) {
this.relativePaths = relativePaths;
}
/**
* Exports the given vault file and writes the META-INF data.
* @param parent the vault file
* @throws RepositoryException if an error occurs
* @throws IOException if an I/O error occurs
*/
public void export(VaultFile parent) throws RepositoryException, IOException {
export(parent, false);
}
/**
* Exports the given vault file and writes the META-INF data.
* @param parent the vault file
* @param noClose if {@code true} exporter will not be closed after export
* @throws RepositoryException if an error occurs
* @throws IOException if an I/O error occurs
*/
public void export(VaultFile parent, boolean noClose)
throws RepositoryException, IOException {
exportInfo.getEntries().clear();
open();
AggregateManager mgr = parent.getFileSystem().getAggregateManager();
mgr.startTracking(tracker == null ? null : tracker.getListener());
if (!noMetaInf) {
// update properties
setProperty(MetaInf.CREATED, Calendar.getInstance());
setProperty(MetaInf.CREATED_BY, mgr.getUserId());
setProperty(MetaInf.PACKAGE_FORMAT_VERSION, String.valueOf(MetaInf.FORMAT_VERSION_2));
// get filter and translate if necessary
WorkspaceFilter filter = mgr.getWorkspaceFilter();
String mountPath = mgr.getRoot().getPath();
String rootPath = parent.getPath();
if ("/".equals(rootPath)) {
rootPath = "";
}
if (mountPath.length() > 0 || rootPath.length() > 0) {
filter = filter.translate(new SimplePathMapping(mountPath, rootPath));
}
// check for package type
if (!properties.containsKey(NAME_PACKAGE_TYPE)) {
properties.setProperty(NAME_PACKAGE_TYPE, detectPackageType(filter).name().toLowerCase());
}
// write Manifest
Manifest mf = new Manifest();
mf.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
String version = properties.getProperty(NAME_VERSION);
if (version == null) {
version = "";
}
String group = properties.getProperty(NAME_GROUP);
String name = properties.getProperty(NAME_NAME);
PackageId id = new PackageId(group, name, version);
Set<String> rts = new HashSet<String>();
for (PathFilterSet p: filter.getFilterSets()) {
rts.add(p.getRoot());
}
String[] filterRoots = rts.toArray(new String[rts.size()]);
addManifestAttribute(mf, MF_PACKAGE_ID, id.toString());
addManifestAttribute(mf, MF_PACKAGE_DESC, properties.getProperty(NAME_DESCRIPTION));
addManifestAttribute(mf, MF_PACKAGE_ROOTS, Text.implode(filterRoots, ","));
addManifestAttribute(mf, MF_PACKAGE_DEPENDENCIES, properties.getProperty(NAME_DEPENDENCIES));
addManifestAttribute(mf, MF_PACKAGE_TYPE, properties.getProperty(NAME_PACKAGE_TYPE));
ByteArrayOutputStream tmpOut = new ByteArrayOutputStream();
mf.write(tmpOut);
writeFile(new ByteArrayInputStream(tmpOut.toByteArray()), JarFile.MANIFEST_NAME);
createDirectory(Constants.META_INF);
createDirectory(Constants.META_DIR);
// add some 'fake' tracking
track("A", Constants.META_INF);
track("A", JarFile.MANIFEST_NAME);
track("A", Constants.META_DIR);
track("A", Constants.META_DIR + "/" + Constants.CONFIG_XML);
track("A", Constants.META_DIR + "/" + Constants.FILTER_XML);
track("A", Constants.META_DIR + "/" + Constants.NODETYPES_CND);
track("A", Constants.META_DIR + "/" + Constants.PROPERTIES_XML);
// write properties
tmpOut = new ByteArrayOutputStream();
properties.storeToXML(tmpOut, "FileVault Package Properties", "utf-8");
writeFile(new ByteArrayInputStream(tmpOut.toByteArray()), Constants.META_DIR + "/" + Constants.PROPERTIES_XML);
writeFile(mgr.getConfig().getSource(), Constants.META_DIR + "/" + Constants.CONFIG_XML);
writeFile(filter.getSource(), Constants.META_DIR + "/" + Constants.FILTER_XML);
}
export(parent, "");
if (!noMetaInf) {
// write node types last, as they are calculated during export.
writeFile(getNodeTypes(mgr.getSession(), mgr.getNodeTypes()), Constants.META_DIR + "/" + Constants.NODETYPES_CND);
}
if (!noClose) {
close();
}
mgr.stopTracking();
}
/**
* Exports the vault file to the relative path.
* @param parent the file
* @param relPath the path
* @throws RepositoryException if an error occurs
* @throws IOException if an I/O error occurs
*/
public void export(VaultFile parent, String relPath)
throws RepositoryException, IOException {
for (VaultFile vaultFile : parent.getChildren()) {
String path = relPath + "/" + vaultFile.getName();
if (vaultFile.isDirectory()) {
createDirectory(vaultFile, path);
export(vaultFile, path);
} else {
writeFile(vaultFile, path);
}
}
}
protected void track(String action, String path) {
if ("E".equals(action)) {
log.error("{} {}", action, path);
} else {
log.debug("{} {}", action, path);
}
if (tracker != null) {
tracker.track(action, path);
}
}
protected void track(Exception e, String path) {
log.error("E {} ({})", path, e.toString());
if (tracker != null) {
tracker.track(e, path);
}
}
protected String getPlatformFilePath(VaultFile file, String relPath) {
StringBuilder buf = new StringBuilder(rootPath);
if (isRelativePaths()) {
// relative paths are only needed for special exports, like the definition export of packaging
if (buf.length() > 0) {
buf.append("/");
}
buf.append(relPath);
} else {
buf.append(file.getPath());
}
return buf.toString();
}
private InputStream getNodeTypes(Session s, Collection<String> nodeTypes)
throws IOException, RepositoryException {
NodeTypeManager ntMgr = s.getWorkspace().getNodeTypeManager();
// init with repository predefined node types
Set<String> written = new HashSet<String>();
written.addAll(ServiceProviderFactory.getProvider().getBuiltInNodeTypeNames());
StringWriter out = new StringWriter();
CNDWriter w = ServiceProviderFactory.getProvider().getCNDWriter(out, s, true);
for (String nt: nodeTypes) {
writeNodeType(ntMgr.getNodeType(nt), w, written);
}
w.close();
return new ByteArrayInputStream(out.getBuffer().toString().getBytes("utf-8"));
}
private void writeNodeType(NodeType nt, CNDWriter w, Set<String> written)
throws IOException, RepositoryException {
if (nt != null && !written.contains(nt.getName())) {
written.add(nt.getName());
w.write(nt);
for (NodeType s: nt.getSupertypes()) {
writeNodeType(s, w, written);
}
for (NodeDefinition n: nt.getChildNodeDefinitions()) {
writeNodeType(n.getDefaultPrimaryType(), w, written);
if (n.getRequiredPrimaryTypes() != null) {
for (NodeType r: n.getRequiredPrimaryTypes()) {
writeNodeType(r, w, written);
}
}
}
}
}
/**
* Adds a new attribute to the given manifest.
* @param manifest the manifest
* @param key attribute name
* @param value attribute value
*/
private static void addManifestAttribute(Manifest manifest, String key, String value) {
if (value != null && value.length() > 0) {
Attributes.Name name = new Attributes.Name(key);
manifest.getMainAttributes().put(name, value);
}
}
/**
* Detects the package type based on the workspace filter.
* @param filter the workspace filter
* @return the package type
*/
private static PackageType detectPackageType(WorkspaceFilter filter) {
boolean hasApps = false;
boolean hasOther = false;
for (PathFilterSet p: filter.getFilterSets()) {
if ("cleanup".equals(p.getType())) {
continue;
}
String root = p.getRoot();
if ("/apps".equals(root) || root.startsWith("/apps/") || "/libs".equals(root) || root.startsWith("/libs/")) {
hasApps = true;
} else {
hasOther = true;
}
}
if (hasApps && !hasOther) {
return PackageType.APPLICATION;
} else if (hasOther && !hasApps) {
return PackageType.CONTENT;
}
return PackageType.MIXED;
}
/**
* Opens the exporter and initializes the undelying structures.
* @throws IOException if an I/O error occurs
* @throws RepositoryException if a repository error occurs
*/
public abstract void open() throws IOException, RepositoryException;
/**
* Closes the exporter and releases the undelying structures.
* @throws IOException if an I/O error occurs
* @throws RepositoryException if a repository error occurs
*/
public abstract void close() throws IOException, RepositoryException;
public abstract void createDirectory(String relPath)
throws IOException;
public abstract void createDirectory(VaultFile file, String relPath)
throws RepositoryException, IOException;
/**
* <p>The specified stream remains open after this method returns.
* @param in
* @param relPath
* @throws IOException
*/
public abstract void writeFile(InputStream in, String relPath)
throws IOException;
public abstract void writeFile(VaultFile file, String relPath)
throws RepositoryException, IOException;
}