/*
* (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others.
*
* 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.
*
* Contributors:
* Nuxeo - initial API and implementation
* bstefanescu, jcarsique
* Anahide Tchertchian
*/
package org.nuxeo.runtime.deployment.preprocessor;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import org.apache.commons.io.FileUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.common.collections.DependencyTree;
import org.nuxeo.common.utils.JarUtils;
import org.nuxeo.common.utils.Path;
import org.nuxeo.common.utils.StringUtils;
import org.nuxeo.common.xmap.XMap;
import org.nuxeo.launcher.config.ConfigurationGenerator;
import org.nuxeo.runtime.deployment.preprocessor.install.CommandContext;
import org.nuxeo.runtime.deployment.preprocessor.install.CommandContextImpl;
import org.nuxeo.runtime.deployment.preprocessor.template.TemplateContribution;
import org.nuxeo.runtime.deployment.preprocessor.template.TemplateParser;
/**
* Initializer for the deployment skeleton, taking care of creating templates, aggregating default components before
* runtime is started.
*
* @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
*/
public class DeploymentPreprocessor {
public static final String FRAGMENT_FILE = "OSGI-INF/deployment-fragment.xml";
public static final String CONTAINER_FILE = "META-INF/nuxeo-preprocessor.xml";
public static final String CONTAINER_FILE_COMPAT = "OSGI-INF/deployment-container.xml";
private static final Pattern ARTIFACT_NAME_PATTERN = Pattern.compile("-[0-9]+");
private static final Log log = LogFactory.getLog(DeploymentPreprocessor.class);
private final File dir;
private final XMap xmap;
private ContainerDescriptor root;
public DeploymentPreprocessor(File dir) {
this.dir = dir;
xmap = new XMap();
xmap.register(ContainerDescriptor.class);
xmap.register(FragmentDescriptor.class);
}
public ContainerDescriptor getRootContainer() {
return root;
}
public void init() throws IOException {
root = getDefaultContainer(dir);
if (root != null) {
// run container commands
init(root);
}
}
public void init(File metadata, File[] files) throws IOException {
if (metadata == null) {
root = getDefaultContainer(dir);
} else {
root = getContainer(dir, metadata);
}
if (root != null) {
root.files = files;
// run container commands
init(root);
}
}
protected void init(ContainerDescriptor cd) throws IOException {
cd.context = new CommandContextImpl(cd.directory);
initContextProperties(cd.context);
// run container install instructions if any
if (cd.install != null) {
cd.install.setLogger(log);
log.info("Running custom installation for container: " + cd.name);
cd.install.exec(cd.context);
}
if (cd.files != null) {
init(cd, cd.files);
} else {
// scan directories
if (cd.directories == null || cd.directories.isEmpty()) {
init(cd, dir);
} else {
for (String dirPath : cd.directories) {
init(cd, new File(dir, dirPath));
}
}
}
}
protected void initContextProperties(CommandContext ctx) {
ConfigurationGenerator confGen = new ConfigurationGenerator();
confGen.init();
Properties props = confGen.getUserConfig();
for (String key : props.stringPropertyNames()) {
ctx.put(key, props.getProperty(key));
}
}
protected void processFile(ContainerDescriptor cd, File file) throws IOException {
String fileName = file.getName();
FragmentDescriptor fd = null;
boolean isBundle = false;
if (fileName.endsWith("-fragment.xml")) {
fd = getXMLFragment(file);
} else if (fileName.endsWith("-fragments.xml")) {
// we allow declaring multiple fragments in the same file
// this is useful to deploy libraries
collectXMLFragments(cd, file);
return;
} else if (fileName.endsWith(".jar") || fileName.endsWith(".war") || fileName.endsWith(".sar")
|| fileName.endsWith(".rar")) {
isBundle = true;
if (file.isDirectory()) {
fd = getDirectoryFragment(file);
} else {
fd = getJARFragment(file);
}
}
// register the fragment if any was found
if (fd != null) {
fd.fileName = fileName;
fd.filePath = getRelativeChildPath(cd.directory.getAbsolutePath(), file.getAbsolutePath());
cd.fragments.add(fd);
if (fd.templates != null) {
for (TemplateDescriptor td : fd.templates.values()) {
td.baseDir = file;
cd.templates.put(td.name, td);
}
}
} else if (isBundle) {
// create markers - for compatibility with versions < 5.4
String name = getSymbolicName(file);
if (name != null) {
cd.fragments.add(new FragmentDescriptor(name, true));
}
}
}
protected String getSymbolicName(File file) {
Manifest mf = JarUtils.getManifest(file);
if (mf != null) {
Attributes attrs = mf.getMainAttributes();
String id = attrs.getValue("Bundle-SymbolicName");
if (id != null) {
int p = id.indexOf(';');
if (p > -1) { // remove properties part if any
id = id.substring(0, p);
}
return id;
}
}
return null;
}
protected String getJarArtifactName(String name) {
if (name.endsWith(".jar")) {
name = name.substring(0, name.length() - 4);
}
Matcher m = ARTIFACT_NAME_PATTERN.matcher(name);
if (m.find()) {
name = name.substring(0, m.start());
}
return name;
}
protected void init(ContainerDescriptor cd, File[] files) throws IOException {
for (File file : files) {
processFile(cd, file);
}
}
protected void init(ContainerDescriptor cd, File dir) throws IOException {
log.info("Scanning directory: " + dir.getName());
if (!dir.exists()) {
log.warn("Directory doesn't exist: " + dir.getPath());
return;
}
// sort input files in alphabetic order -> this way we are sure we get
// the same deploying order on all machines.
File[] files = dir.listFiles();
Arrays.sort(files);
init(cd, files);
}
public void predeploy() throws IOException {
if (root != null) {
predeploy(root);
}
}
protected static String listFragmentDescriptor(FragmentDescriptor fd) {
return fd.name + " (" + fd.fileName + ")";
}
protected static void printInfo(FragmentRegistry fragments) {
List<DependencyTree.Entry<String, FragmentDescriptor>> entries = fragments.getResolvedEntries();
StringBuilder buf = new StringBuilder("Preprocessing order: ");
for (DependencyTree.Entry<String, FragmentDescriptor> entry : entries) {
FragmentDescriptor fd = entry.get();
if (fd != null && !fd.isMarker()) {
buf.append("\n\t");
buf.append(listFragmentDescriptor(entry.get()));
}
}
log.info(buf);
StringBuilder errors = new StringBuilder();
List<DependencyTree.Entry<String, FragmentDescriptor>> missing = fragments.getMissingRequirements();
for (DependencyTree.Entry<String, FragmentDescriptor> entry : missing) {
buf = new StringBuilder("Unknown bundle: ");
buf.append(entry.getKey());
buf.append(" required by: ");
boolean first = true;
for (DependencyTree.Entry<String, FragmentDescriptor> dep : entry.getDependsOnMe()) {
if (!first) {
buf.append(", "); // length 2
}
first = false;
buf.append(listFragmentDescriptor(dep.get()));
}
log.error(buf);
errors.append(buf);
errors.append("\n");
}
for (DependencyTree.Entry<String, FragmentDescriptor> entry : fragments.getPendingEntries()) {
if (!entry.isRegistered()) {
continue;
}
buf = new StringBuilder("Bundle not preprocessed: ");
buf.append(listFragmentDescriptor(entry.get()));
buf.append(" waiting for: ");
boolean first = true;
for (DependencyTree.Entry<String, FragmentDescriptor> dep : entry.getWaitsFor()) {
if (!first) {
buf.append(", "); // length 2
}
first = false;
buf.append(dep.getKey());
}
log.error(buf);
errors.append(buf);
errors.append("\n");
}
if (errors.length() != 0) {
// set system property to log startup errors
// this is read by AbstractRuntimeService
System.setProperty("org.nuxeo.runtime.deployment.errors", errors.toString());
}
}
protected static void predeploy(ContainerDescriptor cd) throws IOException {
// run installer and register contributions for each fragment
List<DependencyTree.Entry<String, FragmentDescriptor>> entries = cd.fragments.getResolvedEntries();
printInfo(cd.fragments);
for (DependencyTree.Entry<String, FragmentDescriptor> entry : entries) {
FragmentDescriptor fd = entry.get();
if (fd == null || fd.isMarker()) {
continue; // should be a marker entry like the "all" one.
}
cd.context.put("bundle.fileName", fd.filePath);
cd.context.put("bundle.shortName", fd.fileName);
cd.context.put("bundle", fd.name);
// execute install instructions if any
if (fd.install != null) {
fd.install.setLogger(log);
log.info("Running custom installation for fragment: " + fd.name);
fd.install.exec(cd.context);
}
if (fd.contributions == null) {
continue; // no contributions
}
// get fragment contributions and register them
for (TemplateContribution tc : fd.contributions) {
// register template contributions if any
// get the target template
TemplateDescriptor td = cd.templates.get(tc.getTemplate());
if (td != null) {
if (td.baseDir == null) {
td.baseDir = cd.directory;
}
if (td.template == null) { // template not yet compiled
File file = new File(td.baseDir, td.src);
// compile it
td.template = TemplateParser.parse(file);
}
} else {
log.warn("No template '" + tc.getTemplate() + "' found for deployment fragment: " + fd.name);
continue;
}
// get the marker where contribution should be inserted
td.template.update(tc, cd.context);
}
}
// process and write templates
// fragments where imported. write down templates
for (TemplateDescriptor td : cd.templates.values()) {
if (td.baseDir == null) {
td.baseDir = cd.directory;
}
// if required process the template even if no contributions were
// made
if (td.template == null && td.isRequired) {
// compile the template
File file = new File(td.baseDir, td.src);
td.template = TemplateParser.parse(file);
}
// process the template
if (td.template != null) {
File file = new File(td.baseDir, td.installPath);
file.getParentFile().mkdirs(); // make sure parents exists
FileUtils.writeStringToFile(file, td.template.getText());
}
}
// process sub containers if any
for (ContainerDescriptor subCd : cd.subContainers) {
predeploy(subCd);
}
}
protected FragmentDescriptor getXMLFragment(File file) throws IOException {
URL url;
try {
url = file.toURI().toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
FragmentDescriptor fd = (FragmentDescriptor) xmap.load(url);
if (fd != null && fd.name == null) {
fd.name = file.getName();
}
return fd;
}
protected void collectXMLFragments(ContainerDescriptor cd, File file) throws IOException {
String fileName = file.getName();
URL url;
try {
url = file.toURI().toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
Object[] result = xmap.loadAll(url);
for (Object entry : result) {
FragmentDescriptor fd = (FragmentDescriptor) entry;
assert fd != null;
if (fd.name == null) {
log.error("Invalid fragments file: " + file.getName()
+ ". Fragments declared in a -fragments.xml file must have names.");
} else {
cd.fragments.add(fd);
fd.fileName = fileName;
fd.filePath = getRelativeChildPath(cd.directory.getAbsolutePath(), file.getAbsolutePath());
}
}
}
protected void processBundleForCompat(FragmentDescriptor fd, File file) {
// TODO disable for now the warning
log.warn("Entering compatibility mode - Please update the deployment-fragment.xml in " + file.getName()
+ " to use new dependency management");
Manifest mf = JarUtils.getManifest(file);
if (mf != null) {
fd.name = file.getName();
processManifest(fd, fd.name, mf);
} else {
throw new RuntimeException("Compat: Fragments without a name must reside in an OSGi bundle");
}
}
protected FragmentDescriptor getDirectoryFragment(File directory) throws IOException {
FragmentDescriptor fd;
File file = new File(directory.getAbsolutePath() + '/' + FRAGMENT_FILE);
if (file.isFile()) {
URL url;
try {
url = file.toURI().toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
fd = (FragmentDescriptor) xmap.load(url);
} else {
return null; // don't need preprocessing
}
if (fd.name == null) {
// fallback on symbolic name
fd.name = getSymbolicName(directory);
}
if (fd.name == null) {
// fallback on artifact id
fd.name = getJarArtifactName(directory.getName());
}
if (fd.version == 0) { // compat with versions < 5.4
processBundleForCompat(fd, directory);
}
return fd;
}
protected FragmentDescriptor getJARFragment(File file) throws IOException {
FragmentDescriptor fd = null;
try (JarFile jar = new JarFile(file)) {
ZipEntry ze = jar.getEntry(FRAGMENT_FILE);
if (ze != null) {
try (InputStream in = new BufferedInputStream(jar.getInputStream(ze))) {
fd = (FragmentDescriptor) xmap.load(in);
}
if (fd.name == null) {
// fallback on symbolic name
fd.name = getSymbolicName(file);
}
if (fd.name == null) {
// fallback on artifact id
fd.name = getJarArtifactName(file.getName());
}
if (fd.version == 0) { // compat with versions < 5.4
processBundleForCompat(fd, file);
}
}
}
return fd;
}
protected void processManifest(FragmentDescriptor fd, String fileName, Manifest mf) {
Attributes attrs = mf.getMainAttributes();
String id = attrs.getValue("Bundle-SymbolicName");
int p = id.indexOf(';');
if (p > -1) { // remove properties part if any
id = id.substring(0, p);
}
fd.name = id;
if (fd.requires != null && !fd.requires.isEmpty()) {
throw new RuntimeException(
"In compatibility mode you must not use <require> tags for OSGi bundles - use Require-Bundle manifest header instead. Bundle: "
+ fileName);
}
// needed to control start-up order (which differs from
// Require-Bundle)
String requires = attrs.getValue("Nuxeo-Require");
if (requires == null) { // if not specific requirement is met use
// Require-Bundle
requires = attrs.getValue("Require-Bundle");
}
if (requires != null) {
String[] ids = StringUtils.split(requires, ',', true);
fd.requires = new ArrayList<>(ids.length);
for (int i = 0; i < ids.length; i++) {
String rid = ids[i];
p = rid.indexOf(';');
if (p > -1) { // remove properties part if any
ids[i] = rid.substring(0, p);
}
fd.requires.add(ids[i]);
}
}
String requiredBy = attrs.getValue("Nuxeo-RequiredBy");
if (requiredBy != null) {
String[] ids = StringUtils.split(requiredBy, ',', true);
for (int i = 0; i < ids.length; i++) {
String rid = ids[i];
p = rid.indexOf(';');
if (p > -1) { // remove properties part if any
ids[i] = rid.substring(0, p);
}
}
fd.requiredBy = ids;
}
}
/**
* Reads a container fragment metadata file and returns the container descriptor.
*/
protected ContainerDescriptor getContainer(File home, File file) throws IOException {
URL url;
try {
url = file.toURI().toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
ContainerDescriptor cd = (ContainerDescriptor) xmap.load(url);
if (cd != null) {
cd.directory = home;
if (cd.name == null) {
cd.name = home.getName();
}
}
return cd;
}
protected ContainerDescriptor getDefaultContainer(File directory) throws IOException {
File file = new File(directory.getAbsolutePath() + '/' + CONTAINER_FILE);
if (!file.isFile()) {
file = new File(directory.getAbsolutePath() + '/' + CONTAINER_FILE_COMPAT);
}
ContainerDescriptor cd = null;
if (file.isFile()) {
cd = getContainer(directory, file);
}
return cd;
}
public static String getRelativeChildPath(String parent, String child) {
// TODO optimize this method
// fix win32 case
if (parent.indexOf('\\') > -1) {
parent = parent.replace('\\', '/');
}
if (child.indexOf('\\') > -1) {
child = child.replace('\\', '/');
} // end fix win32
Path parentPath = new Path(parent);
Path childPath = new Path(child);
if (parentPath.isPrefixOf(childPath)) {
return childPath.removeFirstSegments(parentPath.segmentCount()).makeRelative().toString();
}
return null;
}
/**
* Run preprocessing in the given home directory and using the given list of bundles. Bundles must be ordered by the
* caller to have same deployment order on all computers.
* <p>
* The metadata file is the metadat file to be used to configure the processor. If null the default location will be
* used (relative to home): {@link #CONTAINER_FILE}.
*/
public static void process(File home, File metadata, File[] files) throws IOException {
DeploymentPreprocessor processor = new DeploymentPreprocessor(home);
// initialize
processor.init(metadata, files);
// run preprocessor
processor.predeploy();
}
public static void main(String[] args) throws IOException {
File root;
if (args.length > 0) {
root = new File(args[0]);
} else {
root = new File(".");
}
System.out.println("Preprocessing: " + root);
DeploymentPreprocessor processor = new DeploymentPreprocessor(root);
// initialize
processor.init();
// and predeploy
processor.predeploy();
System.out.println("Done.");
}
}