/* (c) 2015 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.csw;
import java.io.File;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.io.FilenameUtils;
import org.geoserver.catalog.CatalogInfo;
import org.geoserver.catalog.CoverageInfo;
import org.geoserver.ows.Dispatcher;
import org.geoserver.ows.Request;
import org.geoserver.ows.URLMangler.URLType;
import org.geoserver.ows.util.ResponseUtils;
import org.geotools.coverage.grid.io.GridCoverage2DReader;
import org.geotools.data.CloseableIterator;
import org.geotools.data.FileGroupProvider.FileGroup;
import org.geotools.data.FileResourceInfo;
import org.geotools.data.ResourceInfo;
import org.geotools.factory.GeoTools;
import org.geotools.gce.imagemosaic.Utils;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.util.DateRange;
import org.geotools.util.NumberRange;
import org.geotools.util.Range;
import org.geotools.util.logging.Logging;
/**
* Class delegated to setup direct download links for a {@link CatalogInfo}
* instance.
*/
public class DownloadLinkHandler {
private static Set<String> STANDARD_DOMAINS;
public final static String RESOURCE_ID_PARAMETER = "resourceId";
public final static String FILE_PARAMETER = "file";
public final static String FILE_TEMPLATE = "${" + FILE_PARAMETER + "}";
static final Logger LOGGER = Logging.getLogger(DownloadLinkHandler.class);
static {
STANDARD_DOMAINS = new HashSet<String>();
STANDARD_DOMAINS.add(Utils.TIME_DOMAIN);
STANDARD_DOMAINS.add(Utils.ELEVATION_DOMAIN);
STANDARD_DOMAINS.add(Utils.BBOX);
}
/** An implementation of {@link CloseableIterator} for links creation */
static class CloseableLinksIterator<T> implements CloseableIterator<String> {
private static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
private SimpleDateFormat dateFormat = null;
public CloseableLinksIterator(String baseLink, CloseableIterator<FileGroup> dataIterator) {
this.dataIterator = dataIterator;
this.baseLink = baseLink;
}
private String baseLink;
/** The underlying iterator providing files */
private CloseableIterator<FileGroup> dataIterator;
@Override
public boolean hasNext() {
return dataIterator.hasNext();
}
@Override
public void remove() {
throw new UnsupportedOperationException("Remove operation isn't supported");
}
@Override
public String next() {
// Get the file from the underlying iterator
FileGroup element = dataIterator.next();
File mainFile = element.getMainFile();
String canonicalPath = null;
try {
canonicalPath = mainFile.getCanonicalPath();
// Hash the file and setup the download link
String hashFile = hashFile(mainFile);
StringBuilder builder = new StringBuilder(baseLink.replace(FILE_TEMPLATE, hashFile));
Map<String, Object> metadata = element.getMetadata();
if (metadata != null && !metadata.isEmpty()) {
Set<String> keys = metadata.keySet();
// Set bbox in the link
if (keys.contains(Utils.BBOX)) {
Object bbox = metadata.get(Utils.BBOX);
appendBBOXToLink((ReferencedEnvelope) bbox, builder);
}
// Set time and elevation as first domain elements in the link
if (keys.contains(Utils.TIME_DOMAIN)) {
Object time = metadata.get(Utils.TIME_DOMAIN);
appendRangeToLink(Utils.TIME_DOMAIN, time, builder);
}
if (keys.contains(Utils.ELEVATION_DOMAIN)) {
Object elevation = metadata.get(Utils.ELEVATION_DOMAIN);
appendRangeToLink(Utils.ELEVATION_DOMAIN, elevation, builder);
}
for (String key: keys) {
if (!STANDARD_DOMAINS.contains(key)) {
Object additional = metadata.get(key);
appendRangeToLink(key, additional, builder);
}
}
}
return builder.toString();
} catch (IOException e) {
throw new RuntimeException("Unable to encode the specified file:" + canonicalPath,
e.getCause());
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Unable to encode the specified file:" + canonicalPath,
e.getCause());
}
}
/**
* Append the BBOX parameter to the directDownload link
* @param envelope
* @param builder
*/
private void appendBBOXToLink(ReferencedEnvelope envelope, StringBuilder builder) {
if (envelope == null) {
throw new IllegalArgumentException("Envelope can't be null");
}
builder.append("&").append(Utils.BBOX).append("=")
.append(envelope.getMinX()).append(",")
.append(envelope.getMinY()).append(",")
.append(envelope.getMaxX()).append(",")
.append(envelope.getMaxY());
}
/**
* Append a coverage domain (time, elevation, custom) to the direct download link.
* @param key the name of the parameter domain to be added
* @param domain the value of the domain
* @param builder the builder currently used for Link construction
*/
private void appendRangeToLink(String key, Object domain, StringBuilder builder) {
builder.append("&").append(key).append("=");
if (domain instanceof DateRange) {
// instantiate a new DateFormat instead of using a static one since
// it's not thread safe
if (dateFormat == null) {
dateFormat = new SimpleDateFormat(DATE_FORMAT);
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
}
DateRange dateRange = (DateRange) domain;
builder.append(dateFormat.format(dateRange.getMinValue()))
.append("/").append(dateFormat.format(dateRange.getMaxValue()));
} else if (domain instanceof NumberRange) {
NumberRange numberRange = (NumberRange) domain;
builder.append(numberRange.getMinValue())
.append("/").append(numberRange.getMaxValue());
} else if (domain instanceof Range) {
// Generic range
Range range = (Range) domain;
builder.append(range.getMinValue())
.append("/").append(range.getMaxValue());
} else {
throw new IllegalArgumentException("Domain " + domain + " isn't supported");
}
}
@Override
public void close() throws IOException {
dataIterator.close();
}
}
/** Template download link to be updated with actual values */
protected static String LINK = "ows?service=CSW&version=${version}&request="
+ "DirectDownload&" + RESOURCE_ID_PARAMETER + "=${nameSpace}:${layerName}&"
+ FILE_PARAMETER + "=" + FILE_TEMPLATE;
/**
* Generate download links for the specified info object.
*
* @param info
*
*/
public CloseableIterator<String> generateDownloadLinks(CatalogInfo info) {
Request request = Dispatcher.REQUEST.get();
String baseURL = null;
// Retrieve the baseURL (something like: http://host:port/geoserver/...)
try {
if (baseURL == null) {
baseURL = ResponseUtils.baseURL(request.getHttpRequest());
}
baseURL = ResponseUtils.buildURL(baseURL, "/", null, URLType.SERVICE);
} catch (Exception e) {
}
if (baseURL == null) {
throw new IllegalArgumentException("baseURL is required to create download links");
}
baseURL += LINK;
baseURL = baseURL.replace("${version}", request.getVersion());
if (info instanceof CoverageInfo) {
return linksFromCoverage(baseURL, (CoverageInfo) info);
} else {
if (LOGGER.isLoggable(Level.WARNING)) {
LOGGER.warning("Download link for vectors isn't supported."
+ " Returning null");
}
}
return null;
}
/**
* Return an {@link Iterator} containing {@link String}s representing
* the downloadLinks associated to the provided {@link CoverageInfo} object.
*
* @param baseURL
* @param coverageInfo
*
*/
protected CloseableIterator<String> linksFromCoverage(String baseURL, CoverageInfo coverageInfo) {
GridCoverage2DReader reader;
try {
reader = (GridCoverage2DReader) coverageInfo.getGridCoverageReader(null,
GeoTools.getDefaultHints());
String name = DirectDownload.extractName(coverageInfo);
if (reader == null) {
throw new IllegalArgumentException ("No reader available for the specified coverage: " + name);
}
ResourceInfo resourceInfo = reader.getInfo(name);
if (resourceInfo instanceof FileResourceInfo) {
FileResourceInfo fileResourceInfo = (FileResourceInfo) resourceInfo;
// Replace the template URL with proper values
String baseLink = baseURL
.replace("${nameSpace}", coverageInfo.getNamespace().getName())
.replace("${layerName}", coverageInfo.getName());
CloseableIterator<org.geotools.data.FileGroupProvider.FileGroup> dataIterator = fileResourceInfo
.getFiles(null);
return new CloseableLinksIterator(baseLink, dataIterator);
} else {
throw new RuntimeException("Donwload links handler need to provide "
+ "download links to files. The ResourceInfo associated with the store should be a FileResourceInfo instance");
}
} catch (IOException e) {
throw new RuntimeException("Unable to generate download links", e.getCause());
}
}
/**
* Return a SHA-1 based hash for the specified file, by appending the file's base name to the hashed full path. This allows to hide the underlying
* file system structure.
*/
public static String hashFile(File mainFile) throws IOException, NoSuchAlgorithmException {
String canonicalPath = mainFile.getCanonicalPath();
String mainFilePath = FilenameUtils.getPath(canonicalPath);
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(mainFilePath.getBytes());
return Hex.encodeHexString(md.digest()) + "-" + mainFile.getName();
}
/**
* Given a file download link, extract the link with no file references, used to
* request the full layer download.
*
*
*/
public String extractFullDownloadLink(String link) {
int resourceIdIndex = link.indexOf(RESOURCE_ID_PARAMETER);
int nextParamIndex = link.indexOf("&" + FILE_PARAMETER, resourceIdIndex);
return link.substring(0, nextParamIndex);
}
}