/*
* Copyright 2012 JBoss Inc
*
* 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.
*/
package org.artificer.atom.archive;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.artificer.atom.i18n.Messages;
import org.oasis_open.docs.s_ramp.ns.s_ramp_v1.BaseArtifactType;
import javax.xml.bind.JAXBException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* Models the archive format defined in the S-RAMP Atom Binding document.
*
* @author eric.wittmann@redhat.com
*/
public class ArtificerArchive implements Serializable {
private File originalFile;
private boolean shouldDeleteOriginalFile;
private File workDir;
/**
* Creates a new, empty S-RAMP archive.
* @throws ArtificerArchiveException
*/
public ArtificerArchive() throws ArtificerArchiveException {
workDir = null;
this.originalFile = null;
try {
workDir = createWorkDir();
} catch (IOException e) {
if (workDir != null && workDir.exists()) {
try { FileUtils.deleteDirectory(workDir); } catch (IOException e1) { }
}
throw new ArtificerArchiveException(Messages.i18n.format("FAILED_TO_CREATE_WORK_DIR"), e);
}
}
/**
* Creates an S-RAMP archive from an existing archive file.
* @param file
* @throws ArtificerArchiveException
*/
public ArtificerArchive(File file) throws ArtificerArchiveException {
this();
this.originalFile = file;
this.shouldDeleteOriginalFile = false;
try {
ArchiveUtils.unpackToWorkDir(this.originalFile, this.workDir);
} catch (IOException e) {
if (this.workDir != null) {
try { FileUtils.deleteDirectory(this.workDir); } catch (IOException e1) { }
}
throw new ArtificerArchiveException(Messages.i18n.format("FAILED_TO_UNPACK_ARCHIVE_TO_WORK_DIR"), e);
}
}
/**
* Creates an S-RAMP archive from an {@link InputStream}. This will consume and close the
* {@link InputStream}, creating a temporary local file that will be used as the basis for
* the archive input.
* @param input
* @throws ArtificerArchiveException
*/
public ArtificerArchive(InputStream input) throws ArtificerArchiveException {
this();
this.originalFile = null;
this.shouldDeleteOriginalFile = true;
try {
this.originalFile = File.createTempFile("s-ramp-archive", ".zip");
copyZipStream(input, this.originalFile);
ArchiveUtils.unpackToWorkDir(this.originalFile, this.workDir);
} catch (IOException e) {
if (this.workDir != null) {
try { FileUtils.deleteDirectory(this.workDir); } catch (IOException e1) { }
}
if (this.originalFile != null && this.originalFile.exists()) {
this.originalFile.delete();
}
throw new ArtificerArchiveException(Messages.i18n.format("FAILED_TO_UNPACK_ARCHIVE_TO_WORK_DIR"), e);
}
}
/**
* Create the working directory for this archive.
* @throws IOException
*/
private static File createWorkDir() throws IOException {
File tempFile = File.createTempFile("s-ramp-archive", ".work");
tempFile.delete();
tempFile.mkdir();
return tempFile;
}
/**
* Copies the ZIP content from the input stream to the given output file.
* @param zipStream
* @param zipOutputFile
* @throws IOException
*/
private static void copyZipStream(InputStream zipStream, File zipOutputFile) throws IOException {
OutputStream oStream = null;
try {
oStream = FileUtils.openOutputStream(zipOutputFile);
IOUtils.copy(zipStream, oStream);
} finally {
IOUtils.closeQuietly(zipStream);
IOUtils.closeQuietly(oStream);
}
}
/**
* The S-RAMP archive should always be closed when the client is done with it. This will
* clean up all temporary resources created by the archive.
* @throws IOException
*/
public void close() throws IOException {
FileUtils.deleteDirectory(workDir);
if (this.shouldDeleteOriginalFile) {
this.originalFile.delete();
}
}
/**
* Close the archive quietly (eat any {@link IOException}).
* @param archive
*/
public static void closeQuietly(ArtificerArchive archive) {
try {
if (archive != null)
archive.close();
} catch (IOException e) {
}
}
/**
* Gets all of the entries found in this S-RAMP archive. It does this by scanning the
* archive looking for all *.atom files. One entry will be returned for each *.atom
* file found in the archive (assuming it has associated content and the *.atom file is
* properly formatted).
* @throws ArtificerArchiveException
*/
public Collection<ArtificerArchiveEntry> getEntries() throws ArtificerArchiveException {
Collection<File> files = FileUtils.listFiles(workDir, new String[] { "atom" }, true);
Collection<ArtificerArchiveEntry> entries = new ArrayList<ArtificerArchiveEntry>(files.size());
for (File metaDataFile : files) {
String metaDataAbsPath = metaDataFile.getAbsolutePath();
File contentFile = new File(metaDataAbsPath.substring(0, metaDataAbsPath.length() - 5));
String path = contentFile.getAbsolutePath();
path = path.substring(this.workDir.getAbsolutePath().length() + 1);
path = path.replace('\\', '/'); // just in case we're in Windows :(
entries.add(new ArtificerArchiveEntry(path, metaDataFile, contentFile));
}
return entries;
}
/**
* Gets the content {@link InputStream} for the given S-RAMP archive entry.
* @param entry the s-ramp archive entry
* @return an {@link InputStream} over the artifact content or null if no content found (meta-data only)
* @throws IOException
*/
public InputStream getInputStream(ArtificerArchiveEntry entry) throws IOException {
File artifactPath = new File(this.workDir, entry.getPath());
if (artifactPath.exists())
return FileUtils.openInputStream(artifactPath);
else
return null;
}
/**
* Adds an entry to the S-RAMP archive. This method will close the content
* {@link InputStream}.
* @param path the path in the archive (usually just the name of the artifact)
* @param metaData the artifact meta-data
* @param content the entry content (or null if a meta-data only entry)
* @throws ArtificerArchiveException
*/
public void addEntry(String path, BaseArtifactType metaData, InputStream content) throws ArtificerArchiveException {
if (path == null)
throw new ArtificerArchiveException(Messages.i18n.format("INVALID_ENTRY_PATH"));
if (metaData == null)
throw new ArtificerArchiveException(Messages.i18n.format("MISSING_META_DATA"));
File metaDataFile = new File(this.workDir, path + ".atom");
File contentFile = new File(this.workDir, path);
if (metaDataFile.exists())
throw new ArtificerArchiveException(Messages.i18n.format("ARCHIVE_ALREADY_EXISTS"));
// Create any required parent directories
metaDataFile.getParentFile().mkdirs();
if (content != null)
writeContent(contentFile, content);
try {
ArtificerArchiveJaxbUtils.writeMetaData(metaDataFile, metaData);
} catch (JAXBException e) {
throw new ArtificerArchiveException(e);
}
}
/**
* Updates an existing entry in the S-RAMP archive. This method will close the content
* {@link InputStream}.
* @param entry the archive entry (or null if just udpating the content)
* @param content the entry content (or null if just updating meta data)
* @throws ArtificerArchiveException
*/
public void updateEntry(ArtificerArchiveEntry entry, InputStream content) throws ArtificerArchiveException {
if (entry.getPath() == null)
throw new ArtificerArchiveException(Messages.i18n.format("INVALID_ENTRY_PATH"));
File contentFile = new File(this.workDir, entry.getPath());
File metaDataFile = new File(this.workDir, entry.getPath() + ".atom");
if (content != null)
writeContent(contentFile, content);
if (entry.getMetaData() != null) {
try {
ArtificerArchiveJaxbUtils.writeMetaData(metaDataFile, entry.getMetaData());
} catch (JAXBException e) {
throw new ArtificerArchiveException(e);
}
}
}
/**
* Writes the artifact content to the given working path.
* @param workPath
* @param content
* @throws ArtificerArchiveException
*/
private void writeContent(File workPath, InputStream content) throws ArtificerArchiveException {
OutputStream outStream = null;
try {
outStream = new FileOutputStream(workPath);
IOUtils.copy(content, outStream);
} catch (Throwable t) {
throw new ArtificerArchiveException(Messages.i18n.format("ERROR_WRITING_CONTENT"), t);
} finally {
IOUtils.closeQuietly(content);
IOUtils.closeQuietly(outStream);
}
}
/**
* Packs up the current contents of the S-RAMP archive into a single (.zip) file and
* returns a reference to it. This method is guaranteed to either throw an Exception
* or return a valid {@link File}. It will never throw and leave a temporary file
* behind.
* @throws ArtificerArchiveException
*/
public File pack() throws ArtificerArchiveException {
try {
File archiveFile = null;
try {
archiveFile = File.createTempFile("artificer-archive", ".sramp");
FileOutputStream outputStream = FileUtils.openOutputStream(archiveFile);
ZipOutputStream zipOutputStream = null;
try {
zipOutputStream = new ZipOutputStream(outputStream);
Collection<ArtificerArchiveEntry> entries = getEntries();
for (ArtificerArchiveEntry entry : entries) {
packEntry(entry, zipOutputStream);
}
} finally {
IOUtils.closeQuietly(zipOutputStream);
}
} catch (Throwable t) {
// If anything goes wrong, make sure the File is cleaned up, as
// we won't have another chance to do so.
if (archiveFile != null && archiveFile.isFile())
archiveFile.delete();
throw t;
}
return archiveFile;
} catch (Throwable t) {
throw new ArtificerArchiveException(Messages.i18n.format("ERROR_PACKING_ARCHIVE"), t);
}
}
/**
* Pack the given S-RAMP archive entry into the ZIP.
* @param entry an s-ramp archive entry
* @param zipOutputStream the zip file
* @throws IOException
* @throws NoSuchMethodException
* @throws InvocationTargetException
* @throws IllegalAccessException
* @throws URISyntaxException
* @throws SecurityException
* @throws IllegalArgumentException
* @throws JAXBException
*/
private void packEntry(ArtificerArchiveEntry entry, ZipOutputStream zipOutputStream) throws IOException, IllegalArgumentException, SecurityException, URISyntaxException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, JAXBException {
// Store the artifact content in the ZIP
InputStream contentStream = getInputStream(entry);
if (contentStream != null) {
zipOutputStream.putNextEntry(new ZipEntry(entry.getPath()));
try {
IOUtils.copy(contentStream, zipOutputStream);
} finally {
IOUtils.closeQuietly(contentStream);
}
zipOutputStream.closeEntry();
}
// Store the meta-data in the ZIP
zipOutputStream.putNextEntry(new ZipEntry(entry.getPath() + ".atom"));
try {
ArtificerArchiveJaxbUtils.writeMetaData(zipOutputStream, entry.getMetaData());
} finally {
}
zipOutputStream.closeEntry();
}
/**
* Gets a single entry in the archive by path.
* @param archivePath the path of the entry within the archive
* @return the archive entry, or null if not found
*/
public ArtificerArchiveEntry getEntry(String archivePath) {
File contentFile = new File(this.workDir, archivePath);
File metaDataFile = new File(this.workDir, archivePath + ".atom");
ArtificerArchiveEntry rval = null;
if (metaDataFile.exists()) {
rval = new ArtificerArchiveEntry(archivePath, metaDataFile, contentFile);
}
return rval;
}
/**
* Returns true if the s-ramp archive contains an entry at the given path.
* @param archivePath path to the entry within the archive
* @return true if an entry exists at the path
*/
public boolean containsEntry(String archivePath) {
File metaDataFile = new File(this.workDir, archivePath + ".atom");
return metaDataFile.exists();
}
/**
* Removes the s-ramp archive entry at the given path if it exists.
* @param archivePath path to the entry within the archive
* @return true if an entry existed and was removed
*/
public boolean removeEntry(String archivePath) {
File metaDataFile = new File(this.workDir, archivePath + ".atom");
File contentFile = new File(this.workDir, archivePath);
if (metaDataFile.isFile()) {
metaDataFile.delete();
if (contentFile.isFile()) {
contentFile.delete();
}
return true;
}
return false;
}
}