001: /*
002: * PrefixResolver.java July 2003
003: *
004: * Copyright (C) 2003, Niall Gallagher <niallg@users.sf.net>
005: *
006: * This library is free software; you can redistribute it and/or
007: * modify it under the terms of the GNU Lesser General Public
008: * License as published by the Free Software Foundation.
009: *
010: * This library is distributed in the hope that it will be useful,
011: * but WITHOUT ANY WARRANTY; without even the implied warranty of
012: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
013: * GNU Lesser General Public License for more details.
014: *
015: * You should have received a copy of the GNU Lesser General
016: * Public License along with this library; if not, write to the
017: * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
018: * Boston, MA 02111-1307 USA
019: */
020:
021: package simple.http.load;
022:
023: import simple.util.xml.Node;
024: import simple.util.xml.Traverser;
025: import simple.http.serve.Locator;
026: import simple.util.PriorityQueue;
027: import java.io.IOException;
028: import java.util.Properties;
029: import java.util.HashMap;
030: import java.util.Map;
031: import java.util.Set;
032: import java.io.File;
033:
034: /**
035: * The <code>PrefixResolver</code> is used to extract service names
036: * and types from an XML configuration file. Each service name can
037: * be associated with match and prepare XML elements, which can be
038: * used to determine the URI targets used to locate the services
039: * and the properties that are be used to initialize the service.
040: * <p>
041: * This is used to implement a scheme similar to the Java Servlet
042: * context path mapping scheme. In this scheme a prefix path is
043: * used to resolve a Servlet, and the remaining path part is then
044: * used to acquire a resource relative the the Servlet context.
045: *
046: * @author Niall Gallagher
047: */
048: public class PrefixResolver extends Traverser {
049:
050: /**
051: * This is the configuration setup for a service instance.
052: */
053: private Configuration setup;
054:
055: /**
056: * This is used to collect properties taken from the document.
057: */
058: private Properties table;
059:
060: /**
061: * This contains the paths ordered by length decreasing.
062: */
063: private String[] list;
064:
065: /**
066: * This contains a list of offsets used for optimization.
067: */
068: private int[] skip;
069:
070: /**
071: * This is used to store the properties for each service.
072: */
073: private Map data;
074:
075: /**
076: * This map contains the service name to class name pairs.
077: */
078: private Map load;
079:
080: /**
081: * This map that contains the prefix to service name pairs.
082: */
083: private Map map;
084:
085: /**
086: * Constructor for the <code>PrefixResolver</code>. This uses
087: * a configuration file located with the <code>Locator</code>
088: * object supplied. Once the configuration file is located the
089: * service names can be resolved for arbitrary URI paths.
090: *
091: * @param lookup the locator used to find the configuration
092: */
093: public PrefixResolver(Locator lookup) {
094: this (lookup, 256);
095: }
096:
097: /**
098: * Constructor for the <code>PrefixResolver</code>. This uses
099: * a configuration file located with the <code>Locator</code>
100: * object supplied. Once the configuration file is located the
101: * service names can be resolved for arbitrary URI paths.
102: * <p>
103: * This includes a parameter that enables a maximum expected
104: * path length to be entered. This helps to optimize the
105: * resolution of a path prefix. This should typically be at
106: * least big enough to include the maximum possible path.
107: *
108: * @param lookup the object used to perform configuration
109: * @param max this is the maximum path length expected
110: */
111: public PrefixResolver(Locator lookup, int max) {
112: this .setup = new Configuration();
113: this .table = new Properties();
114: this .data = new HashMap();
115: this .load = new HashMap();
116: this .map = new HashMap();
117: this .skip = new int[max];
118: this .init(lookup);
119: }
120:
121: /**
122: * This will attempt to acquire an XML configuration file that
123: * is used to resolve relative URI paths to service names. The
124: * configuration file is located using the <code>Locator</code>
125: * supplied. This will search for the file using the names
126: * "Mapper.xml" and "mapper.xml" in that sequence.
127: *
128: * @param lookup the locator used to find the configuration
129: */
130: private void init(Locator lookup) {
131: try {
132: load(lookup);
133: } catch (Exception e) {
134: e.printStackTrace();
135: }
136: }
137:
138: /**
139: * This <code>load</code> method attempts to load the XML file
140: * file <code>Mapper.xml</code> using the given locator. If
141: * the configuration file exists then it is used to describe
142: * mappings used to load the service instances.
143: * <p>
144: * This will attempt to load the file using the UTF-8 charset
145: * so that international characters can be used for prefixes
146: * that can be used. This is compatible with ISO-8859-1.
147: *
148: * @param lookup this is the locator used to find the file
149: *
150: * @exception Exception thrown if there is an I/O problem
151: */
152: private void load(Locator lookup) throws Exception {
153: try {
154: load(lookup, "Mapper.xml");
155: } catch (IOException e) {
156: load(lookup, "mapper.xml");
157: }
158: }
159:
160: /**
161: * This will load the named file from within the given path. This
162: * is used so that a configuration file can be loaded by a locator
163: * using the specified file name. If the XML configuration file
164: * cannot be loaded this will throw an <code>Exception</code>.
165: *
166: * @param lookup this is the locator used to discover the file
167: * @param name this is the name of the configuration file loaded
168: *
169: * @exception Exception thrown if there is an I/O problem
170: */
171: private void load(Locator lookup, String name) throws Exception {
172: parse(lookup.getFile(name), "utf-8");
173: }
174:
175: /**
176: * This method is used retrieve properties for a service by
177: * using the service name. This will acquire the properties
178: * if any for the named service instance. The properties will
179: * contain zero or more name value pairs. If no properties
180: * are associated with the service the instance returned
181: * will be an empty map rather than a null object.
182: *
183: * @param name this is the name of the service instance
184: *
185: * @return returns a properties object for configuration
186: */
187: public Configuration getConfiguration(String name) {
188: return (Configuration) data.get(name);
189: }
190:
191: /**
192: * Used to resolve the class name using a service name. This is
193: * required to resolve the class name once the service name has
194: * been acquired from the <code>getName</code> method. If there
195: * is no match for the service name then null is returned.
196: *
197: * @param name this is the service name to get a class name for
198: *
199: * @return the class name that matches the service name given
200: */
201: public String getClass(String name) {
202: return (String) load.get(name);
203: }
204:
205: /**
206: * Used to resolve the service name using a path prefix. This is
207: * required to resolve the service name once the prefix of the
208: * path is acquired from the <code>getPrefix</code> method. If
209: * there is no match for the prefix then null is returned.
210: *
211: * @param prefix the path prefix to acquire a service name for
212: *
213: * @return the service name that matches the prefix path given
214: */
215: public String getName(String prefix) {
216: return (String) map.get(prefix);
217: }
218:
219: /**
220: * Used to get the prefix path for the given relative URI path,
221: * which must be normalized. This will attempt to match the
222: * start of the given path to the highest directory path. For
223: * example, given the URI path <code>/pub/bin/README</code>,
224: * the start of the path will be compared for a prefix. So it
225: * should match <code>/pub/bin/</code>, <code>/bin/</code>,
226: * and finally <code>/</code> in that order.
227: *
228: * @param normal the normalized URI path to get a prefix for
229: *
230: * @return the highest matched directory for the given path
231: */
232: public String getPrefix(String normal) {
233: int size = normal.length();
234: int off = 0;
235:
236: if (size < skip.length) {
237: off = skip[size];
238: }
239: for (int i = off; i < list.length; i++) {
240: if (normal.startsWith(list[i])) {
241: return list[i];
242: }
243: }
244: return "/";
245: }
246:
247: /**
248: * Used to acquire the path relative to the prefix. This will
249: * return the path as it is relative to the prefix resolved
250: * for the given normalized path. This will remove the start
251: * of the given normalized path if it matches a prefix path.
252: *
253: * @param normal the normalized URI path to get a path for
254: *
255: * @return the full path once its prefix has been removed
256: */
257: public String getPath(String normal) {
258: String prefix = getPrefix(normal);
259: int size = prefix.length() - 1;
260:
261: return normal.substring(size);
262: }
263:
264: /**
265: * This method is used to initialize this resolver. This will
266: * clear out all data structures used in the parsing process.
267: * It is invoked before the <code>process</code> method is used.
268: * to evaluate the element nodes extracted from the XML document.
269: */
270: protected void start() {
271: setup.clear();
272: table.clear();
273: load.clear();
274: map.clear();
275: data.clear();
276: }
277:
278: /**
279: * This is used to process a element node extracted from the XML
280: * document. It will be given each element that exists within
281: * the document. The elements of interest to this implementation
282: * are the "property", "match", "section", and "service" elements.
283: *
284: * @param node this is the node to be evaluated by this method
285: */
286: protected void process(Node node) {
287: String name = node.getName();
288:
289: if (name.equals("match")) {
290: match(node);
291: }
292: if (name.equals("service")) {
293: setup.clear();
294: table.clear();
295: }
296: if (name.equals("section")) {
297: setup.putAll(table);
298: table.clear();
299: }
300: }
301:
302: /**
303: * This is used to commit any data that has been collected during
304: * the processing of an element node. The elements of interest to
305: * this method are the "service" and "property" elements. This
306: * will save the properties collected for each "service" element.
307: *
308: * @param node this is the node to be committed by this method
309: */
310: protected void commit(Node node) {
311: String name = node.getName();
312:
313: if (name.equals("service")) {
314: service(node);
315: }
316: if (name.equals("property")) {
317: property(node);
318: }
319: if (name.equals("section")) {
320: section(node);
321: }
322: }
323:
324: /**
325: * This method is used to add the collected properties into the
326: * configuration using a section identity. This allows properties
327: * to be grouped, which enables services to use certain property
328: * values to perform specific configuration operations.
329: *
330: * @param node this is the node that represents a section
331: */
332: private void section(Node node) {
333: String name = node.getAttribute("id");
334:
335: if (name != null) {
336: setup.put(name, table.clone());
337: table.clear();
338: }
339: }
340:
341: /**
342: * This method is used to save a service definition along with
343: * all properties collected for that service. This will collect
344: * the "name" and "type" attributes from the "service" element
345: * before saving all properties collected and then storing the
346: * properties and type under the service "name" attribute.
347: *
348: * @param node this is the node that represents a service
349: */
350: private void service(Node node) {
351: String name = node.getAttribute("name");
352: String type = node.getAttribute("type");
353:
354: if (name != null) {
355: setup.putAll(table);
356: data.put(name, setup.clone());
357: load.put(name, type);
358: }
359: }
360:
361: /**
362: * This is used to store the matches extracted from the document.
363: * The attributes taken from the provided element are the "name"
364: * and "path" attributes. The path represents the prefix used to
365: * resolve to a specific service. The "name" attribute represents
366: * an identifier for the service instance.
367: *
368: * @param node this represents a match element from the XML tree
369: */
370: private void match(Node node) {
371: String name = node.getAttribute("name");
372: String path = node.getAttribute("path");
373:
374: if (path != null) {
375: map.put(path, name);
376: }
377: }
378:
379: /**
380: * This method will extract property XML tags from the provided
381: * text. This expression must contain a key attribute and must
382: * also contain a value to be extracted sucessfully. The node,
383: * should look like the XML BNF expression described below.
384: * <pre>
385: *
386: * node = "<property" key ">" *TEXT "</property>"
387: * key = "key" "=" token
388: *
389: * </pre>
390: * If the element does not contain text between the opening
391: * and closing tags then the property will not be saved. Also
392: * if the key attribute is null the entry is not committed.
393: *
394: * @param node the node that represents a property element
395: */
396: private void property(Node node) {
397: String name = node.getAttribute("key");
398:
399: if (name != null) {
400: table.put(name, node.getProperty());
401: }
402: }
403:
404: /**
405: * Used to prepare the prefix paths so that they can be matched
406: * with relative URI paths quickly. The XML configuration file
407: * used to specify the prefix paths with the service and class
408: * names will be loaded unordered into a <code>HashMap</code>.
409: * This ensures the acquired keys are sorted for searching.
410: */
411: protected void finish() {
412: index(map.keySet());
413: }
414:
415: /**
416: * Used to prepare the prefix paths so that they can be matched
417: * with relative URI paths quickly. The XML configuration file
418: * used to specify the prefix paths with the service and class
419: * names will be loaded unordered into a <code>HashMap</code>.
420: * This ensures the acquired keys are sorted for searching.
421: *
422: * @param set this contains the acquired keys to be sorted
423: */
424: private void index(Set set) {
425: list = new String[set.size()];
426: set.toArray(list);
427: prepare(list);
428: sort(list);
429: optimize(skip);
430: }
431:
432: /**
433: * This is used to prepare the prefix paths so that they all
434: * end with the <code>/</code> character. If the prefix paths
435: * within the XML configuration file do not correspond to a
436: * directory path this will simply append a <code>/</code>.
437: * For example if the configuration file was as follows.
438: *
439: * <pre>
440: * <resolve match="/demo" name="demo" type="demo.Demo"/>
441: * <resolve match="/test/" name="test" type="test.Test"/>
442: * </pre>
443: *
444: * The prefix <code>/demo</code>, which does not end in the
445: * <code>/</code> character, becomes <code>/path/</code>.
446: * This ensures that relative paths will be predictable.
447: *
448: * @param list this is the list of prefix paths to be fixed
449: */
450: private void prepare(String[] list) {
451: for (int i = 0; i < list.length; i++) {
452: Object data = map.remove(list[i]);
453:
454: if (!list[i].endsWith("/")) {
455: list[i] += "/";
456: }
457: map.put(list[i], data);
458: }
459: }
460:
461: /**
462: * This method is used to sort the list of strings by length.
463: * Sorting the strings by length is done so that the selection
464: * of a suitable path prefix will match the highest matching
465: * directory. For example if <code>/path/bin/index.html</code>
466: * was the path and the prefix paths loaded were as follows.
467: *
468: * <pre>
469: * "/path/"="package.PathService"
470: * "/path/bin/"="package.BinSerivce"
471: * "/path/doc/"="package.DocService"
472: * </pre>
473: *
474: * Then the path prefix match should be the highest directory,
475: * which would be <code>/path/bin/</code>. In order to make
476: * the match rapidly then the paths should be searched in
477: * order of length, so that when a prefix matches it is used.
478: *
479: * @param list contains the strings that are to be sorted
480: */
481: private void sort(String[] list) {
482: PriorityQueue queue = new PriorityQueue();
483:
484: for (int i = 0; i < list.length; i++) {
485: queue.add(list[i], list[i].length());
486: }
487: for (int i = 0; i < list.length; i++) {
488: list[i] = (String) queue.remove();
489: }
490: }
491:
492: /**
493: * This method is used to optimize the searching for prefixes
494: * by setting a list of offsets within a skip list. The skip
495: * list contains an offset within each index. Each index in
496: * the skip list corrosponds to a path length and the offset
497: * within that index corrosponds to an offset into the list
498: * of prefix paths. Setting up a skip list in this manner is
499: * useful in determining where to start resolutions.
500: * <p>
501: * Taking the path <code>/pub/index.html</code> for example.
502: * This path cannot possibly have a prefix path that has a
503: * length larger than it, like <code>/pub/bin/example/</code>
504: * as it is longer than it. So the skip list will basically
505: * allow a path to determine how many prefixes it can skip
506: * before the prefix size is less than or equal to its size.
507: *
508: * @param skip this is the list of offsets to be prepared
509: */
510: private void optimize(int[] skip) {
511: int size = skip.length - 1;
512: int off = 0;
513:
514: while (off < list.length) {
515: if (list[off].length() < size) {
516: skip[size--] = off;
517: } else {
518: while (off < list.length) {
519: if (list[off].length() < size) {
520: break;
521: }
522: skip[size] = off++;
523: }
524: size--;
525: }
526: }
527: while (size > 0) {
528: skip[size--] = off - 1;
529: }
530: }
531: }
|