001: /* Copyright (c) 2001 - 2007 TOPP - www.openplans.org. All rights reserved.
002: * This code is licensed under the GPL 2.0 license, availible at the root
003: * application directory.
004: */
005: package org.vfny.geoserver.util.requests.readers;
006:
007: import com.vividsolutions.jts.geom.Envelope;
008:
009: import org.geoserver.ows.util.KvpUtils;
010: import org.geotools.factory.CommonFactoryFinder;
011: import org.geotools.filter.FidFilter;
012: import org.geotools.filter.FilterFilter;
013: import org.geotools.filter.parser.ParseException;
014: import org.geotools.filter.text.cql2.CQL;
015: import org.geotools.filter.text.cql2.CQLException;
016: import org.geotools.gml.GMLFilterDocument;
017: import org.geotools.gml.GMLFilterGeometry;
018: import org.opengis.filter.Filter;
019: import org.opengis.filter.FilterFactory;
020: import org.opengis.filter.Id;
021: import org.vfny.geoserver.Request;
022: import org.vfny.geoserver.ServiceException;
023: import org.vfny.geoserver.servlets.AbstractService;
024: import org.vfny.geoserver.util.requests.FilterHandlerImpl;
025: import org.xml.sax.InputSource;
026: import org.xml.sax.SAXException;
027: import org.xml.sax.helpers.ParserAdapter;
028: import java.io.IOException;
029: import java.io.Reader;
030: import java.io.StringReader;
031: import java.util.ArrayList;
032: import java.util.Collections;
033: import java.util.HashMap;
034: import java.util.HashSet;
035: import java.util.Iterator;
036: import java.util.List;
037: import java.util.ListIterator;
038: import java.util.Map;
039: import java.util.StringTokenizer;
040: import java.util.logging.Level;
041: import java.util.logging.Logger;
042: import javax.servlet.http.HttpServletRequest;
043: import javax.xml.parsers.ParserConfigurationException;
044: import javax.xml.parsers.SAXParser;
045: import javax.xml.parsers.SAXParserFactory;
046:
047: /**
048: * Base class for all KVP readers, with some generalized convenience methods.
049: *
050: * <p>
051: * If you pass this utility a KVP request (everything after the '?' in the GET
052: * request URI), it will translate this into a list of key-word value
053: * pairs.These pairs represent every element in the KVP GET request, legal or
054: * otherwise. This class may then be subclassed and used by request-specific
055: * classes. Because there is no error checking for the KVPs in this class,
056: * subclasses must check for validity of their KVPs before passing the their
057: * requests along, but - in return - this parent class is quite flexible. For
058: * example, native KVPs may be easily parsed in its subclasses, since they are
059: * simply read and stored (without analysis) in the constructer in this class.
060: * Note that all keys are translated to upper case to avoid case conflicts.
061: * </p>
062: *
063: * @author Rob Hranac, TOPP
064: * @author Chris Holmes, TOPP
065: * @author Gabriel Roldan
066: * @version $Id: KvpRequestReader.java 8179 2008-01-16 16:56:48Z groldan $
067: */
068: abstract public class KvpRequestReader {
069: /** Class logger */
070: private static Logger LOGGER = org.geotools.util.logging.Logging
071: .getLogger("org.vfny.geoserver.requests.readers");
072:
073: /** Delimeter for KVPs in the raw string */
074: private static final String KEYWORD_DELIMITER = "&";
075:
076: /** Delimeter that seperates keywords from values */
077: private static final String VALUE_DELIMITER = "=";
078:
079: /** Delimeter for outer value lists in the KVPs */
080: protected static final String OUTER_DELIMETER = "()";
081:
082: /** Delimeter for inner value lists in the KVPs */
083: protected static final String INNER_DELIMETER = ",";
084:
085: /** Holds mappings between HTTP and ASCII encodings */
086: protected final static FilterFactory factory = CommonFactoryFinder
087: .getFilterFactory(null);
088:
089: /** KVP pair listing; stores all data from the KVP request */
090: protected Map kvpPairs = new HashMap(10);
091:
092: /** Reference to the service using the reader */
093: protected AbstractService service;
094:
095: /**
096: * Creates a reader from paramters and a service.
097: *
098: * @param kvpPairs The key-value pairs.
099: * @param service The service using the reader.
100: */
101: public KvpRequestReader(Map kvpPairs, AbstractService service) {
102: this .kvpPairs = kvpPairs;
103: this .service = service;
104: }
105:
106: /**
107: * returns the value asociated with <code>key</code> on the set of
108: * key/value pairs of this request reader
109: *
110: * @param key DOCUMENT ME!
111: *
112: * @return DOCUMENT ME!
113: */
114: protected String getValue(String key) {
115: return (String) kvpPairs.get(key);
116: }
117:
118: /**
119: * DOCUMENT ME!
120: *
121: * @param key DOCUMENT ME!
122: *
123: * @return DOCUMENT ME!
124: */
125: protected boolean keyExists(String key) {
126: return kvpPairs.containsKey(key);
127: }
128:
129: /**
130: * returns the propper Request subclass for the set of parameters it was
131: * setted up and the kind of request it is specialized for
132: *
133: * @return DOCUMENT ME!
134: */
135: public abstract Request getRequest(HttpServletRequest request)
136: throws ServiceException;
137:
138: /**
139: * Attempts to parse out the proper typeNames from the FeatureId filters.
140: * It simply uses the value before the '.' character.
141: *
142: * @param rawFidList the strings after the FEATUREID url component. Should be found
143: * using kvpPairs.get("FEATUREID") in this class or one of its
144: * children
145: *
146: * @return A list of typenames, made from the featureId filters.
147: *
148: * @throws WfsException
149: * If the structure can not be read.
150: */
151: protected List getTypesFromFids(String rawFidList) {
152: List typeList = new ArrayList(10);
153: List unparsed = readNested(rawFidList);
154: Iterator i = unparsed.listIterator();
155:
156: while (i.hasNext()) {
157: List ids = (List) i.next();
158: ListIterator innerIterator = ids.listIterator();
159:
160: while (innerIterator.hasNext()) {
161: String fid = innerIterator.next().toString();
162:
163: if (LOGGER.isLoggable(Level.FINER)) {
164: LOGGER.finer("looking at featureId" + fid);
165: }
166:
167: String typeName = fid
168: .substring(0, fid.lastIndexOf("."));
169:
170: if (LOGGER.isLoggable(Level.FINER)) {
171: LOGGER.finer("adding typename: " + typeName
172: + " from fid");
173: }
174:
175: typeList.add(typeName);
176: }
177: }
178:
179: return typeList;
180: }
181:
182: /**
183: * Reads a tokenized string and turns it into a list. In this method, the
184: * tokenizer is quite flexible. Note that if the list is unspecified (ie. is
185: * null) or is unconstrained (ie. is ''), then the method returns an empty
186: * list.
187: *
188: * @param rawList
189: * The tokenized string.
190: * @param delimiter
191: * The delimeter for the string tokens.
192: *
193: * @return A list of the tokenized string.
194: */
195: protected static List readFlat(String rawList, String delimiter) {
196: return KvpUtils.readFlat(rawList, delimiter);
197: }
198:
199: /**
200: * Reads a nested tokenized string and turns it into a list. This method is
201: * much more specific to the KVP get request syntax than the more general
202: * readFlat method. In this case, the outer tokenizer '()' and inner
203: * tokenizer ',' are both from the specification. Returns a list of lists.
204: *
205: * @param rawList
206: * The tokenized string.
207: *
208: * @return A list of lists, containing outer and inner elements.
209: *
210: * @throws WfsException
211: * When the string structure cannot be read.
212: */
213: protected static List readNested(String rawList) {
214: return KvpUtils.readNested(rawList);
215: }
216:
217: /**
218: * creates a Map of key/value pairs from a HTTP style query String
219: *
220: * @param qString
221: * DOCUMENT ME!
222: *
223: * @return DOCUMENT ME!
224: */
225: public static Map parseKvpSet(String qString) {
226: // uses the request cleaner to remove HTTP junk
227: String cleanRequest = clean(qString);
228:
229: if (LOGGER.isLoggable(Level.FINE)) {
230: LOGGER.fine("clean request is " + cleanRequest);
231: }
232:
233: Map kvps = null;
234: kvps = new HashMap(10);
235:
236: // parses initial request sream into KVPs
237: StringTokenizer requestKeywords = new StringTokenizer(
238: cleanRequest.trim(), KEYWORD_DELIMITER);
239:
240: // parses KVPs into values and keywords and puts them in a HashTable
241: while (requestKeywords.hasMoreTokens()) {
242: String kvpPair = requestKeywords.nextToken();
243: String key;
244: String value;
245:
246: // a bit of a horrible hack for filters, which handles problems of
247: // delimeters, which may appear in XML (such as '=' for
248: // attributes. unavoidable and illustrates the problems with
249: // mixing nasty KVP Get syntax and pure XML syntax!
250: // JD-adding SLD_BODY, when wms moves to new architecture this
251: // can be fixed
252: if (kvpPair.toUpperCase().startsWith("FILTER")) {
253: String filterVal = kvpPair.substring(7);
254:
255: // int index = filterVal.lastIndexOf("</Filter>");
256: // String filt2 = kvpPair.subString
257: if (LOGGER.isLoggable(Level.FINEST)) {
258: LOGGER.finest("putting filter value " + filterVal);
259: }
260:
261: kvps.put("FILTER", filterVal);
262: } else if (kvpPair.toUpperCase().startsWith("SLD_BODY")) {
263: String sldBodyVal = kvpPair.substring(9);
264: kvps.put("SLD_BODY", sldBodyVal);
265: } else {
266: // handles all other standard cases by looking for the correct
267: // delimeter and then sticking the KVPs into the hash table
268: StringTokenizer requestValues = new StringTokenizer(
269: kvpPair, VALUE_DELIMITER);
270:
271: // make sure that there is a key token
272: if (requestValues.hasMoreTokens()) {
273: // assign key as uppercase to eliminate case conflict
274: key = requestValues.nextToken().toUpperCase();
275:
276: // make sure that there is a value token
277: if (requestValues.hasMoreTokens()) {
278: // assign value and store in hash with key
279: value = requestValues.nextToken();
280: LOGGER.finest("putting kvp pair: " + key + ": "
281: + value);
282: kvps.put(key, value);
283: } else {
284: kvps.put(key, "");
285: }
286: }
287: }
288: }
289:
290: if (LOGGER.isLoggable(Level.FINE)) {
291: LOGGER.fine("returning parsed " + kvps);
292: }
293:
294: return kvps;
295: }
296:
297: /**
298: * Cleans an HTTP string and returns pure ASCII as a string.
299: *
300: * @param raw
301: * The HTTP-encoded string.
302: *
303: * @return The string with the url escape characters replaced.
304: */
305: private static String clean(String raw) {
306: if (LOGGER.isLoggable(Level.FINEST)) {
307: LOGGER.finest("raw request: " + raw);
308: }
309:
310: String clean = null;
311:
312: if (raw != null) {
313: try {
314: clean = java.net.URLDecoder.decode(raw, "UTF-8");
315: } catch (java.io.UnsupportedEncodingException e) {
316: if (LOGGER.isLoggable(Level.FINER)) {
317: LOGGER.finer("Bad encoding for decoder " + e);
318: }
319: }
320: } else {
321: return "";
322: }
323:
324: if (LOGGER.isLoggable(Level.FINEST)) {
325: LOGGER.finest("cleaned request: " + raw);
326: }
327:
328: return clean;
329: }
330:
331: /**
332: * Returns the service handling request.
333: */
334: public AbstractService getServiceRef() {
335: return service;
336: }
337:
338: /**
339: * sets the service handling request.
340: */
341: public void setServiceRef(AbstractService service) {
342: this .service = service;
343: }
344:
345: /**
346: * parses the BBOX parameter, wich must be a String of the form
347: * <code>minx,miny,maxx,maxy</code> and returns a corresponding
348: * <code>Envelope</code> object
349: * @param bboxParam TODO
350: *
351: * @return the <code>Envelope</code> represented by the request BBOX
352: * parameter
353: *
354: * @throws WmsException if the value of the BBOX request parameter can't be
355: * parsed as four <code>double</code>'s
356: */
357: protected Envelope parseBbox(String bboxParam)
358: throws ServiceException {
359: Envelope bbox = null;
360: Object[] bboxValues = readFlat(bboxParam, INNER_DELIMETER)
361: .toArray();
362:
363: if (bboxValues.length != 4) {
364: throw new ServiceException(bboxParam
365: + " is not a valid pair of coordinates", getClass()
366: .getName());
367: }
368:
369: try {
370: double minx = Double.parseDouble(bboxValues[0].toString());
371: double miny = Double.parseDouble(bboxValues[1].toString());
372: double maxx = Double.parseDouble(bboxValues[2].toString());
373: double maxy = Double.parseDouble(bboxValues[3].toString());
374: bbox = new Envelope(minx, maxx, miny, maxy);
375:
376: if (minx > maxx) {
377: throw new ServiceException("illegal bbox, minX: "
378: + minx + " is " + "greater than maxX: " + maxx);
379: }
380:
381: if (miny > maxy) {
382: throw new ServiceException("illegal bbox, minY: "
383: + miny + " is " + "greater than maxY: " + maxy);
384: }
385: } catch (NumberFormatException ex) {
386: throw new ServiceException(ex,
387: "Illegal value for BBOX parameter: " + bboxParam,
388: getClass().getName() + "::parseBbox()");
389: }
390:
391: return bbox;
392: }
393:
394: /**
395: * Parses an OGC filter
396: * @param filter
397: * @return
398: * @throws ServiceException
399: *
400: *
401: */
402: protected List readOGCFilter(String filter) throws ServiceException {
403: List filters = new ArrayList();
404: List unparsed;
405: ListIterator i;
406: LOGGER.finest("reading filter: " + filter);
407: unparsed = readFlat(filter, OUTER_DELIMETER);
408: i = unparsed.listIterator();
409:
410: while (i.hasNext()) {
411: String next = (String) i.next();
412:
413: if (next.trim().equals("")) {
414: filters.add(Filter.INCLUDE);
415: } else {
416: filters.add(parseXMLFilter(new StringReader(next)));
417: }
418: }
419:
420: return filters;
421: }
422:
423: /**
424: * Parses a CQL filter
425: * @param filter
426: * @return
427: * @throws ServiceException
428: *
429: * @deprecated use {@link CQLFilterKvpParser}
430: */
431: protected List readCQLFilter(String filter) throws ServiceException {
432: try {
433: return CQL.toFilterList(filter);
434: } catch (CQLException pe) {
435: throw new ServiceException(
436: "Could not parse CQL filter list."
437: + pe.getMessage(), pe);
438: }
439: }
440:
441: /**
442: * Parses fid filters
443: * @param fid
444: * @return
445: */
446: protected List readFidFilters(String fid) {
447: List filters = new ArrayList();
448: List unparsed;
449: ListIterator i;
450: LOGGER.finest("reading fid filter: " + fid);
451: unparsed = readNested(fid);
452: i = unparsed.listIterator();
453:
454: while (i.hasNext()) {
455: List ids = (List) i.next();
456: ListIterator innerIterator = ids.listIterator();
457:
458: while (innerIterator.hasNext()) {
459: HashSet set = new HashSet();
460: set.add(factory
461: .featureId((String) innerIterator.next()));
462:
463: Id fidFilter = factory.id(set);
464: filters.add(fidFilter);
465: LOGGER.finest("added fid filter: " + fidFilter);
466: }
467: }
468:
469: return filters;
470: }
471:
472: /**
473: * Reads the Filter XML request into a geotools Feature object.
474: *
475: * @param rawRequest The plain POST text from the client.
476: *
477: * @return The geotools filter constructed from rawRequest.
478: *
479: * @throws WfsException For any problems reading the request.
480: *
481: * @deprecated use {@link FilterKvpParser}
482: */
483: protected Filter parseXMLFilter(Reader rawRequest)
484: throws ServiceException {
485: // translate string into a proper SAX input source
486: InputSource requestSource = new InputSource(rawRequest);
487:
488: // instantiante parsers and content handlers
489: FilterHandlerImpl contentHandler = new FilterHandlerImpl();
490: FilterFilter filterParser = new FilterFilter(contentHandler,
491: null);
492: GMLFilterGeometry geometryFilter = new GMLFilterGeometry(
493: filterParser);
494: GMLFilterDocument documentFilter = new GMLFilterDocument(
495: geometryFilter);
496:
497: // read in XML file and parse to content handler
498: try {
499: SAXParserFactory factory = SAXParserFactory.newInstance();
500: SAXParser parser = factory.newSAXParser();
501: ParserAdapter adapter = new ParserAdapter(parser
502: .getParser());
503:
504: adapter.setContentHandler(documentFilter);
505: adapter.parse(requestSource);
506: LOGGER.fine("just parsed: " + requestSource);
507: } catch (SAXException e) {
508: //SAXException does not sets initCause(). Instead, it holds its own "exception" field.
509: if (e.getException() != null && e.getCause() == null) {
510: e.initCause(e.getException());
511: }
512: throw new ServiceException(e,
513: "XML getFeature request SAX parsing error",
514: XmlRequestReader.class.getName());
515: } catch (IOException e) {
516: throw new ServiceException(e,
517: "XML get feature request input error",
518: XmlRequestReader.class.getName());
519: } catch (ParserConfigurationException e) {
520: throw new ServiceException(e,
521: "Some sort of issue creating parser",
522: XmlRequestReader.class.getName());
523: }
524:
525: LOGGER.fine("passing filter: " + contentHandler.getFilter());
526:
527: return contentHandler.getFilter();
528: }
529: }
|