001: /* *************************************************************************
002:
003: Millstone(TM)
004: Open Sourced User Interface Library for
005: Internet Development with Java
006:
007: Millstone is a registered trademark of IT Mill Ltd
008: Copyright (C) 2000-2005 IT Mill Ltd
009:
010: *************************************************************************
011:
012: This library is free software; you can redistribute it and/or
013: modify it under the terms of the GNU Lesser General Public
014: license version 2.1 as published by the Free Software Foundation.
015:
016: This library is distributed in the hope that it will be useful,
017: but WITHOUT ANY WARRANTY; without even the implied warranty of
018: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
019: Lesser General Public License for more details.
020:
021: You should have received a copy of the GNU Lesser General Public
022: License along with this library; if not, write to the Free Software
023: Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
024:
025: *************************************************************************
026:
027: For more information, contact:
028:
029: IT Mill Ltd phone: +358 2 4802 7180
030: Ruukinkatu 2-4 fax: +358 2 4802 7181
031: 20540, Turku email: info@itmill.com
032: Finland company www: www.itmill.com
033:
034: Primary source for MillStone information and releases: www.millstone.org
035:
036: ********************************************************************** */
037:
038: package org.millstone.webadapter;
039:
040: import org.millstone.base.terminal.SystemError;
041: import org.millstone.base.terminal.Terminal;
042: import org.millstone.base.terminal.VariableOwner;
043: import org.millstone.base.terminal.UploadStream;
044:
045: import java.util.ArrayList;
046: import java.util.Arrays;
047: import java.util.HashMap;
048: import java.util.HashSet;
049: import java.util.Map;
050: import java.util.Set;
051: import java.util.List;
052: import java.util.StringTokenizer;
053: import java.util.Enumeration;
054: import java.util.WeakHashMap;
055: import java.io.IOException;
056: import java.lang.ref.WeakReference;
057:
058: import javax.servlet.http.HttpServletRequest;
059: import java.util.LinkedList;
060: import java.util.Iterator;
061:
062: /**
063: * Class implementing the MillStone WebAdapter variable mappings.
064: *
065: * @author IT Mill Ltd.
066: * @version 3.1.1
067: * @since 3.0
068: */
069: public class HttpVariableMap {
070:
071: // Id <-> (Owner,Name) mapping
072: private Map idToNameMap = new HashMap();
073: private Map idToTypeMap = new HashMap();
074: private Map idToOwnerMap = new HashMap();
075: private Map idToValueMap = new HashMap();
076: private Map ownerToNameToIdMap = new WeakHashMap();
077: private Object mapLock = new Object();
078:
079: // Id generator
080: private long lastId = 0;
081:
082: /** Convert the string to a supported class */
083: private static Object convert(Class type, String value)
084: throws java.lang.ClassCastException {
085: try {
086:
087: // Boolean typed variables
088: if (type.equals(Boolean.class))
089: return new Boolean(!(value.equals("") || value
090: .equals("false")));
091:
092: // Integer typed variables
093: if (type.equals(Integer.class))
094: return new Integer(value.trim());
095:
096: // String typed variables
097: if (type.equals(String.class))
098: return value;
099:
100: throw new ClassCastException("Unsupported type: "
101: + type.getName());
102: } catch (NumberFormatException e) {
103: return null;
104: }
105: }
106:
107: /** Register a new variable.
108: *
109: * @return id to assigned for this variable.
110: */
111: public String registerVariable(String name, Class type,
112: Object value, VariableOwner owner) {
113:
114: // Check that the type of the class is supported
115: if (!(type.equals(Boolean.class) || type.equals(Integer.class)
116: || type.equals(String.class)
117: || type.equals(String[].class) || type
118: .equals(UploadStream.class)))
119: throw new SystemError("Unsupported variable type: "
120: + type.getClass());
121:
122: synchronized (mapLock) {
123:
124: // Check if the variable is already mapped
125: HashMap nameToIdMap = (HashMap) ownerToNameToIdMap
126: .get(owner);
127: if (nameToIdMap == null) {
128: nameToIdMap = new HashMap();
129: ownerToNameToIdMap.put(owner, nameToIdMap);
130: }
131: String id = (String) nameToIdMap.get(name);
132:
133: if (id == null) {
134: // Generate new id and register it
135: id = "v" + String.valueOf(++lastId);
136: nameToIdMap.put(name, id);
137: idToOwnerMap.put(id, new WeakReference(owner));
138: idToNameMap.put(id, name);
139: idToTypeMap.put(id, type);
140: }
141:
142: idToValueMap.put(id, value);
143:
144: return id;
145: }
146: }
147:
148: /** Unregisters a variable. */
149: public void unregisterVariable(String name, VariableOwner owner) {
150:
151: synchronized (mapLock) {
152:
153: // Get the id
154: HashMap nameToIdMap = (HashMap) ownerToNameToIdMap
155: .get(owner);
156: if (nameToIdMap == null)
157: return;
158: String id = (String) nameToIdMap.get(name);
159: if (id != null)
160: return;
161:
162: // Remove all the mappings
163: nameToIdMap.remove(name);
164: if (nameToIdMap.isEmpty())
165: ownerToNameToIdMap.remove(owner);
166: idToNameMap.remove(id);
167: idToTypeMap.remove(id);
168: idToValueMap.remove(id);
169: idToOwnerMap.remove(id);
170:
171: }
172: }
173:
174: /**
175: * @author IT Mill Ltd.
176: * @version 3.1.1
177: * @since 3.0
178: */
179: private class ParameterContainer {
180:
181: /** Construct the mapping: listener to set of listened parameter names */
182: private HashMap parameters = new HashMap();
183:
184: /** Parameter values */
185: private HashMap values = new HashMap();
186:
187: /** Multipart parser used for parsing the request */
188: private ServletMultipartRequest parser = null;
189:
190: /** Name - Value mapping of parameters that are not variables */
191: private HashMap nonVariables = new HashMap();
192:
193: /** Create new parameter container and parse the parameters from the request using
194: * GET, POST and POST/MULTIPART parsing
195: */
196: public ParameterContainer(HttpServletRequest req)
197: throws IOException {
198: // Parse GET / POST parameters
199: for (Enumeration e = req.getParameterNames(); e
200: .hasMoreElements();) {
201: String paramName = (String) e.nextElement();
202: String[] paramValues = req
203: .getParameterValues(paramName);
204: addParam(paramName, paramValues);
205: }
206:
207: // Parse multipart variables
208: try {
209: parser = new ServletMultipartRequest(req,
210: MultipartRequest.MAX_READ_BYTES);
211: } catch (IllegalArgumentException ignored) {
212: parser = null;
213: }
214:
215: if (parser != null) {
216: for (Enumeration e = parser.getFileParameterNames(); e
217: .hasMoreElements();) {
218: String paramName = (String) e.nextElement();
219: addParam(paramName, null);
220: }
221: for (Enumeration e = parser.getParameterNames(); e
222: .hasMoreElements();) {
223: String paramName = (String) e.nextElement();
224: Enumeration val = parser
225: .getURLParameters(paramName);
226:
227: // Create a linked list from enumeration to calculate elements
228: LinkedList l = new LinkedList();
229: while (val.hasMoreElements())
230: l.addLast(val.nextElement());
231:
232: // String array event constructor
233: String[] s = new String[l.size()];
234: Iterator i = l.iterator();
235: for (int j = 0; j < s.length; j++)
236: s[j] = (String) i.next();
237:
238: addParam(paramName, s);
239: }
240: }
241:
242: }
243:
244: /** Add parameter to container */
245: private void addParam(String name, String[] value) {
246:
247: // Support name="set:name=value" value="ignored" notation
248: if (name.startsWith("set:")) {
249: int equalsIndex = name.indexOf('=');
250: value[0] = name.substring(equalsIndex + 1, name
251: .length());
252: name = name.substring(4, equalsIndex);
253: String[] curVal = (String[]) values.get(name);
254: if (curVal != null) {
255: String[] newVal = new String[1 + curVal.length];
256: newVal[curVal.length] = value[0];
257: for (int i = 0; i < curVal.length; i++)
258: newVal[i] = curVal[i];
259: value = newVal;
260:
261: // Special case - if the set:-method is used for
262: // declaring array of length 2, where either of the
263: // following conditions are true:
264: // - the both items are the same
265: // - the both items have the same length and
266: // - the items only differ on last character
267: // - second last character is '.'
268: // - last char of one string is 'x' and other is 'y'
269: // Browser is unporposely modifying the name.
270: if (value.length == 2
271: && value[0].length() == value[1].length()) {
272: boolean same = true;
273: for (int i = 0; i < value[0].length() - 1
274: && same; i++)
275: if (value[0].charAt(i) != value[1]
276: .charAt(i))
277: same = false;
278: if (same
279: && ((value[0]
280: .charAt(value[0].length() - 1) == 'x' && value[1]
281: .charAt(value[1].length() - 1) == 'y') || (value[0]
282: .charAt(value[0].length() - 1) == 'y' && value[1]
283: .charAt(value[1].length() - 1) == 'x'))) {
284: value = new String[] { value[0].substring(
285: 0, value[1].length() - 2) };
286: } else if (same && value[0].equals(value[1]))
287: value = new String[] { value[0] };
288: }
289:
290: // Special case - if the set:-method is used for
291: // declaring array of length 3, where all of the
292: // following conditions are true:
293: // - two last items have the same length
294: // - the first item is 2 chars shorter
295: // - the longer items only differ on last character
296: // - the shortest item is a prefix of the longer ones
297: // - second last character of longer ones is '.'
298: // - last char of one long string is 'x' and other is 'y'
299: // Browser is unporposely modifying the name. (Mozilla, Firefox, ..)
300: if (value.length == 3
301: && value[1].length() == value[2].length()
302: && value[0].length() + 2 == value[1]
303: .length()) {
304: boolean same = true;
305: for (int i = 0; i < value[1].length() - 1
306: && same; i++)
307: if (value[2].charAt(i) != value[1]
308: .charAt(i))
309: same = false;
310: for (int i = 0; i < value[0].length() && same; i++)
311: if (value[0].charAt(i) != value[1]
312: .charAt(i))
313: same = false;
314: if (same
315: && (value[2]
316: .charAt(value[2].length() - 1) == 'x' && value[1]
317: .charAt(value[1].length() - 1) == 'y')
318: || (value[2]
319: .charAt(value[2].length() - 1) == 'y' && value[1]
320: .charAt(value[1].length() - 1) == 'x')) {
321: value = new String[] { value[0] };
322: }
323: }
324:
325: }
326: }
327:
328: // Support for setting arrays in format
329: // set-array:name=value1,value2,value3,...
330: else if (name.startsWith("set-array:")) {
331: int equalsIndex = name.indexOf('=');
332: if (equalsIndex < 0)
333: return;
334:
335: StringTokenizer commalist = new StringTokenizer(name
336: .substring(equalsIndex + 1), ",");
337: name = name.substring(10, equalsIndex);
338: String[] curVal = (String[]) values.get(name);
339: ArrayList elems = new ArrayList();
340:
341: // Add old values if present.
342: if (curVal != null) {
343: for (int i = 0; i < curVal.length; i++)
344: elems.add(curVal[i]);
345: }
346: while (commalist.hasMoreTokens()) {
347: String token = commalist.nextToken();
348: if (token != null && token.length() > 0)
349: elems.add(token);
350: }
351: value = new String[elems.size()];
352: for (int i = 0; i < value.length; i++)
353: value[i] = (String) elems.get(i);
354:
355: }
356:
357: // Support name="array:name" value="val1,val2,val3" notation
358: // All the empty elements are ignored
359: else if (name.startsWith("array:")) {
360:
361: name = name.substring(6);
362: StringTokenizer commalist = new StringTokenizer(
363: value[0], ",");
364: String[] curVal = (String[]) values.get(name);
365: ArrayList elems = new ArrayList();
366:
367: // Add old values if present.
368: if (curVal != null) {
369: for (int i = 0; i < curVal.length; i++)
370: elems.add(curVal[i]);
371: }
372: while (commalist.hasMoreTokens()) {
373: String token = commalist.nextToken();
374: if (token != null && token.length() > 0)
375: elems.add(token);
376: }
377: value = new String[elems.size()];
378: for (int i = 0; i < value.length; i++)
379: value[i] = (String) elems.get(i);
380: }
381:
382: // Support declaring variables with name="declare:name"
383: else if (name.startsWith("declare:")) {
384: name = name.substring(8);
385: value = (String[]) values.get(name);
386: if (value == null)
387: value = new String[0];
388: }
389:
390: // Get the owner
391: WeakReference ref = (WeakReference) idToOwnerMap.get(name);
392: VariableOwner owner = null;
393: if (ref != null)
394: owner = (VariableOwner) ref.get();
395:
396: // Add the parameter to mapping only if they have owners
397: if (owner != null) {
398: Set p = (Set) parameters.get(owner);
399: if (p == null)
400: parameters.put(owner, p = new HashSet());
401: p.add(name);
402: if (value != null)
403: values.put(name, value);
404: }
405:
406: // If the owner can not be found
407: else {
408:
409: // If parameter has been mapped before, remove the old owner mapping
410: if (ref != null) {
411:
412: // The owner has been destroyed, so we remove the mappings
413: idToNameMap.remove(name);
414: idToOwnerMap.remove(name);
415: idToTypeMap.remove(name);
416: idToValueMap.remove(name);
417: }
418:
419: // Add the parameter to set of non-variables
420: nonVariables.put(name, value);
421: }
422:
423: }
424:
425: /** Get the set of all parameters connected to given variable owner */
426: public Set getParameters(VariableOwner owner) {
427: if (owner == null)
428: return null;
429: return (Set) parameters.get(owner);
430: }
431:
432: /** Get the set of all variable owners owning parameters in this request */
433: public Set getOwners() {
434: return parameters.keySet();
435: }
436:
437: /** Get the value of a parameter */
438: public String[] getValue(String parameterName) {
439: return (String[]) values.get(parameterName);
440: }
441:
442: /** Get the servlet multipart parser */
443: public ServletMultipartRequest getParser() {
444: return parser;
445: }
446:
447: /** Get the name - value[] mapping of non variable paramteres */
448: public Map getNonVariables() {
449: return nonVariables;
450: }
451: }
452:
453: /** Handle all variable changes in this request.
454: * @param req Http request to handle
455: * @param listeners If the list is non null, only the listed listeners are
456: * served. Otherwise all the listeners are served.
457: * @return Name to Value[] mapping of unhandled variables
458: */
459: public Map handleVariables(HttpServletRequest req,
460: Terminal.ErrorListener errorListener) throws IOException {
461:
462: // Get the parameters
463: ParameterContainer parcon = new ParameterContainer(req);
464:
465: // Sort listeners to dependency order
466: List listeners = getDependencySortedListenerList(parcon
467: .getOwners());
468:
469: // Handle all parameters for all listeners
470: while (!listeners.isEmpty()) {
471: VariableOwner listener = (VariableOwner) listeners
472: .remove(0);
473: boolean changed = false; // Has any of this owners variabes changed
474: // Handle all parameters for listener
475: Set params = parcon.getParameters(listener);
476: if (params != null) { // Name value mapping
477: Map variables = new HashMap();
478: for (Iterator pi = params.iterator(); pi.hasNext();) {
479: // Get the name of the parameter
480: String param = (String) pi.next();
481: // Extract more information about the parameter
482: String varName = (String) idToNameMap.get(param);
483: Class varType = (Class) idToTypeMap.get(param);
484: Object varOldValue = idToValueMap.get(param);
485: if (varName == null || varType == null)
486: Log
487: .warn("VariableMap: No variable found for parameter "
488: + param
489: + " ("
490: + varName
491: + ","
492: + listener + ")");
493: else {
494:
495: ServletMultipartRequest parser = parcon
496: .getParser();
497:
498: // Upload events
499: if (varType.equals(UploadStream.class)) {
500: if (parser != null
501: && parser.getFileParameter(param,
502: MultipartRequest.FILENAME) != null) {
503: String filename = (String) parser
504: .getFileParameter(
505: param,
506: MultipartRequest.FILENAME);
507: String contentType = (String) parser
508: .getFileParameter(
509: param,
510: MultipartRequest.CONTENT_TYPE);
511: UploadStream upload = new HttpUploadStream(
512: varName,
513: parser.getFileContents(param),
514: filename, contentType);
515: variables.put(varName, upload);
516: changed = true;
517: }
518: }
519:
520: // Normal variable change events
521: else {
522: // First try to parse the event without multipart
523: String[] values = parcon.getValue(param);
524: if (values != null) {
525:
526: if (varType.equals(String[].class)) {
527: variables.put(varName, values);
528: changed |= (!Arrays.equals(values,
529: (String[]) varOldValue));
530: } else {
531: try {
532: if (values.length == 1) {
533: Object val = convert(
534: varType, values[0]);
535: variables.put(varName, val);
536: changed |= ((val == null && varOldValue != null) || (val != null && !val
537: .equals(varOldValue)));
538: } else if (values.length == 0
539: && varType
540: .equals(Boolean.class)) {
541: Object val = new Boolean(
542: false);
543: variables.put(varName, val);
544: changed |= (!val
545: .equals(varOldValue));
546: } else {
547: Log
548: .warn("Empty variable '"
549: + varName
550: + "' of type "
551: + varType
552: .toString());
553: }
554:
555: } catch (java.lang.ClassCastException e) {
556: Log
557: .except(
558: "WebVariableMap conversion exception",
559: e);
560: errorListener
561: .terminalError(new TerminalErrorImpl(
562: e));
563: }
564: }
565: }
566: }
567: }
568: }
569:
570: // Do the valuechange if the listener is enabled
571: if (listener.isEnabled() && changed) {
572: try {
573: listener.changeVariables(req, variables);
574: } catch (Throwable t) {
575: // Notify the error listener
576: errorListener
577: .terminalError(new VariableOwnerErrorImpl(
578: listener, t));
579: }
580: }
581: }
582: }
583:
584: return parcon.getNonVariables();
585: }
586:
587: /** Implementation of VariableOwner.Error interface. */
588: public class TerminalErrorImpl implements Terminal.ErrorEvent {
589: private Throwable throwable;
590:
591: private TerminalErrorImpl(Throwable throwable) {
592: this .throwable = throwable;
593: }
594:
595: /**
596: * @see org.millstone.base.terminal.Terminal.ErrorEvent#getThrowable()
597: */
598: public Throwable getThrowable() {
599: return this .throwable;
600: }
601:
602: }
603:
604: /** Implementation of VariableOwner.Error interface. */
605: public class VariableOwnerErrorImpl extends TerminalErrorImpl
606: implements VariableOwner.ErrorEvent {
607:
608: private VariableOwner owner;
609:
610: private VariableOwnerErrorImpl(VariableOwner owner,
611: Throwable throwable) {
612: super (throwable);
613: this .owner = owner;
614: }
615:
616: /**
617: * @see org.millstone.base.terminal.VariableOwner.ErrorEvent#getVariableOwner()
618: */
619: public VariableOwner getVariableOwner() {
620: return this .owner;
621: }
622:
623: }
624:
625: /** Resolve the VariableOwners needed from the request and sort
626: * them to assure that the dependencies are met (as well as possible).
627: * @return List of variable list changers, that are needed for handling
628: * all the variables in the request
629: * @param req request to be handled
630: * @param parser multipart parser for the request
631: */
632: private List getDependencySortedListenerList(Set listeners) {
633:
634: LinkedList resultNormal = new LinkedList();
635: LinkedList resultImmediate = new LinkedList();
636:
637: // Go trough the listeners and either add them to result or resolve
638: // their dependencies
639: HashMap deepdeps = new HashMap();
640: LinkedList unresolved = new LinkedList();
641: for (Iterator li = listeners.iterator(); li.hasNext();) {
642:
643: VariableOwner listener = (VariableOwner) li.next();
644: if (listener != null) {
645: Set dependencies = listener.getDirectDependencies();
646:
647: // The listeners with no dependencies are added to the front of the
648: // list directly
649: if (dependencies == null || dependencies.isEmpty()) {
650: if (listener.isImmediate())
651: resultImmediate.addFirst(listener);
652: else
653: resultNormal.addFirst(listener);
654: }
655:
656: // Resolve deep dependencies for the listeners with dependencies
657: // (the listeners will be added to the end of results in correct
658: // dependency order later). Also the dependencies of all the
659: // depended listeners are resolved.
660: else if (deepdeps.get(listener) == null) {
661:
662: // Set the fifo for unresolved parents to contain only the
663: // listener to be resolved
664: unresolved.clear();
665: unresolved.add(listener);
666:
667: // Resolve dependencies
668: HashSet tmpdeepdeps = new HashSet();
669: while (!unresolved.isEmpty()) {
670:
671: VariableOwner l = (VariableOwner) unresolved
672: .removeFirst();
673: if (!tmpdeepdeps.contains(l)) {
674: tmpdeepdeps.add(l);
675: if (deepdeps.containsKey(l)) {
676: tmpdeepdeps.addAll((Set) deepdeps
677: .get(l));
678: } else {
679: Set deps = l.getDirectDependencies();
680: if (deps != null && !deps.isEmpty())
681: for (Iterator di = deps.iterator(); di
682: .hasNext();) {
683: Object d = di.next();
684: if (d != null
685: && !tmpdeepdeps
686: .contains(d))
687: unresolved.addLast(d);
688: }
689: }
690: }
691: }
692:
693: tmpdeepdeps.remove(listener);
694: deepdeps.put(listener, tmpdeepdeps);
695: }
696: }
697: }
698:
699: // Add the listeners with dependencies in sane order to the result
700: for (Iterator li = deepdeps.keySet().iterator(); li.hasNext();) {
701: VariableOwner l = (VariableOwner) li.next();
702: boolean immediate = l.isImmediate();
703:
704: // Add each listener after the last depended listener already in
705: // the list
706: int index = -1;
707: for (Iterator di = ((Set) deepdeps.get(l)).iterator(); di
708: .hasNext();) {
709: int k;
710: Object depended = di.next();
711: if (immediate) {
712: k = resultImmediate.lastIndexOf(depended);
713: } else {
714: k = resultNormal.lastIndexOf(depended);
715: }
716: if (k > index)
717: index = k;
718: }
719: if (immediate) {
720: resultImmediate.add(index + 1, l);
721: } else {
722: resultNormal.add(index + 1, l);
723: }
724: }
725:
726: // Append immediate listeners to normal listeners
727: // This way the normal handlers are always called before
728: // immediate ones
729: resultNormal.addAll(resultImmediate);
730: return resultNormal;
731: }
732: }
|