| /* |
| * 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(); |
| } |
| } |
| } |
| } |
| } |