/**
* 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.aries.unittest.fixture;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.osgi.framework.Constants;
/**
* Utility class for creating archive-based fixtures such as EBA archives, jar files etc.
* This class provides a flow based api for defining such fixtures. For example, a simple EBA archive could
* be defined as such:
*
* <code>
* ArchiveFixtures.ZipFixture zip = ArchiveFixtures.newZip()
* .jar("test.jar")
* .manifest()
* .symbolicName("com.ibm.test")
* .version("2.0.0")
* .end()
* .file("random.txt", "Some text")
* .end();
* </code>
*
* This defines a zip archive containing a single jar file (hence no application manifest). The jar file itself has
* a manifest and a text file.
*
* To actually create the physical archive use the <code>writeOut</code> method on the archive fixture.
*/
public class ArchiveFixture
{
/**
* Create a new zip file fixture
* @return
*/
public static ZipFixture newZip() {
return new ZipFixture(null);
}
/**
* Create a new jar file fixture
* @return
*/
public static JarFixture newJar() {
return new JarFixture(null);
}
/**
* Utility to copy an InputStream into an OutputStream. Closes the InputStream afterwards.
* @param in
* @param out
* @throws IOException
*/
private static void copy(InputStream in, OutputStream out) throws IOException
{
try {
int len;
byte[] b = new byte[1024];
while ((len = in.read(b)) != -1)
out.write(b,0,len);
}
finally {
in.close();
}
}
/**
* Base interface for every fixture.
*/
public interface Fixture {
/**
* Write the physical representation of the fixture to the given OutputStream
* @param out
* @throws IOException
*/
void writeOut(OutputStream out) throws IOException;
}
/**
* Abstract base class for fixtures. Archive fixtures are by nature hierarchical.
*/
public static abstract class AbstractFixture implements Fixture {
private ZipFixture parent;
protected AbstractFixture(ZipFixture parent) {
this.parent = parent;
}
/**
* Ends the current flow target and returns the parent flow target. For example, in the
* following code snippet the <code>end</code> after <code>.version("2.0.0")</code> marks
* the end of the manifest. Commands after that relate to the parent jar file of the manifest.
*
* <code>
* ArchiveFixtures.ZipFixture zip = ArchiveFixtures.newZip()
* .jar("test.jar")
* .manifest()
* .symbolicName("com.ibm.test")
* .version("2.0.0")
* .end()
* .file("random.txt", "Some text")
* .end();
* </code>
* @return
*/
public ZipFixture end() {
return (parent == null) ? (ZipFixture) this : parent;
}
}
/**
* Simple fixture for text files.
*/
public static class FileFixture extends AbstractFixture {
private StringBuffer text = new StringBuffer();
protected FileFixture(ZipFixture parent) {
super(parent);
}
/**
* Add a line to the file fixture. The EOL character is added automatically.
* @param line
* @return
*/
public FileFixture line(String line) {
text.append(line);
text.append("\n");
return this;
}
public void writeOut(OutputStream out) throws IOException {
out.write(text.toString().getBytes());
}
}
public static class IStreamFixture extends AbstractFixture {
private byte[] bytes;
protected IStreamFixture(ZipFixture parent, InputStream input) throws IOException {
super(parent);
ByteArrayOutputStream output = new ByteArrayOutputStream();
try {
copy(input, output);
} finally {
output.close();
}
bytes = output.toByteArray();
}
public void writeOut(OutputStream out) throws IOException {
copy(new ByteArrayInputStream(bytes), out);
}
}
/**
* Fixture for (bundle) manifests. By default, they contain the lines
*
* <code>
* Manifest-Version: 1
* Bundle-ManifestVersion: 2
* </code>
*/
public static class ManifestFixture extends AbstractFixture {
private Manifest mf;
protected Manifest getManifest()
{
return mf;
}
protected ManifestFixture(ZipFixture parent) {
super(parent);
mf = new Manifest();
mf.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1");
mf.getMainAttributes().putValue(Constants.BUNDLE_MANIFESTVERSION, "2");
}
/**
* Set the symbolic name of the bundle
* @param name
* @return
*/
public ManifestFixture symbolicName(String name)
{
mf.getMainAttributes().putValue(Constants.BUNDLE_SYMBOLICNAME, name);
return this;
}
/**
* Set the version of the bundle
* @param version
* @return
*/
public ManifestFixture version(String version)
{
mf.getMainAttributes().putValue(Constants.BUNDLE_VERSION, version);
return this;
}
/**
* Add a custom attribute to the manifest. Use the more specific methods for symbolic name and version.
* @param name
* @param value
* @return
*/
public ManifestFixture attribute(String name, String value)
{
mf.getMainAttributes().putValue(name, value);
return this;
}
public void writeOut(OutputStream out) throws IOException
{
mf.write(out);
}
}
/**
* Fixture for a jar archive. It offers the same functionality as zip fixtures.
* The main difference is that in a jar archive the manifest will be output as the first file,
* regardless of when it is added.
*/
public static class JarFixture extends ZipFixture {
private ManifestFixture mfFixture;
protected JarFixture(ZipFixture parent) {
super(parent);
}
@Override
public ManifestFixture manifest()
{
if (mfFixture != null)
throw new IllegalStateException("Only one manifest allowed, you dummy ;)");
mfFixture = new ManifestFixture(this);
return mfFixture;
}
@Override
public InputStream getInputStream() throws IOException
{
if (bytes == null) {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
JarOutputStream jout;
if (mfFixture != null)
jout = new JarOutputStream(bout, mfFixture.getManifest());
else
jout = new JarOutputStream(bout);
try {
writeAllEntries(jout);
} finally {
jout.close();
}
bytes = bout.toByteArray();
}
return new ByteArrayInputStream(bytes);
}
}
/**
* Base fixture for any kind of zip archive. Zip archives can contain any number of child archives
* given by an archive type and a path. The order in which these child archives are added is important
* because it will be the order in which they are added to the zip.
*/
public static class ZipFixture extends AbstractFixture {
protected static class ChildFixture {
public String path;
public Fixture fixture;
public ChildFixture(String path, Fixture fixture)
{
this.path = path;
this.fixture = fixture;
}
}
protected List<ChildFixture> children = new ArrayList<ChildFixture>();
protected byte[] bytes = null;
protected ZipFixture(ZipFixture parent) {
super(parent);
}
/**
* Create a child zip fixture at the given target.
* @param path
* @return
*/
public ZipFixture zip(String path) {
ZipFixture res = new ZipFixture(this);
children.add(new ChildFixture(path, res));
return res;
}
/**
* Create a child jar fixture at the given path.
* @param path
* @return
*/
public ZipFixture jar(String path) {
JarFixture res = new JarFixture(this);
children.add(new ChildFixture(path, res));
return res;
}
/**
* Create a complete child file fixture at the given path and with the content.
* Note: this will return the current zip fixture and not the file fixture.
*
* @param path
* @param content
* @return
*/
public ZipFixture file(String path, String content)
{
return file(path).line(content).end();
}
/**
* Create an empty file fixture at the given path.
*
* @param path
* @return
*/
public FileFixture file(String path)
{
FileFixture res = new FileFixture(this);
children.add(new ChildFixture(path, res));
return res;
}
/**
* Create a binary file with the content from the input stream
* @param path
* @param input
* @return
*/
public ZipFixture binary(String path, InputStream input) throws IOException {
if (input == null) throw new IllegalArgumentException("Provided input stream cannot be null");
IStreamFixture child = new IStreamFixture(this, input);
children.add(new ChildFixture(path, child));
return this;
}
/**
* Create a binary file that is populated from content on the classloader
* @param path
* @param resourcePath Path that the resource can be found in the current classloader
* @return
*/
public ZipFixture binary(String path, String resourcePath) throws IOException {
return binary(path, getClass().getClassLoader().getResourceAsStream(resourcePath));
}
/**
* Create a manifest fixture at the given path.
* @return
*/
public ManifestFixture manifest()
{
ManifestFixture res = new ManifestFixture(this);
children.add(new ChildFixture("META-INF/MANIFEST.MF", res));
return res;
}
/**
* Ensure that the necessary directory entries for the entry are available
* in the zip file. Newly created entries are added to the set of directories.
*
* @param zout
* @param entry
* @param existingDirs
* @throws IOException
*/
private void mkDirs(ZipOutputStream zout, String entry, Set<String> existingDirs) throws IOException
{
String[] parts = entry.split("/");
String dirName = "";
for (int i=0;i<parts.length-1;i++) {
dirName += parts[i] + "/";
if (!!!existingDirs.contains(dirName)) {
ZipEntry ze = new ZipEntry(dirName);
zout.putNextEntry(ze);
zout.closeEntry();
existingDirs.add(dirName);
}
}
}
/**
* Add all entries to the ZipOutputStream
* @param zout
* @throws IOException
*/
protected void writeAllEntries(ZipOutputStream zout) throws IOException
{
Set<String> dirs = new HashSet<String>();
for (ChildFixture child : children) {
mkDirs(zout, child.path, dirs);
ZipEntry ze = new ZipEntry(child.path);
zout.putNextEntry(ze);
child.fixture.writeOut(zout);
zout.closeEntry();
}
}
public void writeOut(OutputStream out) throws IOException
{
copy(getInputStream(), out);
}
public InputStream getInputStream() throws IOException
{
/*
* For better reuse this method delegate the writing to writeAllEntries, which
* can be reused by the JarFixture.
*/
ByteArrayOutputStream bout = new ByteArrayOutputStream();
ZipOutputStream zout = new ZipOutputStream(bout);
try {
writeAllEntries(zout);
} finally {
zout.close();
}
bytes = bout.toByteArray();
return new ByteArrayInputStream(bytes);
}
}
}