/* (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.wcs.response; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.zip.ZipOutputStream; import org.geoserver.config.GeoServer; import org.geoserver.data.util.IOUtils; import org.geoserver.ogr.core.Format; import org.geoserver.ogr.core.FormatAdapter; import org.geoserver.ogr.core.FormatConverter; import org.geoserver.ogr.core.ToolWrapper; import org.geoserver.ogr.core.ToolWrapperFactory; import org.geoserver.platform.ServiceException; import org.geoserver.wcs.WCSInfo; import org.geoserver.wcs.responses.CoverageResponseDelegate; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.coverage.grid.io.AbstractGridFormat; import org.geotools.gce.geotiff.GeoTiffFormat; import org.geotools.gce.geotiff.GeoTiffWriteParams; import org.geotools.gce.geotiff.GeoTiffWriter; import org.geotools.util.Utilities; import org.opengis.parameter.GeneralParameterValue; import org.opengis.parameter.ParameterValueGroup; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.vfny.geoserver.wcs.WcsException; /** * Implementation of {@link CoverageResponseDelegate} that leverages the gdal_translate utility to encode coverages in any output format supported by the * GDAL library available on the system where GeoServer is running. * * <p> * The encoding process involves two steps: * <ol> * <li>the coverage passed as input to the <code>encode()</code> method is dumped to a temporary file on disk in GeoTIFF format</li> * <li>the <code>gdal_translate</code> command is invoked with the provided options to convert the dumped GeoTIFF file to the desired format</li> * </ol> * </p> * * <p> * Configuration for the supported output formats must be passed to the class via its {@link #addFormat(GdalFormat)} method. * </p> * * @author Stefano Costa, GeoSolutions */ public class GdalCoverageResponseDelegate implements CoverageResponseDelegate, FormatConverter { private static final GeoTiffFormat GEOTIF_FORMAT = new GeoTiffFormat(); /** * Facade to GeoServer's configuration */ GeoServer geoServer; /** * Factory to create the gdal_translate wrapper. */ ToolWrapperFactory gdalWrapperFactory; /** * The fs path to gdal_translate. If null, we'll assume gdal_translate is in the PATH and that we can execute it just by running gdal_translate */ String gdalTranslatePath = null; /** * The full path to gdal_translate */ String gdalTranslateExecutable = "gdal_translate"; /** * The environment variables to set before invoking gdal_translate */ Map<String, String> environment = null; /** * Map holding the descriptors of the supported GDAL formats (keyed by format name). */ static Map<String, Format> formats = new HashMap<String, Format>(); /** * Map holding the descriptors of the supported GDAL formats (keyed by mime type). */ static Map<String, Format> formatsByMimeType = new HashMap<String, Format>(); /** * Lock guarding concurrent access to the maps holding the format descriptors. */ private ReadWriteLock formatsLock; /** * @param gs */ public GdalCoverageResponseDelegate(GeoServer gs, ToolWrapperFactory wrapperFactory) { this.formatsLock = new ReentrantReadWriteLock(); this.geoServer = gs; this.gdalWrapperFactory = wrapperFactory; this.environment = new HashMap<String, String>(); } /** * Returns the gdal_translate executable full path. * * */ @Override public String getExecutable() { return gdalTranslateExecutable; } /** * Sets the gdal_translate executable full path. The default value is simply "gdal_translate", which will work if gdal_translate is in the path. * * @param gdalTranslate */ @Override public void setExecutable(String gdalTranslate) { this.gdalTranslateExecutable = gdalTranslate; } /** * Returns the environment variables that are set prior to invoking gdal_translate. * * */ @Override public Map<String, String> getEnvironment() { return environment; } /** * Provides the environment variables that are set prior to invoking gdal_translate (notably the GDAL_DATA variable, specifying the location of * GDAL's data directory). * * @param environment */ @Override public void setEnvironment(Map<String, String> environment) { if (environment != null) { this.environment.clear(); this.environment.putAll(environment); } } /** * Adds a GDAL format among the supported ones * * @param format */ @Override public void addFormat(Format format) { if (format == null) { throw new IllegalArgumentException("No format provided"); } formatsLock.writeLock().lock(); try { addFormatInternal(format); } finally { formatsLock.writeLock().unlock(); } } private void addFormatInternal(Format format) { formats.put(format.getGeoserverFormat().toUpperCase(), format); if (format.getMimeType() != null) { formatsByMimeType.put(format.getMimeType().toUpperCase(), format); } } /** * Get a list of supported GDAL formats * * @return the list of supported formats */ @Override public List<Format> getFormats() { formatsLock.readLock().lock(); try { return new ArrayList<Format>(formats.values()); } finally { formatsLock.readLock().unlock(); } } /** * Programmatically removes all formats */ @Override public void clearFormats() { formatsLock.writeLock().lock(); try { clearFormatsInternal(); } finally { formatsLock.writeLock().unlock(); } } private void clearFormatsInternal() { formats.clear(); formatsByMimeType.clear(); } /** * Replaces currently supported formats with the provided list. * * @param formats */ @Override public void replaceFormats(List<Format> formats) { if (formats == null || formats.isEmpty()) { throw new IllegalArgumentException("No formats provided"); } formatsLock.writeLock().lock(); try { clearFormatsInternal(); for (Format format: formats) { if (format != null) { addFormatInternal(format); } } } finally { formatsLock.writeLock().unlock(); } } @Override public boolean canProduce(String outputFormat) { try { return getGdalFormat(outputFormat) != null; } catch (WcsException e) { // format was not found return false; } } @Override public String getMimeType(String outputFormat) { String mimeType = ""; Format format = getGdalFormat(outputFormat); if (format.isSingleFile()) { if (format.getMimeType() != null) { mimeType = format.getMimeType(); } else { // use a default binary blob mimeType = "application/octet-stream"; } } else { mimeType = "application/zip"; } return mimeType; } @Override public String getFileExtension(String outputFormat) { String extension = ""; Format format = getGdalFormat(outputFormat); if (format.isSingleFile()) { if (format.getFileExtension() != null) { extension = format.getFileExtension(); } else { // default to .bin extension = "bin"; } } else { extension = "zip"; } // strip initial '.' character if (extension.charAt(0) == '.') { extension = extension.substring(1); } return extension; } private Format getGdalFormat(String outputFormat) { Format format = null; formatsLock.readLock().lock(); try { format = formats.get(outputFormat.toUpperCase()); if (format == null) { // try to look it up by mime type format = formatsByMimeType.get(outputFormat.toUpperCase()); } } finally { formatsLock.readLock().unlock(); } if (format == null) { throw new WcsException("Unknown output format: " + outputFormat); } return format; } @Override public void encode(GridCoverage2D coverage, String outputFormat, Map<String, String> econdingParameters, OutputStream output) throws ServiceException, IOException { Utilities.ensureNonNull("sourceCoverage", coverage); // figure out which output format we're going to generate Format format = getGdalFormat(outputFormat); for (FormatAdapter adapter: format.getFormatAdapters()) { coverage = (GridCoverage2D) adapter.adapt(coverage); } // create the first temp directory, used for dumping gs generated // content File tempGS = org.geoserver.data.util.IOUtils.createTempDirectory("gdaltmpin"); File tempGDAL = org.geoserver.data.util.IOUtils.createTempDirectory("gdaltmpout"); // build the gdal wrapper used to run the gdal_translate commands ToolWrapper wrapper = gdalWrapperFactory.createWrapper(gdalTranslateExecutable, environment); // actually export the coverage try { File outputFile = null; // write out the coverage File intermediate = writeToDisk(tempGS, coverage); // convert with gdal_translate final CoordinateReferenceSystem crs = coverage.getCoordinateReferenceSystem(); outputFile = wrapper.convert(intermediate, tempGDAL, coverage.getName().toString(), format, crs); // wipe out the input dir contents IOUtils.emptyDirectory(tempGS); // was it a single file output? if(format.isSingleFile()) { try (FileInputStream fis = new FileInputStream(outputFile)) { org.apache.commons.io.IOUtils.copy(fis, output); } } else { // scan the output directory and zip it all try (ZipOutputStream zipOut = new ZipOutputStream(output)) { IOUtils.zipDirectory(tempGDAL, zipOut, null); zipOut.finish(); } } } catch (Exception e) { throw new ServiceException("Exception occurred during output generation", e); } finally { // delete the input and output directories IOUtils.delete(tempGS); IOUtils.delete(tempGDAL); } } /** * Writes to disk using GeoTIFF format. * * @param tempDir * @param coverage * */ private File writeToDisk(File tempDir, GridCoverage2D coverage) throws Exception { // create the temp file for this output // TODO: sanitize temp file name File outFile = new File(tempDir, coverage.getName().toString() + ".tiff"); // write out GeoTiffWriter writer = null; try { writer = (GeoTiffWriter) GEOTIF_FORMAT.getWriter(outFile); // using default encoding parameters final GeoTiffWriteParams wp = new GeoTiffWriteParams(); final ParameterValueGroup writerParams = GEOTIF_FORMAT.getWriteParameters(); writerParams.parameter(AbstractGridFormat.GEOTOOLS_WRITE_PARAMS.getName().toString()).setValue(wp); WCSInfo wcsService = geoServer.getService(WCSInfo.class); if(wcsService != null && wcsService.isLatLon()){ writerParams.parameter(GeoTiffFormat.RETAIN_AXES_ORDER.getName().toString()).setValue(true); } // write down if (writer != null) writer.write(coverage, (GeneralParameterValue[]) writerParams.values() .toArray(new GeneralParameterValue[1])); } finally { try { if (writer != null) writer.dispose(); } catch (Throwable e) { // eating exception } coverage.dispose(false); } return outFile; } @Override public List<String> getOutputFormats() { List<String> outputFormats = null; formatsLock.readLock().lock(); try { outputFormats = new ArrayList<String>(formats.keySet()); } finally { formatsLock.readLock().unlock(); } Collections.sort(outputFormats); return outputFormats; } @Override public boolean isAvailable() { ToolWrapper gdal = gdalWrapperFactory.createWrapper(gdalTranslateExecutable, environment); return gdal.isAvailable(); } @Override public String getConformanceClass(String format) { return "http://www.opengis.net/spec/WCS_coverage-encoding-x" + getMimeType(format); } }