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