/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2010-2012, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.xml.resolver;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Logger;
import org.geotools.data.DataUtilities;
/**
* Cache containing XML schemas. (Should also work for other file types.)
*
* <p>
*
* If configured to permit downloading, schemas not present in the cache are downloaded from the network.
*
* <p>
*
* Only http/https URLs are supported.
*
* <p>
*
* Files are stored according to the Simple HTTP Resource Path (see {@link SchemaResolver#getSimpleHttpResourcePath(URI))}.
*
* @author Ben Caradoc-Davies (CSIRO Earth Science and Resource Engineering)
*
*
*
* @source $URL$
*/
public class SchemaCache {
private static final Logger LOGGER = org.geotools.util.logging.Logging
.getLogger(SchemaCache.class.getPackage().getName());
/**
* The default block read size used when downloading a file.
*/
private static final int DEFAULT_DOWNLOAD_BLOCK_SIZE = 4096;
/**
* This is the default value of the keep query flag used when building an automatically configured SchemaCache.
*/
private static final boolean DEFAULT_KEEP_QUERY = true;
/**
* Filenames used to recognise a GeoServer data directory if automatic configuration is enabled.
*/
private static final String[] GEOSERVER_DATA_DIRECTORY_FILENAMES = { "global.xml", "wcs.xml",
"wfs.xml", "wms.xml" };
/**
* Subdirectories used to recognise a GeoServer data directory if automatic configuration is enabled.
*/
private static final String[] GEOSERVER_DATA_DIRECTORY_SUBDIRECTORIES = { "styles",
"workspaces" };
/**
* Name of the subdirectory of a GeoServer data directory (or other directory) used for the cache if automatic configuration is enabled.
*/
private static final String CACHE_DIRECTORY_NAME = "app-schema-cache";
/**
* Is support for automatic detection of GeoServer data directories or existing cache directories enabled? It is useful to disable this in tests,
* to prevent downloading.
*/
private static boolean automaticConfigurationEnabled = true;
/**
* Root directory of the cache.
*/
private final File directory;
/**
* True if resources not found in the cache are downloaded from the net.
*/
private final boolean download;
/**
* True if query string components should be part of the discriminator for
*/
private final boolean keepQuery;
/**
* Default download timeout. Change it with -Dschema.cache.download.timeout=<milliseconds>
*/
private static int downloadTimeout = 60000;
static {
if(System.getProperty("schema.cache.download.timeout") != null) {
try {
downloadTimeout = Integer.parseInt(System.getProperty("schema.cache.download.timeout"));
} catch(NumberFormatException e) {
LOGGER.warning("schema.cache.download.timeout has a wrong format: should be a number");
}
}
}
/**
* A cache of XML schemas (or other file types) rooted in the given directory, with optional downloading.
*
* @param directory the directory in which downloaded schemas are stored
* @param download is downloading of schemas permitted. If false, only schemas already present in the cache will be resolved.
*/
public SchemaCache(File directory, boolean download) {
this(directory, download, false);
}
/**
* A cache of XML schemas (or other file types) rooted in the given directory, with optional downloading.
*
* @param directory the directory in which downloaded schemas are stored
* @param download is downloading of schemas permitted. If false, only schemas already present in the cache will be resolved.
* @param keepQuery indicates whether or not the query components should be included in the path. If this is set to true then the query portion is
* converted to an MD5 message digest and that string is used to identify the file in the cache.
*/
public SchemaCache(File directory, boolean download, boolean keepQuery) {
this.directory = directory;
this.download = download;
this.keepQuery = keepQuery;
}
/**
* Return the root directory of the cache.
*/
public File getDirectory() {
return directory;
}
/**
* Return the temp directory for not cached downloads (those
* occurring during another download, to avoid conflicts among threads).
*/
public File getTempDirectory() {
try {
File tempFolder = File.createTempFile("schema", "cache");
tempFolder.delete();
tempFolder.mkdirs();
return tempFolder;
} catch (IOException e) {
LOGGER.severe("Can't create temporary folder");
throw new RuntimeException(e);
}
}
/**
* Are schemas not already present in the cache downloaded from the network?
*/
public boolean isDownloadAllowed() {
return download;
}
/**
* Recursively delete a directory or file.
*
* @param file
*/
static void delete(File file) {
if (file.isDirectory()) {
for (File f : file.listFiles()) {
delete(f);
}
}
file.delete();
}
/**
* Store the bytes in the given file, creating any necessary intervening directories.
*
* @param file
* @param bytes
*/
static void store(File file, byte[] bytes) {
OutputStream output = null;
try {
if (file.getParentFile() != null && !file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
output = new BufferedOutputStream(new FileOutputStream(file));
output.write(bytes);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if (output != null) {
try {
output.close();
} catch (Exception e) {
// we tried
}
}
}
}
/**
* Retrieve the contents of a remote URL.
*
* @param location and absolute http/https URL.
* @return the bytes contained by the resource, or null if it could not be downloaded
*/
static byte[] download(String location) {
URI locationUri;
try {
locationUri = new URI(location);
} catch (URISyntaxException e) {
return null;
}
return download(locationUri);
}
/**
* Retrieve the contents of a remote URL.
*
* @param location and absolute http/https URL.
* @return the bytes contained by the resource, or null if it could not be downloaded
*/
static byte[] download(URI location) {
return download(location, DEFAULT_DOWNLOAD_BLOCK_SIZE);
}
/**
* Retrieve the contents of a remote URL.
*
* @param location and absolute http/https URL.
* @param blockSize download block size
* @return the bytes contained by the resource, or null if it could not be downloaded
*/
static byte[] download(URI location, int blockSize) {
try {
URL url = location.toURL();
String protocol = url.getProtocol();
if (protocol == null || !(protocol.equals("http") || protocol.equals("https"))) {
LOGGER.warning("Unexpected download URL protocol: " + protocol);
return null;
}
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(downloadTimeout);
connection.setReadTimeout(downloadTimeout);
connection.setUseCaches(false);
connection.connect();
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
LOGGER.warning(String.format("Unexpected response \"%d %s\" while downloading %s",
connection.getResponseCode(), connection.getResponseMessage(),
location.toString()));
return null;
}
// read all the blocks into a list
InputStream input = null;
List<byte[]> blocks = new LinkedList<byte[]>();
try {
input = connection.getInputStream();
while (true) {
byte[] block = new byte[blockSize];
int count = input.read(block);
if (count == -1) {
// end-of-file
break;
} else if (count == blockSize) {
// full block
blocks.add(block);
} else {
// short block
byte[] shortBlock = new byte[count];
System.arraycopy(block, 0, shortBlock, 0, count);
blocks.add(shortBlock);
}
}
} finally {
if (input != null) {
try {
input.close();
} catch (Exception e) {
// we tried
}
}
}
// concatenate all the blocks
int totalCount = 0;
for (byte[] b : blocks) {
totalCount += b.length;
}
byte[] bytes = new byte[totalCount];
int position = 0;
for (byte[] b : blocks) {
System.arraycopy(b, 0, bytes, position, b.length);
position += b.length;
}
return bytes;
} catch (Exception e) {
throw new RuntimeException("Error downloading location: "
+ location.toString(), e);
}
}
/**
* Return the local file URL of a schema, downloading it if not found in the cache.
*
* @param location the absolute http/https URL of the schema
* @return the canonical local file URL of the schema, or null if not found
*/
public String resolveLocation(String location) {
String path = SchemaResolver.getSimpleHttpResourcePath(location, this.keepQuery);
if (path == null) {
return null;
}
String relativePath = path.substring(1);
File file;
try {
file = new File(getDirectory(), relativePath).getCanonicalFile();
} catch (IOException e) {
throw new RuntimeException(e);
}
synchronized(SchemaCache.class) {
if(file.exists()) {
return DataUtilities.fileToURL(file).toExternalForm();
}
}
if (isDownloadAllowed()) {
byte[] bytes = download(location);
if (bytes == null) {
return null;
}
synchronized(SchemaCache.class) {
if(!file.exists()) {
store(file, bytes);
LOGGER.info("Cached XML schema: " + location);
}
return DataUtilities.fileToURL(file).toExternalForm();
}
}
return null;
}
/**
* If automatic configuration is enabled, recursively search parent directories of file url for a GeoServer data directory or directory containing
* an existing cache. If found, use it to create a cache in the "app-schema-cache" subdirectory with downloading enabled.
*
* @param url a URL for a file in a GeoServer data directory.
* @return a cache in the "app-schema-cache" subdirectory or null if not found or automatic configuration disabled.
*/
public static SchemaCache buildAutomaticallyConfiguredUsingFileUrl(URL url) {
if (!automaticConfigurationEnabled) {
return null;
}
File file = DataUtilities.urlToFile(url);
while (true) {
if (file == null) {
return null;
}
if (isSuitableDirectoryToContainCache(file)) {
return new SchemaCache(new File(file, CACHE_DIRECTORY_NAME), true,
DEFAULT_KEEP_QUERY);
}
file = file.getParentFile();
}
}
/**
* Turn off support for automatic configuration of a cache in GeoServer data directory or detection of an existing cache. Intended for testing.
* Automatic configuration is enabled by default.
*/
public static void disableAutomaticConfiguration() {
automaticConfigurationEnabled = false;
}
/**
* The opposite of {@link #disableAutomaticConfiguration()}. Automatic configuration is enabled by default.
*/
public static void enableAutomaticConfiguration() {
automaticConfigurationEnabled = true;
}
/**
* Is automatic configuration enabled? Automatic configuration is enabled by default.
*
* @see #disableAutomaticConfiguration()
*/
public static boolean isAutomaticConfigurationEnabled() {
return automaticConfigurationEnabled;
}
/**
* Guess whether a file is a GeoServer data directory or contains an existing app-schema-cache subdirectory.
*
* @param directory the candidate file
* @return true if it has the files and subdirectories expected of a GeoServer data directory, or contains an existing app-schema-cache
* subdirectory
*/
static boolean isSuitableDirectoryToContainCache(File directory) {
if (directory.isDirectory() == false) {
return false;
}
if ((new File(directory, CACHE_DIRECTORY_NAME)).isDirectory()) {
return true;
}
for (String filename : GEOSERVER_DATA_DIRECTORY_FILENAMES) {
File file = new File(directory, filename);
if (!file.isFile()) {
return false;
}
}
for (String subdirectory : GEOSERVER_DATA_DIRECTORY_SUBDIRECTORIES) {
File dir = new File(directory, subdirectory);
if (!dir.isDirectory()) {
return false;
}
}
return true;
}
}