http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateFormatUtil.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateFormatUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateFormatUtil.java new file mode 100644 index 0000000..4029c78 --- /dev/null +++ b/freemarker-core/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 java.util.Date; + +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; + +/** + * 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/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormat.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormat.java new file mode 100644 index 0000000..54873d6 --- /dev/null +++ b/freemarker-core/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.Environment; +import org.apache.freemarker.core.arithmetic.ArithmeticEngine; +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/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormatFactory.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormatFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormatFactory.java new file mode 100644 index 0000000..a4cac22 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormatFactory.java @@ -0,0 +1,67 @@ +/* + * 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.util.Locale; + +import org.apache.freemarker.core.CustomStateKey; +import org.apache.freemarker.core.MutableProcessingConfiguration; +import org.apache.freemarker.core.Configuration; +import org.apache.freemarker.core.Environment; + +/** + * 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 MutableProcessingConfiguration#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 MutableProcessingConfiguration#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#getCustomState(CustomStateKey)}. + * + * @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/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormat.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormat.java new file mode 100644 index 0000000..9203e5a --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormat.java @@ -0,0 +1,42 @@ +/* + * 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(); + + /** + * The implementation in {@link TemplateValueFormat} returns {@code package.className(description)}, where + * description comes from {@link #getDescription()}. + */ + @Override + public String toString() { + return getClass().getName() + "(" + getDescription() + ")"; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatException.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatException.java new file mode 100644 index 0000000..dd538e6 --- /dev/null +++ b/freemarker-core/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/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatFactory.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatFactory.java new file mode 100644 index 0000000..04f706e --- /dev/null +++ b/freemarker-core/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/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UndefinedCustomFormatException.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UndefinedCustomFormatException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UndefinedCustomFormatException.java new file mode 100644 index 0000000..fa4ed3d --- /dev/null +++ b/freemarker-core/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/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnformattableValueException.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnformattableValueException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnformattableValueException.java new file mode 100644 index 0000000..6ef3b10 --- /dev/null +++ b/freemarker-core/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/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeFormattingUnsupportedException.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeFormattingUnsupportedException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeFormattingUnsupportedException.java new file mode 100644 index 0000000..90ae4be --- /dev/null +++ b/freemarker-core/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/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeParsingUnsupportedException.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeParsingUnsupportedException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeParsingUnsupportedException.java new file mode 100644 index 0000000..ef6cca2 --- /dev/null +++ b/freemarker-core/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/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnparsableValueException.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnparsableValueException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnparsableValueException.java new file mode 100644 index 0000000..78af935 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnparsableValueException.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; + +/** + * 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/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTargetTemplateValueFormatException.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTargetTemplateValueFormatException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTargetTemplateValueFormatException.java new file mode 100644 index 0000000..b4625a4 --- /dev/null +++ b/freemarker-core/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 invoke 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/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateDateFormatFactory.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateDateFormatFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateDateFormatFactory.java new file mode 100644 index 0000000..a964bc2 --- /dev/null +++ b/freemarker-core/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.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.TemplateFormatUtil; +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 invoke 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/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateNumberFormatFactory.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateNumberFormatFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateNumberFormatFactory.java new file mode 100644 index 0000000..72e8abd --- /dev/null +++ b/freemarker-core/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.util._LocaleUtil; +import org.apache.freemarker.core.util._StringUtil; +import org.apache.freemarker.core.valueformat.TemplateFormatUtil; +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 invoke 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/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ExtendedDecimalFormatParser.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ExtendedDecimalFormatParser.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ExtendedDecimalFormatParser.java new file mode 100644 index 0000000..dc1709c --- /dev/null +++ b/freemarker-core/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; + } + + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormat.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormat.java new file mode 100644 index 0000000..8790d00 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormat.java @@ -0,0 +1,270 @@ +/* + * 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.Date; +import java.util.TimeZone; + +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.model.TemplateDateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.util.BugException; +import org.apache.freemarker.core.util._DateUtil; +import org.apache.freemarker.core.util._DateUtil.CalendarFieldsToDateConverter; +import org.apache.freemarker.core.util._DateUtil.DateParseException; +import org.apache.freemarker.core.util._DateUtil.DateToISO8601CalendarFactory; +import org.apache.freemarker.core.util._StringUtil; +import org.apache.freemarker.core.valueformat.InvalidFormatParametersException; +import org.apache.freemarker.core.valueformat.TemplateDateFormat; +import org.apache.freemarker.core.valueformat.TemplateFormatUtil; +import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException; +import org.apache.freemarker.core.valueformat.UnparsableValueException; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +abstract class ISOLikeTemplateDateFormat extends TemplateDateFormat { + + private static final String XS_LESS_THAN_SECONDS_ACCURACY_ERROR_MESSAGE + = "Less than seconds accuracy isn't allowed by the XML Schema format"; + private final ISOLikeTemplateDateFormatFactory factory; + private final Environment env; + protected final int dateType; + protected final boolean zonelessInput; + protected final TimeZone timeZone; + protected final Boolean forceUTC; + protected final Boolean showZoneOffset; + protected final int accuracy; + + /** + * @param formatString The value of the ..._format setting, like "iso nz". + * @param parsingStart The index of the char in the {@code settingValue} that directly after the prefix that has + * indicated the exact formatter class (like "iso" or "xs") + */ + public ISOLikeTemplateDateFormat( + final String formatString, int parsingStart, + int dateType, boolean zonelessInput, + TimeZone timeZone, + ISOLikeTemplateDateFormatFactory factory, Environment env) + throws InvalidFormatParametersException, UnknownDateTypeFormattingUnsupportedException { + this.factory = factory; + this.env = env; + if (dateType == TemplateDateModel.UNKNOWN) { + throw new UnknownDateTypeFormattingUnsupportedException(); + } + + this.dateType = dateType; + this.zonelessInput = zonelessInput; + + final int ln = formatString.length(); + boolean afterSeparator = false; + int i = parsingStart; + int accuracy = _DateUtil.ACCURACY_MILLISECONDS; + Boolean showZoneOffset = null; + Boolean forceUTC = Boolean.FALSE; + while (i < ln) { + final char c = formatString.charAt(i++); + if (c == '_' || c == ' ') { + afterSeparator = true; + } else { + if (!afterSeparator) { + throw new InvalidFormatParametersException( + "Missing space or \"_\" before \"" + c + "\" (at char pos. " + i + ")."); + } + + switch (c) { + case 'h': + case 'm': + case 's': + if (accuracy != _DateUtil.ACCURACY_MILLISECONDS) { + throw new InvalidFormatParametersException( + "Character \"" + c + "\" is unexpected as accuracy was already specified earlier " + + "(at char pos. " + i + ")."); + } + switch (c) { + case 'h': + if (isXSMode()) { + throw new InvalidFormatParametersException( + XS_LESS_THAN_SECONDS_ACCURACY_ERROR_MESSAGE); + } + accuracy = _DateUtil.ACCURACY_HOURS; + break; + case 'm': + if (i < ln && formatString.charAt(i) == 's') { + i++; + accuracy = _DateUtil.ACCURACY_MILLISECONDS_FORCED; + } else { + if (isXSMode()) { + throw new InvalidFormatParametersException( + XS_LESS_THAN_SECONDS_ACCURACY_ERROR_MESSAGE); + } + accuracy = _DateUtil.ACCURACY_MINUTES; + } + break; + case 's': + accuracy = _DateUtil.ACCURACY_SECONDS; + break; + } + break; + case 'f': + if (i < ln && formatString.charAt(i) == 'u') { + checkForceUTCNotSet(forceUTC); + i++; + forceUTC = Boolean.TRUE; + break; + } + // Falls through + case 'n': + if (showZoneOffset != null) { + throw new InvalidFormatParametersException( + "Character \"" + c + "\" is unexpected as zone offset visibility was already " + + "specified earlier. (at char pos. " + i + ")."); + } + switch (c) { + case 'n': + if (i < ln && formatString.charAt(i) == 'z') { + i++; + showZoneOffset = Boolean.FALSE; + } else { + throw new InvalidFormatParametersException( + "\"n\" must be followed by \"z\" (at char pos. " + i + ")."); + } + break; + case 'f': + if (i < ln && formatString.charAt(i) == 'z') { + i++; + showZoneOffset = Boolean.TRUE; + } else { + throw new InvalidFormatParametersException( + "\"f\" must be followed by \"z\" (at char pos. " + i + ")."); + } + break; + } + break; + case 'u': + checkForceUTCNotSet(forceUTC); + forceUTC = null; // means UTC will be used except for zonelessInput + break; + default: + throw new InvalidFormatParametersException( + "Unexpected character, " + _StringUtil.jQuote(String.valueOf(c)) + + ". Expected the beginning of one of: h, m, s, ms, nz, fz, u" + + " (at char pos. " + i + ")."); + } // switch + afterSeparator = false; + } // else + } // while + + this.accuracy = accuracy; + this.showZoneOffset = showZoneOffset; + this.forceUTC = forceUTC; + this.timeZone = timeZone; + } + + private void checkForceUTCNotSet(Boolean fourceUTC) throws InvalidFormatParametersException { + if (fourceUTC != Boolean.FALSE) { + throw new InvalidFormatParametersException( + "The UTC usage option was already set earlier."); + } + } + + @Override + public final String formatToPlainText(TemplateDateModel dateModel) throws TemplateModelException { + final Date date = TemplateFormatUtil.getNonNullDate(dateModel); + return format( + date, + dateType != TemplateDateModel.TIME, + dateType != TemplateDateModel.DATE, + showZoneOffset == null + ? !zonelessInput + : showZoneOffset.booleanValue(), + accuracy, + (forceUTC == null ? !zonelessInput : forceUTC.booleanValue()) ? _DateUtil.UTC : timeZone, + factory.getISOBuiltInCalendar(env)); + } + + protected abstract String format(Date date, + boolean datePart, boolean timePart, boolean offsetPart, + int accuracy, + TimeZone timeZone, + DateToISO8601CalendarFactory calendarFactory); + + @Override + @SuppressFBWarnings(value = "RC_REF_COMPARISON_BAD_PRACTICE_BOOLEAN", + justification = "Known to use the singleton Boolean-s only") + public final Date parse(String s, int dateType) throws UnparsableValueException { + CalendarFieldsToDateConverter calToDateConverter = factory.getCalendarFieldsToDateCalculator(env); + TimeZone tz = forceUTC != Boolean.FALSE ? _DateUtil.UTC : timeZone; + try { + if (dateType == TemplateDateModel.DATE) { + return parseDate(s, tz, calToDateConverter); + } else if (dateType == TemplateDateModel.TIME) { + return parseTime(s, tz, calToDateConverter); + } else if (dateType == TemplateDateModel.DATETIME) { + return parseDateTime(s, tz, calToDateConverter); + } else { + throw new BugException("Unexpected date type: " + dateType); + } + } catch (DateParseException e) { + throw new UnparsableValueException(e.getMessage(), e); + } + } + + protected abstract Date parseDate( + String s, TimeZone tz, + CalendarFieldsToDateConverter calToDateConverter) + throws DateParseException; + + protected abstract Date parseTime( + String s, TimeZone tz, + CalendarFieldsToDateConverter calToDateConverter) + throws DateParseException; + + protected abstract Date parseDateTime( + String s, TimeZone tz, + CalendarFieldsToDateConverter calToDateConverter) + throws DateParseException; + + @Override + public final String getDescription() { + switch (dateType) { + case TemplateDateModel.DATE: return getDateDescription(); + case TemplateDateModel.TIME: return getTimeDescription(); + case TemplateDateModel.DATETIME: return getDateTimeDescription(); + default: return "<error: wrong format dateType>"; + } + } + + protected abstract String getDateDescription(); + protected abstract String getTimeDescription(); + protected abstract String getDateTimeDescription(); + + @Override + public final boolean isLocaleBound() { + return false; + } + + @Override + public boolean isTimeZoneBound() { + return true; + } + + protected abstract boolean isXSMode(); + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormatFactory.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormatFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormatFactory.java new file mode 100644 index 0000000..5db8f46 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormatFactory.java @@ -0,0 +1,57 @@ +/* + * 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.CustomStateKey; +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.util._DateUtil.CalendarFieldsToDateConverter; +import org.apache.freemarker.core.util._DateUtil.DateToISO8601CalendarFactory; +import org.apache.freemarker.core.util._DateUtil.TrivialCalendarFieldsToDateConverter; +import org.apache.freemarker.core.util._DateUtil.TrivialDateToISO8601CalendarFactory; +import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory; + +abstract class ISOLikeTemplateDateFormatFactory extends TemplateDateFormatFactory { + + private static final CustomStateKey<TrivialDateToISO8601CalendarFactory> DATE_TO_CAL_CONVERTER_KEY + = new CustomStateKey<TrivialDateToISO8601CalendarFactory>() { + @Override + protected TrivialDateToISO8601CalendarFactory create() { + return new TrivialDateToISO8601CalendarFactory(); + } + }; + private static final CustomStateKey<TrivialCalendarFieldsToDateConverter> CAL_TO_DATE_CONVERTER_KEY + = new CustomStateKey<TrivialCalendarFieldsToDateConverter>() { + @Override + protected TrivialCalendarFieldsToDateConverter create() { + return new TrivialCalendarFieldsToDateConverter(); + } + }; + + protected ISOLikeTemplateDateFormatFactory() { } + + public DateToISO8601CalendarFactory getISOBuiltInCalendar(Environment env) { + return (DateToISO8601CalendarFactory) env.getCustomState(DATE_TO_CAL_CONVERTER_KEY); + } + + public CalendarFieldsToDateConverter getCalendarFieldsToDateCalculator(Environment env) { + return (CalendarFieldsToDateConverter) env.getCustomState(CAL_TO_DATE_CONVERTER_KEY); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormat.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormat.java new file mode 100644 index 0000000..4856ee0 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormat.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.valueformat.impl; + +import java.util.Date; +import java.util.TimeZone; + +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.util._DateUtil; +import org.apache.freemarker.core.util._DateUtil.CalendarFieldsToDateConverter; +import org.apache.freemarker.core.util._DateUtil.DateParseException; +import org.apache.freemarker.core.util._DateUtil.DateToISO8601CalendarFactory; +import org.apache.freemarker.core.valueformat.InvalidFormatParametersException; +import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException; + +class ISOTemplateDateFormat extends ISOLikeTemplateDateFormat { + + ISOTemplateDateFormat( + String settingValue, int parsingStart, + int dateType, boolean zonelessInput, + TimeZone timeZone, + ISOLikeTemplateDateFormatFactory factory, + Environment env) + throws InvalidFormatParametersException, UnknownDateTypeFormattingUnsupportedException { + super(settingValue, parsingStart, dateType, zonelessInput, timeZone, factory, env); + } + + @Override + protected String format(Date date, boolean datePart, boolean timePart, boolean offsetPart, int accuracy, + TimeZone timeZone, DateToISO8601CalendarFactory calendarFactory) { + return _DateUtil.dateToISO8601String( + date, datePart, timePart, timePart && offsetPart, accuracy, timeZone, calendarFactory); + } + + @Override + protected Date parseDate(String s, TimeZone tz, CalendarFieldsToDateConverter calToDateConverter) + throws DateParseException { + return _DateUtil.parseISO8601Date(s, tz, calToDateConverter); + } + + @Override + protected Date parseTime(String s, TimeZone tz, CalendarFieldsToDateConverter calToDateConverter) + throws DateParseException { + return _DateUtil.parseISO8601Time(s, tz, calToDateConverter); + } + + @Override + protected Date parseDateTime(String s, TimeZone tz, + CalendarFieldsToDateConverter calToDateConverter) throws DateParseException { + return _DateUtil.parseISO8601DateTime(s, tz, calToDateConverter); + } + + @Override + protected String getDateDescription() { + return "ISO 8601 (subset) date"; + } + + @Override + protected String getTimeDescription() { + return "ISO 8601 (subset) time"; + } + + @Override + protected String getDateTimeDescription() { + return "ISO 8601 (subset) date-time"; + } + + @Override + protected boolean isXSMode() { + return false; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormatFactory.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormatFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormatFactory.java new file mode 100644 index 0000000..ddace3d --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormatFactory.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.freemarker.core.valueformat.impl; + +import java.util.Locale; +import java.util.TimeZone; + +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.valueformat.InvalidFormatParametersException; +import org.apache.freemarker.core.valueformat.TemplateDateFormat; +import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException; + +/** + * Creates {@link TemplateDateFormat}-s that follows ISO 8601 extended format 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. + */ +public final class ISOTemplateDateFormatFactory extends ISOLikeTemplateDateFormatFactory { + + public static final ISOTemplateDateFormatFactory INSTANCE = new ISOTemplateDateFormatFactory(); + + private ISOTemplateDateFormatFactory() { + // Not meant to be instantiated + } + + @Override + public TemplateDateFormat get(String params, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput, + Environment env) throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException { + // We don't cache these as creating them is cheap (only 10% speedup of ${d?string.xs} with caching) + return new ISOTemplateDateFormat( + params, 3, + dateType, zonelessInput, + timeZone, this, env); + } + +}
