aboutsummaryrefslogtreecommitdiffstats
path: root/src/com/itmill/toolkit/terminal/ajax/VariableMap.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/itmill/toolkit/terminal/ajax/VariableMap.java')
-rw-r--r--src/com/itmill/toolkit/terminal/ajax/VariableMap.java732
1 files changed, 732 insertions, 0 deletions
diff --git a/src/com/itmill/toolkit/terminal/ajax/VariableMap.java b/src/com/itmill/toolkit/terminal/ajax/VariableMap.java
new file mode 100644
index 0000000000..54027ef703
--- /dev/null
+++ b/src/com/itmill/toolkit/terminal/ajax/VariableMap.java
@@ -0,0 +1,732 @@
+/* *************************************************************************
+
+ IT Mill Toolkit
+
+ Development of Browser User Intarfaces Made Easy
+
+ Copyright (C) 2000-2006 IT Mill Ltd
+
+ *************************************************************************
+
+ This product is distributed under commercial license that can be found
+ from the product package on license/license.txt. Use of this product might
+ require purchasing a commercial license from IT Mill Ltd. For guidelines
+ on usage, see license/licensing-guidelines.html
+
+ *************************************************************************
+
+ For more information, contact:
+
+ IT Mill Ltd phone: +358 2 4802 7180
+ Ruukinkatu 2-4 fax: +358 2 4802 7181
+ 20540, Turku email: info@itmill.com
+ Finland company www: www.itmill.com
+
+ Primary source for information and releases: www.itmill.com
+
+ ********************************************************************** */
+
+package com.itmill.toolkit.terminal.ajax;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.WeakHashMap;
+
+import javax.servlet.http.HttpServletRequest;
+
+import com.itmill.toolkit.terminal.SystemError;
+import com.itmill.toolkit.terminal.Terminal;
+import com.itmill.toolkit.terminal.UploadStream;
+import com.itmill.toolkit.terminal.VariableOwner;
+
+/** Variable map for ajax applications.
+ *
+ * @author IT Mill Ltd, Joonas Lehtinen
+ * @version @VERSION@
+ * @since 3.1
+ */
+public class VariableMap {
+
+
+ // Id <-> (Owner,Name) mapping
+ private Map idToNameMap = new HashMap();
+ private Map idToTypeMap = new HashMap();
+ private Map idToOwnerMap = new HashMap();
+ private Map idToValueMap = new HashMap();
+ private Map ownerToNameToIdMap = new WeakHashMap();
+ private Object mapLock = new Object();
+
+ // Id generator
+ private long lastId = 0;
+
+ /** Convert the string to a supported class */
+ private static Object convert(Class type, String value)
+ throws java.lang.ClassCastException {
+ try {
+
+ // Boolean typed variables
+ if (type.equals(Boolean.class))
+ return new Boolean(
+ !(value.equals("") || value.equals("false")));
+
+ // Integer typed variables
+ if (type.equals(Integer.class))
+ return new Integer(value.trim());
+
+ // String typed variables
+ if (type.equals(String.class))
+ return value;
+
+ throw new ClassCastException("Unsupported type: " + type.getName());
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+
+ /** Register a new variable.
+ *
+ * @return id to assigned for this variable.
+ */
+ public String registerVariable(
+ String name,
+ Class type,
+ Object value,
+ VariableOwner owner) {
+
+ // Check that the type of the class is supported
+ if (!(type.equals(Boolean.class)
+ || type.equals(Integer.class)
+ || type.equals(String.class)
+ || type.equals(String[].class)
+ || type.equals(UploadStream.class)))
+ throw new SystemError(
+ "Unsupported variable type: " + type.getClass());
+
+ synchronized (mapLock) {
+
+ // Check if the variable is already mapped
+ HashMap nameToIdMap = (HashMap) ownerToNameToIdMap.get(owner);
+ if (nameToIdMap == null) {
+ nameToIdMap = new HashMap();
+ ownerToNameToIdMap.put(owner, nameToIdMap);
+ }
+ String id = (String) nameToIdMap.get(name);
+
+ if (id == null) {
+ // Generate new id and register it
+ id = "v" + String.valueOf(++lastId);
+ nameToIdMap.put(name, id);
+ idToOwnerMap.put(id, new WeakReference(owner));
+ idToNameMap.put(id, name);
+ idToTypeMap.put(id, type);
+ }
+
+ idToValueMap.put(id, value);
+
+ return id;
+ }
+ }
+
+ /** Unregisters a variable. */
+ public void unregisterVariable(String name, VariableOwner owner) {
+
+ synchronized (mapLock) {
+
+ // Get the id
+ HashMap nameToIdMap = (HashMap) ownerToNameToIdMap.get(owner);
+ if (nameToIdMap == null)
+ return;
+ String id = (String) nameToIdMap.get(name);
+ if (id != null)
+ return;
+
+ // Remove all the mappings
+ nameToIdMap.remove(name);
+ if (nameToIdMap.isEmpty())
+ ownerToNameToIdMap.remove(owner);
+ idToNameMap.remove(id);
+ idToTypeMap.remove(id);
+ idToValueMap.remove(id);
+ idToOwnerMap.remove(id);
+
+ }
+ }
+
+ /**
+ * @author IT Mill Ltd.
+ * @version @VERSION@
+ * @since 3.0
+ */
+ private class ParameterContainer {
+
+ /** Construct the mapping: listener to set of listened parameter names */
+ private HashMap parameters = new HashMap();
+
+ /** Parameter values */
+ private HashMap values = new HashMap();
+
+ /** Multipart parser used for parsing the request */
+ private ServletMultipartRequest parser = null;
+
+ /** Name - Value mapping of parameters that are not variables */
+ private HashMap nonVariables = new HashMap();
+
+ /** Create new parameter container and parse the parameters from the request using
+ * GET, POST and POST/MULTIPART parsing
+ */
+ public ParameterContainer(HttpServletRequest req) throws IOException {
+ // Parse GET / POST parameters
+ for (Enumeration e = req.getParameterNames();
+ e.hasMoreElements();
+ ) {
+ String paramName = (String) e.nextElement();
+ String[] paramValues = req.getParameterValues(paramName);
+ addParam(paramName, paramValues);
+ }
+
+ // Parse multipart variables
+ try {
+ parser =
+ new ServletMultipartRequest(
+ req,
+ MultipartRequest.MAX_READ_BYTES);
+ } catch (IllegalArgumentException ignored) {
+ parser = null;
+ }
+
+ if (parser != null) {
+ for (Enumeration e = parser.getFileParameterNames();
+ e.hasMoreElements();
+ ) {
+ String paramName = (String) e.nextElement();
+ addParam(paramName, null);
+ }
+ for (Enumeration e = parser.getParameterNames();
+ e.hasMoreElements();
+ ) {
+ String paramName = (String) e.nextElement();
+ Enumeration val = parser.getURLParameters(paramName);
+
+ // Create a linked list from enumeration to calculate elements
+ LinkedList l = new LinkedList();
+ while (val.hasMoreElements())
+ l.addLast(val.nextElement());
+
+ // String array event constructor
+ String[] s = new String[l.size()];
+ Iterator i = l.iterator();
+ for (int j = 0; j < s.length; j++)
+ s[j] = (String) i.next();
+
+ addParam(paramName, s);
+ }
+ }
+
+ }
+
+ /** Add parameter to container */
+ private void addParam(String name, String[] value) {
+
+ // Support name="set:name=value" value="ignored" notation
+ if (name.startsWith("set:")) {
+ int equalsIndex = name.indexOf('=');
+ value[0] = name.substring(equalsIndex + 1, name.length());
+ name = name.substring(4, equalsIndex);
+ String[] curVal = (String[]) values.get(name);
+ if (curVal != null) {
+ String[] newVal = new String[1 + curVal.length];
+ newVal[curVal.length] = value[0];
+ for (int i = 0; i < curVal.length; i++)
+ newVal[i] = curVal[i];
+ value = newVal;
+
+ // Special case - if the set:-method is used for
+ // declaring array of length 2, where either of the
+ // following conditions are true:
+ // - the both items are the same
+ // - the both items have the same length and
+ // - the items only differ on last character
+ // - second last character is '.'
+ // - last char of one string is 'x' and other is 'y'
+ // Browser is unporposely modifying the name.
+ if (value.length == 2
+ && value[0].length() == value[1].length()) {
+ boolean same = true;
+ for (int i = 0; i < value[0].length() - 1 && same; i++)
+ if (value[0].charAt(i) != value[1].charAt(i))
+ same = false;
+ if (same
+ && ((value[0].charAt(value[0].length() - 1) == 'x'
+ && value[1].charAt(value[1].length() - 1) == 'y')
+ || (value[0].charAt(value[0].length() - 1) == 'y'
+ && value[1].charAt(value[1].length() - 1)
+ == 'x'))) {
+ value =
+ new String[] {
+ value[0].substring(
+ 0,
+ value[1].length() - 2)};
+ } else
+ if (same && value[0].equals(value[1]))
+ value = new String[] { value[0] };
+ }
+
+ // Special case - if the set:-method is used for
+ // declaring array of length 3, where all of the
+ // following conditions are true:
+ // - two last items have the same length
+ // - the first item is 2 chars shorter
+ // - the longer items only differ on last character
+ // - the shortest item is a prefix of the longer ones
+ // - second last character of longer ones is '.'
+ // - last char of one long string is 'x' and other is 'y'
+ // Browser is unporposely modifying the name. (Mozilla, Firefox, ..)
+ if (value.length == 3
+ && value[1].length() == value[2].length() &&
+ value[0].length() +2 == value[1].length()) {
+ boolean same = true;
+ for (int i = 0; i < value[1].length() - 1 && same; i++)
+ if (value[2].charAt(i) != value[1].charAt(i))
+ same = false;
+ for (int i = 0; i < value[0].length() && same; i++)
+ if (value[0].charAt(i) != value[1].charAt(i))
+ same = false;
+ if (same
+ && (value[2].charAt(value[2].length() - 1) == 'x'
+ && value[1].charAt(value[1].length() - 1) == 'y')
+ || (value[2].charAt(value[2].length() - 1) == 'y'
+ && value[1].charAt(value[1].length() - 1)
+ == 'x')) {
+ value =
+ new String[] {
+ value[0]};
+ }
+ }
+
+ }
+ }
+
+ // Support for setting arrays in format
+ // set-array:name=value1,value2,value3,...
+ else if (name.startsWith("set-array:")) {
+ int equalsIndex = name.indexOf('=');
+ if (equalsIndex < 0)
+ return;
+
+ StringTokenizer commalist =
+ new StringTokenizer(name.substring(equalsIndex + 1), ",");
+ name = name.substring(10, equalsIndex);
+ String[] curVal = (String[]) values.get(name);
+ ArrayList elems = new ArrayList();
+
+ // Add old values if present.
+ if (curVal != null) {
+ for (int i = 0; i < curVal.length; i++)
+ elems.add(curVal[i]);
+ }
+ while (commalist.hasMoreTokens()) {
+ String token = commalist.nextToken();
+ if (token != null && token.length() > 0)
+ elems.add(token);
+ }
+ value = new String[elems.size()];
+ for (int i = 0; i < value.length; i++)
+ value[i] = (String) elems.get(i);
+
+ }
+
+ // Support name="array:name" value="val1,val2,val3" notation
+ // All the empty elements are ignored
+ else if (name.startsWith("array:")) {
+
+ name = name.substring(6);
+ StringTokenizer commalist = new StringTokenizer(value[0], ",");
+ String[] curVal = (String[]) values.get(name);
+ ArrayList elems = new ArrayList();
+
+ // Add old values if present.
+ if (curVal != null) {
+ for (int i = 0; i < curVal.length; i++)
+ elems.add(curVal[i]);
+ }
+ while (commalist.hasMoreTokens()) {
+ String token = commalist.nextToken();
+ if (token != null && token.length() > 0)
+ elems.add(token);
+ }
+ value = new String[elems.size()];
+ for (int i = 0; i < value.length; i++)
+ value[i] = (String) elems.get(i);
+ }
+
+ // Support declaring variables with name="declare:name"
+ else if (name.startsWith("declare:")) {
+ name = name.substring(8);
+ value = (String[]) values.get(name);
+ if (value == null)
+ value = new String[0];
+ }
+
+ // Get the owner
+ WeakReference ref = (WeakReference) idToOwnerMap.get(name);
+ VariableOwner owner = null;
+ if (ref != null)
+ owner = (VariableOwner) ref.get();
+
+ // Add the parameter to mapping only if they have owners
+ if (owner != null) {
+ Set p = (Set) parameters.get(owner);
+ if (p == null)
+ parameters.put(owner, p = new HashSet());
+ p.add(name);
+ if (value != null)
+ values.put(name, value);
+ }
+
+ // If the owner can not be found
+ else {
+
+ // If parameter has been mapped before, remove the old owner mapping
+ if (ref != null) {
+
+ // The owner has been destroyed, so we remove the mappings
+ idToNameMap.remove(name);
+ idToOwnerMap.remove(name);
+ idToTypeMap.remove(name);
+ idToValueMap.remove(name);
+ }
+
+ // Add the parameter to set of non-variables
+ nonVariables.put(name, value);
+ }
+
+ }
+
+ /** Get the set of all parameters connected to given variable owner */
+ public Set getParameters(VariableOwner owner) {
+ if (owner == null)
+ return null;
+ return (Set) parameters.get(owner);
+ }
+
+ /** Get the set of all variable owners owning parameters in this request */
+ public Set getOwners() {
+ return parameters.keySet();
+ }
+
+ /** Get the value of a parameter */
+ public String[] getValue(String parameterName) {
+ return (String[]) values.get(parameterName);
+ }
+
+ /** Get the servlet multipart parser */
+ public ServletMultipartRequest getParser() {
+ return parser;
+ }
+
+ /** Get the name - value[] mapping of non variable paramteres */
+ public Map getNonVariables() {
+ return nonVariables;
+ }
+ }
+
+ /** Handle all variable changes in this request.
+ * @param req Http request to handle
+ * @param listeners If the list is non null, only the listed listeners are
+ * served. Otherwise all the listeners are served.
+ * @return Name to Value[] mapping of unhandled variables
+ */
+ public Map handleVariables(
+ HttpServletRequest req,
+ Terminal.ErrorListener errorListener)
+ throws IOException {
+
+ // Get the parameters
+ ParameterContainer parcon = new ParameterContainer(req);
+
+ // Sort listeners to dependency order
+ List listeners = getDependencySortedListenerList(parcon.getOwners());
+
+ // Handle all parameters for all listeners
+ while (!listeners.isEmpty()) {
+ VariableOwner listener = (VariableOwner) listeners.remove(0);
+ boolean changed = false; // Has any of this owners variabes changed
+ // Handle all parameters for listener
+ Set params = parcon.getParameters(listener);
+ if (params != null) { // Name value mapping
+ Map variables = new HashMap();
+ for (Iterator pi = params.iterator(); pi.hasNext();) {
+ // Get the name of the parameter
+ String param = (String) pi.next();
+ // Extract more information about the parameter
+ String varName = (String) idToNameMap.get(param);
+ Class varType = (Class) idToTypeMap.get(param);
+ Object varOldValue = idToValueMap.get(param);
+ if (varName == null || varType == null)
+ Log.warn(
+ "VariableMap: No variable found for parameter "
+ + param
+ + " ("
+ + varName
+ + ","
+ + listener
+ + ")");
+ else {
+
+ ServletMultipartRequest parser = parcon.getParser();
+
+ // Upload events
+ if (varType.equals(UploadStream.class)) {
+ if (parser != null
+ && parser.getFileParameter(
+ param,
+ MultipartRequest.FILENAME)
+ != null) {
+ String filename =
+ (String) parser.getFileParameter(
+ param,
+ MultipartRequest.FILENAME);
+ String contentType =
+ (String) parser.getFileParameter(
+ param,
+ MultipartRequest.CONTENT_TYPE);
+ UploadStream upload =
+ new HttpUploadStream(
+ varName,
+ parser.getFileContents(param),
+ filename,
+ contentType);
+ variables.put(varName, upload);
+ changed = true;
+ }
+ }
+
+ // Normal variable change events
+ else {
+ // First try to parse the event without multipart
+ String[] values = parcon.getValue(param);
+ if (values != null) {
+
+ if (varType.equals(String[].class)) {
+ variables.put(varName, values);
+ changed
+ |= (!Arrays
+ .equals(
+ values,
+ (String[]) varOldValue));
+ } else {
+ try {
+ if (values.length == 1) {
+ Object val =
+ convert(varType, values[0]);
+ variables.put(varName, val);
+ changed
+ |= ((val == null
+ && varOldValue != null)
+ || (val != null
+ && !val.equals(
+ varOldValue)));
+ } else if (
+ values.length == 0
+ && varType.equals(
+ Boolean.class)) {
+ Object val = new Boolean(false);
+ variables.put(varName, val);
+ changed
+ |= (!val.equals(varOldValue));
+ } else {
+ Log.warn(
+ "Empty variable '"
+ + varName
+ + "' of type "
+ + varType.toString());
+ }
+
+ } catch (java.lang.ClassCastException e) {
+ Log.except(
+ "WebVariableMap conversion exception",
+ e);
+ errorListener.terminalError(
+ new TerminalErrorImpl(e));
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Do the valuechange if the listener is enabled
+ if (listener.isEnabled() && changed) {
+ try {
+ listener.changeVariables(req, variables);
+ } catch (Throwable t) {
+ // Notify the error listener
+ errorListener.terminalError(
+ new VariableOwnerErrorImpl(listener, t));
+ }
+ }
+ }
+ }
+
+ return parcon.getNonVariables();
+ }
+
+ /** Implementation of VariableOwner.Error interface. */
+ public class TerminalErrorImpl implements Terminal.ErrorEvent {
+ private Throwable throwable;
+
+ private TerminalErrorImpl(Throwable throwable) {
+ this.throwable = throwable;
+ }
+
+ /**
+ * @see com.itmill.toolkit.terminal.Terminal.ErrorEvent#getThrowable()
+ */
+ public Throwable getThrowable() {
+ return this.throwable;
+ }
+
+ }
+
+ /** Implementation of VariableOwner.Error interface. */
+ public class VariableOwnerErrorImpl
+ extends TerminalErrorImpl
+ implements VariableOwner.ErrorEvent {
+
+ private VariableOwner owner;
+
+ private VariableOwnerErrorImpl(
+ VariableOwner owner,
+ Throwable throwable) {
+ super(throwable);
+ this.owner = owner;
+ }
+
+ /**
+ * @see com.itmill.toolkit.terminal.VariableOwner.ErrorEvent#getVariableOwner()
+ */
+ public VariableOwner getVariableOwner() {
+ return this.owner;
+ }
+
+ }
+
+ /** Resolve the VariableOwners needed from the request and sort
+ * them to assure that the dependencies are met (as well as possible).
+ * @return List of variable list changers, that are needed for handling
+ * all the variables in the request
+ * @param req request to be handled
+ * @param parser multipart parser for the request
+ */
+ private List getDependencySortedListenerList(Set listeners) {
+
+ LinkedList resultNormal = new LinkedList();
+ LinkedList resultImmediate = new LinkedList();
+
+ // Go trough the listeners and either add them to result or resolve
+ // their dependencies
+ HashMap deepdeps = new HashMap();
+ LinkedList unresolved = new LinkedList();
+ for (Iterator li = listeners.iterator(); li.hasNext();) {
+
+ VariableOwner listener = (VariableOwner) li.next();
+ if (listener != null) {
+ Set dependencies = listener.getDirectDependencies();
+
+ // The listeners with no dependencies are added to the front of the
+ // list directly
+ if (dependencies == null || dependencies.isEmpty()) {
+ if (listener.isImmediate())
+ resultImmediate.addFirst(listener);
+ else
+ resultNormal.addFirst(listener);
+ }
+
+ // Resolve deep dependencies for the listeners with dependencies
+ // (the listeners will be added to the end of results in correct
+ // dependency order later). Also the dependencies of all the
+ // depended listeners are resolved.
+ else if (deepdeps.get(listener) == null) {
+
+ // Set the fifo for unresolved parents to contain only the
+ // listener to be resolved
+ unresolved.clear();
+ unresolved.add(listener);
+
+ // Resolve dependencies
+ HashSet tmpdeepdeps = new HashSet();
+ while (!unresolved.isEmpty()) {
+
+ VariableOwner l =
+ (VariableOwner) unresolved.removeFirst();
+ if (!tmpdeepdeps.contains(l)) {
+ tmpdeepdeps.add(l);
+ if (deepdeps.containsKey(l)) {
+ tmpdeepdeps.addAll((Set) deepdeps.get(l));
+ } else {
+ Set deps = l.getDirectDependencies();
+ if (deps != null && !deps.isEmpty())
+ for (Iterator di = deps.iterator();
+ di.hasNext();
+ ) {
+ Object d = di.next();
+ if (d != null
+ && !tmpdeepdeps.contains(d))
+ unresolved.addLast(d);
+ }
+ }
+ }
+ }
+
+ tmpdeepdeps.remove(listener);
+ deepdeps.put(listener, tmpdeepdeps);
+ }
+ }
+ }
+
+ // Add the listeners with dependencies in sane order to the result
+ for (Iterator li = deepdeps.keySet().iterator(); li.hasNext();) {
+ VariableOwner l = (VariableOwner) li.next();
+ boolean immediate = l.isImmediate();
+
+ // Add each listener after the last depended listener already in
+ // the list
+ int index = -1;
+ for (Iterator di = ((Set) deepdeps.get(l)).iterator();
+ di.hasNext();
+ ) {
+ int k;
+ Object depended = di.next();
+ if (immediate) {
+ k = resultImmediate.lastIndexOf(depended);
+ }else {
+ k = resultNormal.lastIndexOf(depended);
+ }
+ if (k > index)
+ index = k;
+ }
+ if (immediate) {
+ resultImmediate.add(index + 1, l);
+ } else {
+ resultNormal.add(index + 1, l);
+ }
+ }
+
+ // Append immediate listeners to normal listeners
+ // This way the normal handlers are always called before
+ // immediate ones
+ resultNormal.addAll(resultImmediate);
+ return resultNormal;
+ }
+}