001
002/*
003 * Copyright (C) 2010 Archie L. Cobbs. All rights reserved.
004 *
005 * $Id$
006 */
007
008package org.dellroad.jibxbindings;
009
010import java.net.URI;
011import java.net.URISyntaxException;
012import java.text.ParseException;
013import java.text.SimpleDateFormat;
014import java.util.Arrays;
015import java.util.Calendar;
016import java.util.Collections;
017import java.util.Date;
018import java.util.GregorianCalendar;
019import java.util.List;
020import java.util.Locale;
021import java.util.TimeZone;
022import java.util.regex.Matcher;
023import java.util.regex.Pattern;
024
025import org.jibx.runtime.JiBXParseException;
026
027/**
028 * JiBX parsing utility methods.
029 */
030public final class ParseUtil {
031
032    private static final String[] BOOLEAN_TRUES = { "1", "true", "yes" };
033    private static final String[] BOOLEAN_FALSES = { "0", "false", "no" };
034    private static final Pattern RFC3339_PATTERN
035      = Pattern.compile("(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?)(Z|([-+]\\d{2}:\\d{2}))");
036    private static final String RFC3339_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
037    private static final String RFC5322_FORMAT = "EEE, dd MMM yyyy HH:mm:ss Z";
038    private static final String XSD_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ";
039
040    private ParseUtil() {
041    }
042
043    /**
044     * Deserialize an {@link URI}.
045     *
046     * @see #serializeURI
047     */
048    public static URI deserializeURI(String string) throws JiBXParseException {
049
050        // Accept space characters even though they are "strongly discouraged"; see http://www.datypic.com/sc/xsd/t-xsd_anyURI.html
051        try {
052            return new URI(string.replaceAll(" ", "%20"));
053        } catch (URISyntaxException e) {
054            throw new JiBXParseException("invalid URI", string, e);
055        }
056    }
057
058    /**
059     * Serialize an {@link URI}.
060     *
061     * @see #deserializeURI
062     */
063    public static String serializeURI(URI uri) {
064        return uri.toString();
065    }
066
067    /**
068     * JiBX {@link String} deserializer that normalizes a string as is required by the {@code xsd:token} XSD type.
069     * This removes leading and trailing whitespace, and collapses all interior whitespace
070     * down to a single space character.
071     *
072     * @throws NullPointerException if {@code string} is null
073     */
074    public static String normalize(String string) {
075        return string.trim().replaceAll("\\s+", " ");
076    }
077
078    /**
079     * JiBX {@link String} deserializer support method that verifies that the input string matches the
080     * given regular expression. This method can be invoked by custom deserializers that supply the
081     * regular expression to it.
082     *
083     * @throws NullPointerException if {@code string} of {@code regex} is null
084     * @throws JiBXParseException if {@code string} does not match {@code regex}
085     * @throws java.util.regex.PatternSyntaxException if {@code regex} is not a valid regular expression
086     */
087    public static String deserializeMatching(String regex, String string) throws JiBXParseException {
088        if (!string.matches(regex))
089            throw new JiBXParseException("input does not match pattern \"" + regex + "\"", string);
090        return string;
091    }
092
093    /**
094     * Boolean parser that allows "yes" and "no" as well as the usual "true", "false", "0", "1".
095     *
096     * @throws JiBXParseException if the value is not recognizable as a boolean
097     */
098    public static boolean deserializeBoolean(String string) throws JiBXParseException {
099        for (String s : BOOLEAN_TRUES) {
100            if (string.equalsIgnoreCase(s))
101                return true;
102        }
103        for (String s : BOOLEAN_FALSES) {
104            if (string.equalsIgnoreCase(s))
105                return false;
106        }
107        throw new JiBXParseException("invalid Boolean value", string);
108    }
109
110    /**
111     * Deserialize a timestamp in RFC 3339 format.
112     *
113     * @see #serializeRFC3339Timestamp
114     * @see <a href="http://tools.ietf.org/html/rfc3339">RFC 3339</a>
115     */
116    public static Date deserializeRFC3339Timestamp(String string) throws JiBXParseException {
117        Matcher matcher = RFC3339_PATTERN.matcher(string);
118        if (!matcher.matches())
119            throw new JiBXParseException("incorrectly formatted timestamp", string);
120        TimeZone timeZone = TimeZone.getTimeZone("GMT" + (matcher.group(4) != null ? matcher.group(4) : ""));
121        Calendar cal = new GregorianCalendar(timeZone, Locale.US);
122        String fmt = "y-M-d'T'H:m:s" + (matcher.group(2) != null ?  ".S" : "");
123        SimpleDateFormat dateFormat = new SimpleDateFormat(fmt);
124        dateFormat.setLenient(false);
125        dateFormat.setCalendar(cal);
126        try {
127            return dateFormat.parse(matcher.group(1));
128        } catch (ParseException e) {
129            throw new JiBXParseException("incorrectly formatted timestamp", string, e);
130        }
131    }
132
133    /**
134     * Serialize a timestamp in RFC 3339 format.
135     *
136     * @see #deserializeRFC3339Timestamp
137     * @see <a href="http://tools.ietf.org/html/rfc3339">RFC 3339</a>
138     */
139    public static String serializeRFC3339Timestamp(Date timestamp) {
140        if (timestamp == null)
141            return null;
142        return ParseUtil.getDateFormat(RFC3339_FORMAT, timestamp).format(timestamp);
143    }
144
145    /**
146     * Deserialize a timestamp in RFC 5322 format. Treat an empty string as null.
147     *
148     * @see #serializeRFC5322Timestamp
149     * @see <a href="http://tools.ietf.org/html/rfc5322">RFC 5322</a>
150     */
151    public static Date deserializeRFC5322Timestamp(String string) throws JiBXParseException {
152        if (string.length() == 0)
153            return null;
154        try {
155            return ParseUtil.getDateFormat(RFC5322_FORMAT, null).parse(string);
156        } catch (ParseException e) {
157            throw new JiBXParseException("incorrectly formatted date string", string, e);
158        }
159    }
160
161    /**
162     * Serialize a timestamp in RFC 5322 format.
163     *
164     * @see #deserializeRFC5322Timestamp
165     * @see <a href="http://tools.ietf.org/html/rfc5322">RFC 5322</a>
166     */
167    public static String serializeRFC5322Timestamp(Date date) {
168        if (date == null)
169            return null;
170        return ParseUtil.getDateFormat(RFC5322_FORMAT, date).format(date);
171    }
172
173    /**
174     * Deserialize a {@link Date} in XSD dateTime format.
175     *
176     * @see #serializeXSDDateTime
177     * @see <a href="http://www.w3.org/TR/xmlschema-2/#dateTime">XSD dateTime datatype</a>
178     */
179    public static Date deserializeXSDDateTime(String date) throws JiBXParseException {
180        return ParseUtil.deserializeRFC3339Timestamp(date);
181    }
182
183    /**
184     * Serialize a {@link Date} to XSD dateTime format.
185     *
186     * @see #deserializeXSDDateTime
187     * @see <a href="http://www.w3.org/TR/xmlschema-2/#dateTime">XSD dateTime datatype</a>
188     */
189    public static String serializeXSDDateTime(Date date) throws JiBXParseException {
190        return ParseUtil.serializeRFC3339Timestamp(date);
191    }
192
193    private static SimpleDateFormat getDateFormat(String format, Date date) {
194        TimeZone gmt = TimeZone.getTimeZone("GMT");
195        GregorianCalendar cal = new GregorianCalendar(gmt, Locale.US);
196        if (date != null)
197            cal.setTime(date);
198        SimpleDateFormat dateFormat = new SimpleDateFormat(format);
199        dateFormat.setLenient(false);
200        dateFormat.setCalendar(cal);
201        return dateFormat;
202    }
203
204    /**
205     * Deserialize an integer, but treat empty string as zero.
206     */
207    public static int deserializeInt(String string) throws JiBXParseException {
208        if (string.length() == 0)
209            return 0;
210        try {
211            return Integer.parseInt(string);
212        } catch (NumberFormatException e) {
213            throw new JiBXParseException("can't parse integer value `" + string + "'", string, e);
214        }
215    }
216
217    /**
218     * Deserialize a double, but treat empty string as NaN.
219     */
220    public static double deserializeDouble(String string) throws JiBXParseException {
221        if (string.length() == 0)
222            return Double.NaN;
223        try {
224            return Double.parseDouble(string);
225        } catch (NumberFormatException e) {
226            throw new JiBXParseException("can't parse double value `" + string + "'", string, e);
227        }
228    }
229
230    /**
231     * Deserialize a {@link Enum} using the enum name, but treat empty string as null.
232     */
233    public static <T extends Enum<T>> T deserializeEnum(String string, T[] values) throws JiBXParseException {
234        if (string.length() == 0)
235            return null;
236        for (T value : values) {
237            if (value.name().equals(string))
238                return value;
239        }
240        throw new JiBXParseException("no match found for enum value `" + string + "'", string);
241    }
242
243    /**
244     * Deserialize an {@link Enum}. Either the {@link Enum#name name()} or {@linkplain Enum#toString string value}
245     * may match, and treat an empty string like null.
246     */
247    public static <T extends Enum<T>> T deserializeEnumOrNull(String string, Class<T> type) throws JiBXParseException {
248        if (string == null || string.length() == 0)
249            return null;
250        for (T value : ParseUtil.getValues(type)) {
251            if (value.name().equals(string) || value.toString().equals(string))
252                return value;
253        }
254        throw new JiBXParseException("no match found for " + type.getSimpleName() + " enum value `" + string + "'", string);
255    }
256
257    /**
258     * Serialize an {@link Enum} using {@link Enum#toString}.
259     */
260    public static <T extends Enum<T>> String serializeEnumToString(T value) throws JiBXParseException {
261        return value != null ? value.toString() : null;
262    }
263
264    /**
265     * Get all instances of the given {@link Enum} class in a list in their natural ordering.
266     *
267     * @return unmodifiable list of enum values
268     */
269    @SuppressWarnings("unchecked")
270    public static <T extends Enum<T>> List<T> getValues(Class<T> enumClass) {
271
272        // Generate ClassCastException if type is not an enum type
273        enumClass.asSubclass(Enum.class);
274
275        // Get values
276        Object array;
277        try {
278            array = enumClass.getMethod("values").invoke(null);
279        } catch (Exception e) {
280            throw new RuntimeException("unexpected exception", e);
281        }
282        return Collections.unmodifiableList(Arrays.asList((T[])array));
283    }
284
285    /**
286     * Deserialize an integer, but treat empty string as zero.
287     */
288    public static int deserializeIntOrZero(String string) throws JiBXParseException {
289        if (string == null || string.length() == 0)
290            return 0;
291        try {
292            return Integer.parseInt(string);
293        } catch (NumberFormatException e) {
294            throw new JiBXParseException("can't parse integer value `" + string + "'", string, e);
295        }
296    }
297
298    /**
299     * Deserialize a double, but treat empty string as NaN.
300     */
301    public static double deserializeDoubleOrNaN(String string) throws JiBXParseException {
302        if (string == null || string.length() == 0)
303            return Double.NaN;
304        try {
305            return Double.parseDouble(string);
306        } catch (NumberFormatException e) {
307            throw new JiBXParseException("can't parse double value `" + string + "'", string, e);
308        }
309    }
310
311    /**
312     * Deserialize a list of strings. The strings are separated by whitespace.
313     *
314     * @see #serializeStringList
315     */
316    public static List<String> deserializeStringList(String string) throws JiBXParseException {
317        string = string.trim();
318        if (string.length() == 0)
319            return Collections.<String>emptyList();
320        return Arrays.asList(string.split("\\s+"));
321    }
322
323    /**
324     * Serialize a list of strings. The strings are separated by space characters.
325     *
326     * @see #deserializeStringList
327     */
328    public static String serializeStringList(List<String> list) {
329        if (list == null)
330            return null;
331        StringBuilder buf = new StringBuilder();
332        for (String string : list) {
333            if (buf.length() > 0)
334                buf.append(' ');
335            buf.append(string);
336        }
337        return buf.toString();
338    }
339
340    /**
341     * Deserialize an array of {@code double} values.
342     *
343     * @see #serializeDoubleArray
344     */
345    public static double[] deserializeDoubleArray(String string) throws JiBXParseException {
346        List<String> strings = ParseUtil.deserializeStringList(string);
347        double[] values = new double[strings.size()];
348        int i = 0;
349        for (String doubleString : strings) {
350            try {
351                values[i++] = Double.parseDouble(doubleString);
352            } catch (NumberFormatException e) {
353                throw new JiBXParseException("invalid double value", doubleString, e);
354            }
355        }
356        return values;
357    }
358
359    /**
360     * Serialize an array of {@code double} values.
361     *
362     * @see #deserializeDoubleArray
363     */
364    public static String serializeDoubleArray(double[] values) {
365        if (values == null)
366            return null;
367        StringBuilder buf = new StringBuilder();
368        for (int i = 0; i < values.length; i++) {
369            if (i > 0)
370                buf.append(' ');
371            buf.append(values[i]);
372        }
373        return buf.toString();
374    }
375}
376