/* (c) 2014 - 2017 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;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.geoserver.config.GeoServer;
import org.geoserver.ows.KvpParser;
import org.geoserver.ows.URLMangler.URLType;
import org.geoserver.ows.util.KvpUtils;
import org.geoserver.ows.util.ResponseUtils;
import org.geoserver.platform.GeoServerExtensions;
import org.geotools.map.Layer;
import org.geotools.map.MapLayer;
import org.geotools.styling.Style;
import org.vfny.geoserver.util.Requests;
import com.vividsolutions.jts.geom.Envelope;
/**
* Utility class for creating wms requests.
*
* @author Justin Deoliveira, The Open Planning Project, jdeolive@openplans.org
* @author Carlo Cancellieri - Geo-Solutions SAS
* @see Requests
*/
public class WMSRequests {
/**
* Encodes the url of a GetMap request pointing to a tile cache if one exists.
* <p>
* The tile cache location is determined from {@link GeoServer#getTileCache()}. If the above
* method returns null this method falls back to the behaviour of
* {@link #getGetMapUrl(WMSMapContent, MapLayer, Envelope, String[])}.
* </p>
* <p>
* If the <tt>layer</tt> argument is <code>null</code>, the request is made including all layers
* in the <tt>mapContexT</tt>.
* </p>
* <p>
* If the <tt>bbox</tt> argument is <code>null</code>. {@link WMSMapContent#getAreaOfInterest()}
* is used for the bbox parameter.
* </p>
*
* @param req
* The getMap request.
* @param layer
* The Map layer, may be <code>null</code>.
* @param layerIndex
* The index of the layer in the request
* @param bbox
* The bounding box of the request, may be <code>null</code>.
* @param kvp
* Additional or overidding kvp parameters, may be <code>null</code>
* @param geoserver
*
* @return The full url for a getMap request.
*/
public static String getTiledGetMapUrl(GeoServer geoserver, GetMapRequest req, Layer layer,
int layerIndex, Envelope bbox, String[] kvp) {
HashMap<String,String> params = getGetMapParams(req, layer.getTitle(), layerIndex, layer.getStyle().getName(), bbox, kvp);
String baseUrl = getTileCacheBaseUrl(req, geoserver);
if (baseUrl == null) {
return ResponseUtils.buildURL(req.getBaseUrl(), "wms", params, URLType.SERVICE);
}
return ResponseUtils.buildURL(baseUrl, "", params, URLType.EXTERNAL);
}
/**
* Returns the full url to the tile cache used by GeoServer ( if any ).
* <p>
* If the tile cache set in the configuration ({@link GeoServer#getTileCache()}) is set to an
* asbsolute url, it is simply returned. Otherwise the value is appended to the scheme and host
* of the supplied <tt>request</tt>.
* </p>
*
* @param req
* The request.
* @param geoServer
* The geoserver configuration.
*
* @return The url to the tile cache, or <code>null</code> if no tile cache set.
*/
private static String getTileCacheBaseUrl(GetMapRequest req, GeoServer geoServer) {
// first check if tile cache set
String tileCacheBaseUrl = (String) geoServer.getGlobal().getMetadata().get("tileCache");
if (tileCacheBaseUrl != null) {
// two possibilities, local path, or full remote path
try {
new URL(tileCacheBaseUrl);
// full url, return it
return tileCacheBaseUrl;
} catch (MalformedURLException e1) {
// try relative to the same host as request
try {
String baseUrl = req.getBaseUrl();
// GR: this replicates what the old code depending on httpServletRequest was
// doing: req.getScheme() + "://" + req.getServerName()
URL base = new URL(baseUrl);
baseUrl = base.getProtocol() + ":" + base.getPort() + "//" + base.getHost();
String url = Requests.appendContextPath(baseUrl, tileCacheBaseUrl);
new URL(url);
// cool return it
return url;
} catch (MalformedURLException e2) {
// out of guesses
}
}
}
return null;
}
/**
* Encodes the url of a GetMap request.
* <p>
* If the <tt>layer</tt> argument is <code>null</code>, the request is made including all layers
* in the <tt>mapContexT</tt>.
* </p>
* <p>
* If the <tt>bbox</tt> argument is <code>null</code>. {@link WMSMapContent#getAreaOfInterest()}
* is used for the bbox parameter.
* </p>
*
* @param req
* The getMap request
* @param layer
* The Map layer, may be <code>null</code>.
* @param layerIndex
* The index of the layer in the request
* @param bbox
* The bounding box of the request, may be <code>null</code>.
* @param kvp
* Additional or overidding kvp parameters, may be <code>null</code>
*
* @return The full url for a getMap request.
*/
public static String getGetMapUrl(GetMapRequest req, Layer layer, int layerIndex,
Envelope bbox, String[] kvp) {
String layerName = layer != null ? layer.getTitle() : null;
String style = layer != null ? layer.getStyle().getName() : null;
LinkedHashMap<String,String> params = getGetMapParams(req, layerName, layerIndex, style, bbox, kvp);
return ResponseUtils.buildURL(req.getBaseUrl(), "wms", params, URLType.SERVICE);
}
/**
* Encodes the url of a GetMap request.
* <p>
* If the <tt>layer</tt> argument is <code>null</code>, the request is made including all layers
* in the <tt>mapContexT</tt>.
* </p>
* <p>
* If the <tt>style</tt> argument is not <code>null</code> and the <tt>layer</tt> argument is
* <code>null</code>, then the default style for that layer is used.
* </p>
* <p>
* If the <tt>bbox</tt> argument is <code>null</code>. {@link WMSMapContent#getAreaOfInterest()}
* is used for the bbox parameter.
* </p>
*
* @param req
* The getMap request
* @param layer
* The layer name, may be <code>null</code>.
* @param layerIndex
* The index of the layer in the request.
* @param style
* The style name, may be <code>null</code>
* @param bbox
* The bounding box of the request, may be <code>null</code>.
* @param kvp
* Additional or overidding kvp parameters, may be <code>null</code>
*
* @return The full url for a getMap request.
*/
public static String getGetMapUrl(GetMapRequest req, String layer, int layerIndex,
String style, Envelope bbox, String[] kvp) {
HashMap<String,String> params = getGetMapParams(req, layer, layerIndex, style, bbox, kvp);
return ResponseUtils.buildURL(req.getBaseUrl(), "wms", params, URLType.SERVICE);
}
/**
* Encodes the url of a GetLegendGraphic request.
*
* @param req
* The wms request.
* @param published
* The Map layer, may not be <code>null</code>.
* @param kvp
* Additional or overidding kvp parameters, may be <code>null</code>
*
* @return The full url for a getMap request.
*/
public static String getGetLegendGraphicUrl(WMSRequest req, Layer[] layers, String[] kvp) {
// parameters
HashMap<String,String> params = new HashMap<String,String>();
params.put("service", "wms");
params.put("request", "GetLegendGraphic");
params.put("version", "1.1.1");
params.put("format", "image/png");
params.put("layer", getLayerTitles(layers));
params.put("style", getLayerStyles(layers));
params.put("height", "20");
params.put("width", "20");
// overrides / additions
for (int i = 0; (kvp != null) && (i < kvp.length); i += 2) {
params.put(kvp[i], kvp[i + 1]);
}
return ResponseUtils.buildURL(req.getBaseUrl(), "wms", params, URLType.SERVICE);
}
private static String getLayerTitles(Layer[] layers) {
StringBuilder sb = new StringBuilder();
for (Layer layer : layers) {
if(layer != null && layer.getTitle() != null) {
sb.append(layer.getTitle());
}
sb.append(",");
}
return sb.substring(0, sb.length() - 1);
}
private static String getLayerStyles(Layer[] layers) {
StringBuilder sb = new StringBuilder();
for (Layer layer : layers) {
sb.append(layer.getStyle().getName()).append(",");
}
return sb.substring(0, sb.length() - 1);
}
/**
* Helper method for encoding GetMap request parameters.
*
*/
static LinkedHashMap<String,String> getGetMapParams(GetMapRequest req, String layer, int layerIndex,
String style, Envelope bbox, String[] kvp) {
// parameters
LinkedHashMap<String,String> params = new LinkedHashMap<String,String>();
params.put("service", "wms");
params.put("request", "GetMap");
params.put("version", "1.1.1");
params.put("format", req.getFormat());
StringBuffer layers = new StringBuffer();
StringBuffer styles = new StringBuffer();
boolean useLayerIndex = true;
int count = 0;
for (int i = 0; i < req.getLayers().size(); i++) {
if (layer != null && layer.equals(req.getLayers().get(i).getName())) {
++count;
}
}
// only one of each layer in the request
if (count == 1) {
useLayerIndex = false;
}
if (layer != null) {
layers.append(layer);
if (style != null) {
styles.append(style);
} else {
// use default for layer
if (useLayerIndex) {
styles.append(req.getLayers().get(layerIndex).getDefaultStyle().getName());
} else {
for (int i = 0; i < req.getLayers().size(); i++) {
if (layer.equals(req.getLayers().get(i).getName())) {
styles.append(req.getLayers().get(i).getDefaultStyle().getName());
}
}
}
}
} else {
// no layer specified, use layers+styles specified by request
for (int i = 0; i < req.getLayers().size(); i++) {
MapLayerInfo mapLayer = req.getLayers().get(i);
Style s = (Style) req.getStyles().get(0);
layers.append(mapLayer.getName()).append(",");
styles.append(s.getName()).append(",");
}
layers.setLength(layers.length() - 1);
styles.setLength(styles.length() - 1);
}
params.put("layers", layers.toString());
params.put("styles", styles.toString());
// filters, we grab them from the original raw kvp since re-encoding
// them from objects is kind of silly
if (layer != null) {
// only get filters for the layer
int index = 0;
if (useLayerIndex) {
index = layerIndex;
} else {
for (; index < req.getLayers().size(); index++) {
if (req.getLayers().get(index).getName().equals(layer)) {
break;
}
}
}
if (req.getRawKvp().get("filter") != null) {
// split out the filter we need
List filters = KvpUtils.readFlat((String) req.getRawKvp().get("filter"),
KvpUtils.OUTER_DELIMETER);
params.put("filter", (String)filters.get(index));
} else if (req.getRawKvp().get("cql_filter") != null) {
// split out the filter we need
List filters = KvpUtils.readFlat((String) req.getRawKvp().get("cql_filter"),
KvpUtils.CQL_DELIMITER);
params.put("cql_filter", (String)filters.get(index));
} else if (req.getRawKvp().get("featureid") != null) {
// semantics of feature id slightly different, replicate entire value
params.put("featureid", req.getRawKvp().get("featureid"));
}
// Jira: #GEOS-6411: adding time and elevation support in case of a timeserie layer
if (req.getRawKvp().get("time") != null) {
// semantics of feature id slightly different, replicate entire value
params.put("time", req.getRawKvp().get("time"));
}
if (req.getRawKvp().get("elevation") != null) {
// semantics of feature id slightly different, replicate entire value
params.put("elevation", req.getRawKvp().get("elevation"));
}
req.getRawKvp().entrySet().stream()
.filter(e -> e.getKey().toLowerCase().startsWith("dim_"))
.forEach(e -> params.put(e.getKey().toLowerCase(), e.getValue()));
} else {
// include all
if (req.getRawKvp().get("filter") != null) {
params.put("filter", req.getRawKvp().get("filter"));
} else if (req.getRawKvp().get("cql_filter") != null) {
params.put("cql_filter", req.getRawKvp().get("cql_filter"));
} else if (req.getRawKvp().get("featureid") != null) {
params.put("featureid", req.getRawKvp().get("featureid"));
}
}
// image params
params.put("height", String.valueOf(req.getHeight()));
params.put("width", String.valueOf(req.getWidth()));
params.put("transparent", "" + req.isTransparent());
// bbox
if (bbox == null) {
bbox = req.getBbox();
}
if (bbox != null) {
params.put("bbox", encode(bbox));
}
// srs
params.put("srs", req.getSRS());
// format options
if (req.getFormatOptions() != null && !req.getFormatOptions().isEmpty()) {
params.put("format_options", encodeFormatOptions(req.getFormatOptions()));
}
// view params
if (req.getViewParams() != null && !req.getViewParams().isEmpty()) {
params.put("viewParams", encodeFormatOptions(req.getViewParams()));
}
Map<String,String> kvpMap=req.getRawKvp();
String propertyName=kvpMap.get("propertyName");
if (propertyName != null && !propertyName.isEmpty()) {
params.put("propertyName", propertyName);
}
if (req.getSld() != null) {
// the request encoder will url-encode the url, if it has already url encoded
// chars, the will be encoded twice
try {
String sld = URLDecoder.decode(req.getSld().toExternalForm(), "UTF-8");
params.put("sld", sld);
} catch (UnsupportedEncodingException e) {
// this should really never happen
throw new RuntimeException(e);
}
}
if (req.getSldBody() != null) {
params.put("sld_body", req.getSldBody());
}
if (req.getEnv() != null && !req.getEnv().isEmpty()) {
params.put("env", encodeFormatOptions(req.getEnv()));
}
String tilesOrigin=kvpMap.get("tilesorigin");
if (tilesOrigin != null && !tilesOrigin.isEmpty()) {
params.put("tilesorigin", tilesOrigin);
}
if (req.isTiled()) {
params.put("tiled", req.isTiled()?"true":"false");
}
String palette=kvpMap.get("palette");
if (palette!= null && !palette.isEmpty()) {
params.put("palette", palette);
}
if (req.getBuffer()>0){
params.put("buffer", Integer.toString(req.getBuffer()));
}
if (Double.compare(req.getAngle(),0.0)!=0){
params.put("angle", Double.toString(req.getAngle()));
}
// overrides / additions
for (int i = 0; (kvp != null) && (i < kvp.length); i += 2) {
params.put(kvp[i], kvp[i + 1]);
}
return params;
}
/**
* Copy the Entry matching the key from the kvp map and put it into the formatOptions map. If a parameter is already present in formatOption map
* its value will be preserved.
*
* @param kvp
* @param formatOptions
* @param key the key to parse
* @throws Exception - In the event of an unsuccesful parse.
*/
public static void mergeEntry(Map<String, String> kvp, Map<String, Object> formatOptions,
final String key) throws Exception {
// look up parser objects
List<KvpParser> parsers = GeoServerExtensions.extensions(KvpParser.class);
// strip out parsers which do not match current service/request/version
String service = KvpUtils.getSingleValue(kvp, "service");
String version = KvpUtils.getSingleValue(kvp, "version");
String request = KvpUtils.getSingleValue(kvp, "request");
KvpUtils.purgeParsers(parsers, service, version, request);
String val = null;
if ((val = kvp.get(key)) != null) {
Object foValue = formatOptions.get(key);
// if not found in format option
if (foValue == null) {
Object parsed = KvpUtils.parseKey(key, val, service, request, version, parsers);
if (parsed != null) {
formatOptions.put(key, parsed);
} else {
formatOptions.put(key, val);
}
}
}
}
/**
* Encodes a map of formation options to be used as the value in a kvp.
*
* A string of the form 'key1:value1,value2;key2:value1;...', or the empty string if the
* formatOptions map is empty.
* @param formatOptions The map of formation options.
* @param sb StringBuffer to append to.
*/
public static void encodeFormatOptions(Map formatOptions, StringBuffer sb) {
if (formatOptions == null || formatOptions.isEmpty()) {
return;
}
for (Iterator e = formatOptions.entrySet().iterator(); e.hasNext();) {
Map.Entry entry = (Map.Entry) e.next();
String key = (String) entry.getKey();
Object val = entry.getValue();
sb.append(key).append(":");
if (val instanceof Collection) {
Iterator i = ((Collection) val).iterator();
while (i.hasNext()) {
sb.append(i.next()).append(",");
}
sb.setLength(sb.length() - 1);
} else if (val.getClass().isArray()) {
int len = Array.getLength(val);
for (int i = 0; i < len; i++) {
Object o = Array.get(val, i);
if (o != null) {
sb.append(o).append(",");
}
}
sb.setLength(sb.length() - 1);
} else {
sb.append(val.toString());
}
sb.append(";");
}
if(sb.length() > 0) {
sb.setLength(sb.length() - 1);
}
}
/**
* Encodes a map of format options to be used as the value in a kvp.
*
* @param formatOptions The map of format options.
*
* @return A string of the form 'key1:value1,value2;key2:value1;...', or the empty string if the
* formatOptions map is empty.
*
*/
public static String encodeFormatOptions(Map formatOptions) {
StringBuffer sb = new StringBuffer();
encodeFormatOptions(formatOptions, sb);
return sb.toString();
}
/**
* Encodes a list of format option maps to be used as the value in a kvp.
*
* @param formatOptions The list of formation option maps.
* @param sb StringBuffer to append to.
*
* @return A string of the form 'key1.1:value1.1,value1.2;key1.2:value1.1;...[,key2.1:value2.1,value2.2;key2.2:value2.1]',
* or the empty string if the formatOptions list is empty.
*
*/
public static String encodeFormatOptions(List<Map<String, String>> formatOptions) {
if (formatOptions == null || formatOptions.isEmpty()) {
return "";
}
StringBuffer sb = new StringBuffer();
boolean first = true;
for (Map<String, String> map : formatOptions) {
if (first) {
first = false;
} else {
sb.append(",");
}
encodeFormatOptions(map, sb);
}
sb.setLength(sb.length());
return sb.toString();
}
/**
* Helper method to encode an envelope to be used in a wms request.
*/
static String encode(Envelope box) {
return new StringBuffer().append(box.getMinX()).append(",").append(box.getMinY())
.append(",").append(box.getMaxX()).append(",").append(box.getMaxY()).toString();
}
}