/* * Copyright 2005 Joe Walker * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.directwebremoting.extend; import java.util.StringTokenizer; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.directwebremoting.ConversionException; import org.directwebremoting.json.InvalidJsonException; import org.directwebremoting.json.JsonArray; import org.directwebremoting.json.JsonBoolean; import org.directwebremoting.json.JsonNull; import org.directwebremoting.json.JsonNumber; import org.directwebremoting.json.JsonObject; import org.directwebremoting.json.JsonString; import org.directwebremoting.json.JsonValue; /** * A simple struct to hold data about a single converted javascript variable. * An inbound variable will have either a value or a fileValue but not both. * If file is <code>true</code> fileValue will be populated, otherwise value * will be populated. * @author Joe Walker [joe at getahead dot ltd dot uk] */ public final class InboundVariable { /** * Parsing ctor * @param context How we lookup references * @param key The name of the variable that this was transfered as * @param type The type information from javascript * @param value The javascript variable converted to a string */ public InboundVariable(InboundContext context, String key, String type, String value) { this(context, key, type, new FormField(value)); } /** * Parsing ctor * @param context How we lookup references * @param key The name of the variable that this was transfered as * @param type The type information from javascript * @param fileValue The javascript variable converted to a FormField */ public InboundVariable(InboundContext context, String key, String type, FormField fileValue) { this.context = context; this.type = type; this.formField = fileValue; this.key = key; } /** * Accessor of the context of the variable: the other related variables */ public InboundContext getContext() { return context; } /** * Attempt to de-reference an inbound variable. * We try de-referencing as soon as possible (why? there is a good reason * for it, it fixes some bug, but I can't remember what right now) However * the referenced variable may not exist yet, so the de-referencing may * fail, requiring us to have another go later. * @throws ConversionException If cross-references don't add up */ public void dereference() throws ConversionException { int maxDepth = 0; while (ProtocolConstants.TYPE_REFERENCE.equals(type)) { InboundVariable cd = context.getInboundVariable(formField.getString()); if (cd == null) { throw new ConversionException(getClass(), "Found reference to variable named '" + formField.getString() + "', but no variable of that name could be found."); } type = cd.type; formField = cd.getFormField(); // For some reason we used to leave this until the loop finished // and then only set it if the key was null. I think this logic // may have been broken by named objects key = cd.key; maxDepth++; if (maxDepth > 20) { throw new ConversionException(getClass(), "Max depth exceeded when dereferencing " + formField.getString()); } } // For references without an explicit variable name, we use the // name of the thing they point at // if (key == null) // { // key = formField.getString(); // } } /** * @return Returns the lookup table. */ public InboundContext getLookup() { return context; } /** * If we are using object parameters that have specified types then the * {@link ConverterManager} will need to get to know what the required type * is. * @return The requested object type, or null if one was not specified */ public String getNamedObjectType() { if (type.startsWith("Object_")) { return type.substring("Object_".length()); } else { return null; } } public enum OnJsonParseError { /** * If there is anything about the {@link InboundVariable} that can not * be represented in 100% pure JSON, then throw */ Throw, /** * If there is anything about the {@link InboundVariable} that can not * be represented in 100% pure JSON, then insert null and carry on */ Skip, } /** * Convert the set of {@link InboundVariable}s to JSON * @return This object in JSON * @throws InvalidJsonException If this can't be represented as JSON */ public JsonValue getJsonValue(OnJsonParseError onError) throws InvalidJsonException { return getJsonValue(onError, 0); } /** * Convert the set of {@link InboundVariable}s to JSON * @return This object in JSON * @throws InvalidJsonException If this can't be represented as JSON */ private JsonValue getJsonValue(OnJsonParseError onError, int currentDepth) throws InvalidJsonException { if (currentDepth > 50) { throw new InvalidJsonException("JSON structure too deeply nested. Is it recursive?"); } String value = getValue(); if ("boolean".equalsIgnoreCase(type)) { return new JsonBoolean(Boolean.parseBoolean(value)); } else if ("number".equalsIgnoreCase(type)) { return new JsonNumber(Double.parseDouble(value)); } else if ("string".equalsIgnoreCase(type)) { return new JsonString(value); } else if ("date".equalsIgnoreCase(type)) { switch (onError) { case Throw: throw new InvalidJsonException("Can't use date in JSON"); case Skip: return new JsonNull(); } } else if ("xml".equalsIgnoreCase(type)) { switch (onError) { case Throw: throw new InvalidJsonException("Can't use XML in JSON"); case Skip: return new JsonNull(); } } else if ("array".equalsIgnoreCase(type)) { JsonArray array = new JsonArray(); // If the text is null then the whole bean is null if (value.trim().equals(ProtocolConstants.INBOUND_NULL)) { return new JsonNull(); } if (!value.startsWith(ProtocolConstants.INBOUND_ARRAY_START) || !value.endsWith(ProtocolConstants.INBOUND_ARRAY_END)) { log.warn("Expected collection. Passed: " + value); throw new InvalidJsonException("Data conversion error. See logs for more details."); } value = value.substring(1, value.length() - 1); StringTokenizer st = new StringTokenizer(value, ProtocolConstants.INBOUND_ARRAY_SEPARATOR); int size = st.countTokens(); for (int i = 0; i < size; i++) { String token = st.nextToken(); String[] split = ConvertUtil.splitInbound(token); String splitValue = split[ConvertUtil.INBOUND_INDEX_VALUE]; InboundVariable nested = context.getInboundVariable(splitValue); array.add(nested.getJsonValue(onError, currentDepth + 1)); } return array; } else if (type.startsWith("Object_")) { JsonObject object = new JsonObject(); // If the text is null then the whole bean is null if (value.trim().equals(ProtocolConstants.INBOUND_NULL)) { return new JsonNull(); } if (!value.startsWith(ProtocolConstants.INBOUND_MAP_START) || !value.endsWith(ProtocolConstants.INBOUND_MAP_END)) { log.warn("Expected object. Passed: " + value); throw new InvalidJsonException("Data conversion error. See logs for more details."); } value = value.substring(1, value.length() - 1); // Loop through the property declarations StringTokenizer st = new StringTokenizer(value, ","); int size = st.countTokens(); for (int i = 0; i < size; i++) { String token = st.nextToken(); if (token.trim().length() == 0) { continue; } int colonpos = token.indexOf(ProtocolConstants.INBOUND_MAP_ENTRY); if (colonpos == -1) { throw new InvalidJsonException("Missing separator: " + ProtocolConstants.INBOUND_MAP_ENTRY); } // Convert the value part of the token by splitting it into the // type and value (as passed in by Javascript) String valStr = token.substring(colonpos + 1).trim(); String[] splitIv = ConvertUtil.splitInbound(valStr); String splitIvValue = splitIv[ConvertUtil.INBOUND_INDEX_VALUE]; String keyStr = token.substring(0, colonpos).trim(); InboundVariable nested = context.getInboundVariable(splitIvValue); object.put(keyStr, nested.getJsonValue(onError, currentDepth + 1)); } return object; } log.warn("Data type: " + type + " is not one that InboundVariable understands"); throw new InvalidJsonException("Unknown data type"); } /** * Was this type null on the way in * @return true if the javascript variable was null or undefined. */ public boolean isNull() { return type.equals(ProtocolConstants.INBOUND_NULL); } /** * @return Returns the value. */ public String getValue() { return formField.getString(); } /** * @return Returns the file value */ public FormField getFormField() { return formField; } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return type + ProtocolConstants.INBOUND_TYPE_SEPARATOR + formField.getString(); } /* (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof InboundVariable)) { return false; } InboundVariable that = (InboundVariable) obj; if (!this.type.equals(that.type)) { return false; } if (!this.formField.equals(that.formField)) { return false; } if (this.key == null || that.key == null) { return false; } return true; // this.key.equals(that.key); } public String getType() { return type; } /* (non-Javadoc) * @see java.lang.Object#hashCode() */ @Override public int hashCode() { return formField.hashCode() + type.hashCode(); } /** * How do be lookup references? */ private InboundContext context; /** * The variable name */ private String key; /** * The javascript declared variable type */ private String type; /** * The javascript declared file value */ private FormField formField; /** * The log stream */ private static final Log log = LogFactory.getLog(InboundVariable.class); }