http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/internal/StringUtils.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/internal/StringUtils.java b/juneau-core/src/main/java/org/apache/juneau/internal/StringUtils.java new file mode 100644 index 0000000..6ed4d55 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/internal/StringUtils.java @@ -0,0 +1,979 @@ +/*************************************************************************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + ***************************************************************************************************************************/ +package org.apache.juneau.internal; + +import static org.apache.juneau.internal.ThrowableUtils.*; + +import java.io.*; +import java.math.*; +import java.util.*; +import java.util.concurrent.atomic.*; +import java.util.regex.*; + +import javax.xml.bind.*; + +import org.apache.juneau.parser.*; + +/** + * Reusable string utility methods. + */ +public final class StringUtils { + + private static final AsciiSet numberChars = new AsciiSet("-xX.+-#pP0123456789abcdefABCDEF"); + private static final AsciiSet firstNumberChars = new AsciiSet("+-.#0123456789"); + private static final AsciiSet octChars = new AsciiSet("01234567"); + private static final AsciiSet decChars = new AsciiSet("0123456789"); + private static final AsciiSet hexChars = new AsciiSet("0123456789abcdefABCDEF"); + + // Maps 6-bit nibbles to BASE64 characters. + private static final char[] base64m1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray(); + + // Maps BASE64 characters to 6-bit nibbles. + private static final byte[] base64m2 = new byte[128]; + static { + for (int i = 0; i < 64; i++) + base64m2[base64m1[i]] = (byte)i; + } + + /** + * Parses a number from the specified reader stream. + * + * @param r The reader to parse the string from. + * @param type The number type to created. <br> + * Can be any of the following: + * <ul> + * <li> Integer + * <li> Double + * <li> Float + * <li> Long + * <li> Short + * <li> Byte + * <li> BigInteger + * <li> BigDecimal + * </ul> + * If <jk>null</jk>, uses the best guess. + * @throws IOException If a problem occurred trying to read from the reader. + * @return The parsed number. + * @throws Exception + */ + public static Number parseNumber(ParserReader r, Class<? extends Number> type) throws Exception { + return parseNumber(parseNumberString(r), type); + } + + /** + * Reads a numeric string from the specified reader. + * + * @param r The reader to read form. + * @return The parsed number string. + * @throws Exception + */ + public static String parseNumberString(ParserReader r) throws Exception { + r.mark(); + int c = 0; + while (true) { + c = r.read(); + if (c == -1) + break; + if (! numberChars.contains((char)c)) { + r.unread(); + break; + } + } + return r.getMarked(); + } + + /** + * Parses a number from the specified string. + * + * @param s The string to parse the number from. + * @param type The number type to created. <br> + * Can be any of the following: + * <ul> + * <li> Integer + * <li> Double + * <li> Float + * <li> Long + * <li> Short + * <li> Byte + * <li> BigInteger + * <li> BigDecimal + * </ul> + * If <jk>null</jk>, uses the best guess. + * @return The parsed number. + * @throws ParseException + */ + public static Number parseNumber(String s, Class<? extends Number> type) throws ParseException { + + if (s.isEmpty()) + s = "0"; + if (type == null) + type = Number.class; + + try { + // Determine the data type if it wasn't specified. + boolean isAutoDetect = (type == Number.class); + boolean isDecimal = false; + if (isAutoDetect) { + // If we're auto-detecting, then we use either an Integer, Long, or Double depending on how + // long the string is. + // An integer range is -2,147,483,648 to 2,147,483,647 + // An long range is -9,223,372,036,854,775,808 to +9,223,372,036,854,775,807 + isDecimal = isDecimal(s); + if (isDecimal) { + if (s.length() > 20) + type = Double.class; + else if (s.length() >= 10) + type = Long.class; + else + type = Integer.class; + } + else if (isFloat(s)) + type = Double.class; + else + throw new NumberFormatException(s); + } + + if (type == Double.class || type == Double.TYPE) { + Double d = Double.valueOf(s); + if (isAutoDetect && (! isDecimal) && d >= -Float.MAX_VALUE && d <= Float.MAX_VALUE) + return d.floatValue(); + return d; + } + if (type == Float.class || type == Float.TYPE) + return Float.valueOf(s); + if (type == BigDecimal.class) + return new BigDecimal(s); + if (type == Long.class || type == Long.TYPE || type == AtomicLong.class) { + try { + Long l = Long.decode(s); + if (type == AtomicLong.class) + return new AtomicLong(l); + if (isAutoDetect && l >= Integer.MIN_VALUE && l <= Integer.MAX_VALUE) { + // This occurs if the string is 10 characters long but is still a valid integer value. + return l.intValue(); + } + return l; + } catch (NumberFormatException e) { + if (isAutoDetect) { + // This occurs if the string is 20 characters long but still falls outside the range of a valid long. + return Double.valueOf(s); + } + throw e; + } + } + if (type == Integer.class || type == Integer.TYPE) + return Integer.decode(s); + if (type == Short.class || type == Short.TYPE) + return Short.decode(s); + if (type == Byte.class || type == Byte.TYPE) + return Byte.decode(s); + if (type == BigInteger.class) + return new BigInteger(s); + if (type == AtomicInteger.class) + return new AtomicInteger(Integer.decode(s)); + throw new ParseException("Unsupported Number type: {0}", type.getName()); + } catch (NumberFormatException e) { + throw new ParseException("Could not convert string ''{0}'' to class ''{1}''", s, type.getName()).initCause(e); + } + } + + private final static Pattern fpRegex = Pattern.compile( + "[+-]?(NaN|Infinity|((((\\p{Digit}+)(\\.)?((\\p{Digit}+)?)([eE][+-]?(\\p{Digit}+))?)|(\\.((\\p{Digit}+))([eE][+-]?(\\p{Digit}+))?)|(((0[xX](\\p{XDigit}+)(\\.)?)|(0[xX](\\p{XDigit}+)?(\\.)(\\p{XDigit}+)))[pP][+-]?(\\p{Digit}+)))[fFdD]?))[\\x00-\\x20]*" + ); + + /** + * Returns <jk>true</jk> if this string can be parsed by {@link #parseNumber(String, Class)}. + * + * @param s The string to check. + * @return <jk>true</jk> if this string can be parsed without causing an exception. + */ + public static boolean isNumeric(String s) { + if (s == null || s.isEmpty()) + return false; + if (! isFirstNumberChar(s.charAt(0))) + return false; + return isDecimal(s) || isFloat(s); + } + + /** + * Returns <jk>true</jk> if the specified character is a valid first character for a number. + * + * @param c The character to test. + * @return <jk>true</jk> if the specified character is a valid first character for a number. + */ + public static boolean isFirstNumberChar(char c) { + return firstNumberChars.contains(c); + } + + /** + * Returns <jk>true</jk> if the specified string is a floating point number. + * + * @param s The string to check. + * @return <jk>true</jk> if the specified string is a floating point number. + */ + public static boolean isFloat(String s) { + if (s == null || s.isEmpty()) + return false; + if (! firstNumberChars.contains(s.charAt(0))) + return (s.equals("NaN") || s.equals("Infinity")); + int i = 0; + int length = s.length(); + char c = s.charAt(0); + if (c == '+' || c == '-') + i++; + if (i == length) + return false; + c = s.charAt(i++); + if (c == '.' || decChars.contains(c)) { + return fpRegex.matcher(s).matches(); + } + return false; + } + + /** + * Returns <jk>true</jk> if the specified string is numeric. + * + * @param s The string to check. + * @return <jk>true</jk> if the specified string is numeric. + */ + public static boolean isDecimal(String s) { + if (s == null || s.isEmpty()) + return false; + if (! firstNumberChars.contains(s.charAt(0))) + return false; + int i = 0; + int length = s.length(); + char c = s.charAt(0); + boolean isPrefixed = false; + if (c == '+' || c == '-') { + isPrefixed = true; + i++; + } + if (i == length) + return false; + c = s.charAt(i++); + if (c == '0' && length > (isPrefixed ? 2 : 1)) { + c = s.charAt(i++); + if (c == 'x' || c == 'X') { + for (int j = i; j < length; j++) { + if (! hexChars.contains(s.charAt(j))) + return false; + } + } else if (octChars.contains(c)) { + for (int j = i; j < length; j++) + if (! octChars.contains(s.charAt(j))) + return false; + } else { + return false; + } + } else if (c == '#') { + for (int j = i; j < length; j++) { + if (! hexChars.contains(s.charAt(j))) + return false; + } + } else if (decChars.contains(c)) { + for (int j = i; j < length; j++) + if (! decChars.contains(s.charAt(j))) + return false; + } else { + return false; + } + return true; + } + + /** + * Convenience method for getting a stack trace as a string. + * + * @param t The throwable to get the stack trace from. + * @return The same content that would normally be rendered via <code>t.printStackTrace()</code> + */ + public static String getStackTrace(Throwable t) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + t.printStackTrace(pw); + pw.flush(); + pw.close(); + return sw.toString(); + } + + /** + * Join the specified tokens into a delimited string. + * + * @param tokens The tokens to join. + * @param separator The delimiter. + * @return The delimited string. If <code>tokens</code> is <jk>null</jk>, returns <jk>null</jk>. + */ + public static String join(Object[] tokens, String separator) { + if (tokens == null) + return null; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < tokens.length; i++) { + if (i > 0) + sb.append(separator); + sb.append(tokens[i]); + } + return sb.toString(); + } + + /** + * Join the specified tokens into a delimited string. + * + * @param tokens The tokens to join. + * @param d The delimiter. + * @return The delimited string. If <code>tokens</code> is <jk>null</jk>, returns <jk>null</jk>. + */ + public static String join(int[] tokens, String d) { + if (tokens == null) + return null; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < tokens.length; i++) { + if (i > 0) + sb.append(d); + sb.append(tokens[i]); + } + return sb.toString(); + } + + /** + * Join the specified tokens into a delimited string. + * + * @param tokens The tokens to join. + * @param d The delimiter. + * @return The delimited string. If <code>tokens</code> is <jk>null</jk>, returns <jk>null</jk>. + */ + public static String join(Collection<?> tokens, String d) { + if (tokens == null) + return null; + return join(tokens, d, new StringBuilder()).toString(); + } + + /** + * Joins the specified tokens into a delimited string and writes the output to the specified string builder. + * + * @param tokens The tokens to join. + * @param d The delimiter. + * @param sb The string builder to append the response to. + * @return The same string builder passed in as <code>sb</code>. + */ + public static StringBuilder join(Collection<?> tokens, String d, StringBuilder sb) { + if (tokens == null) + return sb; + for (Iterator<?> iter = tokens.iterator(); iter.hasNext();) { + sb.append(iter.next()); + if (iter.hasNext()) + sb.append(d); + } + return sb; + } + + /** + * Joins the specified tokens into a delimited string. + * + * @param tokens The tokens to join. + * @param d The delimiter. + * @return The delimited string. If <code>tokens</code> is <jk>null</jk>, returns <jk>null</jk>. + */ + public static String join(Object[] tokens, char d) { + if (tokens == null) + return null; + return join(tokens, d, new StringBuilder()).toString(); + } + + /** + * Join the specified tokens into a delimited string and writes the output to the specified string builder. + * + * @param tokens The tokens to join. + * @param d The delimiter. + * @param sb The string builder to append the response to. + * @return The same string builder passed in as <code>sb</code>. + */ + public static StringBuilder join(Object[] tokens, char d, StringBuilder sb) { + if (tokens == null) + return sb; + for (int i = 0; i < tokens.length; i++) { + if (i > 0) + sb.append(d); + sb.append(tokens[i]); + } + return sb; + } + + /** + * Join the specified tokens into a delimited string. + * + * @param tokens The tokens to join. + * @param d The delimiter. + * @return The delimited string. If <code>tokens</code> is <jk>null</jk>, returns <jk>null</jk>. + */ + public static String join(int[] tokens, char d) { + if (tokens == null) + return null; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < tokens.length; i++) { + if (i > 0) + sb.append(d); + sb.append(tokens[i]); + } + return sb.toString(); + } + + /** + * Join the specified tokens into a delimited string. + * + * @param tokens The tokens to join. + * @param d The delimiter. + * @return The delimited string. If <code>tokens</code> is <jk>null</jk>, returns <jk>null</jk>. + */ + public static String join(Collection<?> tokens, char d) { + if (tokens == null) + return null; + StringBuilder sb = new StringBuilder(); + for (Iterator<?> iter = tokens.iterator(); iter.hasNext();) { + sb.append(iter.next()); + if (iter.hasNext()) + sb.append(d); + } + return sb.toString(); + } + + /** + * Splits a character-delimited string into a string array. + * Does not split on escaped-delimiters (e.g. "\,"); + * Resulting tokens are trimmed of whitespace. + * NOTE: This behavior is different than the Jakarta equivalent. + * split("a,b,c",',') -> {"a","b","c"} + * split("a, b ,c ",',') -> {"a","b","c"} + * split("a,,c",',') -> {"a","","c"} + * split(",,",',') -> {"","",""} + * split("",',') -> {} + * split(null,',') -> null + * split("a,b\,c,d", ',', false) -> {"a","b\,c","d"} + * split("a,b\\,c,d", ',', false) -> {"a","b\","c","d"} + * split("a,b\,c,d", ',', true) -> {"a","b,c","d"} + * + * @param s The string to split. Can be <jk>null</jk>. + * @param c The character to split on. + * @return The tokens. + */ + public static String[] split(String s, char c) { + + char[] unEscapeChars = new char[]{'\\', c}; + + if (s == null) + return null; + if (isEmpty(s)) + return new String[0]; + + List<String> l = new LinkedList<String>(); + char[] sArray = s.toCharArray(); + int x1 = 0, escapeCount = 0; + for (int i = 0; i < sArray.length; i++) { + if (sArray[i] == '\\') escapeCount++; + else if (sArray[i]==c && escapeCount % 2 == 0) { + String s2 = new String(sArray, x1, i-x1); + String s3 = unEscapeChars(s2, unEscapeChars); + l.add(s3.trim()); + x1 = i+1; + } + if (sArray[i] != '\\') escapeCount = 0; + } + String s2 = new String(sArray, x1, sArray.length-x1); + String s3 = unEscapeChars(s2, unEscapeChars); + l.add(s3.trim()); + + return l.toArray(new String[l.size()]); + } + + /** + * Returns <jk>true</jk> if specified string is <jk>null</jk> or empty. + * + * @param s The string to check. + * @return <jk>true</jk> if specified string is <jk>null</jk> or empty. + */ + public static boolean isEmpty(String s) { + return s == null || s.isEmpty(); + } + + /** + * Returns <jk>true</jk> if specified string is <jk>null</jk> or it's {@link #toString()} method returns an empty string. + * + * @param s The string to check. + * @return <jk>true</jk> if specified string is <jk>null</jk> or it's {@link #toString()} method returns an empty string. + */ + public static boolean isEmpty(Object s) { + return s == null || s.toString().isEmpty(); + } + + /** + * Returns <jk>null</jk> if the specified string is <jk>null</jk> or empty. + * + * @param s The string to check. + * @return <jk>null</jk> if the specified string is <jk>null</jk> or empty, or the same string if not. + */ + public static String nullIfEmpty(String s) { + if (s == null || s.isEmpty()) + return null; + return s; + } + + /** + * Removes escape characters (\) from the specified characters. + * + * @param s The string to remove escape characters from. + * @param toEscape The characters escaped. + * @return A new string if characters were removed, or the same string if not or if the input was <jk>null</jk>. + */ + public static String unEscapeChars(String s, char[] toEscape) { + return unEscapeChars(s, toEscape, '\\'); + } + + /** + * Removes escape characters (specified by escapeChar) from the specified characters. + * + * @param s The string to remove escape characters from. + * @param toEscape The characters escaped. + * @param escapeChar The escape character. + * @return A new string if characters were removed, or the same string if not or if the input was <jk>null</jk>. + */ + public static String unEscapeChars(String s, char[] toEscape, char escapeChar) { + if (s == null) return null; + if (s.length() == 0 || toEscape == null || toEscape.length == 0 || escapeChar == 0) return s; + StringBuffer sb = new StringBuffer(s.length()); + char[] sArray = s.toCharArray(); + for (int i = 0; i < sArray.length; i++) { + char c = sArray[i]; + + if (c == escapeChar) { + if (i+1 != sArray.length) { + char c2 = sArray[i+1]; + boolean isOneOf = false; + for (int j = 0; j < toEscape.length && ! isOneOf; j++) + isOneOf = (c2 == toEscape[j]); + if (isOneOf) { + i++; + } else if (c2 == escapeChar) { + sb.append(escapeChar); + i++; + } + } + } + sb.append(sArray[i]); + } + return sb.toString(); + } + + /** + * Debug method for rendering non-ASCII character sequences. + * + * @param s The string to decode. + * @return A string with non-ASCII characters converted to <js>"[hex]"</js> sequences. + */ + public static String decodeHex(String s) { + if (s == null) + return null; + StringBuilder sb = new StringBuilder(); + for (char c : s.toCharArray()) { + if (c < ' ' || c > '~') + sb.append("["+Integer.toHexString(c)+"]"); + else + sb.append(c); + } + return sb.toString(); + } + + /** + * An efficient method for checking if a string starts with a character. + * + * @param s The string to check. Can be <jk>null</jk>. + * @param c The character to check for. + * @return <jk>true</jk> if the specified string is not <jk>null</jk> and starts with the specified character. + */ + public static boolean startsWith(String s, char c) { + if (s != null) { + int i = s.length(); + if (i > 0) + return s.charAt(0) == c; + } + return false; + } + + /** + * An efficient method for checking if a string ends with a character. + * + * @param s The string to check. Can be <jk>null</jk>. + * @param c The character to check for. + * @return <jk>true</jk> if the specified string is not <jk>null</jk> and ends with the specified character. + */ + public static boolean endsWith(String s, char c) { + if (s != null) { + int i = s.length(); + if (i > 0) + return s.charAt(i-1) == c; + } + return false; + } + + /** + * Tests two strings for equality, but gracefully handles nulls. + * + * @param s1 String 1. + * @param s2 String 2. + * @return <jk>true</jk> if the strings are equal. + */ + public static boolean isEquals(String s1, String s2) { + if (s1 == null) + return s2 == null; + if (s2 == null) + return false; + return s1.equals(s2); + } + + /** + * Shortcut for calling <code>base64Encode(in.getBytes(<js>"UTF-8"</js>))</code> + * + * @param in The input string to convert. + * @return The string converted to BASE-64 encoding. + */ + public static String base64EncodeToString(String in) { + if (in == null) + return null; + return base64Encode(in.getBytes(IOUtils.UTF8)); + } + + /** + * BASE64-encodes the specified byte array. + * + * @param in The input byte array to convert. + * @return The byte array converted to a BASE-64 encoded string. + */ + public static String base64Encode(byte[] in) { + int outLength = (in.length * 4 + 2) / 3; // Output length without padding + char[] out = new char[((in.length + 2) / 3) * 4]; // Length includes padding. + int iIn = 0; + int iOut = 0; + while (iIn < in.length) { + int i0 = in[iIn++] & 0xff; + int i1 = iIn < in.length ? in[iIn++] & 0xff : 0; + int i2 = iIn < in.length ? in[iIn++] & 0xff : 0; + int o0 = i0 >>> 2; + int o1 = ((i0 & 3) << 4) | (i1 >>> 4); + int o2 = ((i1 & 0xf) << 2) | (i2 >>> 6); + int o3 = i2 & 0x3F; + out[iOut++] = base64m1[o0]; + out[iOut++] = base64m1[o1]; + out[iOut] = iOut < outLength ? base64m1[o2] : '='; + iOut++; + out[iOut] = iOut < outLength ? base64m1[o3] : '='; + iOut++; + } + return new String(out); + } + + /** + * Shortcut for calling <code>base64Decode(String)</code> and converting the + * result to a UTF-8 encoded string. + * + * @param in The BASE-64 encoded string to decode. + * @return The decoded string. + */ + public static String base64DecodeToString(String in) { + byte[] b = base64Decode(in); + if (b == null) + return null; + return new String(b, IOUtils.UTF8); + } + + /** + * BASE64-decodes the specified string. + * + * @param in The BASE-64 encoded string. + * @return The decoded byte array. + */ + public static byte[] base64Decode(String in) { + if (in == null) + return null; + + byte bIn[] = in.getBytes(IOUtils.UTF8); + + if (bIn.length % 4 != 0) + illegalArg("Invalid BASE64 string length. Must be multiple of 4."); + + // Strip out any trailing '=' filler characters. + int inLength = bIn.length; + while (inLength > 0 && bIn[inLength - 1] == '=') + inLength--; + + int outLength = (inLength * 3) / 4; + byte[] out = new byte[outLength]; + int iIn = 0; + int iOut = 0; + while (iIn < inLength) { + int i0 = bIn[iIn++]; + int i1 = bIn[iIn++]; + int i2 = iIn < inLength ? bIn[iIn++] : 'A'; + int i3 = iIn < inLength ? bIn[iIn++] : 'A'; + int b0 = base64m2[i0]; + int b1 = base64m2[i1]; + int b2 = base64m2[i2]; + int b3 = base64m2[i3]; + int o0 = (b0 << 2) | (b1 >>> 4); + int o1 = ((b1 & 0xf) << 4) | (b2 >>> 2); + int o2 = ((b2 & 3) << 6) | b3; + out[iOut++] = (byte)o0; + if (iOut < outLength) + out[iOut++] = (byte)o1; + if (iOut < outLength) + out[iOut++] = (byte)o2; + } + return out; + } + + /** + * Generated a random UUID with the specified number of characters. + * Characters are composed of lower-case ASCII letters and numbers only. + * This method conforms to the restrictions for hostnames as specified in <a href='https://tools.ietf.org/html/rfc952'>RFC 952</a> + * Since each character has 36 possible values, the square approximation formula for + * the number of generated IDs that would produce a 50% chance of collision is: + * <code>sqrt(36^N)</code>. + * Dividing this number by 10 gives you an approximation of the number of generated IDs + * needed to produce a <1% chance of collision. + * For example, given 5 characters, the number of generated IDs need to produce a <1% chance of + * collision would be: + * <code>sqrt(36^5)/10=777</code> + * + * @param numchars The number of characters in the generated UUID. + * @return A new random UUID. + */ + public static String generateUUID(int numchars) { + Random r = new Random(); + StringBuilder sb = new StringBuilder(numchars); + for (int i = 0; i < numchars; i++) { + int c = r.nextInt(36) + 97; + if (c > 'z') + c -= ('z'-'0'+1); + sb.append((char)c); + } + return sb.toString(); + } + + /** + * Same as {@link String#trim()} but prevents <code>NullPointerExceptions</code>. + * + * @param s The string to trim. + * @return The trimmed string, or <jk>null</jk> if the string was <jk>null</jk>. + */ + public static String trim(String s) { + if (s == null) + return null; + return s.trim(); + } + + /** + * Parses an ISO8601 string into a date. + * + * @param date The date string. + * @return The parsed date. + * @throws IllegalArgumentException + */ + @SuppressWarnings("nls") + public static Date parseISO8601Date(String date) throws IllegalArgumentException { + if (isEmpty(date)) + return null; + date = date.trim().replace(' ', 'T'); // Convert to 'standard' ISO8601 + if (date.indexOf(',') != -1) // Trim milliseconds + date = date.substring(0, date.indexOf(',')); + if (date.matches("\\d{4}")) + date += "-01-01T00:00:00"; + else if (date.matches("\\d{4}\\-\\d{2}")) + date += "-01T00:00:00"; + else if (date.matches("\\d{4}\\-\\d{2}\\-\\d{2}")) + date += "T00:00:00"; + else if (date.matches("\\d{4}\\-\\d{2}\\-\\d{2}T\\d{2}")) + date += ":00:00"; + else if (date.matches("\\d{4}\\-\\d{2}\\-\\d{2}T\\d{2}\\:\\d{2}")) + date += ":00"; + return DatatypeConverter.parseDateTime(date).getTime(); + } + + /** + * Simple utility for replacing variables of the form <js>"{key}"</js> with values + * in the specified map. + * <p> + * Nested variables are supported in both the input string and map values. + * <p> + * If the map does not contain the specified value, the variable is not replaced. + * <p> + * <jk>null</jk> values in the map are treated as blank strings. + * + * @param s The string containing variables to replace. + * @param m The map containing the variable values. + * @return The new string with variables replaced, or the original string if it didn't have variables in it. + */ + public static String replaceVars(String s, Map<String,Object> m) { + + if (s.indexOf('{') == -1) + return s; + + int S1 = 1; // Not in variable, looking for { + int S2 = 2; // Found {, Looking for } + + int state = S1; + boolean hasInternalVar = false; + int x = 0; + int depth = 0; + int length = s.length(); + StringBuilder out = new StringBuilder(); + for (int i = 0; i < length; i++) { + char c = s.charAt(i); + if (state == S1) { + if (c == '{') { + state = S2; + x = i; + } else { + out.append(c); + } + } else /* state == S2 */ { + if (c == '{') { + depth++; + hasInternalVar = true; + } else if (c == '}') { + if (depth > 0) { + depth--; + } else { + String key = s.substring(x+1, i); + key = (hasInternalVar ? replaceVars(key, m) : key); + hasInternalVar = false; + if (! m.containsKey(key)) + out.append('{').append(key).append('}'); + else { + Object val = m.get(key); + if (val == null) + val = ""; + String v = val.toString(); + // If the replacement also contains variables, replace them now. + if (v.indexOf('{') != -1) + v = replaceVars(v, m); + out.append(v); + } + state = 1; + } + } + } + } + return out.toString(); + } + + /** + * Returns <jk>true</jk> if the specified path string is prefixed with the specified prefix. + * <p> + * Examples: + * <p class='bcode'> + * pathStartsWith(<js>"foo"</js>, <js>"foo"</js>); <jc>// true</jc> + * pathStartsWith(<js>"foo/bar"</js>, <js>"foo"</js>); <jc>// true</jc> + * pathStartsWith(<js>"foo2"</js>, <js>"foo"</js>); <jc>// false</jc> + * pathStartsWith(<js>"foo2"</js>, <js>""</js>); <jc>// false</jc> + * </p> + * + * @param path The path to check. + * @param pathPrefix The prefix. + * @return <jk>true</jk> if the specified path string is prefixed with the specified prefix. + */ + public static boolean pathStartsWith(String path, String pathPrefix) { + if (path == null || pathPrefix == null) + return false; + if (path.startsWith(pathPrefix)) + return path.length() == pathPrefix.length() || path.charAt(pathPrefix.length()) == '/'; + return false; + } + + /** + * Same as {@link #pathStartsWith(String, String)} but returns <jk>true</jk> if at least one prefix matches. + * <p> + * + * @param path The path to check. + * @param pathPrefixes The prefixes. + * @return <jk>true</jk> if the specified path string is prefixed with any of the specified prefixes. + */ + public static boolean pathStartsWith(String path, String[] pathPrefixes) { + for (String p : pathPrefixes) + if (pathStartsWith(path, p)) + return true; + return false; + } + + /** + * Replaces <js>"\\uXXXX"</js> character sequences with their unicode characters. + * + * @param s The string to replace unicode sequences in. + * @return A string with unicode sequences replaced. + */ + public static String replaceUnicodeSequences(String s) { + if (s.indexOf('\\') == -1) + return s; + Pattern p = Pattern.compile("\\\\u(\\p{XDigit}{4})"); + Matcher m = p.matcher(s); + StringBuffer sb = new StringBuffer(s.length()); + while (m.find()) { + String ch = String.valueOf((char) Integer.parseInt(m.group(1), 16)); + m.appendReplacement(sb, Matcher.quoteReplacement(ch)); + } + m.appendTail(sb); + return sb.toString(); + } + + /** + * Returns the specified field in a delimited string without splitting the string. + * <p> + * Equivalent to the following: + * <p class='bcode'> + * String in = <js>"0,1,2"</js>; + * String[] parts = in.split(<js>","</js>); + * String p1 = (parts.<jk>length</jk> > 1 ? parts[1] : <js>""</js>); + * + * @param fieldNum The field number. Zero-indexed. + * @param s The input string. + * @param delim The delimiter character. + * @return The field entry in the string, or a blank string if it doesn't exist or the string is null. + */ + public static String getField(int fieldNum, String s, char delim) { + return getField(fieldNum, s, delim, ""); + } + + /** + * Same as {@link #getField(int, String, char)} except allows you to specify the default value. + * + * @param fieldNum The field number. Zero-indexed. + * @param s The input string. + * @param delim The delimiter character. + * @param def The default value if the field does not exist. + * @return The field entry in the string, or the default value if it doesn't exist or the string is null. + */ + public static String getField(int fieldNum, String s, char delim, String def) { + if (s == null || fieldNum < 0) + return def; + int start = 0; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == delim) { + fieldNum--; + if (fieldNum == 0) + start = i+1; + } + if (fieldNum < 0) + return s.substring(start, i); + } + if (start == 0) + return def; + return s.substring(start); + } +}
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/internal/TeeOutputStream.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/internal/TeeOutputStream.java b/juneau-core/src/main/java/org/apache/juneau/internal/TeeOutputStream.java new file mode 100644 index 0000000..0dc6b7d --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/internal/TeeOutputStream.java @@ -0,0 +1,163 @@ +/*************************************************************************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + ***************************************************************************************************************************/ +package org.apache.juneau.internal; + +import java.io.*; +import java.util.*; + +/** + * Output stream that can send output to multiple output streams. + * + * @author James Bognar ([email protected]) + */ +public class TeeOutputStream extends OutputStream { + private OutputStream[] outputStreams = new OutputStream[0]; + private Map<String,OutputStream> outputStreamMap; + + /** + * Constructor. + * + * @param outputStreams The list of output streams. + */ + public TeeOutputStream(OutputStream...outputStreams) { + this.outputStreams = outputStreams; + } + + /** + * Constructor. + * + * @param outputStreams The list of output streams. + */ + public TeeOutputStream(Collection<OutputStream> outputStreams) { + this.outputStreams = outputStreams.toArray(new OutputStream[outputStreams.size()]); + } + + /** + * Adds an output stream to this tee output stream. + * + * @param os The output stream to add to this tee output stream. + * @param close If <jk>false</jk>, then calling {@link #close()} on this stream + * will not filter to the specified output stream. + * @return This object (for method chaining). + */ + public TeeOutputStream add(OutputStream os, boolean close) { + if (os == null) + return this; + if (! close) + os = new NoCloseOutputStream(os); + if (os == this) + throw new RuntimeException("Cannot add this output stream to itself."); + for (OutputStream os2 : outputStreams) + if (os2 == os) + throw new RuntimeException("Cannot add this output stream again."); + if (os instanceof TeeOutputStream) { + for (OutputStream os2 : ((TeeOutputStream)os).outputStreams) + add(os2, true); + } else { + outputStreams = ArrayUtils.append(outputStreams, os); + } + return this; + } + + /** + * Returns the output stream identified through the <code>id</code> parameter + * passed in through the {@link #add(String, OutputStream, boolean)} method. + * + * @param id The ID associated with the output stream. + * @return The output stream, or <jk>null</jk> if no identifier was specified when the output stream was added. + */ + public OutputStream getOutputStream(String id) { + if (outputStreamMap != null) + return outputStreamMap.get(id); + return null; + } + + /** + * Same as {@link #add(OutputStream, boolean)} but associates the stream with an identifier + * so the stream can be retrieved through {@link #getOutputStream(String)}. + * + * @param id The ID to associate the output stream with. + * @param os The output stream to add. + * @param close Close the specified stream afterwards. + * @return This object (for method chaining). + */ + public TeeOutputStream add(String id, OutputStream os, boolean close) { + if (id != null) { + if (outputStreamMap == null) + outputStreamMap = new TreeMap<String,OutputStream>(); + outputStreamMap.put(id, os); + } + return add(os, close); + } + + /** + * Returns the number of inner streams in this tee stream. + * + * @return The number of streams in this tee stream. + */ + public int size() { + return outputStreams.length; + } + + @Override /* OutputStream */ + public void write(int b) throws IOException { + for (OutputStream os : outputStreams) + os.write(b); + } + + @Override /* OutputStream */ + public void write(byte b[], int off, int len) throws IOException { + for (OutputStream os : outputStreams) + os.write(b, off, len); + } + + @Override /* OutputStream */ + public void flush() throws IOException { + for (OutputStream os : outputStreams) + os.flush(); + } + + @Override /* OutputStream */ + public void close() throws IOException { + for (OutputStream os : outputStreams) + os.close(); + } + + private static class NoCloseOutputStream extends OutputStream { + private OutputStream os; + + private NoCloseOutputStream(OutputStream os) { + this.os = os; + } + + @Override /* OutputStream */ + public void write(int b) throws IOException { + os.write(b); + } + + @Override /* OutputStream */ + public void write(byte b[], int off, int len) throws IOException { + os.write(b, off, len); + } + + @Override /* OutputStream */ + public void flush() throws IOException { + os.flush(); + } + + @Override /* OutputStream */ + public void close() throws IOException { + // Do nothing. + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/internal/TeeWriter.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/internal/TeeWriter.java b/juneau-core/src/main/java/org/apache/juneau/internal/TeeWriter.java new file mode 100644 index 0000000..2e9dffa --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/internal/TeeWriter.java @@ -0,0 +1,165 @@ +/*************************************************************************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + ***************************************************************************************************************************/ +package org.apache.juneau.internal; + +import java.io.*; +import java.util.*; + + +/** + * Writer that can send output to multiple writers. + * + * @author James Bognar ([email protected]) + */ +public class TeeWriter extends Writer { + private Writer[] writers = new Writer[0]; + private Map<String,Writer> writerMap; + + /** + * Constructor. + * + * @param writers The list of writers. + */ + public TeeWriter(Writer...writers) { + this.writers = writers; + } + + /** + * Constructor. + * + * @param writers The list of writers. + */ + public TeeWriter(Collection<Writer> writers) { + this.writers = writers.toArray(new Writer[writers.size()]); + } + + /** + * Adds a writer to this tee writer. + * + * @param w The writer to add to this tee writer. + * @param close If <jk>false</jk>, then calling {@link #close()} on this tee writer + * will not filter to the specified writer. + * @return This object (for method chaining). + */ + public TeeWriter add(Writer w, boolean close) { + if (w == null) + return this; + if (! close) + w = new NoCloseWriter(w); + if (w == this) + throw new RuntimeException("Cannot add this writer to itself."); + for (Writer w2 : writers) + if (w2 == w) + throw new RuntimeException("Cannot add this writer again."); + if (w instanceof TeeWriter) { + for (Writer w2 : ((TeeWriter)w).writers) + add(w2, true); + } else { + writers = ArrayUtils.append(writers, w); + } + return this; + } + + /** + * Same as {@link #add(Writer, boolean)} but associates the writer with an identifier + * so the writer can be retrieved through {@link #getWriter(String)}. + * + * @param id The ID to associate the writer with. + * @param w The writer to add. + * @param close Close the specified writer afterwards. + * @return This object (for method chaining). + */ + public TeeWriter add(String id, Writer w, boolean close) { + if (id != null) { + if (writerMap == null) + writerMap = new TreeMap<String,Writer>(); + writerMap.put(id, w); + } + return add(w, close); + } + + /** + * Returns the number of inner writers in this tee writer. + * + * @return The number of writers. + */ + public int size() { + return writers.length; + } + + /** + * Returns the writer identified through the <code>id</code> parameter + * passed in through the {@link #add(String, Writer, boolean)} method. + * + * @param id The ID associated with the writer. + * @return The writer, or <jk>null</jk> if no identifier was specified when the writer was added. + */ + public Writer getWriter(String id) { + if (writerMap != null) + return writerMap.get(id); + return null; + } + + @Override /* Writer */ + public void write(char[] cbuf, int off, int len) throws IOException { + for (Writer w : writers) + if (w != null) + w.write(cbuf, off, len); + } + + @Override /* Writer */ + public void flush() throws IOException { + for (Writer w : writers) + if (w != null) + w.flush(); + } + + @Override /* Writer */ + public void close() throws IOException { + IOException e = null; + for (Writer w : writers) { + if (w != null) { + try { + w.close(); + } catch (IOException e2) { + e = e2; + } + } + } + if (e != null) + throw e; + } + + private static class NoCloseWriter extends Writer { + private Writer writer; + + private NoCloseWriter(Writer writer) { + this.writer = writer; + } + + @Override /* Writer */ + public void write(char[] cbuf, int off, int len) throws IOException { + writer.write(cbuf, off, len); + } + + @Override /* Writer */ + public void flush() throws IOException { + writer.flush(); + } + + @Override /* Writer */ + public void close() throws IOException { + // Do nothing. + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/internal/ThrowableUtils.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/internal/ThrowableUtils.java b/juneau-core/src/main/java/org/apache/juneau/internal/ThrowableUtils.java new file mode 100644 index 0000000..469dd2a --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/internal/ThrowableUtils.java @@ -0,0 +1,84 @@ +/*************************************************************************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + ***************************************************************************************************************************/ +package org.apache.juneau.internal; + +import java.text.*; + +/** + * Various utility methods for creating and working with throwables. + * + * @author James Bognar ([email protected]) + */ +public class ThrowableUtils { + + /** + * Throws an {@link IllegalArgumentException} if the specified object is <jk>null</jk>. + * + * @param o The object to check. + * @param msg The message of the IllegalArgumentException. + * @param args {@link MessageFormat}-style arguments in the message. + * @throws IllegalArgumentException + */ + public static void assertNotNull(Object o, String msg, Object...args) throws IllegalArgumentException { + if (o == null) + throw new IllegalArgumentException(MessageFormat.format(msg, args)); + } + + /** + * Throws an {@link IllegalArgumentException} if the specified field is <jk>null</jk>. + * + * @param fieldValue The object to check. + * @param fieldName The name of the field. + * @throws IllegalArgumentException + */ + public static void assertFieldNotNull(Object fieldValue, String fieldName) throws IllegalArgumentException { + if (fieldValue == null) + throw new IllegalArgumentException("Field '" + fieldName + "' cannot be null."); + } + + /** + * Throws an {@link IllegalArgumentException} if the specified field is <code><=0</code>. + * + * @param fieldValue The object to check. + * @param fieldName The name of the field. + * @throws IllegalArgumentException + */ + public static void assertFieldPositive(int fieldValue, String fieldName) throws IllegalArgumentException { + if (fieldValue <= 0) + throw new IllegalArgumentException("Field '" + fieldName + "' must be a positive integer."); + } + + /** + * Shortcut for calling <code><jk>new</jk> IllegalArgumentException(MessageFormat.<jsm>format</jsm>(msg, args));</code> + * + * @param msg The message of the IllegalArgumentException. + * @param args {@link MessageFormat}-style arguments in the message. + * @throws IllegalArgumentException + */ + public static void illegalArg(String msg, Object...args) throws IllegalArgumentException { + throw new IllegalArgumentException(MessageFormat.format(msg, args)); + } + + /** + * Throws an exception if the specified thread ID is not the same as the current thread. + * + * @param threadId The ID of the thread to compare against. + * @param msg The message of the IllegalStateException. + * @param args {@link IllegalStateException}-style arguments in the message. + * @throws IllegalStateException + */ + public static void assertSameThread(long threadId, String msg, Object...args) throws IllegalStateException { + if (Thread.currentThread().getId() != threadId) + throw new IllegalArgumentException(MessageFormat.format(msg, args)); + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/internal/Utils.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/internal/Utils.java b/juneau-core/src/main/java/org/apache/juneau/internal/Utils.java new file mode 100644 index 0000000..ddfe685 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/internal/Utils.java @@ -0,0 +1,38 @@ +/*************************************************************************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + ***************************************************************************************************************************/ +package org.apache.juneau.internal; + +/** + * Various utility methods. + * + * @author James Bognar ([email protected]) + */ +public class Utils { + + /** + * Compare two integers numerically. + * + * @param i1 Integer #1 + * @param i2 Integer #2 + * @return the value <code>0</code> if Integer #1 is + * equal to Integer #2; a value less than + * <code>0</code> if Integer #1 numerically less + * than Integer #2; and a value greater + * than <code>0</code> if Integer #1 is numerically + * greater than Integer #2 (signed + * comparison). + */ + public static final int compare(int i1, int i2) { + return (i1<i2 ? -1 : (i1==i2 ? 0 : 1)); + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/internal/Version.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/internal/Version.java b/juneau-core/src/main/java/org/apache/juneau/internal/Version.java new file mode 100644 index 0000000..8d16e46 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/internal/Version.java @@ -0,0 +1,122 @@ +/*************************************************************************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + ***************************************************************************************************************************/ +package org.apache.juneau.internal; + +import static org.apache.juneau.internal.StringUtils.*; + +/** + * Represents a version string such as <js>"1.2"</js> or <js>"1.2.3"</js> + * <p> + * Used to compare version numbers. + * + * @author James Bognar ([email protected]) + */ +public class Version { + + private int[] parts; + + /** + * Constructor + * + * @param versionString - A string of the form <js>"#.#..."</js> where there can be any number of parts. + * <br>Valid values: + * <ul> + * <li><js>"1.2"</js> + * <li><js>"1.2.3"</js> + * <li><js>"0.1"</js> + * <li><js>".1"</js> + * </ul> + * Any parts that are not numeric are interpreted as {@link Integer#MAX_VALUE} + */ + public Version(String versionString) { + if (isEmpty(versionString)) + versionString = "0"; + String[] sParts = split(versionString, '.'); + parts = new int[sParts.length]; + for (int i = 0; i < sParts.length; i++) { + try { + parts[i] = sParts[i].isEmpty() ? 0 : Integer.parseInt(sParts[i]); + } catch (NumberFormatException e) { + parts[i] = Integer.MAX_VALUE; + } + } + } + + /** + * Returns <jk>true</jk> if the specified version is at least this version. + * <p> + * Note that the following is true: + * <p class='bcode'> + * boolean b; + * b = <jk>new</jk> Version(<js>"1.2"</js>).isAtLeast(<jk>new</jk> Version(<js>"1.2.3"</js>)); <jc>// == true </jc> + * b = <jk>new</jk> Version(<js>"1.2.0"</js>).isAtLeast(<jk>new</jk> Version(<js>"1.2.3"</js>)); <jc>// == false</jc> + * </p> + * + * @param v The version to compare to. + * @param exclusive Match down-to-version but not including. + * @return <jk>true</jk> if the specified version is at least this version. + */ + public boolean isAtLeast(Version v, boolean exclusive) { + for (int i = 0; i < Math.min(parts.length, v.parts.length); i++) { + int c = v.parts[i] - parts[i]; + if (c > 0) + return false; + else if (c < 0) + return true; + } + return ! exclusive; + } + + /** + * Returns <jk>true</jk> if the specified version is at most this version. + * <p> + * Note that the following is true: + * <p class='bcode'> + * boolean b; + * b = <jk>new</jk> Version(<js>"1.2.3"</js>).isAtMost(<jk>new</jk> Version(<js>"1.2"</js>)); <jc>// == true </jc> + * b = <jk>new</jk> Version(<js>"1.2.3"</js>).isAtMost(<jk>new</jk> Version(<js>"1.2.0"</js>)); <jc>// == false</jc> + * </p> + * + * @param v The version to compare to. + * @param exclusive Match up-to-version but not including. + * @return <jk>true</jk> if the specified version is at most this version. + */ + public boolean isAtMost(Version v, boolean exclusive) { + return v.isAtLeast(this, exclusive); + } + + /** + * Returns <jk>true</jk> if the specified version is equal to this version. + * <p> + * Note that the following is true: + * <p class='bcode'> + * boolean b; + * b = <jk>new</jk> Version(<js>"1.2.3"</js>).equals(<jk>new</jk> Version(<js>"1.2"</js>)); <jc>// == true </jc> + * b = <jk>new</jk> Version(<js>"1.2"</js>).equals(<jk>new</jk> Version(<js>"1.2.3"</js>)); <jc>// == true</jc> + * </p> + * + * @param v The version to compare to. + * @return <jk>true</jk> if the specified version is equal to this version. + */ + public boolean equals(Version v) { + for (int i = 0; i < Math.min(parts.length, v.parts.length); i++) + if (v.parts[i] - parts[i] != 0) + return false; + return true; + } + + @Override /* Object */ + public String toString() { + return join(parts, '.'); + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/internal/VersionRange.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/internal/VersionRange.java b/juneau-core/src/main/java/org/apache/juneau/internal/VersionRange.java new file mode 100644 index 0000000..1caad9e --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/internal/VersionRange.java @@ -0,0 +1,81 @@ +/*************************************************************************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + ***************************************************************************************************************************/ +package org.apache.juneau.internal; + +/** + * Represents an OSGi-style version range like <js>"1.2"</js> or <js>"[1.0,2.0)"</js>. + * <p> + * The range can be any of the following formats: + * <ul> + * <li><js>"[0,1.0)"</js> = Less than 1.0. 1.0 and 1.0.0 does not match. + * <li><js>"[0,1.0]"</js> = Less than or equal to 1.0. Note that 1.0.1 will match. + * <li><js>"1.0"</js> = At least 1.0. 1.0 and 2.0 will match. + * </ul> + * + * @author James Bognar ([email protected]) + */ +public class VersionRange { + + private final Version minVersion, maxVersion; + private final boolean minExclusive, maxExclusive; + + /** + * Constructor. + * + * @param range The range string to parse. + */ + public VersionRange(String range) { + range = range.trim(); + if (! range.isEmpty()) { + char c1 = range.charAt(0), c2 = range.charAt(range.length()-1); + int c = range.indexOf(','); + if (c > -1 && (c1 == '[' || c1 == '(') && (c2 == ']' || c2 == ')')) { + String v1 = range.substring(1, c), v2 = range.substring(c+1, range.length()-1); + //System.err.println("v1=["+v1+"], v2=["+v2+"]"); + minVersion = new Version(v1); + maxVersion = new Version(v2); + minExclusive = c1 == '('; + maxExclusive = c2 == ')'; + } else { + minVersion = new Version(range); + maxVersion = null; + minExclusive = maxExclusive = false; + } + } else { + minVersion = maxVersion = null; + minExclusive = maxExclusive = false; + } + } + + /** + * Returns <jk>true</jk> if the specified version string matches this version range. + * + * @param v The version string (e.g. <js>"1.2.3"</js>) + * @return <jk>true</jk> if the specified version string matches this version range. + */ + public boolean matches(String v) { + if (StringUtils.isEmpty(v)) + return (minVersion == null && maxVersion == null); + Version ver = new Version(v); + if (minVersion != null && ! ver.isAtLeast(minVersion, minExclusive)) + return false; + if (maxVersion != null && ! ver.isAtMost(maxVersion, maxExclusive)) + return false; + return true; + } + + @Override /* Object */ + public String toString() { + return (minExclusive ? "(" : "[") + minVersion + ',' + maxVersion + (maxExclusive ? ")" : "]"); + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/jena/Constants.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/jena/Constants.java b/juneau-core/src/main/java/org/apache/juneau/jena/Constants.java new file mode 100644 index 0000000..cd32372 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/jena/Constants.java @@ -0,0 +1,95 @@ +/*************************************************************************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + ***************************************************************************************************************************/ +package org.apache.juneau.jena; + +import org.apache.juneau.serializer.*; + +/** + * Constants used by the {@link RdfSerializer} and {@link RdfParser} classes. + * + * @author James Bognar ([email protected]) + */ +public final class Constants { + + //-------------------------------------------------------------------------------- + // Built-in Jena languages. + //-------------------------------------------------------------------------------- + + /** Jena language support: <js>"RDF/XML"</js>.*/ + public static final String LANG_RDF_XML = "RDF/XML"; + + /** Jena language support: <js>"RDF/XML-ABBREV"</js>.*/ + public static final String LANG_RDF_XML_ABBREV = "RDF/XML-ABBREV"; + + /** Jena language support: <js>"N-TRIPLE"</js>.*/ + public static final String LANG_NTRIPLE = "N-TRIPLE"; + + /** Jena language support: <js>"TURTLE"</js>.*/ + public static final String LANG_TURTLE = "TURTLE"; + + /** Jena language support: <js>"N3"</js>.*/ + public static final String LANG_N3 = "N3"; + + + //-------------------------------------------------------------------------------- + // Built-in Juneau properties. + //-------------------------------------------------------------------------------- + + /** + * RDF property identifier <js>"items"</js>. + * <p> + * For resources that are collections, this property identifies the RDF Sequence + * container for the items in the collection. + */ + public static final String RDF_juneauNs_ITEMS = "items"; + + /** + * RDF property identifier <js>"root"</js>. + * <p> + * Property added to root nodes to help identify them as root elements during parsing. + * <p> + * Added if {@link RdfSerializerContext#RDF_addRootProperty} setting is enabled. + */ + public static final String RDF_juneauNs_ROOT = "root"; + + /** + * RDF property identifier <js>"class"</js>. + * <p> + * Property added to bean resources to identify the class type. + * <p> + * Added if {@link SerializerContext#SERIALIZER_addClassAttrs} setting is enabled. + */ + public static final String RDF_juneauNs_CLASS = "class"; + + /** + * RDF property identifier <js>"value"</js>. + * <p> + * Property added to nodes to identify a simple value. + */ + public static final String RDF_juneauNs_VALUE = "value"; + + /** + * RDF resource that identifies a <jk>null</jk> value. + */ + public static final String RDF_NIL = "http://www.w3.org/1999/02/22-rdf-syntax-ns#nil"; + + /** + * RDF resource that identifies a <code>Seq</code> value. + */ + public static final String RDF_SEQ = "http://www.w3.org/1999/02/22-rdf-syntax-ns#Seq"; + + /** + * RDF resource that identifies a <code>Bag</code> value. + */ + public static final String RDF_BAG = "http://www.w3.org/1999/02/22-rdf-syntax-ns#Bag"; +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/jena/RdfBeanPropertyMeta.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/jena/RdfBeanPropertyMeta.java b/juneau-core/src/main/java/org/apache/juneau/jena/RdfBeanPropertyMeta.java new file mode 100644 index 0000000..f3548d9 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/jena/RdfBeanPropertyMeta.java @@ -0,0 +1,82 @@ +/*************************************************************************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + ***************************************************************************************************************************/ +package org.apache.juneau.jena; + +import static org.apache.juneau.jena.RdfCollectionFormat.*; + +import java.util.*; + +import org.apache.juneau.*; +import org.apache.juneau.jena.annotation.*; +import org.apache.juneau.xml.*; + +/** + * Metadata on bean properties specific to the RDF serializers and parsers pulled from the {@link Rdf @Rdf} annotation on the bean property. + * + * @author James Bognar ([email protected]) + * @param <T> The bean class. + */ +public class RdfBeanPropertyMeta<T> { + + private RdfCollectionFormat collectionFormat = DEFAULT; + private Namespace namespace = null; + + /** + * Constructor. + * + * @param bpMeta The metadata of the bean property of this additional metadata. + */ + public RdfBeanPropertyMeta(BeanPropertyMeta<T> bpMeta) { + + List<Rdf> rdfs = bpMeta.findAnnotations(Rdf.class); + List<RdfSchema> schemas = bpMeta.findAnnotations(RdfSchema.class); + + for (Rdf rdf : rdfs) + if (collectionFormat == DEFAULT) + collectionFormat = rdf.collectionFormat(); + + namespace = RdfUtils.findNamespace(rdfs, schemas); + } + + /** + * Returns the RDF collection format of this property from the {@link Rdf#collectionFormat} annotation on this bean property. + * + * @return The RDF collection format, or {@link RdfCollectionFormat#DEFAULT} if annotation not specified. + */ + protected RdfCollectionFormat getCollectionFormat() { + return collectionFormat; + } + + /** + * Returns the RDF namespace associated with this bean property. + * <p> + * Namespace is determined in the following order: + * <ol> + * <li>{@link Rdf#prefix()} annotation defined on bean property field. + * <li>{@link Rdf#prefix()} annotation defined on bean getter. + * <li>{@link Rdf#prefix()} annotation defined on bean setter. + * <li>{@link Rdf#prefix()} annotation defined on bean. + * <li>{@link Rdf#prefix()} annotation defined on bean package. + * <li>{@link Rdf#prefix()} annotation defined on bean superclasses. + * <li>{@link Rdf#prefix()} annotation defined on bean superclass packages. + * <li>{@link Rdf#prefix()} annotation defined on bean interfaces. + * <li>{@link Rdf#prefix()} annotation defined on bean interface packages. + * </ol> + * + * @return The namespace associated with this bean property, or <jk>null</jk> if no namespace is + * associated with it. + */ + public Namespace getNamespace() { + return namespace; + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/jena/RdfClassMeta.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/jena/RdfClassMeta.java b/juneau-core/src/main/java/org/apache/juneau/jena/RdfClassMeta.java new file mode 100644 index 0000000..fcfcfc9 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/jena/RdfClassMeta.java @@ -0,0 +1,86 @@ +/*************************************************************************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + ***************************************************************************************************************************/ +package org.apache.juneau.jena; + +import java.util.*; + +import org.apache.juneau.internal.*; +import org.apache.juneau.jena.annotation.*; +import org.apache.juneau.xml.*; + +/** + * Metadata on classes specific to the RDF serializers and parsers pulled from the {@link Rdf @Rdf} annotation on the class. + * + * @author James Bognar ([email protected]) + */ +public class RdfClassMeta { + + private final Rdf rdf; + private final RdfCollectionFormat collectionFormat; + private final Namespace namespace; + + /** + * Constructor. + * + * @param c The class that this annotation is defined on. + */ + public RdfClassMeta(Class<?> c) { + this.rdf = ReflectionUtils.getAnnotation(Rdf.class, c); + if (rdf != null) { + collectionFormat = rdf.collectionFormat(); + } else { + collectionFormat = RdfCollectionFormat.DEFAULT; + } + List<Rdf> rdfs = ReflectionUtils.findAnnotations(Rdf.class, c); + List<RdfSchema> schemas = ReflectionUtils.findAnnotations(RdfSchema.class, c); + this.namespace = RdfUtils.findNamespace(rdfs, schemas); + } + + /** + * Returns the {@link Rdf} annotation defined on the class. + * + * @return The value of the {@link Rdf} annotation, or <jk>null</jk> if annotation is not specified. + */ + protected Rdf getAnnotation() { + return rdf; + } + + /** + * Returns the {@link Rdf#collectionFormat()} annotation defined on the class. + * + * @return The value of the {@link Rdf#collectionFormat()} annotation, or <jk>null</jk> if annotation is not specified. + */ + protected RdfCollectionFormat getCollectionFormat() { + return collectionFormat; + } + + /** + * Returns the RDF namespace associated with this class. + * <p> + * Namespace is determined in the following order: + * <ol> + * <li>{@link Rdf#prefix()} annotation defined on class. + * <li>{@link Rdf#prefix()} annotation defined on package. + * <li>{@link Rdf#prefix()} annotation defined on superclasses. + * <li>{@link Rdf#prefix()} annotation defined on superclass packages. + * <li>{@link Rdf#prefix()} annotation defined on interfaces. + * <li>{@link Rdf#prefix()} annotation defined on interface packages. + * </ol> + * + * @return The namespace associated with this class, or <jk>null</jk> if no namespace is + * associated with it. + */ + protected Namespace getNamespace() { + return namespace; + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/jena/RdfCollectionFormat.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/jena/RdfCollectionFormat.java b/juneau-core/src/main/java/org/apache/juneau/jena/RdfCollectionFormat.java new file mode 100644 index 0000000..5ce4842 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/jena/RdfCollectionFormat.java @@ -0,0 +1,56 @@ +/*************************************************************************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + ***************************************************************************************************************************/ +package org.apache.juneau.jena; + +import org.apache.juneau.jena.annotation.*; + +/** + * Used in conjunction with the {@link Rdf#collectionFormat() @Rdf.collectionFormat()} annotation to fine-tune how + * classes, beans, and bean properties are serialized, particularly collections. + * <p> + * + * @author James Bognar ([email protected]) + */ +public enum RdfCollectionFormat { + + /** + * Default formatting (default). + * <p> + * Inherit formatting from parent class or parent package. + * If no formatting specified at any level, default is {@link #SEQ}. + */ + DEFAULT, + + /** + * Causes collections and arrays to be rendered as RDF sequences. + */ + SEQ, + + /** + * Causes collections and arrays to be rendered as RDF bags. + */ + BAG, + + /** + * Causes collections and arrays to be rendered as RDF lists. + */ + LIST, + + /** + * Causes collections and arrays to be rendered as multi-valued RDF properties instead of sequences. + * <p> + * Note that enabling this setting will cause order of elements in the collection to be lost. + */ + MULTI_VALUED; + +} \ No newline at end of file
