blob: a050ef32568f040051da4ced892a826882f615c3 [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.felix.deploymentadmin.itest.util;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import org.osgi.framework.Version;
/**
* Builder for deployment packages. Can handle bundles, resource processors and artifacts.
*/
public class DeploymentPackageBuilder {
/**
* Convenience resource filter for manipulating JAR manifests.
*/
public abstract static class JarManifestFilter implements ResourceFilter {
public final InputStream createInputStream(URL url) throws IOException {
byte[] buffer = new byte[BUFFER_SIZE];
JarInputStream jis = new JarInputStream(url.openStream());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
JarOutputStream jos = new JarOutputStream(baos, filterManifest(jis.getManifest()));
JarEntry input;
while ((input = jis.getNextJarEntry()) != null) {
jos.putNextEntry(input);
int read;
while ((read = jis.read(buffer)) > 0) {
jos.write(buffer, 0, read);
}
jos.closeEntry();
}
jos.close();
jis.close();
return new ByteArrayInputStream(baos.toByteArray());
}
protected abstract Manifest filterManifest(Manifest manifest);
}
/**
* Simple manifest JAR manipulator implementation.
*/
public static class JarManifestManipulatingFilter extends JarManifestFilter {
private final String[] m_replacementEntries;
public JarManifestManipulatingFilter(String... replacementEntries) {
if (replacementEntries == null || ((replacementEntries.length) % 2 != 0)) {
throw new IllegalArgumentException("Entries must be a multiple of two!");
}
m_replacementEntries = Arrays.copyOf(replacementEntries, replacementEntries.length);
}
@Override
protected Manifest filterManifest(Manifest manifest) {
for (int i = 0; i < m_replacementEntries.length; i += 2) {
String key = m_replacementEntries[i];
String value = m_replacementEntries[i + 1];
manifest.getMainAttributes().putValue(key, value);
}
return manifest;
}
}
private static final int BUFFER_SIZE = 32 * 1024;
private final DPSigner m_signer;
private final String m_symbolicName;
private final String m_version;
private final List<ArtifactData> m_localizationFiles = new ArrayList<ArtifactData>();
private final List<ArtifactData> m_bundles = new ArrayList<ArtifactData>();
private final List<ArtifactData> m_processors = new ArrayList<ArtifactData>();
private final List<ArtifactData> m_artifacts = new ArrayList<ArtifactData>();
private String m_fixPackageVersion;
private boolean m_verification;
private PrivateKey m_signingKey;
private X509Certificate m_signingCert;
private DeploymentPackageBuilder(String symbolicName, String version) {
m_symbolicName = symbolicName;
m_version = version;
m_verification = true;
m_signer = new DPSigner();
}
/**
* Creates a new deployment package builder.
*
* @param name the name of the deployment package
* @param version the version of the deployment package
* @return a builder to further add data to the deployment package
*/
public static DeploymentPackageBuilder create(String name, String version) {
return new DeploymentPackageBuilder(name, version);
}
static void closeSilently(Closeable resource) {
if (resource != null) {
try {
resource.close();
}
catch (IOException e) {
// Ignore...
}
}
}
/**
* Adds an artifact to the deployment package.
*
* @param builder the artifact data builder to use.
* @return this builder.
* @throws Exception if something goes wrong while building the artifact.
*/
public DeploymentPackageBuilder add(ArtifactDataBuilder builder) throws Exception {
ArtifactData artifactData = builder.build();
if (artifactData.isCustomizer()) {
m_processors.add(artifactData);
}
else if (artifactData.isBundle()) {
m_bundles.add(artifactData);
}
else if (artifactData.isLocalizationFile()) {
m_localizationFiles.add(artifactData);
}
else {
m_artifacts.add(artifactData);
}
return this;
}
/**
* Creates a new deployment package builder with the same symbolic name as this builder.
*
* @param name the name of the deployment package
* @param version the version of the deployment package
* @return a builder to further add data to the deployment package
*/
public DeploymentPackageBuilder create(String version) {
return new DeploymentPackageBuilder(getSymbolicName(), version);
}
public BundleDataBuilder createBundleResource() {
return new BundleDataBuilder();
}
public LocalizationResourceDataBuilder createLocalizationResource() {
return new LocalizationResourceDataBuilder();
}
public ResourceDataBuilder createResource() {
return new ResourceDataBuilder();
}
public ResourceProcessorDataBuilder createResourceProcessorResource() {
return new ResourceProcessorDataBuilder();
}
/**
* Disables the verification of the generated deployment package, potentially causing an erroneous result to be
* generated.
*
* @return this builder.
*/
public DeploymentPackageBuilder disableVerification() {
m_verification = false;
return this;
}
/**
* Generates a deployment package and streams it to the output stream you provide. Before
* it starts generating, it will first validate that you have actually specified a
* resource processor for each type of artifact you provided.
*
* @return the input stream containing the deployment package.
* @throws Exception if something goes wrong while validating or generating
*/
public InputStream generate() throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
generate(baos);
return new ByteArrayInputStream(baos.toByteArray());
}
/**
* Generates a deployment package and streams it to the output stream you provide. Before
* it starts generating, it will first validate that you have actually specified a
* resource processor for each type of artifact you provided.
*
* @param output the output stream to write to
* @throws Exception if something goes wrong while validating or generating
*/
public void generate(OutputStream output) throws Exception {
Manifest m = createManifest();
List<ArtifactData> artifacts = getArtifactList();
writeStream(artifacts, m, output);
}
/**
* @return the symbolic name of the deployment package.
*/
public String getSymbolicName() {
return m_symbolicName;
}
/**
* @return the version of the deployment package.
*/
public String getVersion() {
return m_version;
}
/**
* Marks this deployment package as a 'fix' package.
*
* @return this builder.
*/
public DeploymentPackageBuilder setFixPackage() {
Version v1 = new Version(m_version);
Version v2 = new Version(v1.getMajor() + 1, 0, 0);
String version = String.format("[%d.%d, %d.%d)", v1.getMajor(), v1.getMinor(), v2.getMajor(), v2.getMinor());
return setFixPackage(version);
}
/**
* Marks this deployment package as a 'fix' package.
*
* @param versionRange the version range in which this fix-package should be applied.
* @return this builder.
*/
public DeploymentPackageBuilder setFixPackage(String versionRange) {
m_fixPackageVersion = versionRange;
return this;
}
/**
* Enables the creating of a signed deployment package, equivalent to creating a signed JAR file.
* <p>
* This method assumes the use of self-signed certificates for the signing process.
* </p>
*
* @param signingKey the private key of the signer;
* @param signingCert the public certificate of the signer.
* @return this builder.
*/
public DeploymentPackageBuilder signOutput(PrivateKey signingKey, X509Certificate signingCert) {
m_signingKey = signingKey;
m_signingCert = signingCert;
return this;
}
final Manifest createManifest() throws Exception {
List<ArtifactData> artifacts = new ArrayList<ArtifactData>();
artifacts.addAll(m_localizationFiles);
artifacts.addAll(m_bundles);
artifacts.addAll(m_processors);
artifacts.addAll(m_artifacts);
if (m_verification) {
validateProcessedArtifacts();
validateMissingArtifacts(artifacts);
}
return createManifest(artifacts);
}
final List<ArtifactData> getArtifactList() {
// The order in which the actual entries are added to the JAR is different than we're using for the manifest...
List<ArtifactData> artifacts = new ArrayList<ArtifactData>();
artifacts.addAll(m_bundles);
artifacts.addAll(m_processors);
artifacts.addAll(m_localizationFiles);
artifacts.addAll(m_artifacts);
return artifacts;
}
private Manifest createManifest(List<ArtifactData> files) throws Exception {
Manifest manifest = new Manifest();
Attributes main = manifest.getMainAttributes();
main.putValue("Manifest-Version", "1.0");
main.putValue("DeploymentPackage-SymbolicName", m_symbolicName);
main.putValue("DeploymentPackage-Version", m_version);
if ((m_fixPackageVersion != null) && !"".equals(m_fixPackageVersion)) {
main.putValue("DeploymentPackage-FixPack", m_fixPackageVersion);
}
Map<String, Attributes> entries = manifest.getEntries();
for (ArtifactData file : files) {
Attributes attrs = new Attributes();
attrs.putValue("Name", file.getFilename());
if (file.isBundle()) {
attrs.putValue("Bundle-SymbolicName", file.getSymbolicName());
attrs.putValue("Bundle-Version", file.getVersion());
if (file.isCustomizer()) {
attrs.putValue("DeploymentPackage-Customizer", "true");
attrs.putValue("Deployment-ProvidesResourceProcessor", file.getProcessorPid());
}
}
else if (file.isResourceProcessorNeeded()) {
attrs.putValue("Resource-Processor", file.getProcessorPid());
}
if (file.isMissing()) {
attrs.putValue("DeploymentPackage-Missing", "true");
}
if (isAddSignatures()) {
m_signer.addDigestAttribute(attrs, file);
}
entries.put(file.getFilename(), attrs);
}
return manifest;
}
private boolean isAddSignatures() {
return m_signingKey != null && m_signingCert != null;
}
private void validateMissingArtifacts(List<ArtifactData> files) throws Exception {
boolean missing = false;
Iterator<ArtifactData> artifactIter = files.iterator();
while (artifactIter.hasNext() && !missing) {
ArtifactData data = artifactIter.next();
if (data.isMissing()) {
missing = true;
}
}
if (missing && (m_fixPackageVersion == null || "".equals(m_fixPackageVersion))) {
throw new Exception("Artifact cannot be missing without a fix package version!");
}
}
private void validateProcessedArtifacts() throws Exception {
Iterator<ArtifactData> artifactIter = m_artifacts.iterator();
while (artifactIter.hasNext()) {
ArtifactData data = artifactIter.next();
String pid = data.getProcessorPid();
boolean found = pid == null;
Iterator<ArtifactData> processorIter = m_processors.iterator();
while (!found && processorIter.hasNext()) {
ArtifactData processor = processorIter.next();
if (pid.equals(processor.getProcessorPid())) {
found = true;
}
}
if (!found && data.isResourceProcessorNeeded()) {
throw new Exception("No resource processor found for artifact " + data.getURL() + " with processor PID " + pid);
}
}
}
private void writeStream(List<ArtifactData> files, Manifest manifest, OutputStream outputStream) throws Exception {
byte[] buffer = new byte[BUFFER_SIZE];
try (JarOutputStream output = new JarOutputStream(outputStream)) {
// Write out the manifest...
if (isAddSignatures()) {
m_signer.writeSignedManifest(manifest, output, m_signingKey, m_signingCert);
}
else {
output.putNextEntry(new ZipEntry(JarFile.MANIFEST_NAME));
manifest.write(output);
output.closeEntry();
}
for (ArtifactData file : files) {
if (file.isMissing()) {
// No need to write the 'missing' files...
continue;
}
output.putNextEntry(new JarEntry(file.getFilename()));
try (InputStream is = file.createInputStream()) {
int bytes;
while ((bytes = is.read(buffer)) != -1) {
output.write(buffer, 0, bytes);
}
}
finally {
output.closeEntry();
}
}
}
}
}