/* (c) 2014 - 2015 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.wms.legendgraphic;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geoserver.catalog.CoverageInfo;
import org.geoserver.catalog.LayerGroupInfo;
import org.geoserver.catalog.LayerInfo;
import org.geoserver.catalog.LegendInfo;
import org.geoserver.catalog.PublishedType;
import org.geoserver.catalog.StyleInfo;
import org.geoserver.catalog.impl.LegendInfoImpl;
import org.geoserver.ows.KvpRequestReader;
import org.geoserver.ows.util.KvpUtils;
import org.geoserver.platform.GeoServerResourceLoader;
import org.geoserver.platform.ServiceException;
import org.geoserver.wms.GetLegendGraphicRequest;
import org.geoserver.wms.GetLegendGraphicRequest.LegendRequest;
import org.geoserver.wms.MapLayerInfo;
import org.geoserver.wms.WMS;
import org.geotools.coverage.grid.io.GridCoverage2DReader;
import org.geotools.data.DataUtilities;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.factory.FactoryRegistryException;
import org.geotools.factory.GeoTools;
import org.geotools.feature.SchemaException;
import org.geotools.resources.coverage.FeatureUtilities;
import org.geotools.styling.SLDParser;
import org.geotools.styling.Style;
import org.geotools.styling.StyleFactory;
import org.geotools.util.NullProgressListener;
import org.geotools.util.logging.Logging;
import org.opengis.feature.type.FeatureType;
import org.opengis.feature.type.Name;
import org.opengis.referencing.operation.TransformException;
import org.xml.sax.EntityResolver;
/**
* Key/Value pair set parsed for a GetLegendGraphic request. When calling <code>getRequest</code> produces a {@linkPlain
* org.vfny.geoserver.requests.wms.GetLegendGraphicRequest}
* <p>
* See {@linkplain org.org.geoserver.wms.GetLegendGraphicRequest} for a complete list of expected request parameters.
* </p>
* <p>
* This class is responsible for looking up all the required information for {@link BufferedImageLegendGraphicBuilder} (titles, styles and legend
* graphics used). If requested information (such as a legend graphic) is unavailable as described a warning will be issued rather than
* outright failure. ALl parsed/gathered information is recorded in {@link GetLegendGraphicRequest} for use by BufferedImageLegendGraphicBuilder,
* RasterLayerLegendHelper and similar.
*
* @author Gabriel Roldan
* @version $Id$
* @see org.org.geoserver.wms.GetLegendGraphicRequest
*/
public class GetLegendGraphicKvpReader extends KvpRequestReader {
private static final Logger LOGGER = Logging.getLogger(GetLegendGraphicKvpReader.class);
/**
* Factory to create styles from inline or remote SLD documents (aka, from SLD_BODY or SLD
* parameters).
*/
private static final StyleFactory styleFactory = CommonFactoryFinder.getStyleFactory(GeoTools
.getDefaultHints());
private WMS wms;
/**
* Creates a new GetLegendGraphicKvpReader object.
*
* @param params
* map of key/value pairs with the parameters for a GetLegendGraphic request
* @param wms
* WMS config object.
*/
public GetLegendGraphicKvpReader(WMS wms) {
super(GetLegendGraphicRequest.class);
this.wms = wms;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Override
public GetLegendGraphicRequest read(Object req, Map kvp, Map rawKvp) throws Exception {
GetLegendGraphicRequest request = (GetLegendGraphicRequest) super.read(req, kvp, rawKvp);
request.setRawKvp(rawKvp);
request.setKvp(kvp);
request.setWms(wms);
if (request.getVersion() == null || request.getVersion().length() == 0) {
String version = (String) rawKvp.get("WMTVER");
if (version == null) {
version = wms.getVersion();
}
request.setVersion(version);
}
final String language = (String) rawKvp.get("LANGUAGE");
if(language != null) {
request.setLocale(new Locale(language));
}
// Fix for https://osgeo-org.atlassian.net/browse/GEOS-710
// Since at the moment none of the other request do check the version
// numbers, we
// disable this check for the moment, and wait for a proper fix once the
// we support more than one version of WMS/WFS specs
// if (!GetLegendGraphicRequest.SLD_VERSION.equals(version)) {
// throw new WmsException("Invalid SLD version number \"" + version
// + "\"");
// }
final String layer = (String) rawKvp.get("LAYER");
final boolean strict = rawKvp.containsKey("STRICT") ? Boolean.valueOf((String) rawKvp
.get("STRICT")) : request.isStrict();
request.setStrict(strict);
if (strict && layer == null) {
throw new ServiceException("LAYER parameter not present for GetLegendGraphic",
"LayerNotDefined");
}
if (strict && request.getFormat() == null) {
throw new ServiceException("Missing FORMAT parameter for GetLegendGraphic",
"MissingFormat");
}
// object representing the layer or layer group requested
Object infoObject=null;
// list of layers to render in the legend (we can have more
// than one if a layergroup is requested)
List<LegendRequest> layers = new ArrayList<LegendRequest>();
if (layer != null) {
try {
LayerInfo layerInfo = wms.getLayerByName(layer);
if (layerInfo != null) {
// layer found, fill in LegendRequest details
LegendRequest legend = addLayer(layerInfo,request);
legend.setLayer(layer);
layers.add(legend);
infoObject=layerInfo;
} else {
// check for layer group, and add each layer
LayerGroupInfo layerGroupInfo = wms.getLayerGroupByName(layer);
if(layerGroupInfo != null) {
// add all single layers of the group
for(LayerInfo singleLayer : layerGroupInfo.layers()) {
LegendRequest legend = addLayer(singleLayer,request);
legend.setLayerGroupInfo(layerGroupInfo);
layers.add(legend);
}
infoObject=layerGroupInfo;
} else {
throw new ServiceException(layer + " layer does not exist.");
}
}
} catch (IOException e) {
throw new ServiceException(e);
} catch (NoSuchElementException ne) {
throw new ServiceException(new StringBuffer(layer)
.append(" layer does not exists.").toString(), ne);
} catch (Exception te) {
throw new ServiceException("Can't obtain the schema for the required layer.", te);
}
} else {
// Assume this is "just" a request for a legend graphic representing for a style (no infoObject for context)
LegendRequest styleLegend = request.new LegendRequest();
layers.add( styleLegend );
}
request.getLegends().addAll( layers );
if (request.getFormat() == null) {
request.setFormat(GetLegendGraphicRequest.DEFAULT_FORMAT);
}
if (null == wms.getLegendGraphicOutputFormat(request.getFormat())) {
throw new ServiceException(new StringBuffer("Invalid graphic format: ").append(
request.getFormat()).toString(), "InvalidFormat");
}
try {
// Parse optional parameters into legend data structure created above
parseOptionalParameters(request, infoObject, rawKvp);
if (request.getLayers().size() != request.getStyles().size()) {
String msg = layers.size() + " layers requested, but found " + request.getStyles().size()
+ " styles specified. ";
throw new ServiceException(msg, getClass().getName());
}
if (request.getRules().size()>0 && layers.size() != request.getRules().size()) {
String msg = layers.size() + " layers requested, but found " + request.getRules().size()
+ " rules specified. ";
throw new ServiceException(msg, getClass().getName());
}
} catch (IOException e) {
throw new ServiceException(e);
}
return request;
}
/**
* Creates a new layer for the current list of layers to be drawn on the legend.
*
* Additional LayerInfo details such as title and legend are filled in if available.
*
* @param layerInfo The layer description
* @param req The GetLegendGrapicRequest used for context
* @return created LegendRequest
*
* @throws FactoryRegistryException
* @throws IOException
* @throws TransformException
* @throws SchemaException
*/
private LegendRequest addLayer(LayerInfo layerInfo, GetLegendGraphicRequest request) throws FactoryRegistryException, IOException, TransformException, SchemaException {
FeatureType featureType=getLayerFeatureType(layerInfo);
if(featureType != null) {
LegendRequest legend = request.new LegendRequest(featureType);
legend.setLayerInfo( layerInfo );
MapLayerInfo mli=new MapLayerInfo(layerInfo);
// Temporary MapLayerInfo used to map a title, if label is defined on layer
if(mli.getLabel() != null) {
legend.setTitle( mli.getLabel() );
}
LegendInfo legendInfo = resolveLegendInfo(layerInfo.getLegend(),request);
if(legendInfo != null ){
configureLegendInfo(request, legend, legendInfo);
}
return legend;
} else {
throw new ServiceException("Cannot get FeatureType for Layer",
"MissingFeatureType");
}
}
/**
* Ensures the online resource is stored on the GetLegendGraphicRequest and native
* dimensions are configured if not specified on the original request.
*
* @param request GetLegendGraphicRequest original KWP Request
* @param legend LegendRequest internal Class containing references to Resources
* @param legendInfo LegendInfo used to document use external graphic
*/
private void configureLegendInfo(GetLegendGraphicRequest request, LegendRequest legend,
LegendInfo legendInfo) {
legend.setLegendInfo( legendInfo );
if (legendInfo.getHeight() > 0 && !request.getKvp().containsKey("HEIGHT")) {
request.setHeight(legendInfo.getHeight());
}
if (legendInfo.getWidth() > 0 && !request.getKvp().containsKey("WIDTH")) {
request.setWidth(legendInfo.getWidth());
}
}
/**
* Makes a copy of the provided LegendInfo resolving ExternalGraphics reference
* to local file system.
* <p>
* If external graphic reference cannot be resolved locally null is returned.
*
* @param legendInfo LegendInfo used to document use external graphic
* @return Copy of provided legend info resolved to local file references.
*/
private LegendInfo resolveLegendInfo(LegendInfo legendInfo, GetLegendGraphicRequest request){
if( legendInfo == null){
return null; // not available
}
String onlineResource = legendInfo.getOnlineResource();
String baseUrl = request.getBaseUrl();
if( onlineResource == null ){
return null;
}
URL url = null;
try {
URI uri = new URI(onlineResource);
if( uri.isAbsolute() ){
if( baseUrl != null && onlineResource.startsWith(baseUrl+"styles/")){
// convert relative to styles durectory
onlineResource = onlineResource.substring(baseUrl.length()+7);
}
else {
return legendInfo; // an actual external graphic reference
}
}
GeoServerResourceLoader resources = wms.getCatalog().getResourceLoader();
File styles = resources.findOrCreateDirectory("styles");
URL base = DataUtilities.fileToURL( styles );
url = new URL( base, onlineResource );
}
catch(MalformedURLException invalid){
LOGGER.log(Level.FINER, "Unable to resolve "+onlineResource+" locally", invalid);
return null; // Do not try this online resource
} catch (IOException access) {
LOGGER.log(Level.FINER, "Unable to resolve "+onlineResource+" locally", access);
return null; // Do not try this online resource
} catch (URISyntaxException syntax) {
LOGGER.log(Level.FINER, "Unable to resolve "+onlineResource+" locally", syntax);
return null; // Do not try this online resource
}
LegendInfoImpl resolved = new LegendInfoImpl();
resolved.setOnlineResource( url.toExternalForm() );
resolved.setFormat( legendInfo.getFormat() );
resolved.setHeight( legendInfo.getHeight() );
resolved.setWidth( legendInfo.getWidth() );
return resolved;
}
/**
* Extracts a FeatureType for a given layer.
*
* FeatureType obtained from catalog
*
* @param layerInfo vector or raster layer
* @return the FeatureType for the given layer
* @throws IOException
* @throws FactoryRegistryException
* @throws TransformException
* @throws SchemaException
*/
private FeatureType getLayerFeatureType(LayerInfo layerInfo) throws IOException, FactoryRegistryException, TransformException, SchemaException {
MapLayerInfo mli=new MapLayerInfo(layerInfo);
if (layerInfo.getType() == PublishedType.VECTOR) {
FeatureType featureType = mli.getFeature().getFeatureType();
return featureType;
} else if (layerInfo.getType() == PublishedType.RASTER) {
CoverageInfo coverageInfo = mli.getCoverage();
// it much safer to wrap a reader rather than a coverage in most cases, OOM can
// occur otherwise
final GridCoverage2DReader reader;
reader = (GridCoverage2DReader) coverageInfo.getGridCoverageReader(
new NullProgressListener(), GeoTools.getDefaultHints());
final SimpleFeatureCollection feature;
feature = FeatureUtilities.wrapGridCoverageReader(reader, null);
return feature.getSchema();
}
return null;
}
/**
* Parses the GetLegendGraphic optional parameters.
* <p>
* The parameters parsed by this method are:
* <ul>
* <li>FEATURETYPE for the {@link GetLegendGraphicRequest#getFeatureType() featureType}
* property.</li>
* <li>SCALE for the {@link GetLegendGraphicRequest#getScale() scale} property.</li>
* <li>WIDTH for the {@link GetLegendGraphicRequest#getWidth() width} property.</li>
* <li>HEIGHT for the {@link GetLegendGraphicRequest#getHeight() height} property.</li>
* <li>EXCEPTIONS for the {@link GetLegendGraphicRequest#getExceptions() exceptions} property.</li>
* <li>TRANSPARENT for the {@link GetLegendGraphicRequest#isTransparent() transparent} property.
* </li>
* <li>LEGEND_OPTIONS for the {@link GetLegendGraphicRequest#getLegendOptions() legendOptions}
* property.</li>
* </ul>
* </p>
*
* @param req
* The request to set the properties to.
* @param infoObj a {@link LayerInfo layer} or a {@link LayerGroupInfo layerGroup}
* for which the legend graphic is to be produced,
* from where to extract the style information.
* @throws IOException
*
* @task TODO: validate EXCEPTIONS parameter
*/
private void parseOptionalParameters(GetLegendGraphicRequest req, Object infoObj, Map rawKvp)
throws IOException {
parseStyleAndRule(req, infoObj, rawKvp);
}
/**
* Parses the STYLE, SLD and SLD_BODY parameters, as well as RULE.
*
* <p>
* STYLE, SLD and SLD_BODY are mutually exclusive. STYLE refers to a named style known by the
* server and applicable to the requested layer (i.e., it is exposed as one of the layer's
* styles in the Capabilities document). SLD is a URL to an externally available SLD document,
* and SLD_BODY is a string containing the SLD document itself.
* </p>
*
* <p>
* As I don't completely understand which takes priority over which from the spec, I assume the
* precedence order as follow: SLD, SLD_BODY, STYLE, in decrecent order of precedence.
* </p>
*
* @param req
* @param ftype
* @throws IOException
*/
private void parseStyleAndRule(GetLegendGraphicRequest req, Object infoObj, Map rawKvp)
throws IOException {
// gets the list of styles requested
String listOfStyles = (String) rawKvp.get("STYLE");
if(listOfStyles == null) {
listOfStyles = "";
}
List<String> styleNames = KvpUtils.readFlat(listOfStyles);
String sldUrl = (String) rawKvp.get("SLD");
String sldBody = (String) rawKvp.get("SLD_BODY");
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine(new StringBuffer("looking for styles ").append(listOfStyles).toString());
}
List<Style> sldStyles = new ArrayList<Style>();
if (sldUrl != null) {
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("taking style from SLD parameter");
}
addStylesFrom(sldStyles,styleNames,loadRemoteStyle(sldUrl));
} else if (sldBody != null) {
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("taking style from SLD_BODY parameter");
}
addStylesFrom(sldStyles,styleNames,parseSldBody(sldBody));
} else if (styleNames.size() > 0) {
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("taking style from STYLE parameter");
}
int pos=0;
for(String styleName : styleNames) {
// if we have a layer group and no style is specified
// use the default one for the layer in the current position
if (styleName.equals("") && infoObj instanceof LayerGroupInfo) {
LayerGroupInfo layerGroupInfo = (LayerGroupInfo) infoObj;
List<LayerInfo> groupLayers = layerGroupInfo.layers();
if (pos < groupLayers.size()) {
sldStyles.add(getStyleFromLayer(groupLayers.get(pos)));
}
} else {
sldStyles.add( wms.getStyleByName(styleName) );
if( infoObj instanceof LayerInfo ){
StyleInfo styleInfo = wms.getCatalog().getStyleByName(styleName);
if( styleInfo != null ){
LegendInfo legend = resolveLegendInfo( styleInfo.getLegend(), req );
if( legend != null ){
LayerInfo layerInfo = (LayerInfo) infoObj;
Name name = layerInfo.getResource().getQualifiedName();
LegendRequest legendRequest = req.getLegend(name);
if( legendRequest != null ){
configureLegendInfo(req, legendRequest, legend);
}
else {
LOGGER.log(Level.FINE, "Unable to set LegendInfo for "+name);
}
}
}
}
}
pos++;
}
} else {
if(infoObj instanceof LayerInfo) {
LayerInfo layerInfo = (LayerInfo) infoObj;
sldStyles.add(getStyleFromLayer(layerInfo));
StyleInfo defaultStyle = layerInfo.getDefaultStyle();
LegendInfo legend = defaultStyle.getLegend();
if( legend!=null){
Name name = layerInfo.getResource().getQualifiedName();
LegendRequest legendRequest = req.getLegend(name);
if( legendRequest != null ){
configureLegendInfo(req, legendRequest, legend);
}
else {
LOGGER.log(Level.FINE, "Unable to set LegendInfo for "+name);
}
}
} else if(infoObj instanceof LayerGroupInfo) {
LayerGroupInfo layerGroupInfo=(LayerGroupInfo)infoObj;
List<LayerInfo> groupLayers = layerGroupInfo.layers();
List<StyleInfo> groupStyles = layerGroupInfo.styles();
for (int count = 0; count < groupLayers.size(); count++) {
LayerInfo layerInfo = groupLayers.get(count);
StyleInfo styleInfo = null;
if (count < groupStyles.size() && groupStyles.get(count) != null) {
styleInfo = groupStyles.get(count);
sldStyles.add(styleInfo.getStyle());
} else {
sldStyles.add(getStyleFromLayer(layerInfo));
styleInfo = layerInfo.getDefaultStyle();
}
LegendInfo legend = resolveLegendInfo( styleInfo.getLegend(),req );
if( legend!=null){
Name name = layerInfo.getResource().getQualifiedName();
LegendRequest legendRequest = req.getLegend(name);
if( legendRequest != null ){
configureLegendInfo(req, legendRequest, legend);
}
else {
LOGGER.log(Level.FINE, "Unable to set LegendInfo for "+name);
}
}
}
}
}
req.setStyles(sldStyles);
String rule = (String) rawKvp.get("RULE");
if (rule != null) {
List<String> ruleNames = KvpUtils.readFlat(rule);
req.setRules(ruleNames);
}
}
/**
* Gets the default style for the given layer
* @param layerInfo layer requested
* @return default style of the layer
*/
private Style getStyleFromLayer(LayerInfo layerInfo) {
MapLayerInfo mli=new MapLayerInfo(layerInfo);
return mli.getDefaultStyle();
}
/**
* Adds styles whose name matches names from a given source of styles.
*
* @param sldStyles final styles container
* @param styleNames names of styles to find in the given source
* @param source list of styles from a given source
*/
private void addStylesFrom(List<Style> sldStyles, List<String> styleNames, Style[] source) {
if(styleNames.size() == 0) {
sldStyles.add(findStyle(null, source));
} else {
for(String styleName : styleNames) {
sldStyles.add(findStyle(styleName, source));
}
}
}
/**
* Finds the Style named <code>styleName</code> in <code>styles</code>.
*
* @param styleName
* name of style to search for in the list of styles. If <code>null</code>, it is
* assumed the request is made in literal mode and the user has requested the first
* style.
* @param styles
* non null, non empty, list of styles
*
* @throws NoSuchElementException
* if no style named <code>styleName</code> is found in <code>styles</code>
*/
private Style findStyle(String styleName, Style[] styles) throws NoSuchElementException {
if ((styles == null) || (styles.length == 0)) {
throw new NoSuchElementException("No styles have been provided to search for "
+ styleName);
}
if (styleName == null) {
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("styleName is null, request in literal mode, returning first style");
}
return styles[0];
}
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer(new StringBuffer("request in library mode, looking for style ").append(
styleName).toString());
}
StringBuffer noMatchNames = new StringBuffer();
for (int i = 0; i < styles.length; i++) {
if ((styles[i] != null) && styleName.equals(styles[i].getName())) {
return styles[i];
}
noMatchNames.append(styles[i].getName());
if (i < styles.length) {
noMatchNames.append(", ");
}
}
throw new NoSuchElementException(styleName + " not found. Provided style names: "
+ noMatchNames);
}
/**
* Loads a remote SLD document and parses it to a Style object
*
* @param sldUrl
* an URL to a SLD document
*
* @return the document parsed to a Style object
*
* @throws WmsException
* if <code>sldUrl</code> is not a valid URL, a stream can't be opened or a parsing
* error occurs
*/
private Style[] loadRemoteStyle(String sldUrl) throws ServiceException {
InputStream in;
try {
URL url = new URL(sldUrl);
in = url.openStream();
} catch (MalformedURLException e) {
throw new ServiceException(e, "Not a valid URL to an SLD document " + sldUrl,
"loadRemoteStyle");
} catch (IOException e) {
throw new ServiceException(e, "Can't open the SLD URL " + sldUrl, "loadRemoteStyle");
}
return parseSld(new InputStreamReader(in));
}
/**
* Parses a SLD Style from a xml string
*
* @param sldBody
* the string containing the SLD document
*
* @return the SLD document string parsed to a Style object
*
* @throws WmsException
* if a parsing error occurs.
*/
private Style[] parseSldBody(String sldBody) throws ServiceException {
// return parseSld(new StringBufferInputStream(sldBody));
return parseSld(new StringReader(sldBody));
}
/**
* Parses the content of the given input stream to an SLD Style, provided that a valid SLD
* document can be read from <code>xmlIn</code>.
*
* @param xmlIn
* where to read the SLD document from.
*
* @return the parsed Style
*
* @throws WmsException
* if a parsing error occurs
*/
private Style[] parseSld(Reader xmlIn) throws ServiceException {
SLDParser parser = new SLDParser(styleFactory, xmlIn);
EntityResolver entityResolver = wms.getCatalog().getResourcePool().getEntityResolver();
if(entityResolver != null) {
parser.setEntityResolver(entityResolver);
}
Style[] styles = null;
try {
styles = parser.readXML();
} catch (RuntimeException e) {
throw new ServiceException(e);
}
if ((styles == null) || (styles.length == 0)) {
throw new ServiceException("Document contains no styles");
}
return styles;
}
}