/* (c) 2014-2015 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 java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.platform.GeoServerResourceLoader;
import org.geoserver.platform.resource.Resource.Type;
import org.geoserver.util.Filter;
import org.geotools.data.DataUtilities;
/**
* Utility methods for working with {@link ResourceStore}.
*
* These methods are suitable for static import and are intended automate common tasks.
*
* @author Jody Garnett
*/
public class Resources {
/**
* Test if the file or directory denoted by this resource exists.
*
* @see File#exists()
* @param resource Resource indicated
* @return true If resource is not UNDEFINED
*/
public static boolean exists(Resource resource) {
return resource != null && resource.getType() != Resource.Type.UNDEFINED;
}
/**
* Test if the file or directory can be read.
*
* @see File#canRead()
* @param resource Resource indicated
* @return true If resource is not UNDEFINED
*/
public static boolean canRead(Resource resource) {
try {
InputStream is = resource.in();
is.read();
is.close();
return true;
} catch (IOException | IllegalStateException e) {
return false;
}
}
/**
* Test if the file or directory behind the resource is hidden.
* For file system based resources, the platform-dependent hidden property is used.
* For other resource implementations, filenames starting with a "." are considered hidden, irrespective of the platform.
*
*
* @see File#isHidden()
* @param resource Resource indicated
* @return true If resource is hidden
*/
public static boolean isHidden(Resource resource) {
if (resource instanceof SerializableResourceWrapper) {
resource = ((SerializableResourceWrapper) resource).delegate;
}
if (resource instanceof FileSystemResourceStore.FileSystemResource ||
resource instanceof Files.ResourceAdaptor) {
//this is a file based resource, just check the file
return find(resource).isHidden();
} else {
//not a file system based resource, no point in caching
//we only support linux style hidden file.
return resource.name().startsWith(".");
}
}
/**
* Checks {@link Resource#getType()} and returns existing file() or dir() as appropriate, or null for {@link Resource.Type#UNDEFINED}.
*
* This approach is a reproduction of GeoServerResourceLoader find logic.
*
* @see Resource#dir()
* @see Resource#file()
*
* @param resource Resource indicated
* @return Existing file, or null for {@link Resource.Type#UNDEFINED}.
*/
public static File find(Resource resource) {
if (resource == null) {
return null;
}
switch (resource.getType()) {
case DIRECTORY:
return resource.dir();
case RESOURCE:
return resource.file();
default:
return null;
}
}
/**
* Checks {@link Resource#getType()} and returns existing dir() if available, or null for {@link Resource.Type#UNDEFINED} or
* {@link Resource.Type#RESOURCE}.
*
* This approach is a reproduction of GeoServerDataDirectory findDataDir logic and will not create a new directory.
*
* @see Resource#dir()
*
* @param resource Resource indicated
* @return File reference to existing directory, or null for an existing file (or if directory does not exist)
*/
public static File directory(Resource resource) {
return directory(resource, false);
}
/**
* If create is true or if a directory exists returns resource.dir, otherwise it returns null.
*
* @see Resource#dir()
*
* @param resource Resource indicated
* @param create true to create directory (if it does not exsist)
* @return File reference to (possibly new) directory
*/
public static File directory(Resource resource, boolean create) {
final File f;
if(resource==null) {
f = null;
} else if(create) {
f = resource.dir();
} else {
if (resource.getType() == Type.DIRECTORY) {
f = resource.dir();
} else {
f = null;
}
}
return f;
}
/**
* Checks {@link Resource#getType()} and returns existing file() if available, or null for {@link Resource.Type#UNDEFINED} or
* {@link Resource.Type#DIRECTORY}.
*
* This approach is a reproduction of GeoServerDataDirectory findDataFile logic and will not create a new file.
*
* @see Resource#file()
*
* @param resource Resource indicated
* @return Existing file, or null
*/
public static File file(Resource resource) {
return file(resource, false);
}
/**
* If create is true or if a file exists returns resource.file, otherwise it returns null.
*
* @see Resource#file()
*
* @param resource Resource indicated
* @param create true to create (if needed)
* @return file, or null
*/
public static File file(Resource resource, boolean create) {
final File f;
if(resource==null) {
f = null;
} else if(create) {
f = resource.file();
} else {
if (resource.getType() == Type.RESOURCE) {
f = resource.file();
} else {
f = null;
}
}
return f;
}
/**
* Create a new directory for the provided resource (this will only work for {@link Resource.Type#UNDEFINED}).
*
* This approach is a reproduction of GeoServerResourceLoader createNewDirectory logic.
*
* @param resource Resource indicated
* @return newly created file
* @throws IOException If directory could not be created (as file or directory already exists)
*/
public static File createNewDirectory(Resource resource) throws IOException {
switch (resource.getType()) {
case DIRECTORY:
throw new IOException("New directory " + resource.path()
+ " already exists as DIRECTORY");
case RESOURCE:
throw new IOException("New directory " + resource.path()
+ " already exists as RESOURCE");
case UNDEFINED:
return resource.dir(); // will create directory as needed
default:
return null;
}
}
/**
* Create a new file for the provided resource (this will only work for {@link Resource.Type#UNDEFINED}).
*
* This approach is a reproduction of GeoServerResourceLoader createNewFile logic.
*
* @param resource Resource indicated
* @return newly created file
* @throws IOException If path indicates a file (or directory) that already exists
*/
public static File createNewFile(Resource resource) throws IOException {
switch (resource.getType()) {
case DIRECTORY:
throw new IOException("New file " + resource.path() + " already exists as DIRECTORY");
case RESOURCE:
throw new IOException("New file " + resource.path() + " already exists as RESOURCE");
case UNDEFINED:
return resource.file(); // will create directory as needed
default:
return null;
}
}
/**
* Search for resources using pattern and last modified time.
*
* @param resource Resource indicated
* @param lastModified time stamp to search from
* @return list of modified resources
*/
public static List<Resource> search(Resource resource, long lastModified) {
if (resource.getType() == Type.DIRECTORY) {
ArrayList<Resource> results = new ArrayList<Resource>();
for (Resource child : resource.list()) {
switch (child.getType()) {
case RESOURCE:
if (child.lastmodified() > lastModified) {
results.add(child);
}
break;
default:
break;
}
}
return results;
}
return Collections.emptyList();
}
/**
* Write the contents of a stream into a resource
* @param data data to write
* @param destination resource to write to
* @throws IOException If data could not be copied to destination
*/
public static void copy (InputStream data, Resource destination) throws IOException {
try(OutputStream out = destination.out()) {
IOUtils.copy(data, out);
}
}
/**
* Write the contents of a resource into another resource. Also supports directories (recursively).
*
* @param data resource to read
* @param destination resource to write to
* @throws IOException If data could not be copied to destination
*/
public static void copy (Resource data, Resource destination) throws IOException {
if (data.getType() == Type.DIRECTORY) {
for (Resource child : data.list()) {
copy(child, destination.get(child.name()));
}
} else {
try(InputStream in = data.in()) {
copy(in, destination);
}
}
}
/**
* Write the contents of a stream to a new Resource inside a directory
* @param data data to write
* @param directory parent directory to create the resource in
* @param filename file name of the new resource
* @throws IOException If data could not be copied into indicated location
*/
public static void copy (InputStream data, Resource directory, String filename) throws IOException {
copy(data, directory.get(filename));
}
/**
* Write the contents of a File to a new Resource with the same name inside a directory
* @param data data to write
* @param directory parent directory to create the resource in
* @throws IOException If file could not be copied into directory
*/
public static void copy (File data, Resource directory) throws IOException {
String filename = data.getName();
try(InputStream in = new FileInputStream(data)) {
copy(in, directory.get(filename));
}
}
/**
* Renames a resource by reading it and writing to the new resource, then deleting the old one.
* This is not atomic.
* @param source Resource to rename
* @param destination New resource location
* @return true if successful, false if either the write or delete failed.
*/
public static boolean renameByCopy(Resource source, Resource destination) {
try {
copy(source, destination);
return source.delete();
} catch (IOException e) {
return false;
}
}
/**
* Returns filtered children of a directory
*
* @param dir parent directory
* @param filter the filter that selects children
* @param recursive searches recursively
* @return filtered list
*/
public static List<Resource> list(Resource dir, Filter<Resource> filter, boolean recursive) {
List<Resource> res = new ArrayList<Resource>();
for (Resource child : dir.list()) {
if (filter.accept(child)) {
res.add(child);
}
if (recursive && child.getType() == Type.DIRECTORY) {
res.addAll(list(child, filter, true));
}
}
return res;
}
/**
* Convenience method for non recursive listing
*
* @param dir parent directory
* @param filter parent directory
* @return filtered list
*/
public static List<Resource> list(Resource dir, Filter<Resource> filter) {
return list(dir, filter, false);
}
/**
*
* Recursively loops through directory to provide all children
*
* @param dir Resource of directory to list from
* @return list of children with recursive children
*/
public static List<Resource> listRecursively(Resource dir) {
return list(dir, AnyFilter.INSTANCE, true);
}
/**
* File Extension based filtering
*/
public static class ExtensionFilter implements Filter<Resource> {
private Set<String> extensions;
/**
*
* Create extension filter
*
* @param extensions in upper case
*/
public ExtensionFilter(String... extensions) {
this.extensions = new HashSet<String>(Arrays.asList(extensions));
}
@Override
public boolean accept(Resource obj) {
return extensions.contains(obj.name().substring(obj.name().lastIndexOf(".") + 1).toUpperCase());
}
}
public static class DirectoryFilter implements Filter<Resource> {
public static final DirectoryFilter INSTANCE = new DirectoryFilter();
private DirectoryFilter() {};
@Override
public boolean accept(Resource obj) {
return obj.getType() == Type.DIRECTORY;
}
}
public static class AnyFilter implements Filter<Resource> {
public static final AnyFilter INSTANCE = new AnyFilter();
private AnyFilter() {};
@Override
public boolean accept(Resource obj) {
return true;
}
}
/**
* Creates resource from a path, if the path is relative it will return a resource from the default resource loader
* otherwise it will return a file based resource
*
* @param path relative or absolute path
* @return resource
*/
public static Resource fromPath(String path) {
return ((GeoServerResourceLoader) GeoServerExtensions.bean("resourceLoader")).fromPath(path);
}
/**
* Creates resource from a path, if the path is relative it will return a resource relative to the provided directory
* otherwise it will return a file based resource
*
* @param path relative or absolute path
* @param relativeDir directory to which relative paths are relative
* @return resource
*/
public static org.geoserver.platform.resource.Resource fromPath(String path,
org.geoserver.platform.resource.Resource relativeDir) {
File file = new File(path);
if (file.isAbsolute()) {
return Files.asResource(file);
} else {
return relativeDir.get(path.replace(File.separatorChar, '/'));
}
}
public static Resource createRandom(String prefix, String suffix, Resource dir)
throws IOException {
// Use only the file name from the supplied prefix
prefix = (new File(prefix)).getName();
Resource res;
do {
UUID uuid = UUID.randomUUID();
String name = prefix + uuid + suffix;
res = dir.get(name);
} while(exists(res));
return res;
}
/**
* Used to look up resources based on user provided url (or path) using the Data Directory as base directory.
*
* This method is used to process a URL provided
* by a user: <i>Given a path, tries to interpret it as a file into the data directory, or as an absolute
* location, and returns the actual absolute location of the file.</i>
*
* Over time this url method has grown in the telling to support:
* <ul>
* <li>Actual URL to external resource using http or ftp protocol - will return null</li>
* <li>Resource URL - will support resources from resource store</li>
* <li>File URL - will support absolute file references</li>
* <li>File URL - will support relative file references - this is deprecated, use resource: instead</li>
* <li>Fake URLs - sde://user:pass@server:port - will return null.</li>
* <li>path - user supplied file path (operating specific specific)</li>
* </ul>
*
* @param path File URL, or path, relative to data directory
* @return Resource indicated by provided URL
*/
public static Resource fromURL(String path) {
return ((GeoServerResourceLoader) GeoServerExtensions.bean("resourceLoader")).fromURL(path);
}
/**
* Used to look up resources based on user provided url (or path).
*
* This method is used to process a URL provided
* by a user: <i>iven a path, tries to interpret it as a file into the data directory, or as an absolute
* location, and returns the actual absolute location of the file.</i>
*
* Over time this url method has grown in the telling to support:
* <ul>
* <li>Actual URL to external resoruce using http or ftp protocol - will return null</li>
* <li>Resource URL - will support resources from resource store</li>
* <li>File URL - will support absolute file references</li>
* <li>File URL - will support relative file references - this is deprecated, use resource: instead</li>
* <li>Fake URLs - sde://user:pass@server:port - will return null.</li>
* <li>path - user supplied file path (operating specific specific)</li>
* </ul>
*
* Note that the baseDirectory is optional (and may be null).
*
* @param baseDirectory Optional base directory used to resolve relative file URLs
* @param url File URL or path relative to data directory
*
* @return Resource indicated by provided URL
*/
public static Resource fromURL(Resource baseDirectory, String url) {
String ss;
if ((ss = StringUtils.removeStart(url, "resource:")) != url) {
return baseDirectory.get(ss);
}
// if path looks like an absolute file: URL, try standard conversion
if (url.startsWith("file:/")) {
try {
return Files.asResource(DataUtilities.urlToFile(new URL(url)));
} catch (Exception e) {
// failure, so fall through
}
}
// do we ever have something that is not a file system reference?
// yes. See GEOS-5931: cases like sde://user:pass@server:port or
// pgraster://user:pass@server:port or similar custom store URLs.
if (url.startsWith("file:")) {
url = url.substring(5); // remove 'file:' prefix
File f = new File(url);
if (f.isAbsolute() || f.exists()) {
return Files.asResource(f); // if it's an absolute path, use it as such
} else {
// otherwise try to map it inside the data dir
if( baseDirectory != null ){
return baseDirectory.get(url);
}
return Files.asResource(f); // fine return it as is
}
} else {
// Treating 'url' as a normal file path
File file = new File(url);
if (file.isAbsolute() || file.exists()) {
return Files.asResource(file); // if it's an absolute path, use it as such
}
// otherwise try to map it inside the data dir
if( baseDirectory != null ){
Resource res = baseDirectory.get(url);
if( exists(res) ){
return res;
}
}
// Allows dealing with custom URL Strings. Don't return a file for them
return null;
}
}
/**
* Used to look up resources based on user provided url, using the Data Directory as base directory.
*
* Supports
* <ul>
* <li>Actual URL to external resource using http or ftp protocol - will return null</li>
* <li>Resource URL - will support resources from resource store</li>
* <li>File URL - will support absolute file references</li>
* <li>File URL - will support relative file references - this is deprecated, use resource: instead</li>
* <li>Fake URLs - sde://user:pass@server:port - will return null.</li>
* </ul>
* @param url the url
* @return corresponding Resource
*/
public static Resource fromURL(URL url) {
return ((GeoServerResourceLoader) GeoServerExtensions.bean("resourceLoader")).fromURL(url);
}
/**
* Used to look up a resource based on user provided url.
*
* Supports
* <ul>
* <li>Actual URL to external resource using http or ftp protocol - will return null</li>
* <li>Resource URL - will support resources from resource store</li>
* <li>File URL - will support absolute file references</li>
* <li>File URL - will support relative file references - this is deprecated, use resource: instead</li>
* <li>Fake URLs - sde://user:pass@server:port - will return null.</li>
* </ul>
* @param baseDirectory base directory for resource: or relative file: paths
* @param url the url
* @return corresponding Resource
*/
public static Resource fromURL(Resource baseDirectory, URL url) {
if(url.getProtocol().equalsIgnoreCase("resource")) {
return baseDirectory.get(Paths.convert(url.getPath()));
} else if (url.getProtocol().equalsIgnoreCase("file")){
return Files.asResource(DataUtilities.urlToFile(url));
} else {
return null;
}
}
/**
* Create a URL from a resource.
*
* @param res Resource to represent as a URL
* @return URL from an internal resource
*/
public static URL toURL(final Resource res) {
try {
if (res instanceof Files.ResourceAdaptor) {
return res.file().toURI().toURL();
}
if (res instanceof URIs.ResourceAdaptor) {
return ((URIs.ResourceAdaptor) res).getURL();
}
return new URL("resource", null, -1, String.format(res.getType()==Type.DIRECTORY?"/%s/":"/%s", res.path()),
new URLStreamHandler(){
@Override
protected URLConnection openConnection(URL u)
throws IOException {
return new URLConnection(u){
@Override
public void connect() throws IOException {
}
@Override
public long getLastModified() {
return res.lastmodified();
}
@Override
public InputStream getInputStream() throws IOException {
return res.in();
}
@Override
public OutputStream getOutputStream() throws IOException {
return res.out();
}
};
}
});
} catch (MalformedURLException e) {
throw new IllegalStateException("Should not happen",e);
}
}
/**
* Resource wrapper, serialization using resource path.
*/
private static class SerializableResourceWrapper implements Serializable, Resource {
private static final long serialVersionUID = 1758097257412707071L;
private transient Resource delegate;
private String path;
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
stream.defaultReadObject();
delegate = Resources.fromPath(path);
}
public SerializableResourceWrapper(Resource delegate) {
this.delegate = delegate;
path = delegate.path();
}
@Override
public String path() {
return path;
}
@Override
public String name() {
return delegate.name();
}
@Override
public Lock lock() {
return delegate.lock();
}
@Override
public void addListener(ResourceListener listener) {
delegate.addListener(listener);
}
@Override
public void removeListener(ResourceListener listener) {
delegate.removeListener(listener);
}
@Override
public InputStream in() {
return delegate.in();
}
@Override
public OutputStream out() {
return delegate.out();
}
@Override
public File file() {
return delegate.file();
}
@Override
public File dir() {
return delegate.dir();
}
@Override
public long lastmodified() {
return delegate.lastmodified();
}
@Override
public Resource parent() {
return new SerializableResourceWrapper(delegate.parent());
}
@Override
public Resource get(String resourcePath) {
return delegate.get(resourcePath);
}
@Override
public List<Resource> list() {
List<Resource> children = new ArrayList<Resource>();
for (Resource child : delegate.list()) {
children.add(new SerializableResourceWrapper(child));
}
return children;
}
@Override
public Type getType() {
return delegate.getType();
}
@Override
public boolean delete() {
return delegate.delete();
}
@Override
public boolean renameTo(Resource dest) {
return delegate.renameTo(dest);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof SerializableResourceWrapper)) {
return false;
}
return delegate.equals(((SerializableResourceWrapper) o).delegate);
}
@Override
public int hashCode() {
return delegate.hashCode();
}
}
public static Resource serializable(Resource resource) {
if (resource instanceof Serializable) {
return resource;
}
return new SerializableResourceWrapper(resource);
}
}