/* (c) 2014 - 2016 Open Source Geospatial Foundation - all rights reserved
* (c) 2014 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.platform.resource;
import static org.geoserver.util.IOUtils.rename;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.NoSuchFileException;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geotools.util.logging.Logging;
/**
* Implementation of ResourceStore backed by the file system.
*/
public class FileSystemResourceStore implements ResourceStore {
static final Logger LOGGER = Logging.getLogger(FileSystemResource.class);
/**
* When true, the stack trace that got an input stream that wasn't closed is recorded and then
* printed out when warning the user about this.
*/
protected static final Boolean TRACE_ENABLED = "true".equalsIgnoreCase(System.getProperty("gs.lock.trace"));
/** LockProvider used to secure resources for exclusive access */
protected LockProvider lockProvider = new NullLockProvider();
/** Base directory for ResourceStore content */
protected File baseDirectory = null;
protected FileSystemWatcher watcher;
protected FileSystemResourceStore(){
// Used by Spring, baseDirectory set by subclass
}
/**
* LockProvider used during {@link Resource#out()}.
*
* Client code that insists on using {@link Resource#file()} can do us using:
* <pre><code>
* Resource resource = resoures.get( "example.txt" );
* Lock lock = resources.getLockProvider().acquire( resource.path() );
* try {
* File file = resoruce.file();
* ..
* }
* finally {
* lock.release();
* }
* </code></pre>
*
* @return LockProvider used for {@link Resource#out}
*/
public LockProvider getLockProvider() {
return lockProvider;
}
/**
* Configure LockProvider used during {@link Resource#out()}.
*
* @param lockProvider LockProvider used for Resource#out()
*/
public void setLockProvider(LockProvider lockProvider) {
this.lockProvider = lockProvider;
}
public FileSystemResourceStore(File resourceDirectory) {
if (resourceDirectory == null) {
throw new NullPointerException("root resource directory required");
}
if (resourceDirectory.isFile()) {
throw new IllegalArgumentException("Directory required, file present at this location "
+ resourceDirectory);
}
if (!resourceDirectory.exists()) {
boolean create = resourceDirectory.mkdirs();
if (!create) {
throw new IllegalArgumentException("Unable to create directory "
+ resourceDirectory);
}
}
if (resourceDirectory.exists() && resourceDirectory.isDirectory()) {
this.baseDirectory = resourceDirectory;
} else {
throw new IllegalArgumentException("Unable to acess directory " + resourceDirectory);
}
}
@Override
public Resource get(String path) {
path = Paths.valid(path);
return new FileSystemResource(path);
}
@Override
public boolean remove(String path) {
path = Paths.valid(path);
File file = Paths.toFile(baseDirectory, path);
return Files.delete(file);
}
@Override
public boolean move(String path, String target) {
path = Paths.valid(path);
target = Paths.valid(target);
File file = Paths.toFile(baseDirectory, path);
File dest = Paths.toFile(baseDirectory, target);
if (!file.exists() && !dest.exists()) {
return true; // moving an undefined resource
}
try {
dest.getParentFile().mkdirs(); // Make sure there's somewhere to move to.
java.nio.file.Files.move(java.nio.file.Paths.get(file.getAbsolutePath()),
java.nio.file.Paths.get(dest.getAbsolutePath()),
StandardCopyOption.ATOMIC_MOVE);
return true;
} catch (IOException e) {
throw new IllegalStateException("Unable to move " + path + " to " + target, e);
}
}
@Override
public String toString() {
return "ResourceStore " + baseDirectory;
}
/**
* Direct implementation of Resource.
* <p>
* This implementation is a stateless data object, acting as a simple handle around a File.
*/
class FileSystemResource implements Resource {
String path;
File file;
public FileSystemResource(String path) {
this.path = path;
this.file = Paths.toFile(baseDirectory, path);
}
@Override
public String path() {
return path;
}
@Override
public String name() {
return Paths.name(path);
}
@Override
public Lock lock() {
return lockProvider.acquire(path);
}
@Override
public void addListener(ResourceListener listener) {
getResourceNotificationDispatcher().addListener(path, listener);
}
@Override
public void removeListener(ResourceListener listener) {
getResourceNotificationDispatcher().removeListener(path, listener);
}
@Override
public InputStream in() {
File actualFile = file();
if (!actualFile.exists()) {
throw new IllegalStateException("File not found " + actualFile);
}
final Lock lock = lock();
final Throwable tracer;
if(TRACE_ENABLED) {
tracer = new Exception();
tracer.fillInStackTrace();
} else {
tracer = null;
}
try {
return new FileInputStream(file) {
boolean closed = false;
@Override
public void close() throws IOException {
closed = true;
super.close();
lock.release();
}
@Override
protected void finalize() throws IOException {
if(!closed) {
String warn = "There is code leaving resource input streams open, locks around them might not be cleared! ";
if(!TRACE_ENABLED) {
warn += "Add -D" + TRACE_ENABLED + "=true to your JVM options to get a full stack trace of the code that acquired the input stream";
}
LOGGER.warning(warn);
if(TRACE_ENABLED) {
LOGGER.log(Level.WARNING, "The unclosed input stream originated on this stack trace", tracer);
}
}
super.finalize();
}
};
} catch (FileNotFoundException e) {
lock.release();
throw new IllegalStateException("File not found " + actualFile, e);
}
}
@Override
public OutputStream out() {
final File actualFile = file();
if (!actualFile.exists()) {
throw new IllegalStateException("Cannot access " + actualFile);
}
try {
// first save to a temp file
final File temp;
synchronized(this) {
File tryTemp;
do {
UUID uuid = UUID.randomUUID();
tryTemp = new File(actualFile.getParentFile(), String.format("%s.%s.tmp", actualFile.getName(), uuid));
} while(tryTemp.exists());
temp = tryTemp;
}
// OutputStream wrapper used to write to a temporary file
// (and only lock during move to actualFile)
return new OutputStream() {
FileOutputStream delegate = new FileOutputStream(temp);
@Override
public void close() throws IOException {
delegate.close();
// if already closed, there should be no exception (see spec Closeable)
if (temp.exists()) {
Lock lock = lock();
try {
// no errors, overwrite the original file
Files.move(temp, actualFile);
}
finally {
lock.release();
}
}
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
delegate.write(b, off, len);
}
@Override
public void flush() throws IOException {
delegate.flush();
}
@Override
public void write(byte[] b) throws IOException {
delegate.write(b);
}
@Override
public void write(int b) throws IOException {
delegate.write(b);
}
};
} catch (FileNotFoundException e) {
throw new IllegalStateException("Cannot access " + actualFile, e);
}
}
@Override
public File file() {
if (!file.exists()) {
try {
File parent = file.getParentFile();
if (!parent.exists()) {
boolean created = parent.mkdirs();
if (!created) {
throw new IllegalStateException("Unable to create "
+ parent.getAbsolutePath());
}
}
if (parent.isDirectory()) {
Lock lock = lock();
boolean created;
try {
created = file.createNewFile();
}
finally {
lock.release();
}
if (!created) {
throw new FileNotFoundException("Unable to create "
+ file.getAbsolutePath());
}
} else {
throw new FileNotFoundException("Unable to create" + file.getName()
+ " - not a directory " + parent.getAbsolutePath());
}
} catch (IOException e) {
throw new IllegalStateException("Cannot create " + path, e);
}
}
if (file.isDirectory()) {
throw new IllegalStateException("Directory (not a file) at " + path);
} else {
return file;
}
}
@Override
public File dir() {
if (!file.exists()) {
try {
File parent = file.getParentFile();
if (!parent.exists()) {
boolean created = parent.mkdirs();
if (!created) {
throw new IllegalStateException("Unable to create "
+ parent.getAbsolutePath());
}
}
if (parent.isDirectory()) {
Lock lock = lock();
boolean created;
try {
created = file.mkdir();
}
finally {
lock.release();
}
if (!created) {
throw new FileNotFoundException("Unable to create "
+ file.getAbsolutePath());
}
} else {
throw new FileNotFoundException("Unable to create" + file.getName()
+ " - not a directory " + parent.getAbsolutePath());
}
} catch (IOException e) {
throw new IllegalStateException("Cannot create " + path, e);
}
}
if (file.isFile()) {
throw new IllegalStateException("File (not a directory) at " + path);
} else {
return file;
}
}
@Override
public long lastmodified() {
return file.lastModified();
}
@Override
public List<Resource> list() {
if (!file.exists()) {
return Collections.emptyList();
}
if (file.isFile()) {
return Collections.emptyList();
}
String array[] = file.list();
if (array == null) {
return Collections.emptyList();
}
List<Resource> list = new ArrayList<Resource>(array.length);
for (String filename : array) {
Resource resource = FileSystemResourceStore.this.get(Paths.path(path, filename));
list.add(resource);
}
return list;
}
@Override
public Resource parent() {
int split = path.lastIndexOf('/');
if (split == -1) {
return FileSystemResourceStore.this.get(Paths.BASE); // root
} else {
return FileSystemResourceStore.this.get(path.substring(0, split));
}
}
@Override
public Resource get(String resourcePath) {
if (resourcePath == null) {
throw new NullPointerException("Resource path required");
}
if ("".equals(resourcePath)) {
return this;
}
return FileSystemResourceStore.this.get(Paths.path(path, resourcePath));
}
@Override
public Type getType() {
try {
BasicFileAttributes attributes = java.nio.file.Files.readAttributes(file.toPath(), BasicFileAttributes.class);
if(attributes.isDirectory()) {
return Type.DIRECTORY;
} else if(attributes.isRegularFile()) {
return Type.RESOURCE;
} else {
throw new IllegalStateException(
"Path does not represent a configuration resource: " + path);
}
} catch(NoSuchFileException e) {
return Type.UNDEFINED;
} catch(IOException e) {
throw new IllegalStateException(e);
}
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((path == null) ? 0 : path.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
FileSystemResource other = (FileSystemResource) obj;
if (path == null) {
if (other.path != null)
return false;
} else if (!path.equals(other.path))
return false;
return true;
}
@Override
public String toString() {
return file.getAbsolutePath();
}
@Override
public boolean delete() {
Lock lock = lock();
try {
return Files.delete(file);
} finally {
lock.release();
}
}
@Override
public boolean renameTo(Resource dest) {
if (dest.parent().path().contains(path())) {
LOGGER.log(Level.FINE, "Cannot rename a resource to a descendant of itself");
return false;
}
try {
if(dest instanceof FileSystemResource) {
rename(file, ((FileSystemResource)dest).file);
} else if(dest instanceof Files.ResourceAdaptor) {
rename(file, ((Files.ResourceAdaptor)dest).file);
} else {
return Resources.renameByCopy(this, dest);
}
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Failed to rename file resource "+path+" to "+dest.path(), e);
return false;
}
return true;
}
@Override
public byte[] getContents() throws IOException {
return java.nio.file.Files.readAllBytes(file.toPath());
}
@Override
public void setContents(byte[] byteArray) throws IOException {
final File actualFile = file();
if (!actualFile.exists()) {
throw new IllegalStateException("Cannot access " + actualFile);
}
try {
// first save to a temp file
final File temp;
synchronized(this) {
File tryTemp;
do {
UUID uuid = UUID.randomUUID();
tryTemp = new File(actualFile.getParentFile(), String.format("%s.%s.tmp", actualFile.getName(), uuid));
} while(tryTemp.exists());
temp = tryTemp;
}
java.nio.file.Files.write(temp.toPath(), byteArray);
Lock lock = lock();
try {
// no errors, overwrite the original file
Files.move(temp, actualFile);
} finally {
lock.release();
}
} catch (FileNotFoundException e) {
throw new IllegalStateException("Cannot access " + actualFile, e);
}
}
}
@Override
public ResourceNotificationDispatcher getResourceNotificationDispatcher() {
if( watcher == null ){
watcher = new FileSystemWatcher(new FileSystemWatcher.FileExtractor() {
@Override
public File getFile(String path) {
return Paths.toFile(baseDirectory, path);
}
});
}
return watcher;
}
}