Anyone who has used Web Start has come across the problem that it forces you to store everything in JAR files. This is especially annoying because there are multiple ways to load a file from the classpath (MyClass.class.getClassLoader.getResource(…) and Thread.currentThread().getContextClassLoader().getResource(…)). Also, when you’re developing it’s more convenient to only have to change a local file, instead of having to create the JARs again every time you make a change.
The class below allows you to develop using local files, and to load them from the classpath when deployed as Web Start application. This works without code changes, so you don’t have to have DEVELOP_MODE = true flags in your code. I guess a lot of people who will read this already have a similar (and probably better) way to tackle this problem, but it might just help one guy who is trying to figure out Web Start.
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
/**
* Represents the link to a resource file located at a relative path. When the
* file's contents are requested (as an {@code InputStream}, URL or byte array)
* the local file system is checked first. If the file does not exist there,
* the classpath is checked.
* <p>
* This class can be used for situations where a fallback mechanism for resource
* files is required. For example, applications can be developed using the local
* file system, and use files stored in JARs when deployed using Java Web Start.
*/
public class ResourceFile {
private String path;
private File localFile;
private URL classpath;
/**
* Creates a new {@code ResourceFile} located at the specified path.
* @param path The relative location of the file.
* @throws IllegalArgumentException if the path is an empty string.
*/
public ResourceFile(String path) {
path = path.trim();
if (path.length() == 0) {
throw new IllegalArgumentException("Path is empty. If you want to " +
"indicate the current directory use \".\"");
}
// Normalize the path and make sure it uses slashes as delimiters
path = path.replace('\\', '/');
if (path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
// If the path starts with "./" the file is in the current directory. Remove
// the useless prefix in such cases.
if ((path.length() > 2) && (path.startsWith("./"))) {
path = path.substring(2);
}
this.path = path;
localFile = new File(path);
classpath = getClass().getClassLoader().getResource(path);
}
/**
* Creates a {@code ResourceFile} that is within the specified directory.
* @param dir The file's parent directory.
* @param name The file's name.
*/
public ResourceFile(ResourceFile dir, String name) {
this(dir.getPath() + "/" + name);
}
/**
* Creates a {@code ResourceFile} from the specified local file. The path of
* the file will be the absolute path of the local file.
*/
public ResourceFile(File file) {
this(file.getAbsolutePath());
}
public String getPath() {
return path;
}
public String getName() {
int slashIndex = path.lastIndexOf('/');
if (slashIndex == -1) {
return path;
}
return path.substring(slashIndex + 1);
}
/**
* Returns if this file exists, either in the local file system or in the
* classpath.
*/
public boolean exists() {
return (existsLocal() || existsClassPath());
}
public boolean existsLocal() {
return localFile.exists();
}
public boolean existsClassPath() {
return (classpath != null);
}
/**
* Returns an {@code InputStream} for this resource file.
* @throws ResourceNotFoundException if the file could not be located.
*/
public InputStream getStream() {
if (existsLocal()) {
try {
return new FileInputStream(localFile);
} catch (FileNotFoundException e) {
// File was removed since construction
throw new ResourceNotFoundException(this);
}
} else if (existsClassPath()) {
try {
return classpath.openStream();
} catch (IOException e) {
// Classpath was changed since construction
throw new ResourceNotFoundException(this);
}
} else {
throw new ResourceNotFoundException(this);
}
}
/**
* Returns a {@code URL} pointing to this resource file.
* @throws ResourceNotFoundException if the file could not be located.
*/
public URL getURL() {
if (existsLocal()) {
try {
return localFile.toURI().toURL();
} catch (IOException e) {
// Should be impossible
throw new AssertionError(e);
}
} else if (existsClassPath()) {
return classpath;
} else {
throw new ResourceNotFoundException(this);
}
}
/**
* Returns the contents of this resource file as a byte array.
* @throws ResourceNotFoundException if the file could not be located.
* @throws IllegalStateException if the file could not be converted to bytes.
*/
public final byte[] getBytes() {
try {
return toByteArray(getStream());
} catch (IOException e) {
throw new IllegalStateException("Could not convert file to bytes", e);
}
}
private byte[] toByteArray(InputStream input) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
toStream(input, output);
input.close();
output.close();
return output.toByteArray();
}
private void toStream(InputStream input, OutputStream output) throws IOException {
byte[] buffer = new byte[2048];
int length = -1;
while ((length = input.read(buffer, 0, buffer.length)) != -1) {
output.write(buffer, 0, length);
}
}
/**
* Returns the local file that is represented by this resource file. Note that
* the local file might not actually exist, this can be checked by calling
* {@link #existsLocal()}.
*/
public File toLocalFile() {
return localFile;
}
@Override
public boolean equals(Object o) {
if (o instanceof ResourceFile) {
return path.equals(((ResourceFile) o).path);
}
return false;
}
@Override
public int hashCode() {
return path.hashCode();
}
@Override
public String toString() {
return path;
}
}