/* (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.NoSuchAlgorithmException; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import org.geoserver.catalog.CoverageInfo; import org.geoserver.config.GeoServer; import org.geoserver.csw.store.CatalogStore; import org.geoserver.platform.ServiceException; import org.geotools.coverage.grid.io.GridCoverage2DReader; import org.geotools.coverage.grid.io.StructuredGridCoverage2DReader; import org.geotools.data.CloseableIterator; import org.geotools.data.FileGroupProvider; import org.geotools.data.FileGroupProvider.FileGroup; import org.geotools.data.FileResourceInfo; import org.geotools.data.FileServiceInfo; import org.geotools.data.Query; import org.geotools.data.ResourceInfo; import org.geotools.data.ServiceInfo; import org.geotools.factory.GeoTools; import org.geotools.feature.NameImpl; import org.geotools.resources.coverage.FeatureUtilities; import org.geotools.util.logging.Logging; import org.opengis.feature.type.Name; import org.opengis.filter.FilterFactory2; /** * Runs the DirectDownload request * * @author Daniele Romagnoli - GeoSolutions */ public class DirectDownload { private final static FilterFactory2 FF = FeatureUtilities.DEFAULT_FILTER_FACTORY; /** * Files collector class which populates a {@link File}s {@link List} * by accessing a {@link FileGroupProvider} instance. */ class FilesCollector { public FilesCollector(FileGroupProvider fileGroupProvider) { this.fileGroupProvider = fileGroupProvider; } /** The underlying FileGroupProvider used to collect the files */ private FileGroupProvider fileGroupProvider; /** * Only collect the subset of files available from the fileGroupProvider, * which match the provided fileId. * * a File Identifier is composed of "hash-baseName". * Only the files having same baseName and matching hash will be added to the list */ private void collectSubset(String fileId, List<File> result) { CloseableIterator<FileGroup> files = null; try { String hash = fileId; // SHA-1 are 20 bytes in length String fileBaseName = hash.substring(41); Query query = new Query(); // Look for files in the catalog having the same base name query.setFilter(FF.like(FF.property("location"), "*" + fileBaseName + "*")); files = fileGroupProvider.getFiles(query); while (files.hasNext()) { FileGroup fileGroup = files.next(); File mainFile = fileGroup.getMainFile(); String hashedName = DownloadLinkHandler.hashFile(mainFile); // Only files fully matching the current hash will // be added to the download list if (hash.equalsIgnoreCase(hashedName)) { result.add(mainFile); List<File> supportFile = fileGroup.getSupportFiles(); if (supportFile != null && !supportFile.isEmpty()) { result.addAll(supportFile); } } } } catch (NoSuchAlgorithmException e) { throw new ServiceException("Exception occurred while looking for raw files for :" + fileId, e); } catch (IOException e) { throw new ServiceException("Exception occurred while looking for raw files for :" + fileId, e); } finally { closeIterator(files); } } /** * Collect all files from the fileGroupProvider */ void collectFull(List<File> result) { CloseableIterator<FileGroup> files = null; try { files = fileGroupProvider.getFiles(null); while (files.hasNext()) { FileGroup fileGroup = files.next(); result.add(fileGroup.getMainFile()); List<File> supportFile = fileGroup.getSupportFiles(); if (supportFile != null && !supportFile.isEmpty()) { result.addAll(supportFile); } } } finally { closeIterator(files); } } } private final static int KILO = 1024; private final static int MEGA = KILO * KILO; private final static int GIGA = MEGA * KILO; static final Logger LOGGER = Logging.getLogger(DirectDownload.class); static final String LIMIT_MESSAGE = "This request is trying to download too much data. "; CSWInfo csw; CatalogStore store; GeoServer geoserver; public DirectDownload(CSWInfo csw, CatalogStore store) { this.csw = csw; this.store = store; this.geoserver = csw.getGeoServer(); } /** * Prepare the list of files to be downloaded from the current request. * @param request * */ public List<File> run(DirectDownloadType request) { List<File> result = new ArrayList<File>(); String resourceId = request.getResourceId(); String fileId = request.getFile(); // Extract namespace, layername from the resourceId String [] identifiers = resourceId.split(":"); assert(identifiers.length == 2); String nameSpace = identifiers[0]; String layerName = identifiers[1]; Name coverageName = new NameImpl(nameSpace, layerName); // Get the underlying coverage from the catalog CoverageInfo info = geoserver.getCatalog().getCoverageByName(coverageName); if (info == null) { throw new ServiceException("No object available for the specified name:" + coverageName); } // Get the reader to access the coverage GridCoverage2DReader reader; try { reader = (GridCoverage2DReader) info.getGridCoverageReader(null, GeoTools.getDefaultHints()); } catch (IOException e) { throw new ServiceException("Failed to get a reader for the associated info: " + info, e); } // Get resources for the specified file String name = extractName(info); getFileResources(reader, name, fileId, result); // Only StructuredGridCoverage2DReader can deal with multiple coverages // standard readers return same content for FileInfo and ResourceInfo if (fileId == null && reader instanceof StructuredGridCoverage2DReader) { // Add the serviceInfo content to the returned files // (As an instance, shapefile index, indexers, property files...) getExtraFiles(reader, result); } if (result == null || result.isEmpty()) { throw new ServiceException("Unable to get any data for resourceId=" + resourceId + " and file=" + fileId); } checkSizeLimit(result, info); return result; } /** * Get extra files for the specified reader and add them to the result list. * Extra files are usually auxiliary files like, as an instance, * indexer, properties, config files for a mosaic. * @param reader * @param result */ private void getExtraFiles(GridCoverage2DReader reader, List<File> result) { ServiceInfo info = reader.getInfo(); if (info instanceof FileServiceInfo) { FileServiceInfo fileInfo = (FileServiceInfo) info; FileGroupProvider provider = (FileGroupProvider) fileInfo; FilesCollector collector = new FilesCollector(provider); collector.collectFull(result); } else { throw new ServiceException("Unable to get files from the specified ServiceInfo which" + " doesn't implement FileServiceInfo"); } } /** * Get the data files from the specified {@link GridCoverage2DReader}, related to the * provided coverageName, matching the specified fileId and add them to the result list. * @param reader * @param coverageName * @param fileId * @param result */ private void getFileResources(GridCoverage2DReader reader, String coverageName, String fileId, List<File> result) { ResourceInfo resourceInfo = reader.getInfo(coverageName); if (resourceInfo instanceof FileResourceInfo) { FileResourceInfo fileResourceInfo = (FileResourceInfo) resourceInfo; // Get the resource files FileGroupProvider fileGroupProvider = (FileGroupProvider) fileResourceInfo; FilesCollector collector = new FilesCollector(fileGroupProvider); // Only structuredReaders can support multiple coverages // Standard readers deal with one coverage if (reader instanceof StructuredGridCoverage2DReader && fileId != null) { collector.collectSubset(fileId, result); } else { // Simple reader case. collector.collectFull(result); } } else { throw new ServiceException("Unable to get files from the specified ResourceInfo which" + " doesn't implement FileResourceInfo"); } } /** * Check the current download is not exceeding the maxDownloadSize limit (if activated). * Throws a {@link CSWException} in case the limit is exceeded * @param info */ private void checkSizeLimit(List<File> fileList, CoverageInfo info) { DirectDownloadSettings settings = DirectDownloadSettings.getSettingsFromMetadata(info.getMetadata(), csw); long maxSize = settings != null ? settings.getMaxDownloadSize() : 0; long sizeLimit = maxSize * 1024; if (fileList != null && !fileList.isEmpty() && sizeLimit > 0) { long cumulativeSize = 0; for (File file : fileList) { cumulativeSize += file.length(); } if (cumulativeSize > sizeLimit) { throw new CSWException(LIMIT_MESSAGE + "The limit is " + formatBytes(sizeLimit) + " but the amount of raw data to be downloaded is " + formatBytes(cumulativeSize)); } } } /** * Format a size in a human readable way */ static String formatBytes(long bytes) { if (bytes < KILO) { return bytes + "B"; } else if (bytes < MEGA) { return new DecimalFormat("#.##").format(bytes / 1024.0) + "KB"; } else if (bytes < GIGA) { return new DecimalFormat("#.##").format(bytes / 1048576.0) + "MB"; } else { return new DecimalFormat("#.##").format(bytes / 1073741824.0) + "GB"; } } /** * Gently close a {@link CloseableIterator} * @param files */ private void closeIterator(CloseableIterator<FileGroup> files) { if (files != null) { try { // Make sure to close the iterator files.close(); } catch (Throwable t) { // Simply log it at finer level if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("Exception occurred while closing the file iterator:\n " + t.getLocalizedMessage()); } } } } public static String extractName(CoverageInfo coverageInfo) { String name = null; if (coverageInfo != null) { name = coverageInfo.getNativeCoverageName(); if (name == null) { name = coverageInfo.getName(); } if (name == null) { name = coverageInfo.getNativeName(); } } return name; } }