http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/InvalidFormatStringException.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/InvalidFormatStringException.java b/src/main/java/org/apache/freemarker/core/valueformat/InvalidFormatStringException.java new file mode 100644 index 0000000..091d55c --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/InvalidFormatStringException.java @@ -0,0 +1,41 @@ +/* + * 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.valueformat; + +import org.apache.freemarker.core.valueformat.TemplateDateFormat; +import org.apache.freemarker.core.valueformat.TemplateNumberFormat; +import org.apache.freemarker.core.valueformat.TemplateValueFormatException; + +/** + * Used when creating {@link TemplateDateFormat}-s and {@link TemplateNumberFormat}-s to indicate that the format + * string (like the value of the {@code dateFormat} setting) is malformed. + * + * @since 2.3.24 + */ +public abstract class InvalidFormatStringException extends TemplateValueFormatException { + + public InvalidFormatStringException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidFormatStringException(String message) { + this(message, null); + } + +}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/ParsingNotSupportedException.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/ParsingNotSupportedException.java b/src/main/java/org/apache/freemarker/core/valueformat/ParsingNotSupportedException.java new file mode 100644 index 0000000..8700c88 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/ParsingNotSupportedException.java @@ -0,0 +1,40 @@ +/* + * 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.valueformat; + +import org.apache.freemarker.core.valueformat.TemplateValueFormat; +import org.apache.freemarker.core.valueformat.TemplateValueFormatException; + +/** + * Thrown when the {@link TemplateValueFormat} doesn't support parsing, and parsing was invoked. + * + * @since 2.3.24 + */ +public class ParsingNotSupportedException extends TemplateValueFormatException { + + public ParsingNotSupportedException(String message, Throwable cause) { + super(message, cause); + } + + public ParsingNotSupportedException(String message) { + this(message, null); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/TemplateDateFormat.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/TemplateDateFormat.java b/src/main/java/org/apache/freemarker/core/valueformat/TemplateDateFormat.java new file mode 100644 index 0000000..0154b67 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/TemplateDateFormat.java @@ -0,0 +1,110 @@ +/* + * 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.valueformat; + +import java.text.DateFormat; +import java.util.Date; + +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.model.TemplateDateModel; +import org.apache.freemarker.core.model.TemplateMarkupOutputModel; +import org.apache.freemarker.core.model.TemplateModelException; + +/** + * Represents a date/time/dateTime format; used in templates for formatting and parsing with that format. This is + * similar to Java's {@link DateFormat}, but made to fit the requirements of FreeMarker. Also, it makes easier to define + * formats that can't be represented with Java's existing {@link DateFormat} implementations. + * + * <p> + * Implementations need not be thread-safe if the {@link TemplateNumberFormatFactory} doesn't recycle them among + * different {@link Environment}-s. As far as FreeMarker's concerned, instances are bound to a single + * {@link Environment}, and {@link Environment}-s are thread-local objects. + * + * @since 2.3.24 + */ +public abstract class TemplateDateFormat extends TemplateValueFormat { + + /** + * @param dateModel + * The date/time/dateTime to format; not {@code null}. Most implementations will just work with the return value of + * {@link TemplateDateModel#getAsDate()}, but some may format differently depending on the properties of + * a custom {@link TemplateDateModel} implementation. + * + * @return The date/time/dateTime as text, with no escaping (like no HTML escaping); can't be {@code null}. + * + * @throws TemplateValueFormatException + * When a problem occurs during the formatting of the value. Notable subclass: + * {@link UnknownDateTypeFormattingUnsupportedException} + * @throws TemplateModelException + * Exception thrown by the {@code dateModel} object when calling its methods. + */ + public abstract String formatToPlainText(TemplateDateModel dateModel) + throws TemplateValueFormatException, TemplateModelException; + + /** + * Formats the model to markup instead of to plain text if the result markup will be more than just plain text + * escaped, otherwise falls back to formatting to plain text. If the markup result would be just the result of + * {@link #formatToPlainText(TemplateDateModel)} escaped, it must return the {@link String} that + * {@link #formatToPlainText(TemplateDateModel)} does. + * + * <p>The implementation in {@link TemplateDateFormat} simply calls {@link #formatToPlainText(TemplateDateModel)}. + * + * @return A {@link String} or a {@link TemplateMarkupOutputModel}; not {@code null}. + */ + public Object format(TemplateDateModel dateModel) throws TemplateValueFormatException, TemplateModelException { + return formatToPlainText(dateModel); + } + + /** + * Parsers a string to date/time/datetime, according to this format. Some format implementations may throw + * {@link ParsingNotSupportedException} here. + * + * @param s + * The string to parse + * @param dateType + * The expected date type of the result. Not all {@link TemplateDateFormat}-s will care about this; + * though those who return a {@link TemplateDateModel} instead of {@link Date} often will. When strings + * are parsed via {@code ?date}, {@code ?time}, or {@code ?datetime}, then this parameter is + * {@link TemplateDateModel#DATE}, {@link TemplateDateModel#TIME}, or {@link TemplateDateModel#DATETIME}, + * respectively. This parameter rarely if ever {@link TemplateDateModel#UNKNOWN}, but the implementation + * that cares about this parameter should be prepared for that. If nothing else, it should throw + * {@link UnknownDateTypeParsingUnsupportedException} then. + * + * @return The interpretation of the text either as a {@link Date} or {@link TemplateDateModel}. Typically, a + * {@link Date}. {@link TemplateDateModel} is used if you have to attach some application-specific + * meta-information thats also extracted during {@link #formatToPlainText(TemplateDateModel)} (so if you format + * something and then parse it, you get back an equivalent result). It can't be {@code null}. Known issue + * (at least in FTL 2): {@code ?date}/{@code ?time}/{@code ?datetime}, when not invoked as a method, can't + * return the {@link TemplateDateModel}, only the {@link Date} from inside it, hence the additional + * application-specific meta-info will be lost. + */ + public abstract Object parse(String s, int dateType) throws TemplateValueFormatException; + + /** + * Tells if this formatter should be re-created if the locale changes. + */ + public abstract boolean isLocaleBound(); + + /** + * Tells if this formatter should be re-created if the time zone changes. Currently always {@code true}. + */ + public abstract boolean isTimeZoneBound(); + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/TemplateDateFormatFactory.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/TemplateDateFormatFactory.java b/src/main/java/org/apache/freemarker/core/valueformat/TemplateDateFormatFactory.java new file mode 100644 index 0000000..2abbaa9 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/TemplateDateFormatFactory.java @@ -0,0 +1,92 @@ +/* + * 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.valueformat; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +import org.apache.freemarker.core.*; +import org.apache.freemarker.core.model.TemplateDateModel; + +/** + * Factory for a certain kind of date/time/dateTime formatting ({@link TemplateDateFormat}). Usually a singleton + * (one-per-VM or one-per-{@link Configuration}), and so must be thread-safe. + * + * @see Configurable#setCustomDateFormats(java.util.Map) + * + * @since 2.3.24 + */ +public abstract class TemplateDateFormatFactory extends TemplateValueFormatFactory { + + /** + * Returns a formatter for the given parameters. + * + * <p> + * The returned formatter can be a new instance or a reused (cached) instance. Note that {@link Environment} itself + * caches the returned instances, though that cache is lost with the {@link Environment} (i.e., when the top-level + * template execution ends), also it might flushes lot of entries if the locale or time zone is changed during + * template execution. So caching on the factory level is still useful, unless creating the formatters is + * sufficiently cheap. + * + * @param params + * The string that further describes how the format should look. For example, when the + * {@link Configurable#getDateFormat() dateFormat} is {@code "@fooBar 1, 2"}, then it will be + * {@code "1, 2"} (and {@code "@fooBar"} selects the factory). The format of this string is up to the + * {@link TemplateDateFormatFactory} implementation. Not {@code null}, often an empty string. + * @param dateType + * {@link TemplateDateModel#DATE}, {@link TemplateDateModel#TIME}, {@link TemplateDateModel#DATETIME} or + * {@link TemplateDateModel#UNKNOWN}. Supporting {@link TemplateDateModel#UNKNOWN} is not necessary, in + * which case the method should throw an {@link UnknownDateTypeFormattingUnsupportedException} exception. + * @param locale + * The locale to format for. Not {@code null}. The resulting format should be bound to this locale + * forever (i.e. locale changes in the {@link Environment} must not be followed). + * @param timeZone + * The time zone to format for. Not {@code null}. The resulting format must be bound to this time zone + * forever (i.e. time zone changes in the {@link Environment} must not be followed). + * @param zonelessInput + * Indicates that the input Java {@link Date} is not from a time zone aware source. When this is + * {@code true}, the formatters shouldn't override the time zone provided to its constructor (most + * formatters don't do that anyway), and it shouldn't show the time zone, if it can hide it (like a + * {@link SimpleDateFormat} pattern-based formatter may can't do that, as the pattern prescribes what to + * show). + * <p> + * As of FreeMarker 2.3.21, this is {@code true} exactly when the date is an SQL "date without time of + * the day" (i.e., a {@link java.sql.Date java.sql.Date}) or an SQL "time of the day" value (i.e., a + * {@link java.sql.Time java.sql.Time}, although this rule can change in future, depending on + * configuration settings and such, so you shouldn't rely on this rule, just accept what this parameter + * says. + * @param env + * The runtime environment from which the formatting was called. This is mostly meant to be used for + * {@link Environment#setCustomState(Object, Object)}/{@link Environment#getCustomState(Object)}. + * + * @throws TemplateValueFormatException + * If any problem occurs while parsing/getting the format. Notable subclasses: + * {@link InvalidFormatParametersException} if {@code params} is malformed; + * {@link UnknownDateTypeFormattingUnsupportedException} if {@code dateType} is + * {@link TemplateDateModel#UNKNOWN} and that's unsupported by this factory. + */ + public abstract TemplateDateFormat get( + String params, + int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput, + Environment env) + throws TemplateValueFormatException; + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/TemplateFormatUtil.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/TemplateFormatUtil.java b/src/main/java/org/apache/freemarker/core/valueformat/TemplateFormatUtil.java new file mode 100644 index 0000000..0cff2fe --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/TemplateFormatUtil.java @@ -0,0 +1,77 @@ +/* + * 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.valueformat; + +import org.apache.freemarker.core._EvalUtil; +import org.apache.freemarker.core.model.ObjectWrapper; +import org.apache.freemarker.core.model.TemplateDateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.model.TemplateNumberModel; + +import java.util.Date; + +/** + * Utility classes for implementing {@link TemplateValueFormat}-s. + * + * @since 2.3.24 + */ +public final class TemplateFormatUtil { + + private TemplateFormatUtil() { + // Not meant to be instantiated + } + + public static void checkHasNoParameters(String params) throws InvalidFormatParametersException + { + if (params.length() != 0) { + throw new InvalidFormatParametersException( + "This number format doesn't support any parameters."); + } + } + + /** + * Utility method to extract the {@link Number} from an {@link TemplateNumberModel}, and throws + * {@link TemplateModelException} with a standard error message if that's {@code null}. {@link TemplateNumberModel} + * that store {@code null} are in principle not allowed, and so are considered to be bugs in the + * {@link ObjectWrapper} or {@link TemplateNumberModel} implementation. + */ + public static Number getNonNullNumber(TemplateNumberModel numberModel) + throws TemplateModelException, UnformattableValueException { + Number number = numberModel.getAsNumber(); + if (number == null) { + throw _EvalUtil.newModelHasStoredNullException(Number.class, numberModel, null); + } + return number; + } + + /** + * Utility method to extract the {@link Date} from an {@link TemplateDateModel}, and throw + * {@link TemplateModelException} with a standard error message if that's {@code null}. {@link TemplateDateModel} + * that store {@code null} are in principle not allowed, and so are considered to be bugs in the + * {@link ObjectWrapper} or {@link TemplateNumberModel} implementation. + */ + public static Date getNonNullDate(TemplateDateModel dateModel) throws TemplateModelException { + Date date = dateModel.getAsDate(); + if (date == null) { + throw _EvalUtil.newModelHasStoredNullException(Date.class, dateModel, null); + } + return date; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormat.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormat.java b/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormat.java new file mode 100644 index 0000000..44068e4 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormat.java @@ -0,0 +1,93 @@ +/* + * 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.valueformat; + +import java.text.NumberFormat; + +import org.apache.freemarker.core.ArithmeticEngine; +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.model.TemplateDateModel; +import org.apache.freemarker.core.model.TemplateMarkupOutputModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.model.TemplateNumberModel; + +/** + * Represents a number format; used in templates for formatting and parsing with that format. This is similar to Java's + * {@link NumberFormat}, but made to fit the requirements of FreeMarker. Also, it makes easier to define formats that + * can't be represented with Java's existing {@link NumberFormat} implementations. + * + * <p> + * Implementations need not be thread-safe if the {@link TemplateNumberFormatFactory} doesn't recycle them among + * different {@link Environment}-s. As far as FreeMarker's concerned, instances are bound to a single + * {@link Environment}, and {@link Environment}-s are thread-local objects. + * + * @since 2.3.24 + */ +public abstract class TemplateNumberFormat extends TemplateValueFormat { + + /** + * @param numberModel + * The number to format; not {@code null}. Most implementations will just work with the return value of + * {@link TemplateDateModel#getAsDate()}, but some may format differently depending on the properties of + * a custom {@link TemplateDateModel} implementation. + * + * @return The number as text, with no escaping (like no HTML escaping); can't be {@code null}. + * + * @throws TemplateValueFormatException + * If any problem occurs while parsing/getting the format. Notable subclass: + * {@link UnformattableValueException}. + * @throws TemplateModelException + * Exception thrown by the {@code dateModel} object when calling its methods. + */ + public abstract String formatToPlainText(TemplateNumberModel numberModel) + throws TemplateValueFormatException, TemplateModelException; + + /** + * Formats the model to markup instead of to plain text if the result markup will be more than just plain text + * escaped, otherwise falls back to formatting to plain text. If the markup result would be just the result of + * {@link #formatToPlainText(TemplateNumberModel)} escaped, it must return the {@link String} that + * {@link #formatToPlainText(TemplateNumberModel)} does. + * + * <p> + * The implementation in {@link TemplateNumberFormat} simply calls {@link #formatToPlainText(TemplateNumberModel)}. + * + * @return A {@link String} or a {@link TemplateMarkupOutputModel}; not {@code null}. + */ + public Object format(TemplateNumberModel numberModel) + throws TemplateValueFormatException, TemplateModelException { + return formatToPlainText(numberModel); + } + + /** + * Tells if this formatter should be re-created if the locale changes. + */ + public abstract boolean isLocaleBound(); + + /** + * This method is reserved for future purposes; currently it always throws {@link ParsingNotSupportedException}. We + * don't yet support number parsing with {@link TemplateNumberFormat}-s, because currently FTL parses strings to + * number with the {@link ArithmeticEngine} ({@link TemplateNumberFormat} were only introduced in 2.3.24). If it + * will be support, it will be similar to {@link TemplateDateFormat#parse(String, int)}. + */ + public final Object parse(String s) throws TemplateValueFormatException { + throw new ParsingNotSupportedException("Number formats currenly don't support parsing"); + } + + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormatFactory.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormatFactory.java b/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormatFactory.java new file mode 100644 index 0000000..e2ae8c2 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormatFactory.java @@ -0,0 +1,66 @@ +/* + * 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.valueformat; + +import org.apache.freemarker.core.Configurable; +import org.apache.freemarker.core.Configuration; +import org.apache.freemarker.core.Environment; + +import java.util.Locale; + +/** + * Factory for a certain kind of number formatting ({@link TemplateNumberFormat}). Usually a singleton (one-per-VM or + * one-per-{@link Configuration}), and so must be thread-safe. + * + * @see Configurable#setCustomNumberFormats(java.util.Map) + * + * @since 2.3.24 + */ +public abstract class TemplateNumberFormatFactory extends TemplateValueFormatFactory { + + /** + * Returns a formatter for the given parameters. + * + * <p> + * The returned formatter can be a new instance or a reused (cached) instance. Note that {@link Environment} itself + * caches the returned instances, though that cache is lost with the {@link Environment} (i.e., when the top-level + * template execution ends), also it might flushes lot of entries if the locale or time zone is changed during + * template execution. So caching on the factory level is still useful, unless creating the formatters is + * sufficiently cheap. + * + * @param params + * The string that further describes how the format should look. For example, when the + * {@link Configurable#getNumberFormat() numberFormat} is {@code "@fooBar 1, 2"}, then it will be + * {@code "1, 2"} (and {@code "@fooBar"} selects the factory). The format of this string is up to the + * {@link TemplateNumberFormatFactory} implementation. Not {@code null}, often an empty string. + * @param locale + * The locale to format for. Not {@code null}. The resulting format must be bound to this locale + * forever (i.e. locale changes in the {@link Environment} must not be followed). + * @param env + * The runtime environment from which the formatting was called. This is mostly meant to be used for + * {@link Environment#setCustomState(Object, Object)}/{@link Environment#getCustomState(Object)}. + * + * @throws TemplateValueFormatException + * if any problem occurs while parsing/getting the format. Notable subclasses: + * {@link InvalidFormatParametersException} if the {@code params} is malformed. + */ + public abstract TemplateNumberFormat get(String params, Locale locale, Environment env) + throws TemplateValueFormatException; + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormat.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormat.java b/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormat.java new file mode 100644 index 0000000..ffc61b2 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormat.java @@ -0,0 +1,33 @@ +/* + * 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.valueformat; + +/** + * Superclass of all value format objects; objects that convert values to strings, or parse strings. + * + * @since 2.3.24 + */ +public abstract class TemplateValueFormat { + + /** + * Meant to be used in error messages to tell what format the parsed string didn't fit. + */ + public abstract String getDescription(); + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatException.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatException.java b/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatException.java new file mode 100644 index 0000000..a51abdc --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatException.java @@ -0,0 +1,37 @@ +/* + * 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.valueformat; + +/** + * Error while getting, creating or applying {@link TemplateValueFormat}-s (including its subclasses, like + * {@link TemplateNumberFormat}). + * + * @since 2.3.24 + */ +public abstract class TemplateValueFormatException extends Exception { + + public TemplateValueFormatException(String message, Throwable cause) { + super(message, cause); + } + + public TemplateValueFormatException(String message) { + super(message); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatFactory.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatFactory.java b/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatFactory.java new file mode 100644 index 0000000..c6b77b8 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatFactory.java @@ -0,0 +1,28 @@ +/* + * 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.valueformat; + +/** + * Superclass of all format factories. + * + * @since 2.3.24 + */ +public abstract class TemplateValueFormatFactory { + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/UndefinedCustomFormatException.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/UndefinedCustomFormatException.java b/src/main/java/org/apache/freemarker/core/valueformat/UndefinedCustomFormatException.java new file mode 100644 index 0000000..7f380b4 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/UndefinedCustomFormatException.java @@ -0,0 +1,34 @@ +/* + * 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.valueformat; + +/** + * @since 2.3.24 + */ +public class UndefinedCustomFormatException extends InvalidFormatStringException { + + public UndefinedCustomFormatException(String message, Throwable cause) { + super(message, cause); + } + + public UndefinedCustomFormatException(String message) { + super(message); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/UnformattableValueException.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/UnformattableValueException.java b/src/main/java/org/apache/freemarker/core/valueformat/UnformattableValueException.java new file mode 100644 index 0000000..ebece36 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/UnformattableValueException.java @@ -0,0 +1,41 @@ +/* + * 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.valueformat; + +import org.apache.freemarker.core.model.TemplateModel; + +/** + * Thrown when a {@link TemplateModel} can't be formatted because of the value/properties of it are outside of that the + * {@link TemplateValueFormat} supports. For example, a formatter may not support dates before year 1, or can't format + * NaN. The most often used subclass is {@link UnknownDateTypeFormattingUnsupportedException}. + * + * @since 2.3.24 + */ +public class UnformattableValueException extends TemplateValueFormatException { + + public UnformattableValueException(String message, Throwable cause) { + super(message, cause); + } + + public UnformattableValueException(String message) { + super(message); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeFormattingUnsupportedException.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeFormattingUnsupportedException.java b/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeFormattingUnsupportedException.java new file mode 100644 index 0000000..e9c35b9 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeFormattingUnsupportedException.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.valueformat; + +import org.apache.freemarker.core.model.TemplateDateModel; + +/** + * Thrown when a {@link TemplateDateModel} can't be formatted because its type is {@link TemplateDateModel#UNKNOWN}. + * + * @since 2.3.24 + */ +public final class UnknownDateTypeFormattingUnsupportedException extends UnformattableValueException { + + public UnknownDateTypeFormattingUnsupportedException() { + super("Can't format the date-like value because it isn't " + + "known if it's desired result should be a date (no time part), a time, or a date-time value."); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeParsingUnsupportedException.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeParsingUnsupportedException.java b/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeParsingUnsupportedException.java new file mode 100644 index 0000000..802e2d1 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeParsingUnsupportedException.java @@ -0,0 +1,37 @@ +/* + * 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.valueformat; + +import org.apache.freemarker.core.model.TemplateDateModel; + +/** + * Thrown when a string can't be parsed to {@link TemplateDateModel}, because the provided target type is + * {@link TemplateDateModel#UNKNOWN}. + * + * @since 2.3.24 + */ +public final class UnknownDateTypeParsingUnsupportedException extends UnformattableValueException { + + public UnknownDateTypeParsingUnsupportedException() { + super("Can't parse the string to date-like value because it isn't " + + "known if it's desired result should be a date (no time part), a time, or a date-time value."); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/UnparsableValueException.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/UnparsableValueException.java b/src/main/java/org/apache/freemarker/core/valueformat/UnparsableValueException.java new file mode 100644 index 0000000..8dc6ab1 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/UnparsableValueException.java @@ -0,0 +1,41 @@ +/* + * 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.valueformat; + +import org.apache.freemarker.core.valueformat.TemplateValueFormat; +import org.apache.freemarker.core.valueformat.TemplateValueFormatException; + +/** + * Thrown when the content of the string that should be parsed by the {@link TemplateValueFormat} doesn't match what the + * format expects. + * + * @since 2.3.24 + */ +public class UnparsableValueException extends TemplateValueFormatException { + + public UnparsableValueException(String message, Throwable cause) { + super(message, cause); + } + + public UnparsableValueException(String message) { + this(message, null); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTargetTemplateValueFormatException.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTargetTemplateValueFormatException.java b/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTargetTemplateValueFormatException.java new file mode 100644 index 0000000..631af5d --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTargetTemplateValueFormatException.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.freemarker.core.valueformat.impl; + +import org.apache.freemarker.core.valueformat.TemplateValueFormatException; + +/** + * Can't create a template format that the template format refers to (typically thrown by alias template formats). + * + * @since 2.3.24 + */ +class AliasTargetTemplateValueFormatException extends TemplateValueFormatException { + + public AliasTargetTemplateValueFormatException(String message, Throwable cause) { + super(message, cause); + } + + public AliasTargetTemplateValueFormatException(String message) { + super(message); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateDateFormatFactory.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateDateFormatFactory.java b/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateDateFormatFactory.java new file mode 100644 index 0000000..aefb252 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateDateFormatFactory.java @@ -0,0 +1,97 @@ +/* + * 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.valueformat.impl; + +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; + +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.valueformat.TemplateFormatUtil; +import org.apache.freemarker.core.util._LocaleUtil; +import org.apache.freemarker.core.util._StringUtil; +import org.apache.freemarker.core.valueformat.TemplateDateFormat; +import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory; +import org.apache.freemarker.core.valueformat.TemplateValueFormatException; + +/** + * Creates an alias to another format, so that the format can be referred to with a simple name in the template, rather + * than as a concrete pattern or other kind of format string. + * + * @since 2.3.24 + */ +public final class AliasTemplateDateFormatFactory extends TemplateDateFormatFactory { + + private final String defaultTargetFormatString; + private final Map<Locale, String> localizedTargetFormatStrings; + + /** + * @param targetFormatString + * The format string this format will be an alias to. + */ + public AliasTemplateDateFormatFactory(String targetFormatString) { + defaultTargetFormatString = targetFormatString; + localizedTargetFormatStrings = null; + } + + /** + * @param defaultTargetFormatString + * The format string this format will be an alias to if there's no locale-specific format string for the + * requested locale in {@code localizedTargetFormatStrings} + * @param localizedTargetFormatStrings + * Maps {@link Locale}-s to format strings. If the desired locale doesn't occur in the map, a less + * specific locale is tried, repeatedly until only the language part remains. For example, if locale is + * {@code new Locale("en", "US", "Linux")}, then these keys will be attempted untol a match is found, in + * this order: {@code new Locale("en", "US", "Linux")}, {@code new Locale("en", "US")}, + * {@code new Locale("en")}. If there's still no matching key, the value of the + * {@code targetFormatString} will be used. + */ + public AliasTemplateDateFormatFactory( + String defaultTargetFormatString, Map<Locale, String> localizedTargetFormatStrings) { + this.defaultTargetFormatString = defaultTargetFormatString; + this.localizedTargetFormatStrings = localizedTargetFormatStrings; + } + + @Override + public TemplateDateFormat get(String params, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput, + Environment env) throws TemplateValueFormatException { + TemplateFormatUtil.checkHasNoParameters(params); + try { + String targetFormatString; + if (localizedTargetFormatStrings != null) { + Locale lookupLocale = locale; + targetFormatString = localizedTargetFormatStrings.get(lookupLocale); + while (targetFormatString == null + && (lookupLocale = _LocaleUtil.getLessSpecificLocale(lookupLocale)) != null) { + targetFormatString = localizedTargetFormatStrings.get(lookupLocale); + } + } else { + targetFormatString = null; + } + if (targetFormatString == null) { + targetFormatString = defaultTargetFormatString; + } + return env.getTemplateDateFormat(targetFormatString, dateType, locale, timeZone, zonelessInput); + } catch (TemplateValueFormatException e) { + throw new AliasTargetTemplateValueFormatException("Failed to create format based on target format string, " + + _StringUtil.jQuote(params) + ". Reason given: " + e.getMessage(), e); + } + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateNumberFormatFactory.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateNumberFormatFactory.java b/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateNumberFormatFactory.java new file mode 100644 index 0000000..6e72a61 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateNumberFormatFactory.java @@ -0,0 +1,96 @@ +/* + * 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.valueformat.impl; + +import java.util.Locale; +import java.util.Map; + +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.valueformat.TemplateFormatUtil; +import org.apache.freemarker.core.util._LocaleUtil; +import org.apache.freemarker.core.util._StringUtil; +import org.apache.freemarker.core.valueformat.TemplateNumberFormat; +import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory; +import org.apache.freemarker.core.valueformat.TemplateValueFormatException; + +/** + * Creates an alias to another format, so that the format can be referred to with a simple name in the template, rather + * than as a concrete pattern or other kind of format string. + * + * @since 2.3.24 + */ +public final class AliasTemplateNumberFormatFactory extends TemplateNumberFormatFactory { + + private final String defaultTargetFormatString; + private final Map<Locale, String> localizedTargetFormatStrings; + + /** + * @param targetFormatString + * The format string this format will be an alias to + */ + public AliasTemplateNumberFormatFactory(String targetFormatString) { + defaultTargetFormatString = targetFormatString; + localizedTargetFormatStrings = null; + } + + /** + * @param defaultTargetFormatString + * The format string this format will be an alias to if there's no locale-specific format string for the + * requested locale in {@code localizedTargetFormatStrings} + * @param localizedTargetFormatStrings + * Maps {@link Locale}-s to format strings. If the desired locale doesn't occur in the map, a less + * specific locale is tried, repeatedly until only the language part remains. For example, if locale is + * {@code new Locale("en", "US", "Linux")}, then these keys will be attempted untol a match is found, in + * this order: {@code new Locale("en", "US", "Linux")}, {@code new Locale("en", "US")}, + * {@code new Locale("en")}. If there's still no matching key, the value of the + * {@code targetFormatString} will be used. + */ + public AliasTemplateNumberFormatFactory( + String defaultTargetFormatString, Map<Locale, String> localizedTargetFormatStrings) { + this.defaultTargetFormatString = defaultTargetFormatString; + this.localizedTargetFormatStrings = localizedTargetFormatStrings; + } + + @Override + public TemplateNumberFormat get(String params, Locale locale, Environment env) + throws TemplateValueFormatException { + TemplateFormatUtil.checkHasNoParameters(params); + try { + String targetFormatString; + if (localizedTargetFormatStrings != null) { + Locale lookupLocale = locale; + targetFormatString = localizedTargetFormatStrings.get(lookupLocale); + while (targetFormatString == null + && (lookupLocale = _LocaleUtil.getLessSpecificLocale(lookupLocale)) != null) { + targetFormatString = localizedTargetFormatStrings.get(lookupLocale); + } + } else { + targetFormatString = null; + } + if (targetFormatString == null) { + targetFormatString = defaultTargetFormatString; + } + return env.getTemplateNumberFormat(targetFormatString, locale); + } catch (TemplateValueFormatException e) { + throw new AliasTargetTemplateValueFormatException("Failed to create format based on target format string, " + + _StringUtil.jQuote(params) + ". Reason given: " + e.getMessage(), e); + } + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/impl/ExtendedDecimalFormatParser.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/impl/ExtendedDecimalFormatParser.java b/src/main/java/org/apache/freemarker/core/valueformat/impl/ExtendedDecimalFormatParser.java new file mode 100644 index 0000000..2a51ef8 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/impl/ExtendedDecimalFormatParser.java @@ -0,0 +1,530 @@ +/* + * 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.valueformat.impl; + +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.ParseException; +import java.util.Arrays; +import java.util.Currency; +import java.util.HashMap; +import java.util.Locale; +import java.util.Set; + +import org.apache.freemarker.core.util._StringUtil; + +/** + * Parses a {@link DecimalFormat} pattern string to a {@link DecimalFormat} instance, with the pattern string extensions + * described in the Manual (see "Extended Java decimal format"). The result is a standard {@link DecimalFormat} object, + * but further configured according the extension part. + */ +class ExtendedDecimalFormatParser { + + private static final String PARAM_ROUNDING_MODE = "roundingMode"; + private static final String PARAM_MULTIPIER = "multipier"; + private static final String PARAM_DECIMAL_SEPARATOR = "decimalSeparator"; + private static final String PARAM_MONETARY_DECIMAL_SEPARATOR = "monetaryDecimalSeparator"; + private static final String PARAM_GROUP_SEPARATOR = "groupingSeparator"; + private static final String PARAM_EXPONENT_SEPARATOR = "exponentSeparator"; + private static final String PARAM_MINUS_SIGN = "minusSign"; + private static final String PARAM_INFINITY = "infinity"; + private static final String PARAM_NAN = "nan"; + private static final String PARAM_PERCENT = "percent"; + private static final String PARAM_PER_MILL = "perMill"; + private static final String PARAM_ZERO_DIGIT = "zeroDigit"; + private static final String PARAM_CURRENCY_CODE = "currencyCode"; + private static final String PARAM_CURRENCY_SYMBOL = "currencySymbol"; + + private static final String PARAM_VALUE_RND_UP = "up"; + private static final String PARAM_VALUE_RND_DOWN = "down"; + private static final String PARAM_VALUE_RND_CEILING = "ceiling"; + private static final String PARAM_VALUE_RND_FLOOR = "floor"; + private static final String PARAM_VALUE_RND_HALF_DOWN = "halfDown"; + private static final String PARAM_VALUE_RND_HALF_EVEN = "halfEven"; + private static final String PARAM_VALUE_RND_HALF_UP = "halfUp"; + private static final String PARAM_VALUE_RND_UNNECESSARY = "unnecessary"; + + private static final HashMap<String, ? extends ParameterHandler> PARAM_HANDLERS; + static { + HashMap<String, ParameterHandler> m = new HashMap<>(); + m.put(PARAM_ROUNDING_MODE, new ParameterHandler() { + @Override + public void handle(ExtendedDecimalFormatParser parser, String value) + throws InvalidParameterValueException { + RoundingMode parsedValue; + if (value.equals(PARAM_VALUE_RND_UP)) { + parsedValue = RoundingMode.UP; + } else if (value.equals(PARAM_VALUE_RND_DOWN)) { + parsedValue = RoundingMode.DOWN; + } else if (value.equals(PARAM_VALUE_RND_CEILING)) { + parsedValue = RoundingMode.CEILING; + } else if (value.equals(PARAM_VALUE_RND_FLOOR)) { + parsedValue = RoundingMode.FLOOR; + } else if (value.equals(PARAM_VALUE_RND_HALF_DOWN)) { + parsedValue = RoundingMode.HALF_DOWN; + } else if (value.equals(PARAM_VALUE_RND_HALF_EVEN)) { + parsedValue = RoundingMode.HALF_EVEN; + } else if (value.equals(PARAM_VALUE_RND_HALF_UP)) { + parsedValue = RoundingMode.HALF_UP; + } else if (value.equals(PARAM_VALUE_RND_UNNECESSARY)) { + parsedValue = RoundingMode.UNNECESSARY; + } else { + throw new InvalidParameterValueException("Should be one of: u, d, c, f, hd, he, hu, un"); + } + + parser.roundingMode = parsedValue; + } + }); + m.put(PARAM_MULTIPIER, new ParameterHandler() { + @Override + public void handle(ExtendedDecimalFormatParser parser, String value) + throws InvalidParameterValueException { + try { + parser.multipier = Integer.valueOf(value); + } catch (NumberFormatException e) { + throw new InvalidParameterValueException("Malformed integer."); + } + } + }); + m.put(PARAM_DECIMAL_SEPARATOR, new ParameterHandler() { + @Override + public void handle(ExtendedDecimalFormatParser parser, String value) + throws InvalidParameterValueException { + if (value.length() != 1) { + throw new InvalidParameterValueException("Must contain exactly 1 character."); + } + parser.symbols.setDecimalSeparator(value.charAt(0)); + } + }); + m.put(PARAM_MONETARY_DECIMAL_SEPARATOR, new ParameterHandler() { + @Override + public void handle(ExtendedDecimalFormatParser parser, String value) + throws InvalidParameterValueException { + if (value.length() != 1) { + throw new InvalidParameterValueException("Must contain exactly 1 character."); + } + parser.symbols.setMonetaryDecimalSeparator(value.charAt(0)); + } + }); + m.put(PARAM_GROUP_SEPARATOR, new ParameterHandler() { + @Override + public void handle(ExtendedDecimalFormatParser parser, String value) + throws InvalidParameterValueException { + if (value.length() != 1) { + throw new InvalidParameterValueException("Must contain exactly 1 character."); + } + parser.symbols.setGroupingSeparator(value.charAt(0)); + } + }); + m.put(PARAM_EXPONENT_SEPARATOR, new ParameterHandler() { + @Override + public void handle(ExtendedDecimalFormatParser parser, String value) + throws InvalidParameterValueException { + parser.symbols.setExponentSeparator(value); + } + }); + m.put(PARAM_MINUS_SIGN, new ParameterHandler() { + @Override + public void handle(ExtendedDecimalFormatParser parser, String value) + throws InvalidParameterValueException { + if (value.length() != 1) { + throw new InvalidParameterValueException("Must contain exactly 1 character."); + } + parser.symbols.setMinusSign(value.charAt(0)); + } + }); + m.put(PARAM_INFINITY, new ParameterHandler() { + @Override + public void handle(ExtendedDecimalFormatParser parser, String value) + throws InvalidParameterValueException { + parser.symbols.setInfinity(value); + } + }); + m.put(PARAM_NAN, new ParameterHandler() { + @Override + public void handle(ExtendedDecimalFormatParser parser, String value) + throws InvalidParameterValueException { + parser.symbols.setNaN(value); + } + }); + m.put(PARAM_PERCENT, new ParameterHandler() { + @Override + public void handle(ExtendedDecimalFormatParser parser, String value) + throws InvalidParameterValueException { + if (value.length() != 1) { + throw new InvalidParameterValueException("Must contain exactly 1 character."); + } + parser.symbols.setPercent(value.charAt(0)); + } + }); + m.put(PARAM_PER_MILL, new ParameterHandler() { + @Override + public void handle(ExtendedDecimalFormatParser parser, String value) + throws InvalidParameterValueException { + if (value.length() != 1) { + throw new InvalidParameterValueException("Must contain exactly 1 character."); + } + parser.symbols.setPerMill(value.charAt(0)); + } + }); + m.put(PARAM_ZERO_DIGIT, new ParameterHandler() { + @Override + public void handle(ExtendedDecimalFormatParser parser, String value) + throws InvalidParameterValueException { + if (value.length() != 1) { + throw new InvalidParameterValueException("Must contain exactly 1 character."); + } + parser.symbols.setZeroDigit(value.charAt(0)); + } + }); + m.put(PARAM_CURRENCY_CODE, new ParameterHandler() { + @Override + public void handle(ExtendedDecimalFormatParser parser, String value) + throws InvalidParameterValueException { + Currency currency; + try { + currency = Currency.getInstance(value); + } catch (IllegalArgumentException e) { + throw new InvalidParameterValueException("Not a known ISO 4217 code."); + } + parser.symbols.setCurrency(currency); + } + }); + PARAM_HANDLERS = m; + } + + private static final String SNIP_MARK = "[...]"; + private static final int MAX_QUOTATION_LENGTH = 10; // Must be more than SNIP_MARK.length! + + private final String src; + private int pos = 0; + + private final DecimalFormatSymbols symbols; + private RoundingMode roundingMode; + private Integer multipier; + + static DecimalFormat parse(String formatString, Locale locale) throws ParseException { + return new ExtendedDecimalFormatParser(formatString, locale).parse(); + } + + private DecimalFormat parse() throws ParseException { + String stdPattern = fetchStandardPattern(); + skipWS(); + parseFormatStringExtension(); + + DecimalFormat decimalFormat; + try { + decimalFormat = new DecimalFormat(stdPattern, symbols); + } catch (IllegalArgumentException e) { + ParseException pe = new ParseException(e.getMessage(), 0); + if (e.getCause() != null) { + try { + e.initCause(e.getCause()); + } catch (Exception e2) { + // Supress + } + } + throw pe; + } + + if (roundingMode != null) { + decimalFormat.setRoundingMode(roundingMode); + } + + if (multipier != null) { + decimalFormat.setMultiplier(multipier.intValue()); + } + + return decimalFormat; + } + + private void parseFormatStringExtension() throws ParseException { + int ln = src.length(); + + if (pos == ln) { + return; + } + + String currencySymbol = null; // Exceptional, as must be applied after "currency code" + fetchParamters: do { + int namePos = pos; + String name = fetchName(); + if (name == null) { + throw newExpectedSgParseException("name"); + } + + skipWS(); + + if (!fetchChar('=')) { + throw newExpectedSgParseException("\"=\""); + } + + skipWS(); + + int valuePos = pos; + String value = fetchValue(); + if (value == null) { + throw newExpectedSgParseException("value"); + } + int paramEndPos = pos; + + ParameterHandler handler = PARAM_HANDLERS.get(name); + if (handler == null) { + if (name.equals(PARAM_CURRENCY_SYMBOL)) { + currencySymbol = value; + } else { + throw newUnknownParameterException(name, namePos); + } + } else { + try { + handler.handle(this, value); + } catch (InvalidParameterValueException e) { + throw newInvalidParameterValueException(name, value, valuePos, e); + } + } + + skipWS(); + + // Optional comma + if (fetchChar(',')) { + skipWS(); + } else { + if (pos == ln) { + break fetchParamters; + } + if (pos == paramEndPos) { + throw newExpectedSgParseException("parameter separator whitespace or comma"); + } + } + } while (true); + + // This is brought out to here to ensure that it's applied after "currency code": + if (currencySymbol != null) { + symbols.setCurrencySymbol(currencySymbol); + } + } + + private ParseException newInvalidParameterValueException(String name, String value, int valuePos, + InvalidParameterValueException e) { + return new java.text.ParseException( + _StringUtil.jQuote(value) + " is an invalid value for the \"" + name + "\" parameter: " + + e.message, + valuePos); + } + + private ParseException newUnknownParameterException(String name, int namePos) throws ParseException { + StringBuilder sb = new StringBuilder(128); + sb.append("Unsupported parameter name, ").append(_StringUtil.jQuote(name)); + sb.append(". The supported names are: "); + Set<String> legalNames = PARAM_HANDLERS.keySet(); + String[] legalNameArr = legalNames.toArray(new String[legalNames.size()]); + Arrays.sort(legalNameArr); + for (int i = 0; i < legalNameArr.length; i++) { + if (i != 0) { + sb.append(", "); + } + sb.append(legalNameArr[i]); + } + return new java.text.ParseException(sb.toString(), namePos); + } + + private void skipWS() { + int ln = src.length(); + while (pos < ln && isWS(src.charAt(pos))) { + pos++; + } + } + + private boolean fetchChar(char fetchedChar) { + if (pos < src.length() && src.charAt(pos) == fetchedChar) { + pos++; + return true; + } else { + return false; + } + } + + private boolean isWS(char c) { + return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\u00A0'; + } + + private String fetchName() throws ParseException { + int ln = src.length(); + int startPos = pos; + boolean firstChar = true; + scanUntilEnd: while (pos < ln) { + char c = src.charAt(pos); + if (firstChar) { + if (!Character.isJavaIdentifierStart(c)) { + break scanUntilEnd; + } + firstChar = false; + } else if (!Character.isJavaIdentifierPart(c)) { + break scanUntilEnd; + } + pos++; + } + return !firstChar ? src.substring(startPos, pos) : null; + } + + private String fetchValue() throws ParseException { + int ln = src.length(); + int startPos = pos; + char openedQuot = 0; + boolean needsUnescaping = false; + scanUntilEnd: while (pos < ln) { + char c = src.charAt(pos); + if (c == '\'' || c == '"') { + if (openedQuot == 0) { + if (startPos != pos) { + throw new java.text.ParseException( + "The " + c + " character can only be used for quoting values, " + + "but it was in the middle of an non-quoted value.", + pos); + } + openedQuot = c; + } else if (c == openedQuot) { + if (pos + 1 < ln && src.charAt(pos + 1) == openedQuot) { + pos++; // skip doubled quote (escaping) + needsUnescaping = true; + } else { + String str = src.substring(startPos + 1, pos); + pos++; + return needsUnescaping ? unescape(str, openedQuot) : str; + } + } + } else { + if (openedQuot == 0 && !Character.isJavaIdentifierPart(c)) { + break scanUntilEnd; + } + } + pos++; + } // while + if (openedQuot != 0) { + throw new java.text.ParseException( + "The " + openedQuot + + " quotation wasn't closed when the end of the source was reached.", + pos); + } + return startPos == pos ? null : src.substring(startPos, pos); + } + + private String unescape(String s, char openedQuot) { + return openedQuot == '\'' ? _StringUtil.replace(s, "\'\'", "\'") : _StringUtil.replace(s, "\"\"", "\""); + } + + private String fetchStandardPattern() { + int pos = this.pos; + int ln = src.length(); + int semicolonCnt = 0; + boolean quotedMode = false; + findStdPartEnd: while (pos < ln) { + char c = src.charAt(pos); + if (c == ';' && !quotedMode) { + semicolonCnt++; + if (semicolonCnt == 2) { + break findStdPartEnd; + } + } else if (c == '\'') { + if (quotedMode) { + if (pos + 1 < ln && src.charAt(pos + 1) == '\'') { + // Skips "''" used for escaping "'" + pos++; + } else { + quotedMode = false; + } + } else { + quotedMode = true; + } + } + pos++; + } + + String stdFormatStr; + if (semicolonCnt < 2) { // We have a standard DecimalFormat string + // Note that "0.0;" and "0.0" gives the same result with DecimalFormat, so we leave a ';' there + stdFormatStr = src; + } else { // `pos` points to the 2nd ';' + int stdEndPos = pos; + if (src.charAt(pos - 1) == ';') { // we have a ";;" + // Note that ";;" is illegal in DecimalFormat, so this is backward compatible. + stdEndPos--; + } + stdFormatStr = src.substring(0, stdEndPos); + } + + if (pos < ln) { + pos++; // Skips closing ';' + } + this.pos = pos; + + return stdFormatStr; + } + + private ExtendedDecimalFormatParser(String formatString, Locale locale) { + src = formatString; + symbols = new DecimalFormatSymbols(locale); + } + + private ParseException newExpectedSgParseException(String expectedThing) { + String quotation; + + // Ignore trailing WS when calculating the length: + int i = src.length() - 1; + while (i >= 0 && Character.isWhitespace(src.charAt(i))) { + i--; + } + int ln = i + 1; + + if (pos < ln) { + int qEndPos = pos + MAX_QUOTATION_LENGTH; + if (qEndPos >= ln) { + quotation = src.substring(pos, ln); + } else { + quotation = src.substring(pos, qEndPos - SNIP_MARK.length()) + SNIP_MARK; + } + } else { + quotation = null; + } + + return new ParseException( + "Expected a(n) " + expectedThing + " at position " + pos + " (0-based), but " + + (quotation == null ? "reached the end of the input." : "found: " + quotation), + pos); + } + + private interface ParameterHandler { + + void handle(ExtendedDecimalFormatParser parser, String value) + throws InvalidParameterValueException; + + } + + private static class InvalidParameterValueException extends Exception { + + private final String message; + + public InvalidParameterValueException(String message) { + this.message = message; + } + + } + +}