/* (c) 2014 - 2016 Open Source Geospatial Foundation - all rights reserved * (c) 2001 - 2013 OpenPlans * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.platform; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.servlet.ServletContext; import org.apache.commons.io.IOUtils; import org.geoserver.platform.resource.FileSystemResourceStore; import org.geoserver.platform.resource.Files; import org.geoserver.platform.resource.Paths; import org.geoserver.platform.resource.Resource; import org.geoserver.platform.resource.ResourceNotificationDispatcher; import org.geoserver.platform.resource.ResourceStore; import org.geoserver.platform.resource.Resources; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.web.context.ServletContextAware; /** * Access to resources in GeoServer including configuration information and unmanaged cache or log files. * <p> * The loader maintains a search path in which it will use to look up resources. * </p> * <ul> * <li>Configuration is accessed using {@link ResourceStore#get(String)} which provides stream based access. If required configuration can be unpacked * into a file in the data directory. The most common example is for use as a template. * <li>Files in the data directory can also be used as a temporary cache. These files should be considered temporary and may need to be recreated * (when upgrading or for use on different nodes in a cluster).</li> * <li> * </ul> * <p> * The {@link #baseDirectory} is a member of this path. Files and directories created by the resource loader are made relative to * {@link #baseDirectory}. * </p> * * <pre> * <code> * File dataDirectory = ... * GeoServerResourceLoader loader = new GeoServerResourceLoader( dataDirectory ); * ... * Resource catalog = loader.get("catalog.xml"); * File log = loader.find("logs/geoserver.log"); * </code> * </pre> * * @author Justin Deoliveira, The Open Planning Project, jdeolive@openplans.org * */ public class GeoServerResourceLoader extends DefaultResourceLoader implements ResourceStore, ServletContextAware { private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.vfny.geoserver.global"); static { LOGGER.setLevel(Level.FINER); } /** * ResourceStore used for configuration resources. * * Initially this is configured to access resources in the base directory, however spring may inject an external implementation (jdbc database * blob, github, ...). */ ResourceStore resources; /** * Base directory used to access unmanaged files. */ File baseDirectory; /** * Creates a new resource loader (with no base directory). * <p> * Used to construct a GeoServerResourceLoader for test cases (and is unable to create resources from relative paths. * </p> */ public GeoServerResourceLoader() { baseDirectory = null; resources = ResourceStore.EMPTY; } /** * Creates a new resource loader. * * @param baseDirectory The directory in which */ public GeoServerResourceLoader(File baseDirectory) { this.baseDirectory = baseDirectory; this.resources = new FileSystemResourceStore( baseDirectory ); } /** * Creates a new resource loader. * * @param resourceStore resource store for artifact storage */ public GeoServerResourceLoader(ResourceStore resourceStore) { this.resources = resourceStore; } @Override public void setServletContext(ServletContext servletContext) { if (baseDirectory == null) { String data = lookupGeoServerDataDirectory(servletContext); if (data != null) { this.baseDirectory = new File(data); } else { throw new IllegalStateException("Unable to determine data directory"); } } if (resources == ResourceStore.EMPTY && baseDirectory != null) { // lookup the configuration resources resources = new FileSystemResourceStore(baseDirectory); } } /** * Adds a location to the path used for resource lookups. * * @param searchLocation directory containing resources. * @deprecated No longer used */ public void addSearchLocation(File searchLocation) { //searchLocations.add(searchLocation); } /** * Sets the search locations used for resource lookups. * * @param searchLocations A set of {@link File}. * @deprecated No longer used */ public void setSearchLocations(Set<File> searchLocations) { } /** * @return The base directory. */ public File getBaseDirectory() { return baseDirectory; } /** * Sets the base directory. * * @param baseDirectory base of data directory used for file configuration files */ public void setBaseDirectory(File baseDirectory) { this.baseDirectory = baseDirectory; if (resources == ResourceStore.EMPTY) { resources = new FileSystemResourceStore(baseDirectory); } } @Override public Resource get(String path) { return resources.get(path); } @Override public boolean move(String path, String target) { return resources.move(path, target); } @Override public boolean remove(String path) { return resources.remove( path ); } /** * Used to look up resources based on user provided url (or path) using the Data Directory as base directory. * * Convenience method for Resources.fromURL(resources.get(Paths.BASE), url) * * See {@link Resources#fromURL(Resource, String)} * */ public Resource fromURL(String url) { return Resources.fromURL(resources.get(Paths.BASE), url); } /** * Used to look up resources based on user provided url using the Data Directory as base directory. * * Convenience method for Resources.fromURL(resources.get(Paths.BASE), url) * * See {@link Resources#fromURL(Resource, URL)} * */ public Resource fromURL(URL url) { return Resources.fromURL(resources.get(Paths.BASE), url); } /** * Used to look up resources based on user provided path using the Data Directory as base directory. * * Convenience method for Resources.fromPath(resources.get(Paths.BASE), path) * * See {@link Resources#fromPath(String, Resource)} * */ public Resource fromPath(String path) { return Resources.fromPath(path, resources.get(Paths.BASE)); } /** * * @deprecated use {@link Resources#fromURL(Resource, String)} */ @Deprecated public File url(String url) { return Files.url(baseDirectory, url); } /** * Performs file lookup. * * @param location The name of the resource to lookup, can be absolute or * relative. * * @return The file handle representing the resource, or null if the * resource could not be found. * * @throws IOException In the event of an I/O error. */ public File find( String location ) throws IOException { Resource resource = get( Paths.convert(location) ); return Resources.find( resource ); } /** * Performs a resource lookup, optionally specifying the containing directory. * * @param parentFile The containing directory, optionally null. * @param location The name of the resource to lookup, can be absolute or * relative. * * @return The file handle representing the resource, or null if the * resource could not be found. * * @throws IOException In the event of an I/O error. */ public File find(File parentFile, String location) throws IOException { if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest("Looking up resource " + location + " with parent " + (parentFile != null ? parentFile.getPath() : "null")); } Resource resource = get( Paths.convert(getBaseDirectory(), parentFile, location )); return Resources.find( resource ); } /** * Performs a resource lookup. * * <pre> * Example: * File f = resourceLoader.find( "data", "shapefiles", "foo.shp" ); * </pre> * * @param location The components of the path of the resource to lookup. * @return The file handle representing the resource, or null if the resource could not be found. * * @throws IOException Any I/O errors that occur. */ public File find( String... location ) throws IOException { Resource resource = get( Paths.path(location) ); return Resources.find( resource ); } /** * Performs a resource lookup, optionally specifying a containing directory. * * <pre> * Example: * File f = resourceLoader.find( "data", "shapefiles", "foo.shp" ); * </pre> * * @param parentFile The parent directory, may be null. * @param location The components of the path of the resource to lookup. * @return The file handle representing the resource, or null if the resource could not be found. * * @throws IOException Any I/O errors that occur. */ public File find( File parentFile, String... location ) throws IOException { Resource resource = get(Paths.convert(getBaseDirectory(), parentFile, location)); return Resources.find(resource); } /** * Helper method to build up a file path from components. */ String concat( String... location ) { StringBuffer loc = new StringBuffer(); for ( int i = 0; i < location.length; i++ ) { loc.append( location[i] ).append( File.separator ); } loc.setLength(loc.length()-1); return loc.toString(); } /** * Performs a directory lookup, creating the file if it does not exist. * * @param location The components of the path that make up the location of the directory to * find or create. */ public File findOrCreateDirectory( String... location ) throws IOException { Resource directory = get( Paths.path(location) ); return directory.dir(); // will create directory as needed } /** * Performs a directory lookup, creating the file if it does not exist. * * @param parentFile The containing directory, possibly null. * @param location The components of the path that make up the location of the directory to * find or create. */ public File findOrCreateDirectory( File parentFile, String... location ) throws IOException { Resource directory = get(Paths.convert(getBaseDirectory(), parentFile, location)); return directory.dir(); // will create directory as needed } /** * Performs a directory lookup, creating the file if it does not exist. * * @param location The location of the directory to find or create. * * @return The file handle. * * @throws IOException If any i/o errors occur. */ public File findOrCreateDirectory( String location ) throws IOException { Resource directory = get( Paths.convert(location) ); return directory.dir(); // will create directory as needed } /** * Performs a directory lookup, creating the file if it does not exist. * * @param parentFile The containing directory, may be null. * @param location The location of the directory to find or create. * * @return The file handle. * * @throws IOException If any i/o errors occur. */ public File findOrCreateDirectory( File parentFile, String location ) throws IOException { Resource directory = get( Paths.convert(getBaseDirectory(),parentFile,location) ); return directory.dir(); // will create directory as needed } /** * Creates a new directory specifying components of the location. * <p> * Calls through to {@link #createDirectory(String)} * </p> */ public File createDirectory(String... location) throws IOException { Resource directory = get( Paths.path(location) ); return Resources.createNewDirectory(directory); } /** * Creates a new directory specifying components of the location, and the containing directory. * <p> * Calls through to {@link #createDirectory(String)} * </p> * @param parentFile The containing directory, possibly null. * @param location The components of the path that make up the location of the directory to create * @return newly created directory */ public File createDirectory(File parentFile, String... location) throws IOException { Resource directory = get(Paths.convert(getBaseDirectory(), parentFile, location)); return Resources.createNewDirectory(directory); } /** * Creates a new directory. * <p> * Relative paths are created relative to {@link #baseDirectory}. * If {@link #baseDirectory} is not set, an IOException is thrown. * </p> * <p> * If <code>location</code> already exists as a file, an IOException is thrown. * </p> * @param location Location of directory to create, either absolute or * relative. * * @return The file handle of the created directory. */ public File createDirectory(String location) throws IOException { Resource directory = get( Paths.convert(location) ); return Resources.createNewDirectory(directory); } /** * Creates a new directory, optionally specifying a containing directory. * <p> * Relative paths are created relative to {@link #baseDirectory}. * If {@link #baseDirectory} is not set, an IOException is thrown. * </p> * <p> * If <code>location</code> already exists as a file, an IOException is thrown. * </p> * @param parentFile The containing directory, may be null. * @param location Location of directory to create, either absolute or * relative. * * @return The file handle of the created directory. */ public File createDirectory(File parentFile, String location) throws IOException { Resource directory = get(Paths.convert(getBaseDirectory(), parentFile, location)); return Resources.createNewDirectory(directory); } /** * Creates a new file. * <p> * Calls through to {@link #createFile(String)}. * </p> * * @param location The components of the location. * * @return The file handle of the created file. * * @throws IOException In the event of an I/O error. */ public File createFile(String ...location) throws IOException { Resource resource = get( Paths.path(location) ); return Resources.createNewFile( resource ); } /** * Creates a new file. * <p> * Calls through to {@link #createFile(File, String)} * </p> * @param location Location of file to create, either absolute or relative. * * @return The file handle of the created file. * * @throws IOException In the event of an I/O error. */ public File createFile(String location) throws IOException { Resource resource = get( Paths.convert(location) ); return Resources.createNewFile( resource ); } /** * Creates a new file. * <p> * Calls through to {@link #createFile(File, String)} * </p> * @param location Location of file to create, either absolute or relative. * @param parentFile The containing directory for the file. * * @return The file handle of the created file. * * @throws IOException In the event of an I/O error. */ public File createFile(File parentFile, String... location) throws IOException{ Resource resource = get( Paths.convert(getBaseDirectory(), parentFile, location )); return Resources.createNewFile(resource); } /** * Creates a new file. * <p> * Relative paths are created relative to {@link #baseDirectory}. * </p> * <p> * If {@link #baseDirectory} is not set, an IOException is thrown. * </p> * <p> * If <code>location</code> already exists as a directory, an IOException is thrown. * </p> * * @param location Location of file to create, either absolute or relative. * @param parentFile The containing directory for the file. * * @return The file handle of the created file. * * @throws IOException In the event of an I/O error. */ public File createFile(File parentFile, String location) throws IOException{ Resource resource = get( Paths.convert(getBaseDirectory(), parentFile, location )); return Resources.createNewFile(resource); } /** * Copies a resource located on the classpath to a specified path. * <p> * The <tt>resource</tt> is obtained from teh context class loader of the * current thread. When the <tt>to</tt> parameter is specified as a relative * path it is considered to be relative to {@link #getBaseDirectory()}. </p> * * @param classpathResource The resource to copy. * @param location The destination to copy to. */ public void copyFromClassPath( String classpathResource, String location ) throws IOException { Resource resource = get(Paths.convert(location)); copyFromClassPath( classpathResource, resource.file() ); } /** * Copies a resource from the classpath to a specified file. * * @param classpathResource Path to classpath content to be copied * @param target File to copy content into (must be already created) */ public void copyFromClassPath( String classpathResource, File target ) throws IOException { copyFromClassPath( classpathResource, target, null ); } /** * Copies a resource relative to a particular class from the classpath to the specified file. * * @param classpathResource Path to classpath content to be copied * @param target File to copy content into (must be already created) * @param scope Class used as base for classpathResource */ public void copyFromClassPath( String classpathResource, File target, Class<?> scope ) throws IOException { InputStream is = null; OutputStream os = null; byte[] buffer = new byte[4096]; int read; try{ // Get the resource if (scope == null) { is = Thread.currentThread().getContextClassLoader().getResourceAsStream(classpathResource); if(is==null) { throw new IOException("Could not load " + classpathResource + " from scope "+ Thread.currentThread().getContextClassLoader().toString()+"."); } } else { is = scope.getResourceAsStream(classpathResource); if(is==null) { throw new IOException("Could not load " + classpathResource + " from scope "+ scope.toString()+"."); } } // Write it to the target try { os = new FileOutputStream(target); while((read = is.read(buffer)) > 0) os.write(buffer, 0, read); } catch (FileNotFoundException targetException) { throw new IOException("Can't write to file " + target.getAbsolutePath() + ". Check write permissions on target folder for user " + System.getProperty("user.name")); } catch (IOException e) { LOGGER.log(Level.WARNING, "Error trying to copy logging configuration file", e); } } finally { // Clean up IOUtils.closeQuietly(is); IOUtils.closeQuietly(os); } } /** * Determines the location of the geoserver data directory based on the following lookup * mechanism: * * 1) Java environment variable * 2) Servlet context variable * 3) System variable * * For each of these, the methods checks that * 1) The path exists * 2) Is a directory * 3) Is writable * * @param servContext The servlet context. * @return String The absolute path to the data directory, or <code>null</code> if it could not * be found. */ public static String lookupGeoServerDataDirectory(ServletContext servContext) { final String[] typeStrs = { "Java environment variable ", "Servlet context parameter ", "System environment variable " }; String requireFileVar = "GEOSERVER_REQUIRE_FILE"; requireFile(System.getProperty(requireFileVar), typeStrs[0] + requireFileVar); requireFile(servContext.getInitParameter(requireFileVar), typeStrs[1] + requireFileVar); requireFile(System.getenv(requireFileVar), typeStrs[2] + requireFileVar); final String[] varStrs = { "GEOSERVER_DATA_DIR", "GEOSERVER_DATA_ROOT" }; String dataDirStr = null; String msgPrefix = null; // Loop over variable names for (int i = 0; i < varStrs.length && dataDirStr == null; i++) { // Loop over variable access methods for (int j = 0; j < typeStrs.length && dataDirStr == null; j++) { String value = null; String varStr = varStrs[i]; String typeStr = typeStrs[j]; // Lookup section switch (j) { case 0: value = System.getProperty(varStr); break; case 1: value = servContext.getInitParameter(varStr); break; case 2: value = System.getenv(varStr); break; } if (value == null || value.equalsIgnoreCase("")) { LOGGER.finer("Found " + typeStr + varStr + " to be unset"); continue; } // Verify section File fh = new File(value); // Being a bit pessimistic here msgPrefix = "Found " + typeStr + varStr + " set to " + value; if (!fh.exists()) { LOGGER.warning(msgPrefix + " , but this path does not exist"); continue; } if (!fh.isDirectory()) { LOGGER.warning(msgPrefix + " , which is not a directory"); continue; } if (!fh.canWrite()) { LOGGER.warning(msgPrefix + " , which is not writeable"); continue; } // Sweet, we can work with this dataDirStr = value; } } // fall back to embedded data dir if(dataDirStr == null) { dataDirStr = servContext.getRealPath("/data"); LOGGER.info("Falling back to embedded data directory: " + dataDirStr); } return dataDirStr; } /** * Check that required files exist and throw {@link IllegalArgumentException} if they do not. * * @param files either a single file name or a list of file names separated by {@link File#pathSeparator} * @param source description of source from which file name(s) obtained */ static void requireFile(String files, String source) { if (files == null || files.isEmpty()) { return; } else { for (String file : files.split(File.pathSeparator)) { if (!(new File(file)).exists()) { throw new IllegalArgumentException( "Missing required file: " + file + " From: " + source + ": " + files); } } } } @Override public ResourceNotificationDispatcher getResourceNotificationDispatcher() { return resources.getResourceNotificationDispatcher(); } public ResourceStore getResourceStore() { return resources; } }