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