http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ClassUtil.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ClassUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ClassUtil.java new file mode 100644 index 0000000..2670c8c --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ClassUtil.java @@ -0,0 +1,182 @@ +/* + * 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.freemarker.core.util; + +import org.apache.freemarker.core.model.impl.BeanModel; + +public class _ClassUtil { + + private static final String ORG_APACHE_FREEMARKER = "org.apache.freemarker."; + private static final String ORG_APACHE_FREEMARKER_CORE = "org.apache.freemarker.core."; + private static final String ORG_APACHE_FREEMARKER_CORE_TEMPLATERESOLVER + = "org.apache.freemarker.core.templateresolver."; + private static final String ORG_APACHE_FREEMARKER_CORE_MODEL = "org.apache.freemarker.core.model."; + + private _ClassUtil() { + } + + /** + * Similar to {@link Class#forName(java.lang.String)}, but attempts to load + * through the thread context class loader. Only if thread context class + * loader is inaccessible, or it can't find the class will it attempt to + * fall back to the class loader that loads the FreeMarker classes. + */ + public static Class forName(String className) + throws ClassNotFoundException { + try { + ClassLoader ctcl = Thread.currentThread().getContextClassLoader(); + if (ctcl != null) { // not null: we don't want to fall back to the bootstrap class loader + return Class.forName(className, true, ctcl); + } + } catch (ClassNotFoundException e) { + // Intentionally ignored + } catch (SecurityException e) { + // Intentionally ignored + } + // Fall back to the defining class loader of the FreeMarker classes + return Class.forName(className); + } + + /** + * Same as {@link #getShortClassName(Class, boolean) getShortClassName(pClass, false)}. + * + * @since 2.3.20 + */ + public static String getShortClassName(Class pClass) { + return getShortClassName(pClass, false); + } + + /** + * Returns a class name without "java.lang." and "java.util." prefix, also shows array types in a format like + * {@code int[]}; useful for printing class names in error messages. + * + * @param pClass can be {@code null}, in which case the method returns {@code null}. + * @param shortenFreeMarkerClasses if {@code true}, it will also shorten FreeMarker class names. The exact rules + * aren't specified and might change over time, but right now, {@link BeanModel} for + * example becomes to {@code o.a.f.c.m.BeanModel}. + * + * @since 2.3.20 + */ + public static String getShortClassName(Class pClass, boolean shortenFreeMarkerClasses) { + if (pClass == null) { + return null; + } else if (pClass.isArray()) { + return getShortClassName(pClass.getComponentType()) + "[]"; + } else { + String cn = pClass.getName(); + if (cn.startsWith("java.lang.") || cn.startsWith("java.util.")) { + return cn.substring(10); + } else { + if (shortenFreeMarkerClasses) { + if (cn.startsWith(ORG_APACHE_FREEMARKER_CORE_MODEL)) { + return "o.a.f.c.m." + cn.substring(ORG_APACHE_FREEMARKER_CORE_MODEL.length()); + } else if (cn.startsWith(ORG_APACHE_FREEMARKER_CORE_TEMPLATERESOLVER)) { + return "o.a.f.c.t." + cn.substring(ORG_APACHE_FREEMARKER_CORE_TEMPLATERESOLVER.length()); + } else if (cn.startsWith(ORG_APACHE_FREEMARKER_CORE)) { + return "o.a.f.c." + cn.substring(ORG_APACHE_FREEMARKER_CORE.length()); + } else if (cn.startsWith(ORG_APACHE_FREEMARKER)) { + return "o.a.f." + cn.substring(ORG_APACHE_FREEMARKER.length()); + } + // Falls through + } + return cn; + } + } + } + + /** + * Same as {@link #getShortClassNameOfObject(Object, boolean) getShortClassNameOfObject(pClass, false)}. + * + * @since 2.3.20 + */ + public static String getShortClassNameOfObject(Object obj) { + return getShortClassNameOfObject(obj, false); + } + + /** + * {@link #getShortClassName(Class, boolean)} called with {@code object.getClass()}, but returns the fictional + * class name {@code Null} for a {@code null} value. + * + * @since 2.3.20 + */ + public static String getShortClassNameOfObject(Object obj, boolean shortenFreeMarkerClasses) { + if (obj == null) { + return "Null"; + } else { + return _ClassUtil.getShortClassName(obj.getClass(), shortenFreeMarkerClasses); + } + } + + /** + * Gets the wrapper class for a primitive class, like {@link Integer} for {@code int}, also returns {@link Void} + * for {@code void}. + * + * @param primitiveClass A {@link Class} like {@code int.type}, {@code boolean.type}, etc. If it's not a primitive + * class, or it's {@code null}, then the parameter value is returned as is. Note that performance-wise the + * method assumes that it's a primitive class. + * + * @since 2.3.21 + */ + public static Class primitiveClassToBoxingClass(Class primitiveClass) { + // Tried to sort these with decreasing frequency in API-s: + if (primitiveClass == int.class) return Integer.class; + if (primitiveClass == boolean.class) return Boolean.class; + if (primitiveClass == long.class) return Long.class; + if (primitiveClass == double.class) return Double.class; + if (primitiveClass == char.class) return Character.class; + if (primitiveClass == float.class) return Float.class; + if (primitiveClass == byte.class) return Byte.class; + if (primitiveClass == short.class) return Short.class; + if (primitiveClass == void.class) return Void.class; // not really a primitive, but we normalize it + return primitiveClass; + } + + /** + * The exact reverse of {@link #primitiveClassToBoxingClass}. + * + * @since 2.3.21 + */ + public static Class boxingClassToPrimitiveClass(Class boxingClass) { + // Tried to sort these with decreasing frequency in API-s: + if (boxingClass == Integer.class) return int.class; + if (boxingClass == Boolean.class) return boolean.class; + if (boxingClass == Long.class) return long.class; + if (boxingClass == Double.class) return double.class; + if (boxingClass == Character.class) return char.class; + if (boxingClass == Float.class) return float.class; + if (boxingClass == Byte.class) return byte.class; + if (boxingClass == Short.class) return short.class; + if (boxingClass == Void.class) return void.class; // not really a primitive, but we normalize to it + return boxingClass; + } + + /** + * Tells if a type is numerical; works both for primitive types and classes. + * + * @param type can't be {@code null} + * + * @since 2.3.21 + */ + public static boolean isNumerical(Class type) { + return Number.class.isAssignableFrom(type) + || type.isPrimitive() && type != Boolean.TYPE && type != Character.TYPE && type != Void.TYPE; + } + +}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_CollectionUtil.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_CollectionUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_CollectionUtil.java new file mode 100644 index 0000000..5d532de --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_CollectionUtil.java @@ -0,0 +1,36 @@ +/* + * 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.freemarker.core.util; + +/** Don't use this; used internally by FreeMarker, might changes without notice. */ +public class _CollectionUtil { + + private _CollectionUtil() { } + + public static final Object[] EMPTY_OBJECT_ARRAY = new Object[] { }; + public static final Class[] EMPTY_CLASS_ARRAY = new Class[] { }; + public static final String[] EMPTY_STRING_ARRAY = new String[] { }; + + /** + * @since 2.3.22 + */ + public static final char[] EMPTY_CHAR_ARRAY = new char[] { }; + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_DateUtil.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_DateUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_DateUtil.java new file mode 100644 index 0000000..0cf2fea --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_DateUtil.java @@ -0,0 +1,914 @@ +/* + * 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.freemarker.core.util; + +import java.text.ParseException; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Don't use this; used internally by FreeMarker, might changes without notice. + * Date and time related utilities. + */ +public class _DateUtil { + + /** + * Show hours (24h); always 2 digits, like {@code 00}, {@code 05}, etc. + */ + public static final int ACCURACY_HOURS = 4; + + /** + * Show hours and minutes (even if minutes is 00). + */ + public static final int ACCURACY_MINUTES = 5; + + /** + * Show hours, minutes and seconds (even if seconds is 00). + */ + public static final int ACCURACY_SECONDS = 6; + + /** + * Show hours, minutes and seconds and up to 3 fraction second digits, without trailing 0-s in the fraction part. + */ + public static final int ACCURACY_MILLISECONDS = 7; + + /** + * Show hours, minutes and seconds and exactly 3 fraction second digits (even if it's 000) + */ + public static final int ACCURACY_MILLISECONDS_FORCED = 8; + + public static final TimeZone UTC = TimeZone.getTimeZone("UTC"); + + private static final String REGEX_XS_TIME_ZONE + = "Z|(?:[-+][0-9]{2}:[0-9]{2})"; + private static final String REGEX_ISO8601_BASIC_TIME_ZONE + = "Z|(?:[-+][0-9]{2}(?:[0-9]{2})?)"; + private static final String REGEX_ISO8601_EXTENDED_TIME_ZONE + = "Z|(?:[-+][0-9]{2}(?::[0-9]{2})?)"; + + private static final String REGEX_XS_OPTIONAL_TIME_ZONE + = "(" + REGEX_XS_TIME_ZONE + ")?"; + private static final String REGEX_ISO8601_BASIC_OPTIONAL_TIME_ZONE + = "(" + REGEX_ISO8601_BASIC_TIME_ZONE + ")?"; + private static final String REGEX_ISO8601_EXTENDED_OPTIONAL_TIME_ZONE + = "(" + REGEX_ISO8601_EXTENDED_TIME_ZONE + ")?"; + + private static final String REGEX_XS_DATE_BASE + = "(-?[0-9]+)-([0-9]{2})-([0-9]{2})"; + private static final String REGEX_ISO8601_BASIC_DATE_BASE + = "(-?[0-9]{4,}?)([0-9]{2})([0-9]{2})"; + private static final String REGEX_ISO8601_EXTENDED_DATE_BASE + = "(-?[0-9]{4,})-([0-9]{2})-([0-9]{2})"; + + private static final String REGEX_XS_TIME_BASE + = "([0-9]{2}):([0-9]{2}):([0-9]{2})(?:\\.([0-9]+))?"; + private static final String REGEX_ISO8601_BASIC_TIME_BASE + = "([0-9]{2})(?:([0-9]{2})(?:([0-9]{2})(?:[\\.,]([0-9]+))?)?)?"; + private static final String REGEX_ISO8601_EXTENDED_TIME_BASE + = "([0-9]{2})(?::([0-9]{2})(?::([0-9]{2})(?:[\\.,]([0-9]+))?)?)?"; + + private static final Pattern PATTERN_XS_DATE = Pattern.compile( + REGEX_XS_DATE_BASE + REGEX_XS_OPTIONAL_TIME_ZONE); + private static final Pattern PATTERN_ISO8601_BASIC_DATE = Pattern.compile( + REGEX_ISO8601_BASIC_DATE_BASE); // No time zone allowed here + private static final Pattern PATTERN_ISO8601_EXTENDED_DATE = Pattern.compile( + REGEX_ISO8601_EXTENDED_DATE_BASE); // No time zone allowed here + + private static final Pattern PATTERN_XS_TIME = Pattern.compile( + REGEX_XS_TIME_BASE + REGEX_XS_OPTIONAL_TIME_ZONE); + private static final Pattern PATTERN_ISO8601_BASIC_TIME = Pattern.compile( + REGEX_ISO8601_BASIC_TIME_BASE + REGEX_ISO8601_BASIC_OPTIONAL_TIME_ZONE); + private static final Pattern PATTERN_ISO8601_EXTENDED_TIME = Pattern.compile( + REGEX_ISO8601_EXTENDED_TIME_BASE + REGEX_ISO8601_EXTENDED_OPTIONAL_TIME_ZONE); + + private static final Pattern PATTERN_XS_DATE_TIME = Pattern.compile( + REGEX_XS_DATE_BASE + + "T" + REGEX_XS_TIME_BASE + + REGEX_XS_OPTIONAL_TIME_ZONE); + private static final Pattern PATTERN_ISO8601_BASIC_DATE_TIME = Pattern.compile( + REGEX_ISO8601_BASIC_DATE_BASE + + "T" + REGEX_ISO8601_BASIC_TIME_BASE + + REGEX_ISO8601_BASIC_OPTIONAL_TIME_ZONE); + private static final Pattern PATTERN_ISO8601_EXTENDED_DATE_TIME = Pattern.compile( + REGEX_ISO8601_EXTENDED_DATE_BASE + + "T" + REGEX_ISO8601_EXTENDED_TIME_BASE + + REGEX_ISO8601_EXTENDED_OPTIONAL_TIME_ZONE); + + private static final Pattern PATTERN_XS_TIME_ZONE = Pattern.compile( + REGEX_XS_TIME_ZONE); + + private static final String MSG_YEAR_0_NOT_ALLOWED + = "Year 0 is not allowed in XML schema dates. BC 1 is -1, AD 1 is 1."; + + private _DateUtil() { + // can't be instantiated + } + + /** + * Returns the time zone object for the name (or ID). This differs from + * {@link TimeZone#getTimeZone(String)} in that the latest returns GMT + * if it doesn't recognize the name, while this throws an + * {@link UnrecognizedTimeZoneException}. + * + * @throws UnrecognizedTimeZoneException If the time zone name wasn't understood + */ + public static TimeZone getTimeZone(String name) + throws UnrecognizedTimeZoneException { + if (isGMTish(name)) { + if (name.equalsIgnoreCase("UTC")) { + return UTC; + } + return TimeZone.getTimeZone(name); + } + TimeZone tz = TimeZone.getTimeZone(name); + if (isGMTish(tz.getID())) { + throw new UnrecognizedTimeZoneException(name); + } + return tz; + } + + /** + * Tells if a offset or time zone is GMT. GMT is a fuzzy term, it used to + * referred both to UTC and UT1. + */ + private static boolean isGMTish(String name) { + if (name.length() < 3) { + return false; + } + char c1 = name.charAt(0); + char c2 = name.charAt(1); + char c3 = name.charAt(2); + if ( + !( + (c1 == 'G' || c1 == 'g') + && (c2 == 'M' || c2 == 'm') + && (c3 == 'T' || c3 == 't') + ) + && + !( + (c1 == 'U' || c1 == 'u') + && (c2 == 'T' || c2 == 't') + && (c3 == 'C' || c3 == 'c') + ) + && + !( + (c1 == 'U' || c1 == 'u') + && (c2 == 'T' || c2 == 't') + && (c3 == '1') + ) + ) { + return false; + } + + if (name.length() == 3) { + return true; + } + + String offset = name.substring(3); + if (offset.startsWith("+")) { + return offset.equals("+0") || offset.equals("+00") + || offset.equals("+00:00"); + } else { + return offset.equals("-0") || offset.equals("-00") + || offset.equals("-00:00"); + } + } + + /** + * Format a date, time or dateTime with one of the ISO 8601 extended + * formats that is also compatible with the XML Schema format (as far as you + * don't have dates in the BC era). Examples of possible outputs: + * {@code "2005-11-27T15:30:00+02:00"}, {@code "2005-11-27"}, + * {@code "15:30:00Z"}. Note the {@code ":00"} in the time zone offset; + * this is not required by ISO 8601, but included for compatibility with + * the XML Schema format. Regarding the B.C. issue, those dates will be + * one year off when read back according the XML Schema format, because of a + * mismatch between that format and ISO 8601:2000 Second Edition. + * + * <p>This method is thread-safe. + * + * @param date the date to convert to ISO 8601 string + * @param datePart whether the date part (year, month, day) will be included + * or not + * @param timePart whether the time part (hours, minutes, seconds, + * milliseconds) will be included or not + * @param offsetPart whether the time zone offset part will be included or + * not. This will be shown as an offset to UTC (examples: + * {@code "+01"}, {@code "-02"}, {@code "+04:30"}) or as {@code "Z"} + * for UTC (and for UT1 and for GMT+00, since the Java platform + * doesn't really care about the difference). + * Note that this can't be {@code true} when {@code timePart} is + * {@code false}, because ISO 8601 (2004) doesn't mention such + * patterns. + * @param accuracy tells which parts of the date/time to drop. The + * {@code datePart} and {@code timePart} parameters are stronger than + * this. Note that when {@link #ACCURACY_MILLISECONDS} is specified, + * the milliseconds part will be displayed as fraction seconds + * (like {@code "15:30.00.25"}) with the minimum number of + * digits needed to show the milliseconds without precision lose. + * Thus, if the milliseconds happen to be exactly 0, no fraction + * seconds will be shown at all. + * @param timeZone the time zone in which the date/time will be shown. (You + * may find {@link _DateUtil#UTC} handy here.) Note + * that although date-only formats has no time zone offset part, + * the result still depends on the time zone, as days start and end + * at different points on the time line in different zones. + * @param calendarFactory the factory that will invoke the calendar used + * internally for calculations. The point of this parameter is that + * creating a new calendar is relatively expensive, so it's desirable + * to reuse calendars and only set their time and zone. (This was + * tested on Sun JDK 1.6 x86 Win, where it gave 2x-3x speedup.) + */ + public static String dateToISO8601String( + Date date, + boolean datePart, boolean timePart, boolean offsetPart, + int accuracy, + TimeZone timeZone, + DateToISO8601CalendarFactory calendarFactory) { + return dateToString(date, datePart, timePart, offsetPart, accuracy, timeZone, false, calendarFactory); + } + + /** + * Same as {@link #dateToISO8601String}, but gives XML Schema compliant format. + */ + public static String dateToXSString( + Date date, + boolean datePart, boolean timePart, boolean offsetPart, + int accuracy, + TimeZone timeZone, + DateToISO8601CalendarFactory calendarFactory) { + return dateToString(date, datePart, timePart, offsetPart, accuracy, timeZone, true, calendarFactory); + } + + private static String dateToString( + Date date, + boolean datePart, boolean timePart, boolean offsetPart, + int accuracy, + TimeZone timeZone, boolean xsMode, + DateToISO8601CalendarFactory calendarFactory) { + if (!xsMode && !timePart && offsetPart) { + throw new IllegalArgumentException( + "ISO 8601:2004 doesn't specify any formats where the " + + "offset is shown but the time isn't."); + } + + if (timeZone == null) { + timeZone = UTC; + } + + GregorianCalendar cal = calendarFactory.get(timeZone, date); + + int maxLength; + if (!timePart) { + maxLength = 10 + (xsMode ? 6 : 0); // YYYY-MM-DD+00:00 + } else { + if (!datePart) { + maxLength = 12 + 6; // HH:MM:SS.mmm+00:00 + } else { + maxLength = 10 + 1 + 12 + 6; + } + } + char[] res = new char[maxLength]; + int dstIdx = 0; + + if (datePart) { + int x = cal.get(Calendar.YEAR); + if (x > 0 && cal.get(Calendar.ERA) == GregorianCalendar.BC) { + x = -x + (xsMode ? 0 : 1); + } + if (x >= 0 && x < 9999) { + res[dstIdx++] = (char) ('0' + x / 1000); + res[dstIdx++] = (char) ('0' + x % 1000 / 100); + res[dstIdx++] = (char) ('0' + x % 100 / 10); + res[dstIdx++] = (char) ('0' + x % 10); + } else { + String yearString = String.valueOf(x); + + // Re-allocate buffer: + maxLength = maxLength - 4 + yearString.length(); + res = new char[maxLength]; + + for (int i = 0; i < yearString.length(); i++) { + res[dstIdx++] = yearString.charAt(i); + } + } + + res[dstIdx++] = '-'; + + x = cal.get(Calendar.MONTH) + 1; + dstIdx = append00(res, dstIdx, x); + + res[dstIdx++] = '-'; + + x = cal.get(Calendar.DAY_OF_MONTH); + dstIdx = append00(res, dstIdx, x); + + if (timePart) { + res[dstIdx++] = 'T'; + } + } + + if (timePart) { + int x = cal.get(Calendar.HOUR_OF_DAY); + dstIdx = append00(res, dstIdx, x); + + if (accuracy >= ACCURACY_MINUTES) { + res[dstIdx++] = ':'; + + x = cal.get(Calendar.MINUTE); + dstIdx = append00(res, dstIdx, x); + + if (accuracy >= ACCURACY_SECONDS) { + res[dstIdx++] = ':'; + + x = cal.get(Calendar.SECOND); + dstIdx = append00(res, dstIdx, x); + + if (accuracy >= ACCURACY_MILLISECONDS) { + x = cal.get(Calendar.MILLISECOND); + int forcedDigits = accuracy == ACCURACY_MILLISECONDS_FORCED ? 3 : 0; + if (x != 0 || forcedDigits != 0) { + if (x > 999) { + // Shouldn't ever happen... + throw new RuntimeException( + "Calendar.MILLISECOND > 999"); + } + res[dstIdx++] = '.'; + do { + res[dstIdx++] = (char) ('0' + (x / 100)); + forcedDigits--; + x = x % 100 * 10; + } while (x != 0 || forcedDigits > 0); + } + } + } + } + } + + if (offsetPart) { + if (timeZone == UTC) { + res[dstIdx++] = 'Z'; + } else { + int dt = timeZone.getOffset(date.getTime()); + boolean positive; + if (dt < 0) { + positive = false; + dt = -dt; + } else { + positive = true; + } + + dt /= 1000; + int offS = dt % 60; + dt /= 60; + int offM = dt % 60; + dt /= 60; + int offH = dt; + + if (offS == 0 && offM == 0 && offH == 0) { + res[dstIdx++] = 'Z'; + } else { + res[dstIdx++] = positive ? '+' : '-'; + dstIdx = append00(res, dstIdx, offH); + res[dstIdx++] = ':'; + dstIdx = append00(res, dstIdx, offM); + if (offS != 0) { + res[dstIdx++] = ':'; + dstIdx = append00(res, dstIdx, offS); + } + } + } + } + + return new String(res, 0, dstIdx); + } + + /** + * Appends a number between 0 and 99 padded to 2 digits. + */ + private static int append00(char[] res, int dstIdx, int x) { + res[dstIdx++] = (char) ('0' + x / 10); + res[dstIdx++] = (char) ('0' + x % 10); + return dstIdx; + } + + /** + * Parses an W3C XML Schema date string (not time or date-time). + * Unlike in ISO 8601:2000 Second Edition, year -1 means B.C 1, and year 0 is invalid. + * + * @param dateStr the string to parse. + * @param defaultTimeZone used if the date doesn't specify the + * time zone offset explicitly. Can't be {@code null}. + * @param calToDateConverter Used internally to calculate the result from the calendar field values. + * If you don't have a such object around, you can just use + * {@code new }{@link TrivialCalendarFieldsToDateConverter}{@code ()}. + * + * @throws DateParseException if the date is malformed, or if the time + * zone offset is unspecified and the {@code defaultTimeZone} is + * {@code null}. + */ + public static Date parseXSDate( + String dateStr, TimeZone defaultTimeZone, + CalendarFieldsToDateConverter calToDateConverter) + throws DateParseException { + Matcher m = PATTERN_XS_DATE.matcher(dateStr); + if (!m.matches()) { + throw new DateParseException("The value didn't match the expected pattern: " + PATTERN_XS_DATE); + } + return parseDate_parseMatcher( + m, defaultTimeZone, true, calToDateConverter); + } + + /** + * Same as {@link #parseXSDate(String, TimeZone, CalendarFieldsToDateConverter)}, but for ISO 8601 dates. + */ + public static Date parseISO8601Date( + String dateStr, TimeZone defaultTimeZone, + CalendarFieldsToDateConverter calToDateConverter) + throws DateParseException { + Matcher m = PATTERN_ISO8601_EXTENDED_DATE.matcher(dateStr); + if (!m.matches()) { + m = PATTERN_ISO8601_BASIC_DATE.matcher(dateStr); + if (!m.matches()) { + throw new DateParseException("The value didn't match the expected pattern: " + + PATTERN_ISO8601_EXTENDED_DATE + " or " + + PATTERN_ISO8601_BASIC_DATE); + } + } + return parseDate_parseMatcher( + m, defaultTimeZone, false, calToDateConverter); + } + + private static Date parseDate_parseMatcher( + Matcher m, TimeZone defaultTZ, + boolean xsMode, + CalendarFieldsToDateConverter calToDateConverter) + throws DateParseException { + _NullArgumentException.check("defaultTZ", defaultTZ); + try { + int year = groupToInt(m.group(1), "year", Integer.MIN_VALUE, Integer.MAX_VALUE); + + int era; + // Starting from ISO 8601:2000 Second Edition, 0001 is AD 1, 0000 is BC 1, -0001 is BC 2. + // However, according to http://www.w3.org/TR/2004/REC-xmlschema-2-20041028/, XML schemas are based + // on the earlier version where 0000 didn't exist, and year -1 is BC 1. + if (year <= 0) { + era = GregorianCalendar.BC; + year = -year + (xsMode ? 0 : 1); + if (year == 0) { + throw new DateParseException(MSG_YEAR_0_NOT_ALLOWED); + } + } else { + era = GregorianCalendar.AD; + } + + int month = groupToInt(m.group(2), "month", 1, 12) - 1; + int day = groupToInt(m.group(3), "day-of-month", 1, 31); + + TimeZone tz = xsMode ? parseMatchingTimeZone(m.group(4), defaultTZ) : defaultTZ; + + return calToDateConverter.calculate(era, year, month, day, 0, 0, 0, 0, false, tz); + } catch (IllegalArgumentException e) { + // Calendar methods used to throw this for illegal dates. + throw new DateParseException( + "Date calculation faliure. " + + "Probably the date is formally correct, but refers " + + "to an unexistent date (like February 30)."); + } + } + + /** + * Parses an W3C XML Schema time string (not date or date-time). + * If the time string doesn't specify the time zone offset explicitly, + * the value of the {@code defaultTZ} paramter will be used. + */ + public static Date parseXSTime( + String timeStr, TimeZone defaultTZ, CalendarFieldsToDateConverter calToDateConverter) + throws DateParseException { + Matcher m = PATTERN_XS_TIME.matcher(timeStr); + if (!m.matches()) { + throw new DateParseException("The value didn't match the expected pattern: " + PATTERN_XS_TIME); + } + return parseTime_parseMatcher(m, defaultTZ, calToDateConverter); + } + + /** + * Same as {@link #parseXSTime(String, TimeZone, CalendarFieldsToDateConverter)} but for ISO 8601 times. + */ + public static Date parseISO8601Time( + String timeStr, TimeZone defaultTZ, CalendarFieldsToDateConverter calToDateConverter) + throws DateParseException { + Matcher m = PATTERN_ISO8601_EXTENDED_TIME.matcher(timeStr); + if (!m.matches()) { + m = PATTERN_ISO8601_BASIC_TIME.matcher(timeStr); + if (!m.matches()) { + throw new DateParseException("The value didn't match the expected pattern: " + + PATTERN_ISO8601_EXTENDED_TIME + " or " + + PATTERN_ISO8601_BASIC_TIME); + } + } + return parseTime_parseMatcher(m, defaultTZ, calToDateConverter); + } + + private static Date parseTime_parseMatcher( + Matcher m, TimeZone defaultTZ, + CalendarFieldsToDateConverter calToDateConverter) + throws DateParseException { + _NullArgumentException.check("defaultTZ", defaultTZ); + try { + // ISO 8601 allows both 00:00 and 24:00, + // but Calendar.set(...) doesn't if the Calendar is not lenient. + int hours = groupToInt(m.group(1), "hour-of-day", 0, 24); + boolean hourWas24; + if (hours == 24) { + hours = 0; + hourWas24 = true; + // And a day will be added later... + } else { + hourWas24 = false; + } + + final String minutesStr = m.group(2); + int minutes = minutesStr != null ? groupToInt(minutesStr, "minute", 0, 59) : 0; + + final String secsStr = m.group(3); + // Allow 60 because of leap seconds + int secs = secsStr != null ? groupToInt(secsStr, "second", 0, 60) : 0; + + int millisecs = groupToMillisecond(m.group(4)); + + // As a time is just the distance from the beginning of the day, + // the time-zone offest should be 0 usually. + TimeZone tz = parseMatchingTimeZone(m.group(5), defaultTZ); + + // Continue handling the 24:00 special case + int day; + if (hourWas24) { + if (minutes == 0 && secs == 0 && millisecs == 0) { + day = 2; + } else { + throw new DateParseException( + "Hour 24 is only allowed in the case of " + + "midnight."); + } + } else { + day = 1; + } + + return calToDateConverter.calculate( + GregorianCalendar.AD, 1970, 0, day, hours, minutes, secs, millisecs, false, tz); + } catch (IllegalArgumentException e) { + // Calendar methods used to throw this for illegal dates. + throw new DateParseException( + "Unexpected time calculation faliure."); + } + } + + /** + * Parses an W3C XML Schema date-time string (not date or time). + * Unlike in ISO 8601:2000 Second Edition, year -1 means B.C 1, and year 0 is invalid. + * + * @param dateTimeStr the string to parse. + * @param defaultTZ used if the dateTime doesn't specify the + * time zone offset explicitly. Can't be {@code null}. + * + * @throws DateParseException if the dateTime is malformed. + */ + public static Date parseXSDateTime( + String dateTimeStr, TimeZone defaultTZ, CalendarFieldsToDateConverter calToDateConverter) + throws DateParseException { + Matcher m = PATTERN_XS_DATE_TIME.matcher(dateTimeStr); + if (!m.matches()) { + throw new DateParseException( + "The value didn't match the expected pattern: " + PATTERN_XS_DATE_TIME); + } + return parseDateTime_parseMatcher( + m, defaultTZ, true, calToDateConverter); + } + + /** + * Same as {@link #parseXSDateTime(String, TimeZone, CalendarFieldsToDateConverter)} but for ISO 8601 format. + */ + public static Date parseISO8601DateTime( + String dateTimeStr, TimeZone defaultTZ, CalendarFieldsToDateConverter calToDateConverter) + throws DateParseException { + Matcher m = PATTERN_ISO8601_EXTENDED_DATE_TIME.matcher(dateTimeStr); + if (!m.matches()) { + m = PATTERN_ISO8601_BASIC_DATE_TIME.matcher(dateTimeStr); + if (!m.matches()) { + throw new DateParseException("The value (" + dateTimeStr + ") didn't match the expected pattern: " + + PATTERN_ISO8601_EXTENDED_DATE_TIME + " or " + + PATTERN_ISO8601_BASIC_DATE_TIME); + } + } + return parseDateTime_parseMatcher( + m, defaultTZ, false, calToDateConverter); + } + + private static Date parseDateTime_parseMatcher( + Matcher m, TimeZone defaultTZ, + boolean xsMode, + CalendarFieldsToDateConverter calToDateConverter) + throws DateParseException { + _NullArgumentException.check("defaultTZ", defaultTZ); + try { + int year = groupToInt(m.group(1), "year", Integer.MIN_VALUE, Integer.MAX_VALUE); + + int era; + // Starting from ISO 8601:2000 Second Edition, 0001 is AD 1, 0000 is BC 1, -0001 is BC 2. + // However, according to http://www.w3.org/TR/2004/REC-xmlschema-2-20041028/, XML schemas are based + // on the earlier version where 0000 didn't exist, and year -1 is BC 1. + if (year <= 0) { + era = GregorianCalendar.BC; + year = -year + (xsMode ? 0 : 1); + if (year == 0) { + throw new DateParseException(MSG_YEAR_0_NOT_ALLOWED); + } + } else { + era = GregorianCalendar.AD; + } + + int month = groupToInt(m.group(2), "month", 1, 12) - 1; + int day = groupToInt(m.group(3), "day-of-month", 1, 31); + + // ISO 8601 allows both 00:00 and 24:00, + // but cal.set(...) doesn't if the Calendar is not lenient. + int hours = groupToInt(m.group(4), "hour-of-day", 0, 24); + boolean hourWas24; + if (hours == 24) { + hours = 0; + hourWas24 = true; + // And a day will be added later... + } else { + hourWas24 = false; + } + + final String minutesStr = m.group(5); + int minutes = minutesStr != null ? groupToInt(minutesStr, "minute", 0, 59) : 0; + + final String secsStr = m.group(6); + // Allow 60 because of leap seconds + int secs = secsStr != null ? groupToInt(secsStr, "second", 0, 60) : 0; + + int millisecs = groupToMillisecond(m.group(7)); + + // As a time is just the distance from the beginning of the day, + // the time-zone offest should be 0 usually. + TimeZone tz = parseMatchingTimeZone(m.group(8), defaultTZ); + + // Continue handling the 24:00 specail case + if (hourWas24) { + if (minutes != 0 || secs != 0 || millisecs != 0) { + throw new DateParseException( + "Hour 24 is only allowed in the case of " + + "midnight."); + } + } + + return calToDateConverter.calculate( + era, year, month, day, hours, minutes, secs, millisecs, hourWas24, tz); + } catch (IllegalArgumentException e) { + // Calendar methods used to throw this for illegal dates. + throw new DateParseException( + "Date-time calculation faliure. " + + "Probably the date-time is formally correct, but " + + "refers to an unexistent date-time " + + "(like February 30)."); + } + } + + /** + * Parses the time zone part from a W3C XML Schema date/time/dateTime. + * @throws DateParseException if the zone is malformed. + */ + public static TimeZone parseXSTimeZone(String timeZoneStr) + throws DateParseException { + Matcher m = PATTERN_XS_TIME_ZONE.matcher(timeZoneStr); + if (!m.matches()) { + throw new DateParseException( + "The time zone offset didn't match the expected pattern: " + PATTERN_XS_TIME_ZONE); + } + return parseMatchingTimeZone(timeZoneStr, null); + } + + private static int groupToInt(String g, String gName, + int min, int max) + throws DateParseException { + if (g == null) { + throw new DateParseException("The " + gName + " part " + + "is missing."); + } + + int start; + + // Remove minus sign, so we can remove the 0-s later: + boolean negative; + if (g.startsWith("-")) { + negative = true; + start = 1; + } else { + negative = false; + start = 0; + } + + // Remove leading 0-s: + while (start < g.length() - 1 && g.charAt(start) == '0') { + start++; + } + if (start != 0) { + g = g.substring(start); + } + + try { + int r = Integer.parseInt(g); + if (negative) { + r = -r; + } + if (r < min) { + throw new DateParseException("The " + gName + " part " + + "must be at least " + min + "."); + } + if (r > max) { + throw new DateParseException("The " + gName + " part " + + "can't be more than " + max + "."); + } + return r; + } catch (NumberFormatException e) { + throw new DateParseException("The " + gName + " part " + + "is a malformed integer."); + } + } + + private static TimeZone parseMatchingTimeZone( + String s, TimeZone defaultZone) + throws DateParseException { + if (s == null) { + return defaultZone; + } + if (s.equals("Z")) { + return _DateUtil.UTC; + } + + StringBuilder sb = new StringBuilder(9); + sb.append("GMT"); + sb.append(s.charAt(0)); + + String h = s.substring(1, 3); + groupToInt(h, "offset-hours", 0, 23); + sb.append(h); + + String m; + int ln = s.length(); + if (ln > 3) { + int startIdx = s.charAt(3) == ':' ? 4 : 3; + m = s.substring(startIdx, startIdx + 2); + groupToInt(m, "offset-minutes", 0, 59); + sb.append(':'); + sb.append(m); + } + + return TimeZone.getTimeZone(sb.toString()); + } + + private static int groupToMillisecond(String g) + throws DateParseException { + if (g == null) { + return 0; + } + + if (g.length() > 3) { + g = g.substring(0, 3); + } + int i = groupToInt(g, "partial-seconds", 0, Integer.MAX_VALUE); + return g.length() == 1 ? i * 100 : (g.length() == 2 ? i * 10 : i); + } + + /** + * Used internally by {@link _DateUtil}; don't use its implementations for + * anything else. + */ + public interface DateToISO8601CalendarFactory { + + /** + * Returns a {@link GregorianCalendar} with the desired time zone and + * time and US locale. The returned calendar is used as read-only. + * It must be guaranteed that within a thread the instance returned last time + * is not in use anymore when this method is called again. + */ + GregorianCalendar get(TimeZone tz, Date date); + + } + + /** + * Used internally by {@link _DateUtil}; don't use its implementations for anything else. + */ + public interface CalendarFieldsToDateConverter { + + /** + * Calculates the {@link Date} from the specified calendar fields. + */ + Date calculate(int era, int year, int month, int day, int hours, int minutes, int secs, int millisecs, + boolean addOneDay, + TimeZone tz); + + } + + /** + * Non-thread-safe factory that hard-references a calendar internally. + */ + public static final class TrivialDateToISO8601CalendarFactory + implements DateToISO8601CalendarFactory { + + private GregorianCalendar calendar; + private TimeZone lastlySetTimeZone; + + @Override + public GregorianCalendar get(TimeZone tz, Date date) { + if (calendar == null) { + calendar = new GregorianCalendar(tz, Locale.US); + calendar.setGregorianChange(new Date(Long.MIN_VALUE)); // never use Julian calendar + } else { + // At least on Java 6, calendar.getTimeZone is slow due to a bug, so we need lastlySetTimeZone. + if (lastlySetTimeZone != tz) { // Deliberately `!=` instead of `!<...>.equals()` + calendar.setTimeZone(tz); + lastlySetTimeZone = tz; + } + } + calendar.setTime(date); + return calendar; + } + + } + + /** + * Non-thread-safe implementation that hard-references a calendar internally. + */ + public static final class TrivialCalendarFieldsToDateConverter + implements CalendarFieldsToDateConverter { + + private GregorianCalendar calendar; + private TimeZone lastlySetTimeZone; + + @Override + public Date calculate(int era, int year, int month, int day, int hours, int minutes, int secs, int millisecs, + boolean addOneDay, TimeZone tz) { + if (calendar == null) { + calendar = new GregorianCalendar(tz, Locale.US); + calendar.setLenient(false); + calendar.setGregorianChange(new Date(Long.MIN_VALUE)); // never use Julian calendar + } else { + // At least on Java 6, calendar.getTimeZone is slow due to a bug, so we need lastlySetTimeZone. + if (lastlySetTimeZone != tz) { // Deliberately `!=` instead of `!<...>.equals()` + calendar.setTimeZone(tz); + lastlySetTimeZone = tz; + } + } + + calendar.set(Calendar.ERA, era); + calendar.set(Calendar.YEAR, year); + calendar.set(Calendar.MONTH, month); + calendar.set(Calendar.DAY_OF_MONTH, day); + calendar.set(Calendar.HOUR_OF_DAY, hours); + calendar.set(Calendar.MINUTE, minutes); + calendar.set(Calendar.SECOND, secs); + calendar.set(Calendar.MILLISECOND, millisecs); + if (addOneDay) { + calendar.add(Calendar.DAY_OF_MONTH, 1); + } + + return calendar.getTime(); + } + + } + + public static final class DateParseException extends ParseException { + + public DateParseException(String message) { + super(message, 0); + } + + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_JavaVersions.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_JavaVersions.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_JavaVersions.java new file mode 100644 index 0000000..10f79fe --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_JavaVersions.java @@ -0,0 +1,80 @@ +/* + * 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.freemarker.core.util; + +import org.apache.freemarker.core.Version; +import org.apache.freemarker.core._CoreLogs; +import org.apache.freemarker.core._Java8; + +/** + * Used internally only, might changes without notice! + */ +public final class _JavaVersions { + + private _JavaVersions() { + // Not meant to be instantiated + } + + private static final boolean IS_AT_LEAST_8; + static { + boolean result = false; + String vStr = _SecurityUtil.getSystemProperty("java.version", null); + if (vStr != null) { + try { + Version v = new Version(vStr); + result = v.getMajor() == 1 && v.getMinor() >= 8 || v.getMajor() > 1; + } catch (Exception e) { + // Ignore + } + } else { + try { + Class.forName("java.time.Instant"); + result = true; + } catch (Exception e) { + // Ignore + } + } + IS_AT_LEAST_8 = result; + } + + /** + * {@code null} if Java 8 is not available, otherwise the object through with the Java 8 operations are available. + */ + static public final _Java8 JAVA_8; + static { + _Java8 java8; + if (IS_AT_LEAST_8) { + try { + java8 = (_Java8) Class.forName("org.apache.freemarker.core._Java8Impl") + .getField("INSTANCE").get(null); + } catch (Exception e) { + try { + _CoreLogs.RUNTIME.error("Failed to access Java 8 functionality", e); + } catch (Exception e2) { + // Suppressed + } + java8 = null; + } + } else { + java8 = null; + } + JAVA_8 = java8; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_KeyValuePair.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_KeyValuePair.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_KeyValuePair.java new file mode 100644 index 0000000..d88d8e4 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_KeyValuePair.java @@ -0,0 +1,61 @@ +/* + * 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.freemarker.core.util; + +public class _KeyValuePair<K, V> { + private final K key; + private final V value; + + public _KeyValuePair(K key, V value) { + this.key = key; + this.value = value; + } + + public K getKey() { + return key; + } + + public V getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + _KeyValuePair<?, ?> that = (_KeyValuePair<?, ?>) o; + + if (key != null ? !key.equals(that.key) : that.key != null) return false; + return value != null ? value.equals(that.value) : that.value == null; + } + + @Override + public int hashCode() { + int result = key != null ? key.hashCode() : 0; + result = 31 * result + (value != null ? value.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "_KeyValuePair{key=" + key + ", value=" + value + '}'; + } +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_LocaleUtil.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_LocaleUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_LocaleUtil.java new file mode 100644 index 0000000..2f09c88 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_LocaleUtil.java @@ -0,0 +1,43 @@ +/* + * 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.freemarker.core.util; + +import java.util.Locale; + +/** + * For internal use only; don't depend on this, there's no backward compatibility guarantee at all! + */ +public class _LocaleUtil { + + /** + * Returns a locale that's one less specific, or {@code null} if there's no less specific locale. + */ + public static Locale getLessSpecificLocale(Locale locale) { + String country = locale.getCountry(); + if (locale.getVariant().length() != 0) { + String language = locale.getLanguage(); + return country != null ? new Locale(language, country) : new Locale(language); + } + if (country.length() != 0) { + return new Locale(locale.getLanguage()); + } + return null; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_NullArgumentException.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_NullArgumentException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_NullArgumentException.java new file mode 100644 index 0000000..5b3ea5f --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_NullArgumentException.java @@ -0,0 +1,59 @@ +/* + * 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.freemarker.core.util; + +/** + * Indicates that an argument that must be non-{@code null} was {@code null}. + * + * @since 2.3.20 + */ +public class _NullArgumentException extends IllegalArgumentException { + + public _NullArgumentException() { + super("The argument can't be null"); + } + + public _NullArgumentException(String argumentName) { + super("The \"" + argumentName + "\" argument can't be null"); + } + + public _NullArgumentException(String argumentName, String details) { + super("The \"" + argumentName + "\" argument can't be null. " + details); + } + + /** + * Convenience method to protect against a {@code null} argument. + */ + public static void check(String argumentName, Object argumentValue) { + if (argumentValue == null) { + throw new _NullArgumentException(argumentName); + } + } + + /** + * @since 2.3.22 + */ + public static void check(Object argumentValue) { + if (argumentValue == null) { + throw new _NullArgumentException(); + } + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_NullWriter.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_NullWriter.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_NullWriter.java new file mode 100644 index 0000000..399fca4 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_NullWriter.java @@ -0,0 +1,90 @@ +/* + * 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.freemarker.core.util; + +import java.io.IOException; +import java.io.Writer; + +/** + * A {@link Writer} that simply drops what it gets. + * + * @since 2.3.20 + */ +public final class _NullWriter extends Writer { + + public static final _NullWriter INSTANCE = new _NullWriter(); + + /** Can't be instantiated; use {@link #INSTANCE}. */ + private _NullWriter() { } + + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + // Do nothing + } + + @Override + public void flush() throws IOException { + // Do nothing + } + + @Override + public void close() throws IOException { + // Do nothing + } + + @Override + public void write(int c) throws IOException { + // Do nothing + } + + @Override + public void write(char[] cbuf) throws IOException { + // Do nothing + } + + @Override + public void write(String str) throws IOException { + // Do nothing + } + + @Override + public void write(String str, int off, int len) throws IOException { + // Do nothing + } + + @Override + public Writer append(CharSequence csq) throws IOException { + // Do nothing + return this; + } + + @Override + public Writer append(CharSequence csq, int start, int end) throws IOException { + // Do nothing + return this; + } + + @Override + public Writer append(char c) throws IOException { + // Do nothing + return this; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_NumberUtil.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_NumberUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_NumberUtil.java new file mode 100644 index 0000000..500a185 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_NumberUtil.java @@ -0,0 +1,228 @@ +/* + * 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.freemarker.core.util; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** Don't use this; used internally by FreeMarker, might changes without notice. */ +public class _NumberUtil { + + private static final BigDecimal BIG_DECIMAL_INT_MIN = BigDecimal.valueOf(Integer.MIN_VALUE); + private static final BigDecimal BIG_DECIMAL_INT_MAX = BigDecimal.valueOf(Integer.MAX_VALUE); + private static final BigInteger BIG_INTEGER_INT_MIN = BIG_DECIMAL_INT_MIN.toBigInteger(); + private static final BigInteger BIG_INTEGER_INT_MAX = BIG_DECIMAL_INT_MAX.toBigInteger(); + private static final BigInteger BIG_INTEGER_LONG_MIN = BigInteger.valueOf(Long.MIN_VALUE); + private static final BigInteger BIG_INTEGER_LONG_MAX = BigInteger.valueOf(Long.MAX_VALUE); + + private _NumberUtil() { } + + public static boolean isInfinite(Number num) { + if (num instanceof Double) { + return ((Double) num).isInfinite(); + } else if (num instanceof Float) { + return ((Float) num).isInfinite(); + } else if (isNonFPNumberOfSupportedClass(num)) { + return false; + } else { + throw new UnsupportedNumberClassException(num.getClass()); + } + } + + public static boolean isNaN(Number num) { + if (num instanceof Double) { + return ((Double) num).isNaN(); + } else if (num instanceof Float) { + return ((Float) num).isNaN(); + } else if (isNonFPNumberOfSupportedClass(num)) { + return false; + } else { + throw new UnsupportedNumberClassException(num.getClass()); + } + } + + /** + * @return -1 for negative, 0 for zero, 1 for positive. + * @throws ArithmeticException if the number is NaN + */ + public static int getSignum(Number num) throws ArithmeticException { + if (num instanceof Integer) { + int n = num.intValue(); + return n > 0 ? 1 : (n == 0 ? 0 : -1); + } else if (num instanceof BigDecimal) { + BigDecimal n = (BigDecimal) num; + return n.signum(); + } else if (num instanceof Double) { + double n = num.doubleValue(); + if (n > 0) return 1; + else if (n == 0) return 0; + else if (n < 0) return -1; + else throw new ArithmeticException("The signum of " + n + " is not defined."); // NaN + } else if (num instanceof Float) { + float n = num.floatValue(); + if (n > 0) return 1; + else if (n == 0) return 0; + else if (n < 0) return -1; + else throw new ArithmeticException("The signum of " + n + " is not defined."); // NaN + } else if (num instanceof Long) { + long n = num.longValue(); + return n > 0 ? 1 : (n == 0 ? 0 : -1); + } else if (num instanceof Short) { + short n = num.shortValue(); + return n > 0 ? 1 : (n == 0 ? 0 : -1); + } else if (num instanceof Byte) { + byte n = num.byteValue(); + return n > 0 ? 1 : (n == 0 ? 0 : -1); + } else if (num instanceof BigInteger) { + BigInteger n = (BigInteger) num; + return n.signum(); + } else { + throw new UnsupportedNumberClassException(num.getClass()); + } + } + + /** + * Tells if a {@link BigDecimal} stores a whole number. For example, it returns {@code true} for {@code 1.0000}, + * but {@code false} for {@code 1.0001}. + * + * @since 2.3.21 + */ + static public boolean isIntegerBigDecimal(BigDecimal bd) { + // [Java 1.5] Try to utilize BigDecimal.toXxxExact methods + return bd.scale() <= 0 // A fast check that whole numbers usually (not always) match + || bd.setScale(0, BigDecimal.ROUND_DOWN).compareTo(bd) == 0; // This is rather slow + // Note that `bd.signum() == 0 || bd.stripTrailingZeros().scale() <= 0` was also tried for the last + // condition, but stripTrailingZeros was slower than setScale + compareTo. + } + + private static boolean isNonFPNumberOfSupportedClass(Number num) { + return num instanceof Integer || num instanceof BigDecimal || num instanceof Long + || num instanceof Short || num instanceof Byte || num instanceof BigInteger; + } + + /** + * Converts a {@link Number} to {@code int} whose mathematical value is exactly the same as of the original number. + * + * @throws ArithmeticException + * if the conversion to {@code int} is not possible without losing precision or overflow/underflow. + * + * @since 2.3.22 + */ + public static int toIntExact(Number num) { + if (num instanceof Integer || num instanceof Short || num instanceof Byte) { + return num.intValue(); + } else if (num instanceof Long) { + final long n = num.longValue(); + final int result = (int) n; + if (n != result) { + throw newLossyConverionException(num, Integer.class); + } + return result; + } else if (num instanceof Double || num instanceof Float) { + final double n = num.doubleValue(); + if (n % 1 != 0 || n < Integer.MIN_VALUE || n > Integer.MAX_VALUE) { + throw newLossyConverionException(num, Integer.class); + } + return (int) n; + } else if (num instanceof BigDecimal) { + // [Java 1.5] Use BigDecimal.toIntegerExact() + BigDecimal n = (BigDecimal) num; + if (!isIntegerBigDecimal(n) + || n.compareTo(BIG_DECIMAL_INT_MAX) > 0 || n.compareTo(BIG_DECIMAL_INT_MIN) < 0) { + throw newLossyConverionException(num, Integer.class); + } + return n.intValue(); + } else if (num instanceof BigInteger) { + BigInteger n = (BigInteger) num; + if (n.compareTo(BIG_INTEGER_INT_MAX) > 0 || n.compareTo(BIG_INTEGER_INT_MIN) < 0) { + throw newLossyConverionException(num, Integer.class); + } + return n.intValue(); + } else { + throw new UnsupportedNumberClassException(num.getClass()); + } + } + + private static ArithmeticException newLossyConverionException(Number fromValue, Class/*<Number>*/ toType) { + return new ArithmeticException( + "Can't convert " + fromValue + " to type " + _ClassUtil.getShortClassName(toType) + " without loss."); + } + + /** + * This is needed to reverse the extreme conversions in arithmetic + * operations so that numbers can be meaningfully used with models that + * don't know what to do with a BigDecimal. Of course, this will make + * impossible for these models (i.e. Jython) to receive a BigDecimal even if + * it was originally placed as such in the data model. However, since + * arithmetic operations aggressively erase the information regarding the + * original number type, we have no other choice to ensure expected operation + * in majority of cases. + */ + public static Number optimizeNumberRepresentation(Number number) { + if (number instanceof BigDecimal) { + BigDecimal bd = (BigDecimal) number; + if (bd.scale() == 0) { + // BigDecimal -> BigInteger + number = bd.unscaledValue(); + } else { + double d = bd.doubleValue(); + if (d != Double.POSITIVE_INFINITY && d != Double.NEGATIVE_INFINITY) { + // BigDecimal -> Double + return Double.valueOf(d); + } + } + } + if (number instanceof BigInteger) { + BigInteger bi = (BigInteger) number; + if (bi.compareTo(BIG_INTEGER_INT_MAX) <= 0 && bi.compareTo(BIG_INTEGER_INT_MIN) >= 0) { + // BigInteger -> Integer + return Integer.valueOf(bi.intValue()); + } + if (bi.compareTo(BIG_INTEGER_LONG_MAX) <= 0 && bi.compareTo(BIG_INTEGER_LONG_MIN) >= 0) { + // BigInteger -> Long + return Long.valueOf(bi.longValue()); + } + } + return number; + } + + public static BigDecimal toBigDecimal(Number num) { + try { + return num instanceof BigDecimal ? (BigDecimal) num : new BigDecimal(num.toString()); + } catch (NumberFormatException e) { + // The exception message is useless, so we add a new one: + throw new NumberFormatException("Can't parse this as BigDecimal number: " + _StringUtil.jQuote(num)); + } + } + + public static Number toBigDecimalOrDouble(String s) { + if (s.length() > 2) { + char c = s.charAt(0); + if (c == 'I' && (s.equals("INF") || s.equals("Infinity"))) { + return Double.valueOf(Double.POSITIVE_INFINITY); + } else if (c == 'N' && s.equals("NaN")) { + return Double.valueOf(Double.NaN); + } else if (c == '-' && s.charAt(1) == 'I' && (s.equals("-INF") || s.equals("-Infinity"))) { + return Double.valueOf(Double.NEGATIVE_INFINITY); + } + } + return new BigDecimal(s); + } +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ObjectHolder.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ObjectHolder.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ObjectHolder.java new file mode 100644 index 0000000..cbd7e11 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ObjectHolder.java @@ -0,0 +1,55 @@ +/* + * 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.freemarker.core.util; + +/** + * For internal use only; don't depend on this, there's no backward compatibility guarantee at all! + */ +public class _ObjectHolder<T> { + + private T object; + + public _ObjectHolder(T object) { + this.object = object; + } + + public T get() { + return object; + } + + public void set(T object) { + this.object = object; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + _ObjectHolder<?> that = (_ObjectHolder<?>) o; + + return object != null ? object.equals(that.object) : that.object == null; + } + + @Override + public int hashCode() { + return object != null ? object.hashCode() : 0; + } +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_SecurityUtil.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_SecurityUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_SecurityUtil.java new file mode 100644 index 0000000..60125a5 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_SecurityUtil.java @@ -0,0 +1,87 @@ +/* + * 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.freemarker.core.util; + +import java.security.AccessControlException; +import java.security.AccessController; +import java.security.PrivilegedAction; + +import org.apache.freemarker.core._CoreLogs; +import org.slf4j.Logger; + +/** + */ +public class _SecurityUtil { + + private static final Logger LOG = _CoreLogs.SECURITY; + + private _SecurityUtil() { + } + + public static String getSystemProperty(final String key) { + return (String) AccessController.doPrivileged( + new PrivilegedAction() + { + @Override + public Object run() { + return System.getProperty(key); + } + }); + } + + public static String getSystemProperty(final String key, final String defValue) { + try { + return (String) AccessController.doPrivileged( + new PrivilegedAction() + { + @Override + public Object run() { + return System.getProperty(key, defValue); + } + }); + } catch (AccessControlException e) { + if (LOG.isWarnEnabled()) { + LOG.warn("Insufficient permissions to read system property " + + _StringUtil.jQuoteNoXSS(key) + ", using default value " + + _StringUtil.jQuoteNoXSS(defValue)); + } + return defValue; + } + } + + public static Integer getSystemProperty(final String key, final int defValue) { + try { + return (Integer) AccessController.doPrivileged( + new PrivilegedAction() + { + @Override + public Object run() { + return Integer.getInteger(key, defValue); + } + }); + } catch (AccessControlException e) { + if (LOG.isWarnEnabled()) { + LOG.warn("Insufficient permissions to read system property " + + _StringUtil.jQuote(key) + ", using default value " + defValue); + } + return Integer.valueOf(defValue); + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_SortedArraySet.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_SortedArraySet.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_SortedArraySet.java new file mode 100644 index 0000000..e60d08d --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_SortedArraySet.java @@ -0,0 +1,80 @@ +/* + * 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.freemarker.core.util; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; + +/** Don't use this; used internally by FreeMarker, might changes without notice. */ +public class _SortedArraySet<E> extends _UnmodifiableSet<E> { + + private final E[] array; + + public _SortedArraySet(E[] array) { + this.array = array; + } + + @Override + public int size() { + return array.length; + } + + @Override + public boolean contains(Object o) { + return Arrays.binarySearch(array, o) >= 0; + } + + @Override + public Iterator<E> iterator() { + return new _ArrayIterator(array); + } + + @Override + public boolean add(E o) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection<? extends E> c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(Collection<?> c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(Collection<?> c) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + +}
