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.geoserver.ows.util;
006:
007: import java.util.ArrayList;
008: import java.util.Arrays;
009: import java.util.Collections;
010: import java.util.HashMap;
011: import java.util.Iterator;
012: import java.util.List;
013: import java.util.ListIterator;
014: import java.util.Map;
015: import java.util.StringTokenizer;
016: import java.util.logging.Level;
017: import java.util.logging.Logger;
018:
019: /**
020: * Utility class for reading Key Value Pairs from a http query string.
021: *
022: * @author Rob Hranac, TOPP
023: * @author Chris Holmes, TOPP
024: * @author Gabriel Rold?n, Axios
025: * @author Justin Deoliveira, TOPP
026: *
027: * @version $Id: KvpUtils.java 8488 2008-02-29 19:32:41Z arneke $
028: */
029: public class KvpUtils {
030: /** Class logger */
031: private static Logger LOGGER = org.geotools.util.logging.Logging
032: .getLogger("org.vfny.geoserver.requests.readers");
033:
034: /**
035: * Defines how to tokenize a string by using some sort of delimiter.
036: * <p>
037: * Default implementation uses {@link String#split(String)} with the
038: * regular expression provided at the constructor. More specialized
039: * subclasses may just override <code>readFlat(String)</code>.
040: * </p>
041: * @author Gabriel Roldan
042: * @since 1.6.1
043: */
044: public static class Tokenizer {
045: private String regExp;
046:
047: public Tokenizer(String regExp) {
048: this .regExp = regExp;
049: }
050:
051: private String getRegExp() {
052: return regExp;
053: }
054:
055: public String toString() {
056: return getRegExp();
057: }
058:
059: public List readFlat(final String rawList) {
060: if ((rawList == null || rawList.trim().equals(""))) {
061: return Collections.EMPTY_LIST;
062: } else if (rawList.equals("*")) {
063: // handles explicit unconstrained case
064: return Collections.EMPTY_LIST;
065: }
066: // -1 keeps trailing empty strings in the pack
067: String[] split = rawList.split(getRegExp(), -1);
068: return new ArrayList(Arrays.asList(split));
069: }
070: }
071:
072: /** Delimeter for KVPs in the raw string */
073: public static final Tokenizer KEYWORD_DELIMITER = new Tokenizer("&");
074:
075: /** Delimeter that seperates keywords from values */
076: public static final Tokenizer VALUE_DELIMITER = new Tokenizer("=");
077:
078: /** Delimeter for outer value lists in the KVPs */
079: public static final Tokenizer OUTER_DELIMETER = new Tokenizer(
080: "\\)\\(") {
081: public List readFlat(final String rawList) {
082: List list = new ArrayList(super .readFlat(rawList));
083: final int len = list.size();
084: if (len > 0) {
085: String first = (String) list.get(0);
086: if (first.startsWith("(")) {
087: list.set(0, first.substring(1));
088: }
089: String last = (String) list.get(len - 1);
090: if (last.endsWith(")")) {
091: list.set(len - 1, last.substring(0,
092: last.length() - 1));
093: }
094: }
095: return list;
096: }
097: };
098:
099: /** Delimeter for inner value lists in the KVPs */
100: public static final Tokenizer INNER_DELIMETER = new Tokenizer(",");
101:
102: /** Delimeter for multiple filters in a CQL filter list (<code>";"</code>) */
103: public static final Tokenizer CQL_DELIMITER = new Tokenizer(";");
104:
105: /**
106: * Attempts to parse out the proper typeNames from the FeatureId filters.
107: * It simply uses the value before the '.' character.
108: *
109: * @param rawFidList the strings after the FEATUREID url component. Should
110: * be found using kvpPairs.get("FEATUREID") in this class or one of
111: * its children
112: *
113: * @return A list of typenames, made from the featureId filters.
114: *
115: * @throws WfsException If the structure can not be read.
116: */
117: public static List getTypesFromFids(String rawFidList) {
118: List typeList = new ArrayList();
119: List unparsed = readNested(rawFidList);
120: Iterator i = unparsed.listIterator();
121:
122: while (i.hasNext()) {
123: List ids = (List) i.next();
124: ListIterator innerIterator = ids.listIterator();
125:
126: while (innerIterator.hasNext()) {
127: String fid = innerIterator.next().toString();
128: LOGGER.finer("looking at featureId" + fid);
129:
130: String typeName = fid
131: .substring(0, fid.lastIndexOf("."));
132: LOGGER.finer("adding typename: " + typeName
133: + " from fid");
134: typeList.add(typeName);
135: }
136: }
137:
138: return typeList;
139: }
140:
141: /**
142: * Calls {@link #readFlat(String)} with the {@link #INNER_DELIMETER}.
143: *
144: */
145: public static List readFlat(String rawList) {
146: return readFlat(rawList, INNER_DELIMETER);
147: }
148:
149: /**
150: * Reads a tokenized string and turns it into a list.
151: * <p>
152: * In this method, the tokenizer is actually responsible to scan the string,
153: * so this method is just a convenience to maintain backwards compatibility
154: * with the old {@link #readFlat(String, String)} and to easy the use of the
155: * default tokenizers {@link #KEYWORD_DELIMITER}, {@link #INNER_DELIMETER},
156: * {@link #OUTER_DELIMETER} and {@value #VALUE_DELIMITER}.
157: * </p>
158: * <p>
159: * Note that if the list is unspecified (ie. is null) or is unconstrained
160: * (ie. is ''), then the method returns an empty list.
161: * </p>
162: *
163: * @param rawList
164: * The tokenized string.
165: * @param tokenizer
166: * The delimeter for the string tokens.
167: *
168: * @return A list of the tokenized string.
169: * @see Tokenizer
170: */
171: public static List readFlat(final String rawList,
172: final Tokenizer tokenizer) {
173: return tokenizer.readFlat(rawList);
174: }
175:
176: /**
177: * Reads a tokenized string and turns it into a list. In this method, the
178: * tokenizer is quite flexible. Note that if the list is unspecified (ie. is
179: * null) or is unconstrained (ie. is ''), then the method returns an empty
180: * list.
181: * <p>
182: * If possible, use the method version that receives a well known
183: * {@link #readFlat(String, org.geoserver.ows.util.KvpUtils.Tokenizer) Tokenizer},
184: * as there might be special cases to catch out, like for the
185: * {@link #OUTER_DELIMETER outer delimiter "()"}. If this method delimiter
186: * argument does not match a well known Tokenizer, it'll use a simple string
187: * tokenization based on splitting out the strings with the raw passed in
188: * delimiter.
189: * </p>
190: *
191: * @param rawList
192: * The tokenized string.
193: * @param delimiter
194: * The delimeter for the string tokens.
195: *
196: * @return A list of the tokenized string.
197: *
198: * @see #readFlat(String, org.geoserver.ows.util.KvpUtils.Tokenizer)
199: */
200: public static List readFlat(String rawList, String delimiter) {
201: Tokenizer delim;
202: if (KEYWORD_DELIMITER.getRegExp().equals(delimiter)) {
203: delim = KEYWORD_DELIMITER;
204: } else if (VALUE_DELIMITER.getRegExp().equals(delimiter)) {
205: delim = VALUE_DELIMITER;
206: } else if (OUTER_DELIMETER.getRegExp().equals(delimiter)) {
207: delim = OUTER_DELIMETER;
208: } else if (INNER_DELIMETER.getRegExp().equals(delimiter)) {
209: delim = INNER_DELIMETER;
210: } else if (CQL_DELIMITER.getRegExp().equals(delimiter)) {
211: delim = CQL_DELIMITER;
212: } else {
213: LOGGER
214: .fine("Using not a well known kvp tokenization delimiter: "
215: + delimiter);
216: delim = new Tokenizer(delimiter);
217: }
218: return readFlat(rawList, delim);
219: }
220:
221: /**
222: * Reads a nested tokenized string and turns it into a list. This method is
223: * much more specific to the KVP get request syntax than the more general
224: * readFlat method. In this case, the outer tokenizer '()' and inner
225: * tokenizer ',' are both from the specification. Returns a list of lists.
226: *
227: * @param rawList
228: * The tokenized string.
229: *
230: * @return A list of lists, containing outer and inner elements.
231: *
232: * @throws WfsException
233: * When the string structure cannot be read.
234: */
235: public static List readNested(String rawList) {
236: if (LOGGER.isLoggable(Level.FINEST)) {
237: LOGGER.finest("reading nested: " + rawList);
238: }
239:
240: List kvpList = new ArrayList(10);
241:
242: // handles implicit unconstrained case
243: if (rawList == null) {
244: if (LOGGER.isLoggable(Level.FINEST)) {
245: LOGGER.finest("found implicit all requested");
246: }
247:
248: kvpList.add(Collections.EMPTY_LIST);
249: return kvpList;
250:
251: // handles explicit unconstrained case
252: } else if (rawList.equals("*")) {
253: if (LOGGER.isLoggable(Level.FINEST)) {
254: LOGGER.finest("found explicit all requested");
255: }
256:
257: kvpList.add(Collections.EMPTY_LIST);
258: return kvpList;
259:
260: // handles explicit, constrained element lists
261: } else {
262: if (LOGGER.isLoggable(Level.FINEST)) {
263: LOGGER.finest("found explicit requested");
264: }
265:
266: // handles multiple elements list case
267: if (rawList.startsWith("(")) {
268: if (LOGGER.isLoggable(Level.FINEST)) {
269: LOGGER.finest("reading complex list");
270: }
271:
272: List outerList = readFlat(rawList, OUTER_DELIMETER);
273: Iterator i = outerList.listIterator();
274:
275: while (i.hasNext()) {
276: kvpList.add(readFlat((String) i.next(),
277: INNER_DELIMETER));
278: }
279:
280: // handles single element list case
281: } else {
282: if (LOGGER.isLoggable(Level.FINEST)) {
283: LOGGER.finest("reading simple list");
284: }
285:
286: kvpList.add(readFlat(rawList, INNER_DELIMETER));
287: }
288:
289: return kvpList;
290: }
291: }
292:
293: /**
294: * creates a Map of key/value pairs from a HTTP style query String
295: *
296: * @param qString DOCUMENT ME!
297: *
298: * @return DOCUMENT ME!
299: * @deprecated not being used code wise
300: */
301: public static Map parseKvpSet(String qString) {
302: // uses the request cleaner to remove HTTP junk
303: String cleanRequest = clean(qString);
304: LOGGER.fine("clean request is " + cleanRequest);
305:
306: Map kvps = new HashMap();
307:
308: // parses initial request sream into KVPs
309: StringTokenizer requestKeywords = new StringTokenizer(
310: cleanRequest.trim(), KEYWORD_DELIMITER.getRegExp());
311:
312: // parses KVPs into values and keywords and puts them in a HashTable
313: while (requestKeywords.hasMoreTokens()) {
314: String kvpPair = requestKeywords.nextToken();
315: String key;
316: String value;
317:
318: // a bit of a horrible hack for filters, which handles problems of
319: // delimeters, which may appear in XML (such as '=' for
320: // attributes. unavoidable and illustrates the problems with
321: // mixing nasty KVP Get syntax and pure XML syntax!
322: if (kvpPair.toUpperCase().startsWith("FILTER")) {
323: String filterVal = kvpPair.substring(7);
324:
325: //int index = filterVal.lastIndexOf("</Filter>");
326: //String filt2 = kvpPair.subString
327: LOGGER.finest("putting filter value " + filterVal);
328: kvps.put("FILTER", filterVal);
329: } else {
330: // handles all other standard cases by looking for the correct
331: // delimeter and then sticking the KVPs into the hash table
332: StringTokenizer requestValues = new StringTokenizer(
333: kvpPair, VALUE_DELIMITER.getRegExp());
334:
335: // make sure that there is a key token
336: if (requestValues.hasMoreTokens()) {
337: // assign key as uppercase to eliminate case conflict
338: key = requestValues.nextToken().toUpperCase();
339:
340: // make sure that there is a value token
341: if (requestValues.hasMoreTokens()) {
342: // assign value and store in hash with key
343: value = requestValues.nextToken();
344: LOGGER.finest("putting kvp pair: " + key + ": "
345: + value);
346: kvps.put(key, value);
347: }
348: }
349: }
350: }
351:
352: LOGGER.fine("returning parsed " + kvps);
353:
354: return kvps;
355: }
356:
357: /**
358: * Cleans an HTTP string and returns pure ASCII as a string.
359: *
360: * @param raw The HTTP-encoded string.
361: *
362: * @return The string with the url escape characters replaced.
363: */
364: public static String clean(String raw) {
365: LOGGER.finest("raw request: " + raw);
366:
367: String clean = null;
368:
369: if (raw != null) {
370: try {
371: clean = java.net.URLDecoder.decode(raw, "UTF-8");
372: } catch (java.io.UnsupportedEncodingException e) {
373: LOGGER.finer("Bad encoding for decoder " + e);
374: }
375: } else {
376: return "";
377: }
378:
379: LOGGER.finest("cleaned request: " + raw);
380:
381: return clean;
382: }
383: }
|