ExplodedJarInputStream.java
/*-
* #%L
* io.earcam.instrumental.io
* %%
* Copyright (C) 2018 earcam
* %%
* SPDX-License-Identifier: (BSD-3-Clause OR EPL-1.0 OR Apache-2.0 OR MIT)
*
* You <b>must</b> choose to accept, in full - any individual or combination of
* the following licenses:
* <ul>
* <li><a href="https://opensource.org/licenses/BSD-3-Clause">BSD-3-Clause</a></li>
* <li><a href="https://www.eclipse.org/legal/epl-v10.html">EPL-1.0</a></li>
* <li><a href="https://www.apache.org/licenses/LICENSE-2.0">Apache-2.0</a></li>
* <li><a href="https://opensource.org/licenses/MIT">MIT</a></li>
* </ul>
* #L%
*/
package io.earcam.utilitarian.io;
import static io.earcam.unexceptional.Closing.closeAfterAccepting;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.Iterator;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import javax.annotation.concurrent.NotThreadSafe;
import io.earcam.unexceptional.Exceptional;
/**
* BEWARE OF LIMITATIONS; CANNOT BE WRAPPED BY ANOTHER JarInputStream, CANNOT BE READ AS A NORMAL INPUT STREAM
*
* TODO performance: {@link ExplodedJarEntry#loadContents()} should just be wrapping FileInputStream...
*/
@SuppressWarnings("squid:MaximumInheritanceDepth") // SonarQube; Not much can be done about this... dirty hack anyhoo
@NotThreadSafe
public final class ExplodedJarInputStream extends JarInputStream {
private static final Path MANIFEST_PATH = Paths.get("META-INF", "MANIFEST.MF");
private static class EmptyInputStream extends InputStream {
public static final InputStream EMPTY_INPUTSTREAM = new EmptyInputStream();
@Override
public int read()
{
return -1;
}
}
public class ExplodedJarEntry extends JarEntry {
private Path path;
private byte[] contents;
private int position = 0;
public ExplodedJarEntry(Path path)
{
super(directory.relativize(path).toString());
this.path = path;
setMethod(STORED);
}
public Path path()
{
return path;
}
@Override
public boolean isDirectory()
{
return path().toFile().isDirectory();
}
@Override
public FileTime getCreationTime()
{
return Exceptional.apply(Files::readAttributes, path(), BasicFileAttributes.class).creationTime();
}
@Override
public FileTime getLastModifiedTime()
{
return Exceptional.apply(Files::getLastModifiedTime, path());
}
@Override
public long getTime()
{
return getLastModifiedTime().toMillis();
}
@Override
public long getSize()
{
return Exceptional.apply(Files::size, path());
}
private void loadContents()
{
if(contents == null) {
contents = Exceptional.apply(Files::readAllBytes, path());
}
}
/**
* @deprecated Never intended for public use.
* Aggressively deprecated, class will become final once dropped.
*
* @return nothing
* @throws UnsupportedOperationException everytime
*/
@Deprecated
public int read()
{
throw new UnsupportedOperationException("Never intended for public use. Agressively deprecated.");
}
public int read(byte[] b, int off, int len)
{
loadContents();
int remaining = available();
if(remaining == 0) {
return -1;
}
int length = Math.min(remaining, len);
System.arraycopy(contents, position, b, off, length);
position += length;
return length;
}
int available()
{
loadContents();
return contents.length - position;
}
}
private Iterator<Path> iterator;
private Path directory;
private ExplodedJarEntry current;
private ExplodedJarInputStream(Path directory, Iterator<Path> iterator) throws IOException
{
super(EmptyInputStream.EMPTY_INPUTSTREAM, false);
this.directory = directory.toRealPath();
this.iterator = iterator;
}
/**
* If the {@code path} parameter is a directory then returns an {@link ExplodedJarInputStream},
* otherwise, the {@code path} parameter is a file so, returns a {@link JarInputStream}
*
* @param path the location on a filesystem
* @return a jar input stream
* @throws IOException
*
* @see {@link #explodedJar(Path)}
*/
public static JarInputStream jarInputStreamFrom(Path path) throws IOException
{
return jarInputStreamFrom(path.toFile());
}
/**
* If the {@code path} parameter is a directory then returns an {@link ExplodedJarInputStream},
* otherwise, the {@code path} parameter is a file so, returns a {@link JarInputStream}
*
* @param path the location on a filesystem
* @return a jar input stream
* @throws IOException
*/
public static JarInputStream jarInputStreamFrom(File path) throws IOException
{
return path.isDirectory() ? explodedJar(path) : new JarInputStream(new FileInputStream(path));
}
/**
* Treat the <b>directory</b> as an exploded JAR file
*
* @param directory
* @return a jar input stream, which can be read using {@link JarEntry} methods
* @throws IOException
*/
public static ExplodedJarInputStream explodedJar(File directory) throws IOException
{
return explodedJar(directory.toPath());
}
/**
* Treat the <b>directory</b> as an exploded JAR file
*
* @param directory
* @return a jar input stream, which can be read using {@link JarEntry} methods
* @throws IOException
*/
public static ExplodedJarInputStream explodedJar(Path directory) throws IOException
{
if(!directory.toFile().isDirectory()) {
throw new IOException("'" + directory + "' is not a directory");
}
RecursivePathIterator rpi = new RecursivePathIterator(directory);
return new ExplodedJarInputStream(directory, new Filterator<Path>(rpi, MANIFEST_PATH));
}
@Override
public JarEntry getNextJarEntry() throws IOException
{
current = iterator.hasNext() ? new ExplodedJarEntry(iterator.next().toRealPath()) : null;
return current;
}
@Override
public Manifest getManifest()
{
Manifest manifest = null;
Path file = directory.resolve(MANIFEST_PATH);
if(file.toFile().exists()) {
manifest = new Manifest();
closeAfterAccepting(FileInputStream::new, file.toFile(), manifest::read);
}
return manifest;
}
private void checkCurrent()
{
if(current == null) {
throw new UnsupportedOperationException(ExplodedJarInputStream.class + " does not work as a regular InputStream");
}
}
@Override
public int read(byte[] b, int off, int len) throws IOException
{
checkCurrent();
return current.read(b, off, len);
}
@Override
public int available() throws IOException
{
checkCurrent();
return current.available();
}
}