1 | |
|
2 | |
|
3 | |
|
4 | |
|
5 | |
|
6 | |
|
7 | |
|
8 | |
|
9 | |
|
10 | |
|
11 | |
|
12 | |
|
13 | |
|
14 | |
|
15 | |
package net.sf.practicalxml.converter.bean; |
16 | |
|
17 | |
import java.lang.reflect.Array; |
18 | |
import java.lang.reflect.Method; |
19 | |
import java.util.ArrayList; |
20 | |
import java.util.Calendar; |
21 | |
import java.util.Collection; |
22 | |
import java.util.Collections; |
23 | |
import java.util.Date; |
24 | |
import java.util.EnumSet; |
25 | |
import java.util.List; |
26 | |
import java.util.Map; |
27 | |
|
28 | |
import org.w3c.dom.Element; |
29 | |
|
30 | |
import net.sf.kdgcommons.bean.Introspection; |
31 | |
import net.sf.kdgcommons.bean.IntrospectionCache; |
32 | |
import net.sf.kdgcommons.codec.Base64Codec; |
33 | |
import net.sf.kdgcommons.codec.HexCodec; |
34 | |
import net.sf.kdgcommons.lang.StringUtil; |
35 | |
import net.sf.practicalxml.DomUtil; |
36 | |
import net.sf.practicalxml.XmlUtil; |
37 | |
import net.sf.practicalxml.converter.ConversionException; |
38 | |
import net.sf.practicalxml.converter.ConversionConstants; |
39 | |
import net.sf.practicalxml.converter.bean.Bean2XmlAppenders.Appender; |
40 | |
import net.sf.practicalxml.converter.bean.Bean2XmlAppenders.BasicAppender; |
41 | |
import net.sf.practicalxml.converter.bean.Bean2XmlAppenders.DirectAppender; |
42 | |
import net.sf.practicalxml.converter.bean.Bean2XmlAppenders.IndexedAppender; |
43 | |
import net.sf.practicalxml.converter.bean.Bean2XmlAppenders.MapAppender; |
44 | |
import net.sf.practicalxml.converter.internal.ConversionUtils; |
45 | |
import net.sf.practicalxml.converter.internal.JavaStringConversions; |
46 | |
import net.sf.practicalxml.converter.internal.TypeUtils; |
47 | |
|
48 | |
|
49 | |
|
50 | |
|
51 | |
|
52 | |
|
53 | |
|
54 | |
|
55 | |
|
56 | |
|
57 | |
|
58 | |
|
59 | |
|
60 | |
public class Bean2XmlConverter |
61 | |
{ |
62 | |
private EnumSet<Bean2XmlOptions> _options; |
63 | |
private IntrospectionCache _introspections; |
64 | |
private JavaStringConversions _converter; |
65 | |
List<ConversionException> _deferredExceptions; |
66 | |
|
67 | |
private boolean _setAccessible; |
68 | |
|
69 | |
public Bean2XmlConverter(Bean2XmlOptions... options) |
70 | 104 | { |
71 | 104 | _options = EnumSet.noneOf(Bean2XmlOptions.class); |
72 | 168 | for (Bean2XmlOptions option : options) |
73 | 64 | _options.add(option); |
74 | |
|
75 | 104 | _introspections = new IntrospectionCache(_options.contains(Bean2XmlOptions.CACHE_INTROSPECTIONS)); |
76 | 104 | _converter = new JavaStringConversions(shouldUseXsdFormatting()); |
77 | |
|
78 | 104 | _setAccessible = _options.contains(Bean2XmlOptions.SET_ACCESSIBLE); |
79 | 104 | } |
80 | |
|
81 | |
|
82 | |
|
83 | |
|
84 | |
|
85 | |
|
86 | |
|
87 | |
|
88 | |
|
89 | |
|
90 | |
|
91 | |
|
92 | |
public Element convert(Object obj, String rootName) |
93 | |
{ |
94 | 135 | return convert(obj, null, rootName); |
95 | |
} |
96 | |
|
97 | |
|
98 | |
|
99 | |
|
100 | |
|
101 | |
|
102 | |
|
103 | |
|
104 | |
|
105 | |
|
106 | |
public Element convert(Object obj, String nsUri, String rootName) |
107 | |
{ |
108 | 137 | Element root = DomUtil.newDocument(nsUri, rootName); |
109 | 137 | doNamespaceHack(root); |
110 | 137 | convert(obj, rootName, new DirectAppender(_options, root, obj)); |
111 | 135 | return root; |
112 | |
} |
113 | |
|
114 | |
|
115 | |
|
116 | |
|
117 | |
|
118 | |
|
119 | |
|
120 | |
|
121 | |
|
122 | |
public List<ConversionException> getDeferredExceptions() |
123 | |
{ |
124 | 1 | if (_deferredExceptions != null) |
125 | 1 | return Collections.unmodifiableList(_deferredExceptions); |
126 | |
else |
127 | 0 | return Collections.emptyList(); |
128 | |
} |
129 | |
|
130 | |
|
131 | |
|
132 | |
|
133 | |
|
134 | |
|
135 | |
|
136 | |
|
137 | |
|
138 | |
private void convert(Object obj, String name, Appender appender) |
139 | |
{ |
140 | |
try |
141 | |
{ |
142 | 379 | if (obj == null) |
143 | 8 | convertAsNull(null, name, appender); |
144 | 371 | else if (_converter.isConvertableToString(obj)) |
145 | 264 | convertSimple(obj, name, appender); |
146 | 107 | else if (obj instanceof Enum<?>) |
147 | 5 | convertAsEnum(obj, name, appender); |
148 | 102 | else if (obj instanceof byte[]) |
149 | 5 | convertAsByteArray(obj, name, appender); |
150 | 97 | else if (obj.getClass().isArray()) |
151 | 11 | convertAsArray(obj, name, appender); |
152 | 86 | else if (obj instanceof Map) |
153 | 11 | convertAsMap(obj, name, appender); |
154 | 75 | else if (obj instanceof Collection) |
155 | 23 | convertAsCollection(obj, name, appender); |
156 | 52 | else if (obj instanceof Date) |
157 | 12 | convertAsDate(obj, name, appender); |
158 | 40 | else if (obj instanceof Calendar) |
159 | 2 | convertAsCalendar(obj, name, appender); |
160 | |
else |
161 | 38 | convertAsBean(obj, name, appender); |
162 | |
} |
163 | 4 | catch (Exception ex) |
164 | |
{ |
165 | 4 | ConversionException ex2 = (ex instanceof ConversionException) |
166 | |
? new ConversionException((ConversionException)ex, name) |
167 | |
: new ConversionException("unable to convert", name, ex); |
168 | 3 | if (! exceptionDeferred(ex2)) |
169 | 3 | throw ex2; |
170 | 375 | } |
171 | 375 | } |
172 | |
|
173 | |
|
174 | |
private boolean exceptionDeferred(ConversionException ex) |
175 | |
{ |
176 | 7 | if (! _options.contains(Bean2XmlOptions.DEFER_EXCEPTIONS)) |
177 | 5 | return false; |
178 | |
|
179 | 2 | if (_deferredExceptions == null) |
180 | 1 | _deferredExceptions = new ArrayList<ConversionException>(); |
181 | |
|
182 | 2 | _deferredExceptions.add(ex); |
183 | 2 | return true; |
184 | |
} |
185 | |
|
186 | |
|
187 | |
private boolean shouldUseXsdFormatting() |
188 | |
{ |
189 | 104 | return _options.contains(Bean2XmlOptions.XSD_FORMAT) |
190 | |
|| _options.contains(Bean2XmlOptions.USE_TYPE_ATTR); |
191 | |
} |
192 | |
|
193 | |
|
194 | |
|
195 | |
|
196 | |
|
197 | |
|
198 | |
|
199 | |
|
200 | |
|
201 | |
|
202 | |
|
203 | |
|
204 | |
private void doNamespaceHack(Element root) |
205 | |
{ |
206 | 137 | if (_options.contains(Bean2XmlOptions.NULL_AS_XSI_NIL)) |
207 | |
{ |
208 | 4 | ConversionUtils.setXsiNil(root, false); |
209 | |
} |
210 | |
|
211 | |
|
212 | |
|
213 | 137 | boolean addCnvNS = _options.contains(Bean2XmlOptions.USE_INDEX_ATTR); |
214 | 137 | addCnvNS |= !_options.contains(Bean2XmlOptions.MAP_KEYS_AS_ELEMENT_NAME); |
215 | 137 | addCnvNS &= _options.contains(Bean2XmlOptions.USE_TYPE_ATTR); |
216 | 137 | if (addCnvNS) |
217 | |
{ |
218 | 27 | ConversionUtils.setAttribute(root, ConversionConstants.AT_DUMMY, ""); |
219 | |
} |
220 | 137 | } |
221 | |
|
222 | |
|
223 | |
private void convertAsNull(Class<?> klass, String name, Appender appender) |
224 | |
{ |
225 | 21 | appender.appendValue(name, klass, null); |
226 | 21 | } |
227 | |
|
228 | |
|
229 | |
private void convertSimple(Object obj, String name, Appender appender) |
230 | |
{ |
231 | 264 | appender.appendValue(name, obj.getClass(), _converter.stringify(obj)); |
232 | 263 | } |
233 | |
|
234 | |
|
235 | |
private void convertAsDate(Object obj, String name, Appender appender) |
236 | |
{ |
237 | 12 | String value = _options.contains(Bean2XmlOptions.XSD_FORMAT) |
238 | |
? XmlUtil.formatXsdDatetime((Date)obj) |
239 | |
: obj.toString(); |
240 | 12 | appender.appendValue(name, obj.getClass(), value); |
241 | 12 | } |
242 | |
|
243 | |
|
244 | |
private void convertAsEnum(Object obj, String name, Appender appender) |
245 | |
{ |
246 | 5 | String enumName = ((Enum<?>)obj).name(); |
247 | 5 | if (_options.contains(Bean2XmlOptions.ENUM_AS_NAME_AND_VALUE)) |
248 | |
{ |
249 | 2 | Element elem = appender.appendValue(name, obj.getClass(), obj.toString()); |
250 | 2 | ConversionUtils.setAttribute( |
251 | |
elem, |
252 | |
ConversionConstants.AT_ENUM_NAME, |
253 | |
enumName); |
254 | 2 | } |
255 | |
else |
256 | |
{ |
257 | 3 | appender.appendValue(name, obj.getClass(), enumName); |
258 | |
} |
259 | 5 | } |
260 | |
|
261 | |
|
262 | |
private void convertAsByteArray(Object obj, String name, Appender appender) |
263 | |
{ |
264 | 5 | if (_options.contains(Bean2XmlOptions.BYTE_ARRAYS_AS_BASE64)) |
265 | |
{ |
266 | 2 | String value = new Base64Codec().toString((byte[])obj); |
267 | 2 | Element child = appender.appendValue(name, obj.getClass(), value); |
268 | 2 | appender.overrideType(child, TypeUtils.XSD_BASE64); |
269 | 2 | } |
270 | 3 | else if (_options.contains(Bean2XmlOptions.BYTE_ARRAYS_AS_HEX)) |
271 | |
{ |
272 | 2 | String value = new HexCodec().toString((byte[])obj); |
273 | 2 | Element child = appender.appendValue(name, obj.getClass(), value); |
274 | 2 | appender.overrideType(child, TypeUtils.XSD_HEXBINARY); |
275 | 2 | } |
276 | |
else |
277 | 1 | convertAsArray(obj, name, appender); |
278 | 5 | } |
279 | |
|
280 | |
|
281 | |
private void convertAsArray(Object obj, String name, Appender appender) |
282 | |
{ |
283 | 12 | String childName = determineChildNameForSequence(name); |
284 | 12 | Appender childAppender = appender; |
285 | 12 | if (!_options.contains(Bean2XmlOptions.SEQUENCE_AS_REPEATED_ELEMENTS)) |
286 | |
{ |
287 | 10 | Element parent = appender.appendContainer(name, obj.getClass()); |
288 | 10 | childAppender = new IndexedAppender(appender, parent, obj); |
289 | |
} |
290 | |
|
291 | 12 | int length = Array.getLength(obj); |
292 | 54 | for (int idx = 0 ; idx < length ; idx++) |
293 | |
{ |
294 | 42 | Object value = Array.get(obj, idx); |
295 | 42 | convert(value, childName, childAppender); |
296 | |
} |
297 | 12 | } |
298 | |
|
299 | |
|
300 | |
private void convertAsMap(Object obj, String name, Appender appender) |
301 | |
{ |
302 | 11 | Element parent = appender.appendContainer(name, obj.getClass()); |
303 | 11 | Appender childAppender = new MapAppender(appender, parent, obj); |
304 | 11 | for (Map.Entry<?,?> entry : ((Map<?,?>)obj).entrySet()) |
305 | |
{ |
306 | 23 | convert(entry.getValue(), String.valueOf(entry.getKey()), childAppender); |
307 | 22 | } |
308 | 10 | } |
309 | |
|
310 | |
|
311 | |
private void convertAsCollection(Object obj, String name, Appender appender) |
312 | |
{ |
313 | 23 | String childName = determineChildNameForSequence(name); |
314 | 23 | Appender childAppender = appender; |
315 | 23 | if (!_options.contains(Bean2XmlOptions.SEQUENCE_AS_REPEATED_ELEMENTS)) |
316 | |
{ |
317 | 22 | Element parent = appender.appendContainer(name, obj.getClass()); |
318 | 22 | childAppender = new IndexedAppender(appender, parent, obj); |
319 | |
} |
320 | |
|
321 | 23 | for (Object value : (Collection<?>)obj) |
322 | |
{ |
323 | 68 | convert(value, childName, childAppender); |
324 | 68 | } |
325 | 23 | } |
326 | |
|
327 | |
private void convertAsCalendar(Object obj, String name, Appender appender) |
328 | |
{ |
329 | 2 | Element parent = appender.appendContainer(name, obj.getClass()); |
330 | 2 | Appender childAppender = new BasicAppender(appender, parent, obj); |
331 | |
|
332 | 2 | Calendar cal = (Calendar)obj; |
333 | 2 | convert(cal.getTime(), ConversionConstants.EL_CALENDAR_DATE, childAppender); |
334 | 2 | convert(cal.getTimeZone(), ConversionConstants.EL_CALENDAR_TIMEZONE, childAppender); |
335 | 2 | convert(cal.getTimeInMillis(), ConversionConstants.EL_CALENDAR_MILLIS, childAppender); |
336 | |
|
337 | |
|
338 | 2 | convert(Integer.valueOf(cal.getFirstDayOfWeek()), ConversionConstants.EL_CALENDAR_FIRST_DAY, childAppender); |
339 | 2 | convert(Integer.valueOf(cal.getMinimalDaysInFirstWeek()), ConversionConstants.EL_CALENDAR_MIN_DAYS, childAppender); |
340 | 2 | } |
341 | |
|
342 | |
|
343 | |
private void convertAsBean(Object obj, String name, Appender appender) |
344 | |
{ |
345 | 38 | Element parent = appender.appendContainer(name, obj.getClass()); |
346 | 38 | Appender childAppender = new BasicAppender(appender, parent, obj); |
347 | 38 | Introspection ispec = _introspections.lookup(obj.getClass(), _setAccessible); |
348 | 38 | for (String propName : ispec.propertyNames()) |
349 | |
{ |
350 | 117 | convertBeanProperty(obj, ispec, propName, childAppender); |
351 | 115 | } |
352 | 36 | } |
353 | |
|
354 | |
|
355 | |
private void convertBeanProperty(Object bean, Introspection ispec, String propName, Appender appender) |
356 | |
{ |
357 | |
Object value; |
358 | |
try |
359 | |
{ |
360 | 117 | Method getter = ispec.getter(propName); |
361 | 117 | value = (getter != null) |
362 | |
? getter.invoke(bean) |
363 | |
: null; |
364 | |
|
365 | 115 | if (value == null) |
366 | 13 | convertAsNull(ispec.type(propName), propName, appender); |
367 | 102 | else if (appender.shouldSkip(value)) |
368 | 2 | return; |
369 | |
else |
370 | 99 | convert(value, propName, appender); |
371 | |
} |
372 | 4 | catch (Exception ex) |
373 | |
{ |
374 | |
|
375 | 4 | ConversionException ex2 = (ex instanceof ConversionException) |
376 | |
? new ConversionException((ConversionException)ex, propName) |
377 | |
: new ConversionException("unable to retrieve bean property", propName, ex); |
378 | 4 | if (! exceptionDeferred(ex2)) |
379 | 2 | throw ex2; |
380 | 111 | } |
381 | 113 | } |
382 | |
|
383 | |
|
384 | |
private String determineChildNameForSequence(String parentName) |
385 | |
{ |
386 | 35 | if (StringUtil.isEmpty(parentName)) |
387 | 0 | return ConversionConstants.EL_COLLECTION_ITEM; |
388 | |
|
389 | 35 | if (_options.contains(Bean2XmlOptions.SEQUENCE_AS_REPEATED_ELEMENTS)) |
390 | 3 | return parentName; |
391 | |
|
392 | 32 | if (!_options.contains(Bean2XmlOptions.SEQUENCE_NAMED_BY_PARENT)) |
393 | 25 | return ConversionConstants.EL_COLLECTION_ITEM; |
394 | |
|
395 | 7 | if (parentName.endsWith("s") || parentName.endsWith("S")) |
396 | 3 | return parentName.substring(0, parentName.length() - 1); |
397 | |
|
398 | 4 | return parentName; |
399 | |
} |
400 | |
} |