001
002 /*
003 * Copyright (C) 2011 Archie L. Cobbs. All rights reserved.
004 *
005 * $Id: ParseUtil.java 303 2012-03-06 22:56:30Z archie.cobbs $
006 */
007
008 package org.dellroad.stuff.jibx;
009
010 import java.net.Inet4Address;
011 import java.net.URI;
012 import java.net.URISyntaxException;
013 import java.text.SimpleDateFormat;
014 import java.util.Arrays;
015 import java.util.Date;
016 import java.util.HashSet;
017 import java.util.TimeZone;
018 import java.util.UUID;
019 import java.util.regex.Matcher;
020 import java.util.regex.Pattern;
021 import java.util.regex.PatternSyntaxException;
022
023 import org.dellroad.stuff.java.IdGenerator;
024 import org.dellroad.stuff.net.IPv4Util;
025 import org.dellroad.stuff.string.ByteArrayEncoder;
026 import org.dellroad.stuff.string.DateEncoder;
027 import org.dellroad.stuff.string.StringEncoder;
028 import org.jibx.runtime.JiBXParseException;
029
030 /**
031 * JiBX parsing utility methods. These methods can be used as JiBX value serializer/deserializer methods.
032 */
033 public final class ParseUtil {
034
035 private static final String[] BOOLEAN_TRUES = { "1", "true", "yes" };
036 private static final String[] BOOLEAN_FALSES = { "0", "false", "no" };
037 private static final String XSD_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ";
038
039 private static final String TIME_INTERVAL_PATTERN
040 = "(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?((([0-9]+(\\.[0-9]+)?)|(\\.[0-9]+))s)?";
041
042 private ParseUtil() {
043 }
044
045 /**
046 * Deserialize a {@link String}, allowing arbitrary characters via backslash escapes.
047 * The string will be decoded by {@link StringEncoder#decode}.
048 *
049 * @see StringEncoder#decode
050 */
051 public static String deserializeString(String string) throws JiBXParseException {
052 try {
053 return StringEncoder.decode(string);
054 } catch (IllegalArgumentException e) {
055 throw new JiBXParseException("invalid encoded string", string, e);
056 }
057 }
058
059 /**
060 * Serialize a {@link String}.
061 * The string will be encoded by {@link StringEncoder#encode}.
062 *
063 * @see StringEncoder#encode
064 */
065 public static String serializeString(String string) {
066 return StringEncoder.encode(string, false);
067 }
068
069 /**
070 * Deserialize a {@link TimeZone}.
071 *
072 * @see #serializeTimeZone
073 */
074 public static TimeZone deserializeTimeZone(String string) throws JiBXParseException {
075 if (!new HashSet<String>(Arrays.asList(TimeZone.getAvailableIDs())).contains(string))
076 throw new JiBXParseException("unrecognized time zone", string);
077 return TimeZone.getTimeZone(string);
078 }
079
080 /**
081 * Serialize a {@link TimeZone}.
082 *
083 * @see #deserializeTimeZone
084 */
085 public static String serializeTimeZone(TimeZone timeZone) {
086 return timeZone.getID();
087 }
088
089 /**
090 * Deserialize an {@link URI}.
091 *
092 * <p>
093 * Note: there is no need for a custom serializer, as {@link URI#toString} already does the right thing.
094 */
095 public static URI deserializeURI(String string) throws JiBXParseException {
096 try {
097 return new URI(string);
098 } catch (URISyntaxException e) {
099 throw new JiBXParseException("invalid URI", string, e);
100 }
101 }
102
103 /**
104 * Serialize an {@link URI}.
105 *
106 * @deprecated This method is does the same thing as JiBX's default serialization via {@link Object#toString()}
107 */
108 @Deprecated
109 public static String serializeURI(URI uri) {
110 return uri.toString();
111 }
112
113 /**
114 * Deserialize an object by reference.
115 *
116 * <p>
117 * Invoke this method from your own custom deserializer to produce an result of the correct type.
118 *
119 * <p>
120 * The object must have been unmarshalled already and had its ID registered via {@link IdMapper#setId}.
121 *
122 * @see #serializeReference
123 * @see IdMapper
124 */
125 public static <T> T deserializeReference(String string, Class<T> type) throws JiBXParseException {
126 if (string == null)
127 return null;
128 long id;
129 try {
130 id = IdMapper.parseId(string);
131 } catch (IllegalArgumentException e) {
132 throw new JiBXParseException("invalid object reference", string, e);
133 }
134 Object obj = IdGenerator.get().getObject(id);
135 if (obj == null)
136 throw new JiBXParseException("unregistered object reference", string);
137 try {
138 return type.cast(obj);
139 } catch (ClassCastException e) {
140 throw new JiBXParseException("object reference `" + string + "' is assigned to an instance of "
141 + obj.getClass() + " which is not assignable to " + type, string);
142 }
143 }
144
145 /**
146 * Serialize an object by reference.
147 *
148 * <p>
149 * The object must have been marshalled already and had its ID assigned via {@link IdMapper#getId}.
150 *
151 * @see #deserializeReference
152 * @see IdMapper
153 */
154 public static String serializeReference(Object obj) {
155 if (obj == null)
156 return null;
157 return IdMapper.formatId(IdGenerator.get().getId(obj));
158 }
159
160 /**
161 * Deserialize a {@link UUID}.
162 *
163 * <p>
164 * Note: there is no need for a custom serializer, as {@link UUID#toString} already does the right thing.
165 */
166 public static UUID deserializeUUID(String string) throws JiBXParseException {
167 try {
168 return UUID.fromString(string);
169 } catch (IllegalArgumentException e) {
170 throw new JiBXParseException("invalid UUID", string, e);
171 }
172 }
173
174 /**
175 * Derialize a millisecond-granularity time interval, e.g., "30s", "1d12h", "0.250s", etc.
176 *
177 * @see #serializeTimeInterval
178 */
179 public static long deserializeTimeInterval(String string) throws JiBXParseException {
180
181 // Apply regular expression
182 Matcher m = Pattern.compile(TIME_INTERVAL_PATTERN).matcher(string);
183 if (string.length() == 0 || !m.matches())
184 throw new JiBXParseException("invalid time interval", string);
185
186 // Parse regex groups
187 Object[][] groups = new Object[][] {
188 { 2, 24 * 60 * 60 * 1000 },
189 { 4, 60 * 60 * 1000 },
190 { 6, 60 * 1000 },
191 { 8, 1000 },
192 };
193 long value = 0;
194 for (int i = 0; i < groups.length; i++) {
195 String group = m.group((Integer)groups[i][0]);
196 if (group == null || group.length() == 0)
197 continue;
198 double num;
199 try {
200 num = Double.parseDouble(group);
201 } catch (NumberFormatException e) {
202 throw new JiBXParseException("invalid time interval", string);
203 }
204 value += Math.round(num * (Integer)groups[i][1]);
205 if (value < 0)
206 throw new JiBXParseException("time interval is too large", string);
207 }
208
209 // Done
210 return value;
211 }
212
213 /**
214 * Serialize a millisecond-granularity time interval, e.g., "30s", "1d12h", "0.250s", etc.
215 *
216 * @see #deserializeTimeInterval
217 */
218 public static String serializeTimeInterval(long value) {
219 if (value < 0)
220 throw new IllegalArgumentException("negative value");
221 StringBuilder b = new StringBuilder(32);
222 long days = value / (24 * 60 * 60 * 1000);
223 value = value % (24 * 60 * 60 * 1000);
224 if (days > 0)
225 b.append(days).append('d');
226 long hours = value / (60 * 60 * 1000);
227 value = value % (60 * 60 * 1000);
228 if (hours > 0)
229 b.append(hours).append('h');
230 long minutes = value / (60 * 1000);
231 value = value % (60 * 1000);
232 if (minutes > 0)
233 b.append(minutes).append('m');
234 long millis = value;
235 if (millis != 0 || b.length() == 0) {
236 if (millis >= 1000 && millis % 1000 == 0)
237 b.append(String.format("%ds", millis / 1000));
238 else
239 b.append(String.format("%.3fs", (double)millis / 1000.0));
240 }
241 return b.toString();
242 }
243
244 /**
245 * Deserialize a byte array using {@link ByteArrayEncoder}.
246 *
247 * @see ByteArrayEncoder
248 */
249 public static byte[] deserializeByteArray(String string) throws JiBXParseException {
250 try {
251 return ByteArrayEncoder.decode(string);
252 } catch (IllegalArgumentException e) {
253 throw new JiBXParseException("invalid byte array", string, e);
254 }
255 }
256
257 /**
258 * Serialize a byte array using {@link ByteArrayEncoder}.
259 *
260 * @see ByteArrayEncoder
261 */
262 public static String serializeByteArray(byte[] array) {
263 return ByteArrayEncoder.encode(array);
264 }
265
266 /**
267 * Deserialize a byte array using MAC address notation (colon-separated). Each byte must have two digits.
268 *
269 * @see #serializeByteArrayWithColons
270 */
271 public static byte[] deserializeByteArrayWithColons(String string) throws JiBXParseException {
272 if (string.length() == 0)
273 return new byte[0];
274 if (string.length() % 3 != 2)
275 throw new JiBXParseException("invalid byte array", string);
276 char[] nocolons = new char[((string.length() + 1) / 3) * 2];
277 int j = 0;
278 for (int i = 0; i < string.length(); i++) {
279 if (i % 3 == 2) {
280 if (string.charAt(i) != ':')
281 throw new JiBXParseException("invalid byte array", string);
282 continue;
283 }
284 nocolons[j++] = string.charAt(i);
285 }
286 try {
287 return ByteArrayEncoder.decode(new String(nocolons));
288 } catch (IllegalArgumentException e) {
289 throw new JiBXParseException("invalid byte array", string, e);
290 }
291 }
292
293 /**
294 * Serialize a byte array using MAC address notation (colon-separated). Each byte will have two digits.
295 *
296 * @see #deserializeByteArrayWithColons
297 */
298 public static String serializeByteArrayWithColons(byte[] array) {
299 char[] colons = new char[array.length * 3 - 1];
300 String nocolons = ByteArrayEncoder.encode(array);
301 int j = 0;
302 for (int i = 0; i < nocolons.length(); i += 2) {
303 colons[j++] = nocolons.charAt(i);
304 colons[j++] = nocolons.charAt(i + 1);
305 if (j < colons.length)
306 colons[j++] = ':';
307 }
308 return new String(colons);
309 }
310
311 /**
312 * Deserialize an {@link Inet4Address}. No DNS name resolution of any kind is performed.
313 *
314 * @see #serializeInet4Address
315 */
316 public static Inet4Address deserializeInet4Address(String string) throws JiBXParseException {
317 try {
318 return IPv4Util.fromString(string);
319 } catch (IllegalArgumentException e) {
320 throw new JiBXParseException("invalid IPv4 address", string);
321 }
322 }
323
324 /**
325 * Serialize an {@link Inet4Address}.
326 *
327 * @see #deserializeInet4Address
328 */
329 public static String serializeInet4Address(Inet4Address addr) {
330 return IPv4Util.toString(addr);
331 }
332
333 /**
334 * Deserialize a {@link SimpleDateFormat}.
335 *
336 * @see #serializeSimpleDateFormat
337 */
338 public static SimpleDateFormat deserializeSimpleDateFormat(String string) throws JiBXParseException {
339 try {
340 return new SimpleDateFormat(string);
341 } catch (IllegalArgumentException e) {
342 throw new JiBXParseException("invalid date format", string, e);
343 }
344 }
345
346 /**
347 * Serialize a {@link SimpleDateFormat}.
348 *
349 * @see #deserializeSimpleDateFormat
350 */
351 public static String serializeSimpleDateFormat(SimpleDateFormat simpleDateFormat) {
352 return simpleDateFormat.toPattern();
353 }
354
355 /**
356 * Deserialize a {@link Date}. This method can be used as a deserialize support method.
357 *
358 * @param date date string to parse
359 * @param format format for {@link SimpleDateFormat}.
360 * @see #serializeDate
361 */
362 public static Date deserializeDate(String date, String format) throws JiBXParseException {
363 try {
364 return deserializeSimpleDateFormat(format).parse(date);
365 } catch (java.text.ParseException e) {
366 throw new JiBXParseException("invalid date", date, e);
367 }
368 }
369
370 /**
371 * Serialize a {@link Date}. This method can be used as a serialize support method.
372 *
373 * @param date date to serialize
374 * @param format format for {@link SimpleDateFormat}.
375 * @see #deserializeDate
376 */
377 public static String serializeDate(Date date, String format) throws JiBXParseException {
378 return deserializeSimpleDateFormat(format).format(date);
379 }
380
381 /**
382 * Deserialize a {@link Date} in the format supported by {@link DateEncoder}.
383 *
384 * @see DateEncoder
385 */
386 public static Date deserializeDate(String date) throws JiBXParseException {
387 try {
388 return DateEncoder.decode(date);
389 } catch (IllegalArgumentException e) {
390 throw new JiBXParseException("invalid date", date, e);
391 }
392 }
393
394 /**
395 * Serialize a {@link Date} in the format supported by {@link DateEncoder}.
396 *
397 * @see DateEncoder
398 */
399 public static String serializeDate(Date date) throws JiBXParseException {
400 return DateEncoder.encode(date);
401 }
402
403 /**
404 * Deserialize a {@link Date} in XSD dateTime format.
405 *
406 * @see #serializeXSDDateTime
407 * @see <a href="http://www.w3.org/TR/xmlschema-2/#dateTime">XSD dateTime datatype</a>
408 */
409 public static Date deserializeXSDDateTime(String date) throws JiBXParseException {
410 try {
411 return deserializeSimpleDateFormat(XSD_DATE_FORMAT).parse(date);
412 } catch (java.text.ParseException e) {
413 throw new JiBXParseException("invalid date", date, e);
414 }
415 }
416
417 /**
418 * Serialize a {@link Date} to XSD dateTime format.
419 *
420 * @see #deserializeXSDDateTime
421 * @see <a href="http://www.w3.org/TR/xmlschema-2/#dateTime">XSD dateTime datatype</a>
422 */
423 public static String serializeXSDDateTime(Date date) throws JiBXParseException {
424 return deserializeSimpleDateFormat(XSD_DATE_FORMAT).format(date);
425 }
426
427 /**
428 * Deserialize a {@link Pattern}.
429 *
430 * <p>
431 * Note: there is no need for a custom serializer, as {@link Pattern#toString} already does the right thing.
432 */
433 public static Pattern deserializePattern(String string) throws JiBXParseException {
434 try {
435 return Pattern.compile(string);
436 } catch (PatternSyntaxException e) {
437 throw new JiBXParseException("invalid regular expression", string, e);
438 }
439 }
440
441 /**
442 * Serialize an {@link Pattern}.
443 *
444 * @deprecated This method is does the same thing as JiBX's default serialization via {@link Object#toString()}
445 */
446 @Deprecated
447 public static String serializePattern(Pattern pattern) {
448 return pattern.toString();
449 }
450
451 /**
452 * JiBX {@link String} deserializer that normalizes a string as is required by the {@code xsd:token} XSD type.
453 * This removes leading and trailing whitespace, and collapses all interior whitespace
454 * down to a single space character.
455 *
456 * @throws NullPointerException if {@code string} is null
457 */
458 public static String normalize(String string) {
459 return string.trim().replaceAll("\\s+", " ");
460 }
461
462 /**
463 * JiBX {@link String} deserializer support method that verifies that the input string matches the
464 * given regular expression. This method can be invoked by custom deserializers that supply the
465 * regular expression to it.
466 *
467 * @throws NullPointerException if {@code string} of {@code regex} is null
468 * @throws JiBXParseException if {@code string} does not match {@code regex}
469 * @throws java.util.regex.PatternSyntaxException
470 * if {@code regex} is not a valid regular expression
471 */
472 public static String deserializeMatching(String regex, String string) throws JiBXParseException {
473 if (!string.matches(regex))
474 throw new JiBXParseException("input does not match pattern \"" + regex + "\"", string);
475 return string;
476 }
477
478 /**
479 * Boolean parser that allows "yes" and "no" as well as the usual "true", "false", "0", "1".
480 * Comparisons are case-insensitive.
481 *
482 * @throws JiBXParseException if the value is not recognizable as a boolean
483 *
484 * @see #deserializeBooleanStrictly
485 */
486 public static boolean deserializeBoolean(String string) throws JiBXParseException {
487 for (String s : BOOLEAN_TRUES) {
488 if (string.equalsIgnoreCase(s))
489 return true;
490 }
491 for (String s : BOOLEAN_FALSES) {
492 if (string.equalsIgnoreCase(s))
493 return false;
494 }
495 throw new JiBXParseException("invalid Boolean value", string);
496 }
497
498 /**
499 * Deserialize a boolean strictly, only allowing the values {@code true} or {@code false}.
500 *
501 * @see #deserializeBoolean
502 */
503 public static boolean deserializeBooleanStrictly(String string) throws JiBXParseException {
504 if ("true".equals(string))
505 return true;
506 if ("false".equals(string))
507 return false;
508 throw new JiBXParseException("invalid boolean value; must be `true' or `false'", string);
509 }
510
511 /**
512 * Deserialize an array of integers separated by commas and/or whitespace.
513 *
514 * @see #serializeIntArray
515 */
516 public static int[] deserializeIntArray(String string) throws JiBXParseException {
517 String[] ints = string.trim().split("(\\s*,\\s*|\\s+)");
518 if (ints.length == 0 || ints[0].length() == 0)
519 return new int[0];
520 int[] array = new int[ints.length];
521 try {
522 for (int i = 0; i < array.length; i++)
523 array[i] = Integer.parseInt(ints[i]);
524 } catch (NumberFormatException e) {
525 throw new JiBXParseException("invalid integer list", string, e);
526 }
527 return array;
528 }
529
530 /**
531 * Serialize an array of integers. Example: "1, 2, 3".
532 *
533 * @see #deserializeIntArray
534 */
535 public static String serializeIntArray(int[] array) throws JiBXParseException {
536 StringBuilder buf = new StringBuilder(array.length * 2);
537 for (int i = 0; i < array.length; i++) {
538 if (i > 0)
539 buf.append(", ");
540 buf.append(array[i]);
541 }
542 return buf.toString();
543 }
544 }
545