/* (c) 2014 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.wps.gs.download;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geoserver.catalog.LayerInfo;
import org.geoserver.catalog.ProjectionPolicy;
import org.geoserver.catalog.ResourceInfo;
import org.geoserver.catalog.StyleInfo;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.platform.GeoServerResourceLoader;
import org.geoserver.platform.resource.Paths;
import org.geoserver.platform.resource.Resource;
import org.geoserver.platform.resource.Resources;
import org.geoserver.wps.ppio.ComplexPPIO;
import org.geoserver.wps.ppio.LiteralPPIO;
import org.geoserver.wps.ppio.ProcessParameterIO;
import org.geotools.data.Parameter;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.feature.FeatureCollection;
import org.geotools.geometry.jts.JTS;
import org.geotools.referencing.CRS;
import org.geotools.util.logging.Logging;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.MathTransform;
import org.springframework.context.ApplicationContext;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
/**
* Various Utilities for Download Services.
*
* @author Simone Giannecchini, GeoSolutions SAS
*
*/
final class DownloadUtilities {
/** The LOGGER. */
private static final Logger LOGGER = Logging.getLogger(DownloadUtilities.class);
/**
* Singleton
*/
private DownloadUtilities() {
}
/**
* This method checks whether or not the provided geometry is valid {@link Polygon} or not.
* <p>
* In case the egometry is not a valid polygon, it throws an {@link IllegalStateException};
*
* @param roi the {@link Geometry} to check.
* @throws IllegalStateException
*/
static void checkPolygonROI(Geometry roi) throws IllegalStateException {
// Null check
if (roi == null) {
throw new NullPointerException("The provided ROI is null!");
}
// Check that the Geometry is only Polygon or MultiPolygon
if (roi instanceof Point || roi instanceof MultiPoint || roi instanceof LineString
|| roi instanceof MultiLineString) {
throw new IllegalStateException(
"The Region of Interest is not a Polygon or Multipolygon!");
}
// Empty check and validity check
if (roi.isEmpty() || !roi.isValid()) {
throw new IllegalStateException("The Region of Interest is empyt or invalid!");
}
}
/**
* Looks for a valid PPIO given the provided mime type and process parameter.
*
* @param p
* @param context <p>
* The lenient approach makes this method try harder to send back a result but it is preferrable to be non-lenient since otherwise we might
* get s a PPIO which is not really what we need.
*
* @param mime the mime-type for which we are searching for a {@link ProcessParameterIO}
* @param lenient whether or not trying to be lenient when returning a suitable {@link ProcessParameterIO}.
* @return either <code>null</code> or the found
*/
final static ProcessParameterIO find(Parameter<?> p, ApplicationContext context, String mime,
boolean lenient) {
//
// lenient approach, try to give something back in any case
//
if (lenient) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Lenient approach used");
}
return ProcessParameterIO.find(p, context, mime);
}
//
// Strict match case. If we don't find a match we return null
//
// enum special treatment
if (p.type.isEnum()) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Trying to find the PPIO for the Enum = " + p.type);
}
return new LiteralPPIO(p.type);
}
// TODO: come up with some way to flag one as "default"
List<ProcessParameterIO> all = ProcessParameterIO.findAll(p, context);
if (all.isEmpty()) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "No PPIO found for the parameter " + p.getName());
}
return null;
}
// Get the PPIO for the mimetype
if (mime != null) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE,
"Trying to search for a PPIO for the parameter " + p.getName());
}
for (ProcessParameterIO ppio : all) {
if (ppio instanceof ComplexPPIO && ((ComplexPPIO) ppio).getMimeType().equals(mime)) {
return ppio;
}
}
}
// unable to find a match
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Unable to find a PPIO for the parameter " + p.getName());
}
return null;
}
/**
* This methods checks if the provided {@link FeatureCollection} is empty or not.
* <p>
* In case the provided feature collection is empty it throws an {@link IllegalStateException};
*
* @param features the {@link SimpleFeatureCollection} to check
* @throws IllegalStateException
*/
final static void checkIsEmptyFeatureCollection(SimpleFeatureCollection features)
throws IllegalStateException {
if (features == null || features.isEmpty()) {
throw new IllegalStateException("Got an empty feature collection.");
}
}
/**
* Retrieves the native {@link CoordinateReferenceSystem} for the provided {@link ResourceInfo}.
*
* @param resourceInfo
* @return the native {@link CoordinateReferenceSystem} for the provided {@link ResourceInfo}.
* @throws IOException in case something bad happems!
*/
static CoordinateReferenceSystem getNativeCRS(ResourceInfo resourceInfo) throws IOException {
// prepare native CRS
ProjectionPolicy pp = resourceInfo.getProjectionPolicy();
if (pp == null || pp == ProjectionPolicy.FORCE_DECLARED) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "PropjectionPolicy null or FORCE_DECLARED");
}
return resourceInfo.getCRS();
} else {
return resourceInfo.getNativeCRS();
}
}
/**
* Reprojects the input Geometry from its CRS to the defined CRS.
*
* @param geometry Geometry to transform
* @param crs target CRS for the transformation
* @return a transformed Geometry object
* @throws IOException
*/
static Geometry transformGeometry(Geometry geometry, CoordinateReferenceSystem crs)
throws IOException {
final CoordinateReferenceSystem geometryCRS = (CoordinateReferenceSystem) geometry
.getUserData();
// find math transform between the two coordinate reference systems
MathTransform targetTX = null;
if (!CRS.equalsIgnoreMetadata(geometry, crs)) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE,
"Geometry CRS is not equal to the target CRS, we might have to reproject");
}
// we MIGHT have to reproject
try {
targetTX = CRS.findMathTransform(geometryCRS, crs, true);
} catch (Exception e) {
throw new IOException(e);
}
// reproject
if (!targetTX.isIdentity()) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE,
"CRS transform is not an identity, we have to reproject the Geometry");
}
try {
geometry = JTS.transform(geometry, targetTX);
} catch (Exception e) {
throw new IOException(e);
}
// checks
if (geometry == null) {
throw new IllegalStateException(
"The Region of Interest is null after going back to native CRS!");
}
geometry.setUserData(crs); // set new CRS
DownloadUtilities.checkPolygonROI(geometry); // Check if the geometry is a Polygon or MultiPolygon
}
}
return geometry;
}
/**
* Retrieves the underlying SLD {@link File} for the provided GeoSerevr Style.
*
* @param style the underlying SLD {@link File} for the provided GeoSerevr Style.
* @return the underlying SLD {@link File} for the provided GeoSerevr Style.
* @throws IOException
*/
static Resource findStyle(StyleInfo style) throws IOException {
GeoServerResourceLoader loader = GeoServerExtensions.bean(GeoServerResourceLoader.class);
Resource styleFile = loader.get(Paths.path("styles", style.getFilename()));
if (styleFile != null && styleFile.getType() == Resource.Type.RESOURCE
&& Resources.canRead(styleFile)) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Style " + style.getName() + " found");
}
// the SLD file is public and avaialble, we can attach it to the download.
return styleFile;
} else {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Style " + style.getName()
+ " not found. Trying to search in the layer workspace");
}
// the SLD file is not public, most probably it is located under a workspace.
// lets try to search for the file inside the same layer workspace folder ...
styleFile = loader.get(Paths.path("workspaces", style.getWorkspace().getName(),
"styles", style.getFilename()));
if (styleFile != null && styleFile.getType() == Resource.Type.RESOURCE
&& Resources.canRead(styleFile)) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE,
"The style file cannot be found anywhere. We need to skip the SLD file");
}
// unfortunately the style file cannot be found anywhere. We need to skip the SLD file!
return null;
}
return styleFile;
}
}
/**
* Collect all the underlying SLD {@link File}s for the provided GeoServer layer.
*
* @param layerInfo the provided GeoServer layer.
* @return all the underlying SLD {@link File}s for the provided GeoServer layer.
* @throws IOException
*/
static List<Resource> collectStyles(LayerInfo layerInfo) throws IOException {
final List<Resource> styleFiles = new ArrayList<Resource>();
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Searching for default style");
}
// collect in a set to avoid duplicates (the styles can contain a copy of the
// default style)
LinkedHashSet<StyleInfo> styles = new LinkedHashSet<>();
styles.add(layerInfo.getDefaultStyle());
if (layerInfo.getStyles() != null) {
styles.addAll(layerInfo.getStyles());
}
for (StyleInfo si : styles) {
Resource styleFile = findStyle(si);
if (styleFile != null) {
styleFiles.add(styleFile);
}
}
return styleFiles;
}
}