Hello GeoServer Developers,
We have been assessing the use of GeoServer for some projects and have found
it to be very promising -- so far it has addressed the majority of our
functional requirements. Thank you for the great work that you have done.
Our system tracks a reasonably large number of layers which are stored in a
database and serves a reasonably large number of real-time daily image
rendering requests which are ad-hoc in nature. Individual layers are
rendered many times and are joined to a wide range of non-spatial data that
is determined at runtime. As such, we are very interested in the Parametric
SQL View Feature of the GeoServer 2.1 release. For the sake of efficiency
and scaling we are hoping to avoid the creation of many on-demand database
tables, database views or GeoServer Feature Type and Layer Definitions. The
one problem that we are encountering is that our image requests will often
reference the same layer multiple times (but joined to different data
sets). Because the current capabilities of the Parametric SQL View Feature
of GeoServer scopes the incoming SQL parameters to the request instead of
individual layers, we have not been able to emulate this behavior with
GeoServer.
After some review of the GeoServer trunk code I was able to put together a
proof-of-concept that would allow WMS requests using the Parametric SQL View
Feature to take advantage of positionally-specified parameters (just as
styles are positionally specified). For example, the layer
gisws:filter_pg_layer_19 is defined using parameterized SQL (the parameters
are rdid and sid). The changes that I describe allow WMS to render an image
that includes two copies of the layer joined to different data sets through
the specification of view parameters by layer. The syntax that I use is
compatible with the current syntax -- if there is only one layer in use, the
syntax is unchanged. An example WMS request using this syntax is:
http://localhost:8080/geoserver/wms?
VERSION=1.1.0&
SERVICE=wms&
REQUEST=getmap&
WIDTH=600&
HEIGHT=400&
SRS=EPSG:4326&
BBOX=-24.542,51.297,-0.018,66.565&
FORMAT=image/png&
STYLES=red,blue&
LAYERS=gisws:filter_pg_layer_19,gisws:filter_pg_layer_19&
VIEWPARAMS=rdid:1;sid:2,rdid:1;sid:1
or via the reflector:
http://localhost:8080/geoserver/wms/reflect?
WIDTH=800&
BBOX=-24.542,51.297,-0.018,66.565&
FORMAT=image/png&
STYLES=red,blue&
LAYERS=gisws:filter_pg_layer_19,gisws:filter_pg_layer_19&
VIEWPARAMS=rdid:1;sid:2,rdid:1;sid:1
The only scenarios that I can think of where these changes could cause
behavioral incompatibilities with the current methodology are:
1. Multiple layers are being used and the view parameters apply to
several of them. In this case, the request form would need to replicate the
parameters for each layer in the VIEWPARAMS (e.g. VIEWPARAMS=a:1;b:2 would
need to be changed to VIEWPARAMS=a:1;b:2,a:1;b:2[, ...]).
2. Multiple layers are being used, one of which is a parameterized SQL
view but is not listed first in the sequence (e.g.
LAYERS=static,parameterized&VIEWPARAMS=a:1;b:2). In this case the
VIEWPARAMS would need to be left-padded so that the parameters were in the
same position as the parametrized layer
(LAYERS=static,parameterized&VIEWPARAMS=,a:1;b:2) or, if viable, the layer
sequence could instead be changed
(LAYERS=parameterized,static&VIEWPARAMS=a:1;b:2).
I have assessed support for other WMS request types given these changes.
Everything works as expected for GetFeatureInfo:
http://localhost:8080/geoserver/wms?
VERSION=1.1.0&
REQUEST=GetFeatureInfo&
SERVICE=wms&
WIDTH=600&
HEIGHT=400&
SRS=EPSG:4326&
BBOX=-24.542,51.297,-0.018,66.565&
LAYERS=gisws:filter_pg_layer_19,gisws:filter_pg_layer_19&
VIEWPARAMS=rdid:1;sid:2,rdid:1;sid:1&
QUERY_LAYERS=gisws:filter_pg_layer_19,gisws:filter_pg_layer_19&
X=100&
Y=50&
INFO_FORMAT=application/vnd.ogc.gml
As best I can tell these changes do not seem to be relevant to the
DescribeLayer or GetLegendGraphic request types.
The proof-of-concept changes that I made were limited to OWS and WMS and
include the following:
1. The view params object was switched from Map to List<Map>.
2. A new WMS ViewParamsKvpParser was created (which delegates its tokens
to the FormatOptionsKvpParser).
3. References to view params now index into the list based on layer
sequencing.
4. The GetMapKvpRequestReaderTest was expanded to include appropriate
new tests.
I specifically avoided making any changes in WFS, as it seems like the
approach is not as well defined but I would be happy to work on integrating
these changes into WFS if anyone thinks it would be worthwhile and they
would be willing to provide some guidance. On first glance it seems like
positional referencing may not be the best approach for WFS.
While testing things I noticed a couple of issues that I'd be happy to
elaborate on via JIRA if appropriate:
1. The FormatOptionsKvpParser does not provide any escaping mechanism
(I'm thinking along the lines of the comment in WFS 1.1.0 14.2.2). I don't
know that this section of the specification is directly relevant but it does
seem like VIEWPARAMS should allow any values to be specified (particularly
if character-based SQL parameters are being used). I have included an
alternate implementation of FormatOptionsKvpParser that allows for values
from KVPs to support backslash as an escape character and added supporting
test cases.
14.2.2 Parameter lists
Parameters consisting of lists shall use the comma (",") as the
delimiter between items in
the list. In addition, multiple lists can be specified as the value
of a parameter by
enclosing each list in parentheses; "(", ")". The characters “,”,
“(“ and “)” may be
escaped using the “\” character.
2. The output for GetFeatureInfo is incomplete if INFO_FORMAT is not
specified (only the first feature is output).
3. GetLegendGraphic is not properly handling the FORMAT parameter -- it
seems to require specification of both OUTPUTFORMAT and FORMAT (and it uses
OUTPUTFORMAT to determine the Response instance(s) to use). I assume that
this is a regression or is due to work-in-progress.
I hope that there is interest in these enhancements (and it would be great
if there was a chance of them getting into the 2.1 release). I would be
happy to assist with this in any manner that I can.
Best regards,
Eli Miller
***************************************************
The information contained in this e-mail message
is intended only for the use of the recipient(s)
named above and may contain information that is
privileged, confidential, and/or proprietary.
If you are not the intended recipient, you may not
review, copy or distribute this message. If you have
received this communication in error, please notify
the sender immediately by e-mail, and delete the original message.
***************************************************
Property changes on: wms
___________________________________________________________________
Modified: svn:ignore
- target
cobertura.ser
.classpath
.project
.settings
+ target
cobertura.ser
.classpath
.project
.settings
bin
Index: wms/src/test/java/org/geoserver/wms/map/GetMapKvpRequestReaderTest.java
===================================================================
--- wms/src/test/java/org/geoserver/wms/map/GetMapKvpRequestReaderTest.java (revision 14944)
+++ wms/src/test/java/org/geoserver/wms/map/GetMapKvpRequestReaderTest.java (working copy)
@@ -467,9 +467,37 @@
GetMapRequest request = (GetMapRequest) reader.createRequest();
request = (GetMapRequest) reader.read(request, parseKvp(raw), caseInsensitiveKvp(raw));
- Map viewParams = request.getViewParams();
- assertEquals(2, viewParams.size());
+ List<Map<String, String>> viewParamsList = request.getViewParams();
+ assertEquals(1, viewParamsList.size());
+ Map viewParams = viewParamsList.get(0);
assertEquals("WHERE PERSONS > 1000000", viewParams.get("where"));
assertEquals("ABCD", viewParams.get("str"));
}
+
+ public void testMultipleViewParams() throws Exception {
+ HashMap raw = new HashMap();
+ raw.put("layers", getLayerId(MockData.BASIC_POLYGONS) + "," + getLayerId(MockData.BASIC_POLYGONS));
+ raw.put("styles", "");
+ raw.put("format", "image/jpeg");
+ raw.put("srs", "epsg:3003");
+ raw.put("bbox", "-10,-10,10,10");
+ raw.put("height", "600");
+ raw.put("width", "800");
+ raw.put("request", "GetMap");
+ raw.put("service", "wms");
+ raw.put("viewParams", "where:WHERE PERSONS > 1000000;str:ABCD,where:WHERE PERSONS > 10;str:FOO");
+
+ GetMapRequest request = (GetMapRequest) reader.createRequest();
+ request = (GetMapRequest) reader.read(request, parseKvp(raw), caseInsensitiveKvp(raw));
+
+ List<Map<String, String>> viewParamsList = request.getViewParams();
+ assertEquals(2, viewParamsList.size());
+ Map viewParams = viewParamsList.get(0);
+ assertEquals("WHERE PERSONS > 1000000", viewParams.get("where"));
+ assertEquals("ABCD", viewParams.get("str"));
+ viewParams = viewParamsList.get(1);
+ assertEquals("WHERE PERSONS > 10", viewParams.get("where"));
+ assertEquals("FOO", viewParams.get("str"));
+ }
+
}
Index: wms/src/main/java/org/geoserver/wms/GetMap.java
===================================================================
--- wms/src/main/java/org/geoserver/wms/GetMap.java (revision 14944)
+++ wms/src/main/java/org/geoserver/wms/GetMap.java (working copy)
@@ -261,9 +261,9 @@
.getLocalPart());
definitionQuery.setVersion(featureVersion);
definitionQuery.setFilter(layerFilter);
- if (request.getViewParams() != null && request.getViewParams().size() > 0) {
+ if (request.getViewParams() != null && i < request.getViewParams().size() ) {
definitionQuery.setHints(new Hints(Hints.VIRTUAL_TABLE_PARAMETERS, request
- .getViewParams()));
+ .getViewParams().get(i)));
}
// check for startIndex + offset
Index: wms/src/main/java/org/geoserver/wms/WMSRequests.java
===================================================================
--- wms/src/main/java/org/geoserver/wms/WMSRequests.java (revision 14944)
+++ wms/src/main/java/org/geoserver/wms/WMSRequests.java (working copy)
@@ -424,19 +424,18 @@
/**
* Encodes a map of formation options to be used as the value in a kvp.
*
- * @param formatOptions
- * The map of formation options.
+ * @param formatOptions The map of formation options.
+ * @param sb StringBuffer to append to.
*
* @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) {
+ public static void encodeFormatOptions(Map formatOptions, StringBuffer sb) {
if (formatOptions == null || formatOptions.isEmpty()) {
- return "";
+ return;
}
- StringBuffer sb = new StringBuffer();
for (Iterator e = formatOptions.entrySet().iterator(); e.hasNext();) {
Map.Entry entry = (Map.Entry) e.next();
String key = (String) entry.getKey();
@@ -465,6 +464,50 @@
}
sb.setLength(sb.length());
+ }
+
+ /**
+ * 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();
}
Index: wms/src/main/java/org/geoserver/wms/GetFeatureInfo.java
===================================================================
--- wms/src/main/java/org/geoserver/wms/GetFeatureInfo.java (revision 14944)
+++ wms/src/main/java/org/geoserver/wms/GetFeatureInfo.java (working copy)
@@ -123,7 +123,7 @@
} finally {
EnvFunction.clearLocalValues();
}
-
+
FeatureCollectionType ret = buildResults(results);
return ret;
@@ -193,7 +193,7 @@
final int x = request.getXPixel();
final int y = request.getYPixel();
final int buffer = request.getGetMapRequest().getBuffer();
- final Map<String, String> viewParams = request.getGetMapRequest().getViewParams();
+ final List<Map<String, String>> viewParams = request.getGetMapRequest().getViewParams();
final GetMapRequest getMapReq = request.getGetMapRequest();
final CoordinateReferenceSystem requestedCRS = getMapReq.getCrs(); // optional, may be null
@@ -228,9 +228,13 @@
FeatureCollection collection = null;
if (layer.getType() == MapLayerInfo.TYPE_VECTOR) {
- collection = identifyVectorLayer(request, filters, x, y, buffer, viewParams,
+ Map layerParams = null;
+ if (viewParams != null && i < viewParams.size()) {
+ layerParams = viewParams.get(i);
+ }
+ collection = identifyVectorLayer(request, filters, x, y, buffer, layerParams,
requestedCRS, width, height, bbox, ff, results, i, layer, rules);
-
+
} else if (layer.getType() == MapLayerInfo.TYPE_RASTER) {
final CoverageInfo cinfo = requestedLayers.get(i).getCoverage();
final AbstractGridCoverage2DReader reader = (AbstractGridCoverage2DReader) cinfo
Index: wms/src/main/java/org/geoserver/wms/GetMapRequest.java
===================================================================
--- wms/src/main/java/org/geoserver/wms/GetMapRequest.java (revision 14944)
+++ wms/src/main/java/org/geoserver/wms/GetMapRequest.java (working copy)
@@ -49,7 +49,7 @@
private Map /* <String,Object> */env = new HashMap();
/** sql view parameters */
- private Map<String, String> viewParams = new HashMap<String, String>();
+ private List<Map<String, String>> viewParams = new ArrayList<Map<String, String>>();
public GetMapRequest() {
super("GetMap");
@@ -108,7 +108,7 @@
*
* @return
*/
- public Map<String, String> getViewParams() {
+ public List<Map<String, String>> getViewParams() {
return viewParams;
}
@@ -373,7 +373,7 @@
*
* @param viewParams
*/
- public void setViewParams(Map<String, String> viewParams) {
+ public void setViewParams(List<Map<String, String>> viewParams) {
this.viewParams = viewParams;
}
Index: wms/src/main/java/applicationContext.xml
===================================================================
--- wms/src/main/java/applicationContext.xml (revision 14944)
+++ wms/src/main/java/applicationContext.xml (working copy)
@@ -145,8 +145,7 @@
<bean id="wmsEnviromentKvpParser" class="org.geoserver.ows.kvp.FormatOptionsKvpParser">
<constructor-arg index="0" value="env"/>
</bean>
- <bean id="wmsSqlViewKvpParser" class="org.geoserver.ows.kvp.FormatOptionsKvpParser">
- <constructor-arg index="0" value="viewParams"/>
+ <bean id="wmsSqlViewKvpParser" class="org.geoserver.ows.kvp.ViewParamsKvpParser">
<!-- cannot set the service or it won't work for the reflectors -->
</bean>
<bean id="bgColorKvpParser" class="org.geoserver.wms.kvp.ColorKvpParser">
Index: ows/src/test/java/org/geoserver/ows/kvp/KvpUtilsTest.java
===================================================================
--- ows/src/test/java/org/geoserver/ows/kvp/KvpUtilsTest.java (revision 14944)
+++ ows/src/test/java/org/geoserver/ows/kvp/KvpUtilsTest.java (working copy)
@@ -82,4 +82,66 @@
assertEquals(expectedList, actual);
}
+ public void testEscapedTokens() {
+ // test trivial scenarios
+ List<String> actual = KvpUtils.escapedTokens("", ',');
+ assertEquals(Arrays.asList(""), actual);
+
+ actual = KvpUtils.escapedTokens(",", ',');
+ assertEquals(Arrays.asList("", ""), actual);
+
+ actual = KvpUtils.escapedTokens("a,b", ',');
+ assertEquals(Arrays.asList("a", "b"), actual);
+
+ actual = KvpUtils.escapedTokens("a,b,c", ',');
+ assertEquals(Arrays.asList("a", "b", "c"), actual);
+
+ // test escaped data
+ actual = KvpUtils.escapedTokens("\\\\,\\\\", ',');
+ assertEquals(Arrays.asList("\\\\", "\\\\"), actual);
+
+ actual = KvpUtils.escapedTokens("a\\,b,c", ',');
+ assertEquals(Arrays.asList("a\\,b", "c"), actual);
+
+ // test error conditions
+ try {
+ KvpUtils.escapedTokens(null, ',');
+ fail("Expected IllegalArgumentException.");
+ } catch (IllegalArgumentException e) { ; }
+
+ try {
+ KvpUtils.escapedTokens("", '\\');
+ fail("Expected IllegalArgumentException.");
+ } catch (IllegalArgumentException e) { ; }
+
+ try {
+ KvpUtils.escapedTokens("\\", '\\');
+ fail("Expected IllegalArgumentException.");
+ } catch (IllegalArgumentException e) { ; }
+ }
+
+ public static void testUnescape() {
+ // test trivial scenarios
+ String actual = KvpUtils.unescape("abc");
+ assertEquals("abc", actual);
+
+ // test escape sequences
+ actual = KvpUtils.unescape("abc\\\\");
+ assertEquals("abc\\", actual);
+
+ actual = KvpUtils.unescape("abc\\d");
+ assertEquals("abcd", actual);
+
+ // test error conditions
+ try {
+ KvpUtils.unescape(null);
+ fail("Expected IllegalArgumentException.");
+ } catch (IllegalArgumentException e) { ; }
+
+ try {
+ KvpUtils.unescape("\\");
+ fail("Expected IllegalArgumentException.");
+ } catch (IllegalArgumentException e) { ; }
+ }
+
}
Index: ows/src/main/java/org/geoserver/ows/kvp/FormatOptionsKvpParser.java
===================================================================
--- ows/src/main/java/org/geoserver/ows/kvp/FormatOptionsKvpParser.java (revision 14944)
+++ ows/src/main/java/org/geoserver/ows/kvp/FormatOptionsKvpParser.java (working copy)
@@ -13,6 +13,7 @@
import org.geoserver.ows.KvpParser;
import org.geoserver.ows.util.CaseInsensitiveMap;
+import org.geoserver.ows.util.KvpUtils;
import org.geoserver.platform.GeoServerExtensions;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
@@ -55,26 +56,16 @@
List parsers = GeoServerExtensions.extensions(KvpParser.class, applicationContext);
Map formatOptions = new CaseInsensitiveMap(new HashMap());
- //TODO: refactor some of this routine out into utility class since
- // much of the logic is duplicated from the dispatcher
- StringTokenizer st = new StringTokenizer(value, ";");
-
- while (st.hasMoreTokens()) {
- String kvp = (String) st.nextToken();
- String[] kv = kvp.split(":");
-
- String key = null;
- String raw = null;
-
- if (kv.length == 1) {
- //assume its a on/off (boolean) kvp
- key = kv[0];
- raw = "true";
- } else {
- key = kv[0];
- raw = kv[1];
- }
-
+ List<String> kvps = KvpUtils.escapedTokens(value, ';');
+
+ for (String kvp : kvps) {
+ List<String> kv = KvpUtils.escapedTokens(kvp, ':');
+ if (kv.size() > 2) {
+ throw new IllegalArgumentException("Invalid key-value pair length (" + kv.size() + " elements).");
+ }
+ String key = kv.get(0);
+ String raw = kv.size() == 1 ? "true" : KvpUtils.unescape(kv.get(1));
+
Object parsed = null;
for (Iterator p = parsers.iterator(); p.hasNext();) {
@@ -96,7 +87,7 @@
formatOptions.put(key, parsed);
}
-
+
return formatOptions;
}
}
Index: ows/src/main/java/org/geoserver/ows/kvp/ViewParamsKvpParser.java
===================================================================
--- ows/src/main/java/org/geoserver/ows/kvp/ViewParamsKvpParser.java (revision 0)
+++ ows/src/main/java/org/geoserver/ows/kvp/ViewParamsKvpParser.java (revision 0)
@@ -0,0 +1,59 @@
+/* Copyright (c) 2001 - 2007 TOPP - www.openplans.org. All rights reserved.
+ * This code is licensed under the GPL 2.0 license, availible at the root
+ * application directory.
+ */
+package org.geoserver.ows.kvp;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.geoserver.ows.KvpParser;
+import org.geoserver.ows.util.KvpUtils;
+import org.geoserver.platform.GeoServerExtensions;
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+
+/**
+ * Parses view parameters which are of the form:
+ *
+ * <pre>VIEWPARAMS=opt1:val1,val2;opt2:val1;opt3:...[,opt1:val1,val2;opt2:val1;opt3:...]</pre>
+ *
+ * @see FormatOptionsKvpParser
+ */
+public class ViewParamsKvpParser extends KvpParser implements ApplicationContextAware {
+ /**
+ * application context used to lookup KvpParsers
+ */
+ ApplicationContext applicationContext;
+
+ public ViewParamsKvpParser() {
+ super("viewparams", List.class);
+ }
+
+ public void setApplicationContext(ApplicationContext applicationContext)
+ throws BeansException {
+ this.applicationContext = applicationContext;
+ }
+
+ public Object parse(String value) throws Exception {
+ List ret = new ArrayList();
+ List parsers = GeoServerExtensions.extensions(KvpParser.class, applicationContext);
+ KvpParser formatOptionsParser = null;
+ for (Object o : parsers) {
+ KvpParser parser = (KvpParser) o;
+ if ( parser.getKey().equalsIgnoreCase("format_options") ) {
+ formatOptionsParser = parser;
+ break;
+ }
+ }
+ if (formatOptionsParser == null) {
+ throw new IllegalStateException("Missing format options parser.");
+ }
+ for (String kvp : KvpUtils.escapedTokens(value, ',')) {
+ ret.add(formatOptionsParser.parse(kvp));
+ }
+
+ return ret;
+ }
+}
Index: ows/src/main/java/org/geoserver/ows/util/KvpUtils.java
===================================================================
--- ows/src/main/java/org/geoserver/ows/util/KvpUtils.java (revision 14968)
+++ ows/src/main/java/org/geoserver/ows/util/KvpUtils.java (working copy)
@@ -484,4 +484,81 @@
return result;
}
+
+ /**
+ * Tokenize a String using the specified separator character and the backslash as an escape
+ * character (see OGC WFS 1.1.0 14.2.2). Escape characters within the tokens are not resolved.
+ *
+ * @param s the String to parse
+ * @param separator the character that separates tokens
+ *
+ * @return list of tokens
+ */
+ public static List<String> escapedTokens(String s, char separator) {
+ if (s == null) {
+ throw new IllegalArgumentException("The String to parse may not be null.");
+ }
+ if (separator == '\\') {
+ throw new IllegalArgumentException("The separator may not be a backslash.");
+ }
+ List<String> ret = new ArrayList<String>();
+ StringBuilder sb = new StringBuilder();
+ boolean escaped = false;
+
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ if (c == separator && !escaped) {
+ ret.add(sb.toString());
+ sb.setLength(0);
+ } else {
+ if (escaped) {
+ escaped = false;
+ sb.append('\\');
+ sb.append(c);
+ } else if (c == '\\') {
+ escaped = true;
+ } else {
+ sb.append(c);
+ }
+ }
+ }
+ if (escaped) {
+ throw new IllegalStateException("The specified String ends with an incomplete escape sequence.");
+ }
+ ret.add(sb.toString());
+ return ret;
+ }
+
+ /**
+ * Resolve escape sequences in a String.
+ *
+ * @param s the String to unescape
+ *
+ * @return resolved String
+ */
+ public static String unescape(String s) {
+ if (s == null) {
+ throw new IllegalArgumentException("The String to unescape may not be null.");
+ }
+ StringBuilder sb = new StringBuilder();
+ boolean escaped = false;
+
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ if (escaped) {
+ escaped = false;
+ sb.append(c);
+ } else if (c == '\\') {
+ escaped = true;
+ } else {
+ sb.append(c);
+ }
+ }
+ if (escaped) {
+ throw new IllegalArgumentException("The specified String ends with an incomplete escape sequence.");
+ }
+ return sb.toString();
+ }
+
+
}
------------------------------------------------------------------------------
Beautiful is writing same markup. Internet Explorer 9 supports
standards for HTML5, CSS3, SVG 1.1, ECMAScript5, and DOM L2 & L3.
Spend less time writing and rewriting code and more time creating great
experiences on the web. Be a part of the beta today.
http://p.sf.net/sfu/beautyoftheweb
_______________________________________________
Geoserver-devel mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/geoserver-devel