001: /*
002: * Copyright 2005 Joe Walker
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016: package org.directwebremoting.extend;
017:
018: import java.util.StringTokenizer;
019:
020: import org.apache.commons.logging.Log;
021: import org.apache.commons.logging.LogFactory;
022: import org.directwebremoting.dwrp.ParseUtil;
023: import org.directwebremoting.dwrp.ProtocolConstants;
024: import org.directwebremoting.json.InvalidJsonException;
025: import org.directwebremoting.json.JsonArray;
026: import org.directwebremoting.json.JsonBoolean;
027: import org.directwebremoting.json.JsonHack;
028: import org.directwebremoting.json.JsonNull;
029: import org.directwebremoting.json.JsonNumber;
030: import org.directwebremoting.json.JsonObject;
031: import org.directwebremoting.json.JsonString;
032: import org.directwebremoting.json.JsonValue;
033: import org.directwebremoting.util.LocalUtil;
034: import org.directwebremoting.util.Messages;
035:
036: /**
037: * A simple struct to hold data about a single converted javascript variable.
038: * An inbound variable will have either a value or a fileValue but not both.
039: * If file is <code>true</code> fileValue will be populated, otherwise value
040: * will be populated.
041: * @author Joe Walker [joe at getahead dot ltd dot uk]
042: */
043: public final class InboundVariable {
044: /**
045: * Parsing ctor
046: * @param context How we lookup references
047: * @param key The name of the variable that this was transfered as
048: * @param type The type information from javascript
049: * @param value The javascript variable converted to a string
050: */
051: public InboundVariable(InboundContext context, String key,
052: String type, String value) {
053: this (context, key, type, new FormField(value));
054: }
055:
056: /**
057: * Parsing ctor
058: * @param context How we lookup references
059: * @param key The name of the variable that this was transfered as
060: * @param type The type information from javascript
061: * @param fileValue The javascript variable converted to a FormField
062: */
063: public InboundVariable(InboundContext context, String key,
064: String type, FormField fileValue) {
065: this .context = context;
066: this .type = type;
067: this .formField = fileValue;
068: this .key = key;
069: }
070:
071: /**
072: * Attempt to de-reference an inbound variable.
073: * We try de-referencing as soon as possible (why? there is a good reason
074: * for it, it fixes some bug, but I can't remember what right now) However
075: * the referenced variable may not exist yet, so the de-referencing may
076: * fail, requiring us to have another go later.
077: * @throws MarshallException If cross-references don't add up
078: */
079: public void dereference() throws MarshallException {
080: int maxDepth = 0;
081:
082: while (ProtocolConstants.TYPE_REFERENCE.equals(type)) {
083: InboundVariable cd = context.getInboundVariable(formField
084: .getString());
085: if (cd == null) {
086: throw new MarshallException(getClass(), Messages
087: .getString("InboundVariable.MissingVariable",
088: formField.getString()));
089: }
090:
091: type = cd.type;
092: formField = cd.getFormField();
093:
094: // For some reason we used to leave this until the loop finished
095: // and then only set it if the key was null. I think this logic
096: // may have been broken by named objects
097: key = cd.key;
098:
099: maxDepth++;
100: if (maxDepth > 20) {
101: throw new MarshallException(getClass(),
102: "Max depth exceeded when dereferencing "
103: + formField.getString());
104: }
105: }
106:
107: // For references without an explicit variable name, we use the
108: // name of the thing they point at
109: // if (key == null)
110: // {
111: // key = formField.getString();
112: // }
113: }
114:
115: /**
116: * @return Returns the lookup table.
117: */
118: public InboundContext getLookup() {
119: return context;
120: }
121:
122: /**
123: * If we are using object parameters that have specified types then the
124: * {@link ConverterManager} will need to get to know what the required type
125: * is.
126: * @return The requested object type, or null if one was not specified
127: */
128: public String getNamedObjectType() {
129: if (type.startsWith("Object_")) {
130: return type.substring("Object_".length());
131: } else {
132: return null;
133: }
134: }
135:
136: public enum OnJsonParseError {
137: /**
138: * If there is anything about the {@link InboundVariable} that can not
139: * be represented in 100% pure JSON, then throw
140: */
141: Throw,
142:
143: /**
144: * If there is anything about the {@link InboundVariable} that can not
145: * be represented in 100% pure JSON, then insert null and carry on
146: */
147: Skip,
148:
149: /**
150: * If there is anything about the {@link InboundVariable} that can not
151: * be represented in 100% pure JSON, then find some hack to do the best
152: * we can and carry on. This option may produce invalid JSON
153: */
154: Hack,
155: }
156:
157: /**
158: * Convert the set of {@link InboundVariable}s to JSON
159: * @return This object in JSON
160: * @throws InvalidJsonException If this can't be represented as JSON
161: */
162: public JsonValue getJsonValue(OnJsonParseError onError)
163: throws InvalidJsonException {
164: return getJsonValue(onError, 0);
165: }
166:
167: /**
168: * Convert the set of {@link InboundVariable}s to JSON
169: * @return This object in JSON
170: * @throws InvalidJsonException If this can't be represented as JSON
171: */
172: private JsonValue getJsonValue(OnJsonParseError onError,
173: int currentDepth) throws InvalidJsonException {
174: if (currentDepth > 50) {
175: throw new InvalidJsonException(
176: "JSON structure too deeply nested. Is it recursive?");
177: }
178:
179: String value = getValue();
180:
181: if (type.equalsIgnoreCase("boolean")) {
182: return new JsonBoolean(Boolean.parseBoolean(value));
183: } else if (type.equalsIgnoreCase("number")) {
184: return new JsonNumber(Double.parseDouble(value));
185: } else if (type.equalsIgnoreCase("string")) {
186: return new JsonString(value);
187: } else if (type.equalsIgnoreCase("date")) {
188: switch (onError) {
189: case Throw:
190: throw new InvalidJsonException("Can't use date in JSON");
191: case Skip:
192: return new JsonNull();
193: case Hack:
194: return new JsonHack("new Date(" + value + ")");
195: }
196: } else if (type.equalsIgnoreCase("xml")) {
197: switch (onError) {
198: case Throw:
199: throw new InvalidJsonException("Can't use XML in JSON");
200: case Skip:
201: return new JsonNull();
202: case Hack:
203: return new JsonHack(EnginePrivate
204: .xmlStringToJavascriptDom(value));
205: }
206: } else if (type.equalsIgnoreCase("array")) {
207: JsonArray array = new JsonArray();
208:
209: // If the text is null then the whole bean is null
210: if (value.trim().equals(ProtocolConstants.INBOUND_NULL)) {
211: return new JsonNull();
212: }
213:
214: if (!value
215: .startsWith(ProtocolConstants.INBOUND_ARRAY_START)) {
216: throw new InvalidJsonException(Messages.getString(
217: "CollectionConverter.FormatError",
218: ProtocolConstants.INBOUND_ARRAY_START));
219: }
220:
221: if (!value.endsWith(ProtocolConstants.INBOUND_ARRAY_END)) {
222: throw new InvalidJsonException(Messages.getString(
223: "CollectionConverter.FormatError",
224: ProtocolConstants.INBOUND_ARRAY_END));
225: }
226:
227: value = value.substring(1, value.length() - 1);
228: StringTokenizer st = new StringTokenizer(value,
229: ProtocolConstants.INBOUND_ARRAY_SEPARATOR);
230: int size = st.countTokens();
231: for (int i = 0; i < size; i++) {
232: String token = st.nextToken();
233:
234: String[] split = ParseUtil.splitInbound(token);
235: String splitValue = split[LocalUtil.INBOUND_INDEX_VALUE];
236:
237: InboundVariable nested = context
238: .getInboundVariable(splitValue);
239: array.add(nested
240: .getJsonValue(onError, currentDepth + 1));
241: }
242:
243: return array;
244: } else if (type.startsWith("Object_")) {
245: JsonObject object = new JsonObject();
246:
247: // If the text is null then the whole bean is null
248: if (value.trim().equals(ProtocolConstants.INBOUND_NULL)) {
249: return new JsonNull();
250: }
251:
252: if (!value.startsWith(ProtocolConstants.INBOUND_MAP_START)) {
253: throw new InvalidJsonException(Messages.getString(
254: "MapConverter.FormatError",
255: ProtocolConstants.INBOUND_MAP_START));
256: }
257:
258: if (!value.endsWith(ProtocolConstants.INBOUND_MAP_END)) {
259: throw new InvalidJsonException(Messages.getString(
260: "MapConverter.FormatError",
261: ProtocolConstants.INBOUND_MAP_END));
262: }
263:
264: value = value.substring(1, value.length() - 1);
265:
266: // Loop through the property declarations
267: StringTokenizer st = new StringTokenizer(value, ",");
268: int size = st.countTokens();
269: for (int i = 0; i < size; i++) {
270: String token = st.nextToken();
271: if (token.trim().length() == 0) {
272: continue;
273: }
274:
275: int colonpos = token
276: .indexOf(ProtocolConstants.INBOUND_MAP_ENTRY);
277: if (colonpos == -1) {
278: throw new InvalidJsonException(Messages.getString(
279: "MapConverter.MissingSeparator",
280: ProtocolConstants.INBOUND_MAP_ENTRY, token));
281: }
282:
283: // Convert the value part of the token by splitting it into the
284: // type and value (as passed in by Javascript)
285: String valStr = token.substring(colonpos + 1).trim();
286: String[] splitIv = ParseUtil.splitInbound(valStr);
287: String splitIvValue = splitIv[LocalUtil.INBOUND_INDEX_VALUE];
288:
289: String keyStr = token.substring(0, colonpos).trim();
290:
291: InboundVariable nested = context
292: .getInboundVariable(splitIvValue);
293: object.put(keyStr, nested.getJsonValue(onError,
294: currentDepth + 1));
295: }
296:
297: return object;
298: }
299:
300: log.warn("Data type: " + type
301: + " is not one that InboundVariable understands");
302: throw new InvalidJsonException("Unknown data type");
303: }
304:
305: /**
306: * Was this type null on the way in
307: * @return true if the javascript variable was null or undefined.
308: */
309: public boolean isNull() {
310: return type.equals(ProtocolConstants.INBOUND_NULL);
311: }
312:
313: /**
314: * @return Returns the value.
315: */
316: public String getValue() {
317: return formField.getString();
318: }
319:
320: /**
321: * @return Returns the file value
322: */
323: public FormField getFormField() {
324: return formField;
325: }
326:
327: /* (non-Javadoc)
328: * @see java.lang.Object#toString()
329: */
330: @Override
331: public String toString() {
332: return type + ProtocolConstants.INBOUND_TYPE_SEPARATOR
333: + formField.getString();
334: }
335:
336: /* (non-Javadoc)
337: * @see java.lang.Object#equals(java.lang.Object)
338: */
339: @Override
340: public boolean equals(Object obj) {
341: if (this == obj) {
342: return true;
343: }
344:
345: if (!(obj instanceof InboundVariable)) {
346: return false;
347: }
348:
349: InboundVariable that = (InboundVariable) obj;
350:
351: if (!this .type.equals(that.type)) {
352: return false;
353: }
354:
355: if (!this .formField.equals(that.formField)) {
356: return false;
357: }
358:
359: if (this .key == null || that.key == null) {
360: return false;
361: }
362:
363: return true; // this.key.equals(that.key);
364: }
365:
366: /* (non-Javadoc)
367: * @see java.lang.Object#hashCode()
368: */
369: @Override
370: public int hashCode() {
371: return formField.hashCode() + type.hashCode();
372: }
373:
374: /**
375: * How do be lookup references?
376: */
377: private InboundContext context;
378:
379: /**
380: * The variable name
381: */
382: private String key;
383:
384: /**
385: * The javascript declared variable type
386: */
387: private String type;
388:
389: /**
390: * The javascript declared file value
391: */
392: private FormField formField;
393:
394: /**
395: * The log stream
396: */
397: private static final Log log = LogFactory
398: .getLog(InboundVariable.class);
399: }
|