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.convert;
017:
018: import java.util.ArrayList;
019: import java.util.HashMap;
020: import java.util.Iterator;
021: import java.util.List;
022: import java.util.Map;
023: import java.util.StringTokenizer;
024: import java.util.TreeMap;
025: import java.util.Map.Entry;
026:
027: import org.apache.commons.logging.Log;
028: import org.apache.commons.logging.LogFactory;
029: import org.directwebremoting.dwrp.MapOutboundVariable;
030: import org.directwebremoting.dwrp.ObjectJsonOutboundVariable;
031: import org.directwebremoting.dwrp.ObjectNonJsonOutboundVariable;
032: import org.directwebremoting.dwrp.ParseUtil;
033: import org.directwebremoting.dwrp.ProtocolConstants;
034: import org.directwebremoting.extend.ConverterManager;
035: import org.directwebremoting.extend.InboundContext;
036: import org.directwebremoting.extend.InboundVariable;
037: import org.directwebremoting.extend.JsonModeMarshallException;
038: import org.directwebremoting.extend.MarshallException;
039: import org.directwebremoting.extend.NamedConverter;
040: import org.directwebremoting.extend.OutboundContext;
041: import org.directwebremoting.extend.OutboundVariable;
042: import org.directwebremoting.extend.Property;
043: import org.directwebremoting.extend.TypeHintContext;
044: import org.directwebremoting.util.LocalUtil;
045: import org.directwebremoting.util.Messages;
046:
047: /**
048: * BasicObjectConverter is a parent to {@link BeanConverter} and
049: * {@link ObjectConverter} an provides support for include and exclude lists,
050: * and instanceTypes.
051: * @author Joe Walker [joe at getahead dot ltd dot uk]
052: */
053: public abstract class BasicObjectConverter extends BaseV20Converter
054: implements NamedConverter {
055: /* (non-Javadoc)
056: * @see org.directwebremoting.Converter#convertInbound(java.lang.Class, org.directwebremoting.InboundVariable, org.directwebremoting.InboundContext)
057: */
058: public Object convertInbound(Class<?> paramType,
059: InboundVariable data, InboundContext inctx)
060: throws MarshallException {
061: String value = data.getValue();
062:
063: // If the text is null then the whole bean is null
064: if (value.trim().equals(ProtocolConstants.INBOUND_NULL)) {
065: return null;
066: }
067:
068: if (!value.startsWith(ProtocolConstants.INBOUND_MAP_START)) {
069: throw new MarshallException(paramType, Messages.getString(
070: "BeanConverter.FormatError",
071: ProtocolConstants.INBOUND_MAP_START));
072: }
073:
074: if (!value.endsWith(ProtocolConstants.INBOUND_MAP_END)) {
075: throw new MarshallException(paramType, Messages.getString(
076: "BeanConverter.FormatError",
077: ProtocolConstants.INBOUND_MAP_START));
078: }
079:
080: value = value.substring(1, value.length() - 1);
081:
082: try {
083: Object bean;
084: if (instanceType != null) {
085: bean = instanceType.newInstance();
086: } else {
087: bean = paramType.newInstance();
088: }
089:
090: // We should put the new object into the working map in case it
091: // is referenced later nested down in the conversion process.
092: if (instanceType != null) {
093: inctx.addConverted(data, instanceType, bean);
094: } else {
095: inctx.addConverted(data, paramType, bean);
096: }
097:
098: Map<String, Property> properties = getPropertyMapFromObject(
099: bean, false, true);
100:
101: // Loop through the properties passed in
102: Map<String, String> tokens = extractInboundTokens(
103: paramType, value);
104: for (Entry<String, String> entry : tokens.entrySet()) {
105: String key = entry.getKey();
106: String val = entry.getValue();
107:
108: Property property = properties.get(key);
109: if (property == null) {
110: log
111: .warn("Missing java bean property to match javascript property: "
112: + key
113: + ". For causes see debug level logs:");
114:
115: log
116: .debug("- The javascript may be refer to a property that does not exist");
117: log
118: .debug("- You may be missing the correct setter: set"
119: + Character.toTitleCase(key
120: .charAt(0))
121: + key.substring(1) + "()");
122: log
123: .debug("- The property may be excluded using include or exclude rules.");
124:
125: StringBuffer all = new StringBuffer();
126: for (Iterator<String> pit = properties.keySet()
127: .iterator(); pit.hasNext();) {
128: all.append(pit.next());
129: if (pit.hasNext()) {
130: all.append(',');
131: }
132: }
133: log.debug("Fields exist for (" + all + ").");
134: continue;
135: }
136:
137: Class<?> propType = property.getPropertyType();
138:
139: String[] split = ParseUtil.splitInbound(val);
140: String splitValue = split[LocalUtil.INBOUND_INDEX_VALUE];
141: String splitType = split[LocalUtil.INBOUND_INDEX_TYPE];
142:
143: InboundVariable nested = new InboundVariable(data
144: .getLookup(), null, splitType, splitValue);
145: nested.dereference();
146: TypeHintContext incc = createTypeHintContext(inctx,
147: property);
148:
149: Object output = converterManager.convertInbound(
150: propType, nested, inctx, incc);
151: property.setValue(bean, output);
152: }
153:
154: return bean;
155: } catch (MarshallException ex) {
156: throw ex;
157: } catch (Exception ex) {
158: throw new MarshallException(paramType, ex);
159: }
160: }
161:
162: /**
163: * {@link #convertInbound(Class, InboundVariable, InboundContext)} needs to
164: * create a {@link TypeHintContext} for the {@link Property} it is
165: * converting so that the type guessing system can do its work.
166: * <p>The method of generating a {@link TypeHintContext} is different for
167: * the {@link BeanConverter} and the {@link ObjectConverter}.
168: * @param inctx The parent context
169: * @param property The property being converted
170: * @return The new TypeHintContext
171: */
172: protected abstract TypeHintContext createTypeHintContext(
173: InboundContext inctx, Property property);
174:
175: /* (non-Javadoc)
176: * @see org.directwebremoting.Converter#convertOutbound(java.lang.Object, org.directwebremoting.OutboundContext)
177: */
178: public OutboundVariable convertOutbound(Object data,
179: OutboundContext outctx) throws MarshallException {
180: // Where we collect out converted children
181: Map<String, OutboundVariable> ovs = new TreeMap<String, OutboundVariable>();
182:
183: // We need to do this before collecting the children to save recursion
184: MapOutboundVariable ov;
185: if (outctx.isJsonMode()) {
186: if (javascript != null) {
187: throw new JsonModeMarshallException(data.getClass(),
188: "Can't used named Javascript objects in JSON mode");
189: }
190:
191: ov = new ObjectJsonOutboundVariable();
192: } else {
193: ov = new ObjectNonJsonOutboundVariable(outctx,
194: getJavascript());
195: }
196: outctx.put(data, ov);
197:
198: try {
199: Map<String, Property> properties = getPropertyMapFromObject(
200: data, true, false);
201: for (Entry<String, Property> entry : properties.entrySet()) {
202: String name = entry.getKey();
203: Property property = entry.getValue();
204:
205: Object value = property.getValue(data);
206: OutboundVariable nested = getConverterManager()
207: .convertOutbound(value, outctx);
208:
209: ovs.put(name, nested);
210: }
211: } catch (MarshallException ex) {
212: throw ex;
213: } catch (Exception ex) {
214: throw new MarshallException(data.getClass(), ex);
215: }
216:
217: ov.setChildren(ovs);
218:
219: return ov;
220: }
221:
222: /**
223: * Set a list of properties excluded from conversion
224: * @param excludes The space or comma separated list of properties to exclude
225: */
226: public void setExclude(String excludes) {
227: if (inclusions != null) {
228: throw new IllegalArgumentException(Messages
229: .getString("BeanConverter.OnlyIncludeOrExclude"));
230: }
231:
232: exclusions = new ArrayList<String>();
233:
234: String toSplit = excludes.replace(",", " ");
235: StringTokenizer st = new StringTokenizer(toSplit);
236: while (st.hasMoreTokens()) {
237: String rule = st.nextToken();
238: if (rule.startsWith("get")) {
239: log
240: .warn("Exclusions are based on property names and not method names. '"
241: + rule
242: + "' starts with 'get' so it looks like a method name and not a property name.");
243: }
244:
245: exclusions.add(rule);
246: }
247: }
248:
249: /**
250: * Set a list of properties included from conversion
251: * @param includes The space or comma separated list of properties to exclude
252: */
253: public void setInclude(String includes) {
254: if (exclusions != null) {
255: throw new IllegalArgumentException(Messages
256: .getString("BeanConverter.OnlyIncludeOrExclude"));
257: }
258:
259: inclusions = new ArrayList<String>();
260:
261: String toSplit = includes.replace(",", " ");
262: StringTokenizer st = new StringTokenizer(toSplit);
263: while (st.hasMoreTokens()) {
264: String rule = st.nextToken();
265: if (rule.startsWith("get")) {
266: log
267: .warn("Inclusions are based on property names and not method names. '"
268: + rule
269: + "' starts with 'get' so it looks like a method name and not a property name.");
270: }
271:
272: inclusions.add(rule);
273: }
274: }
275:
276: /**
277: * @param name The class name to use as an implementation of the converted bean
278: * @throws ClassNotFoundException If the given class can not be found
279: */
280: public void setImplementation(String name)
281: throws ClassNotFoundException {
282: setInstanceType(LocalUtil.classForName(name));
283: }
284:
285: /* (non-Javadoc)
286: * @see org.directwebremoting.convert.NamedConverter#getInstanceType()
287: */
288: public Class<?> getInstanceType() {
289: return instanceType;
290: }
291:
292: /* (non-Javadoc)
293: * @see org.directwebremoting.convert.NamedConverter#setInstanceType(java.lang.Class)
294: */
295: public void setInstanceType(Class<?> instanceType) {
296: this .instanceType = instanceType;
297: }
298:
299: /* (non-Javadoc)
300: * @see org.directwebremoting.convert.BaseV20Converter#setConverterManager(org.directwebremoting.ConverterManager)
301: */
302: @Override
303: public void setConverterManager(ConverterManager converterManager) {
304: this .converterManager = converterManager;
305: }
306:
307: /**
308: * Accessor for the current ConverterManager
309: * @return the current ConverterManager
310: */
311: public ConverterManager getConverterManager() {
312: return converterManager;
313: }
314:
315: /**
316: * Check with the access rules to see if we are allowed to convert a property
317: * @param property The property to test
318: * @return true if the property may be marshalled
319: */
320: protected boolean isAllowedByIncludeExcludeRules(String property) {
321: if (exclusions != null) {
322: // Check each exclusions and return false if we get a match
323: for (String exclusion : exclusions) {
324: if (property.equals(exclusion)) {
325: return false;
326: }
327: }
328:
329: // So we passed all the exclusions. The setters enforce mutual
330: // exclusion between exclusions and inclusions so we don't need to
331: // 'return true' here, we can carry on. This has the advantage that
332: // we can relax the mutual exclusion at some stage.
333: }
334:
335: if (inclusions != null) {
336: // Check each inclusion and return true if we get a match
337: for (String inclusion : inclusions) {
338: if (property.equals(inclusion)) {
339: return true;
340: }
341: }
342:
343: // Since we are white-listing with inclusions and there was not
344: // match, this property is not allowed.
345: return false;
346: }
347:
348: // default to allow if there are no inclusions or exclusions
349: return true;
350: }
351:
352: /**
353: * Loop over all the inputs and extract a Map of key:value pairs
354: * @param paramType The type we are converting to
355: * @param value The input string
356: * @return A Map of the tokens in the string
357: * @throws MarshallException If the marshalling fails
358: */
359: protected static Map<String, String> extractInboundTokens(
360: Class<?> paramType, String value) throws MarshallException {
361: Map<String, String> tokens = new HashMap<String, String>();
362: StringTokenizer st = new StringTokenizer(value,
363: ProtocolConstants.INBOUND_MAP_SEPARATOR);
364: int size = st.countTokens();
365:
366: for (int i = 0; i < size; i++) {
367: String token = st.nextToken();
368: if (token.trim().length() == 0) {
369: continue;
370: }
371:
372: int colonpos = token
373: .indexOf(ProtocolConstants.INBOUND_MAP_ENTRY);
374: if (colonpos == -1) {
375: throw new MarshallException(paramType, Messages
376: .getString("BeanConverter.MissingSeparator",
377: ProtocolConstants.INBOUND_MAP_ENTRY,
378: token));
379: }
380:
381: String key = token.substring(0, colonpos).trim();
382: String val = token.substring(colonpos + 1).trim();
383: tokens.put(key, val);
384: }
385:
386: return tokens;
387: }
388:
389: /* (non-Javadoc)
390: * @see org.directwebremoting.convert.NamedConverter#getJavascript()
391: */
392: public String getJavascript() {
393: return javascript;
394: }
395:
396: /* (non-Javadoc)
397: * @see org.directwebremoting.convert.NamedConverter#setJavascript(java.lang.String)
398: */
399: public void setJavascript(String javascript) {
400: this .javascript = javascript;
401: }
402:
403: /**
404: * The javascript class name for the converted objects
405: */
406: protected String javascript = null;
407:
408: /**
409: * The list of excluded properties
410: */
411: protected List<String> exclusions = null;
412:
413: /**
414: * The list of included properties
415: */
416: protected List<String> inclusions = null;
417:
418: /**
419: * A type that allows us to fulfill an interface or subtype requirement
420: */
421: protected Class<?> instanceType = null;
422:
423: /**
424: * To forward marshalling requests
425: */
426: protected ConverterManager converterManager = null;
427:
428: /**
429: * The log stream
430: */
431: private static final Log log = LogFactory
432: .getLog(BasicObjectConverter.class);
433: }
|