http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormat.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormat.java new file mode 100644 index 0000000..e30c2e4 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormat.java @@ -0,0 +1,75 @@ +/* + * 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.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.apache.freemarker.core.model.TemplateDateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.valueformat.TemplateDateFormat; +import org.apache.freemarker.core.valueformat.TemplateFormatUtil; +import org.apache.freemarker.core.valueformat.UnparsableValueException; + +/** + * Java {@link DateFormat}-based format. + */ +class JavaTemplateDateFormat extends TemplateDateFormat { + + private final DateFormat javaDateFormat; + + public JavaTemplateDateFormat(DateFormat javaDateFormat) { + this.javaDateFormat = javaDateFormat; + } + + @Override + public String formatToPlainText(TemplateDateModel dateModel) throws TemplateModelException { + return javaDateFormat.format(TemplateFormatUtil.getNonNullDate(dateModel)); + } + + @Override + public Date parse(String s, int dateType) throws UnparsableValueException { + try { + return javaDateFormat.parse(s); + } catch (ParseException e) { + throw new UnparsableValueException(e.getMessage(), e); + } + } + + @Override + public String getDescription() { + return javaDateFormat instanceof SimpleDateFormat + ? ((SimpleDateFormat) javaDateFormat).toPattern() + : javaDateFormat.toString(); + } + + @Override + public boolean isLocaleBound() { + return true; + } + + @Override + public boolean isTimeZoneBound() { + return true; + } + +}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormatFactory.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormatFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormatFactory.java new file mode 100644 index 0000000..093e110 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormatFactory.java @@ -0,0 +1,187 @@ +/* + * 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.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Locale; +import java.util.StringTokenizer; +import java.util.TimeZone; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core._CoreLogs; +import org.apache.freemarker.core.model.TemplateDateModel; +import org.apache.freemarker.core.valueformat.InvalidFormatParametersException; +import org.apache.freemarker.core.valueformat.TemplateDateFormat; +import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory; +import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException; +import org.slf4j.Logger; + +/** + * Deals with {@link TemplateDateFormat}-s that wrap a Java {@link DateFormat}. The parameter string is usually a + * {@link java.text.SimpleDateFormat} pattern, but it also recognized the names "short", "medium", "long" + * and "full", which correspond to formats defined by {@link DateFormat} with similar names. + * + * <p>Note that the resulting {@link java.text.SimpleDateFormat}-s are globally cached, and threading issues are + * addressed by cloning the cached instance before returning it. So it just makes object creation faster, but doesn't + * eliminate it. + */ +public class JavaTemplateDateFormatFactory extends TemplateDateFormatFactory { + + public static final JavaTemplateDateFormatFactory INSTANCE = new JavaTemplateDateFormatFactory(); + + private static final Logger LOG = _CoreLogs.RUNTIME; + + private static final ConcurrentHashMap<CacheKey, DateFormat> GLOBAL_FORMAT_CACHE = new ConcurrentHashMap<>(); + private static final int LEAK_ALERT_DATE_FORMAT_CACHE_SIZE = 1024; + + private JavaTemplateDateFormatFactory() { + // Can't be instantiated + } + + /** + * @param zonelessInput + * Has no effect in this implementation. + */ + @Override + public TemplateDateFormat get(String params, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput, + Environment env) throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException { + return new JavaTemplateDateFormat(getJavaDateFormat(dateType, params, locale, timeZone)); + } + + /** + * Returns a "private" copy (not in the global cache) for the given format. + */ + private DateFormat getJavaDateFormat(int dateType, String nameOrPattern, Locale locale, TimeZone timeZone) + throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException { + + // Get DateFormat from global cache: + CacheKey cacheKey = new CacheKey(dateType, nameOrPattern, locale, timeZone); + DateFormat jFormat; + + jFormat = GLOBAL_FORMAT_CACHE.get(cacheKey); + if (jFormat == null) { + // Add format to global format cache. + StringTokenizer tok = new StringTokenizer(nameOrPattern, "_"); + int tok1Style = tok.hasMoreTokens() ? parseDateStyleToken(tok.nextToken()) : DateFormat.DEFAULT; + if (tok1Style != -1) { + switch (dateType) { + case TemplateDateModel.UNKNOWN: { + throw new UnknownDateTypeFormattingUnsupportedException(); + } + case TemplateDateModel.TIME: { + jFormat = DateFormat.getTimeInstance(tok1Style, cacheKey.locale); + break; + } + case TemplateDateModel.DATE: { + jFormat = DateFormat.getDateInstance(tok1Style, cacheKey.locale); + break; + } + case TemplateDateModel.DATETIME: { + int tok2Style = tok.hasMoreTokens() ? parseDateStyleToken(tok.nextToken()) : tok1Style; + if (tok2Style != -1) { + jFormat = DateFormat.getDateTimeInstance(tok1Style, tok2Style, cacheKey.locale); + } + break; + } + } + } + if (jFormat == null) { + try { + jFormat = new SimpleDateFormat(nameOrPattern, cacheKey.locale); + } catch (IllegalArgumentException e) { + final String msg = e.getMessage(); + throw new InvalidFormatParametersException( + msg != null ? msg : "Invalid SimpleDateFormat pattern", e); + } + } + jFormat.setTimeZone(cacheKey.timeZone); + + if (GLOBAL_FORMAT_CACHE.size() >= LEAK_ALERT_DATE_FORMAT_CACHE_SIZE) { + boolean triggered = false; + synchronized (JavaTemplateDateFormatFactory.class) { + if (GLOBAL_FORMAT_CACHE.size() >= LEAK_ALERT_DATE_FORMAT_CACHE_SIZE) { + triggered = true; + GLOBAL_FORMAT_CACHE.clear(); + } + } + if (triggered) { + LOG.warn("Global Java DateFormat cache has exceeded {} entries => cache flushed. " + + "Typical cause: Some template generates high variety of format pattern strings.", + LEAK_ALERT_DATE_FORMAT_CACHE_SIZE); + } + } + + DateFormat prevJFormat = GLOBAL_FORMAT_CACHE.putIfAbsent(cacheKey, jFormat); + if (prevJFormat != null) { + jFormat = prevJFormat; + } + } // if cache miss + + return (DateFormat) jFormat.clone(); // For thread safety + } + + private static final class CacheKey { + private final int dateType; + private final String pattern; + private final Locale locale; + private final TimeZone timeZone; + + CacheKey(int dateType, String pattern, Locale locale, TimeZone timeZone) { + this.dateType = dateType; + this.pattern = pattern; + this.locale = locale; + this.timeZone = timeZone; + } + + @Override + public boolean equals(Object o) { + if (o instanceof CacheKey) { + CacheKey fk = (CacheKey) o; + return dateType == fk.dateType && fk.pattern.equals(pattern) && fk.locale.equals(locale) + && fk.timeZone.equals(timeZone); + } + return false; + } + + @Override + public int hashCode() { + return dateType ^ pattern.hashCode() ^ locale.hashCode() ^ timeZone.hashCode(); + } + } + + private int parseDateStyleToken(String token) { + if ("short".equals(token)) { + return DateFormat.SHORT; + } + if ("medium".equals(token)) { + return DateFormat.MEDIUM; + } + if ("long".equals(token)) { + return DateFormat.LONG; + } + if ("full".equals(token)) { + return DateFormat.FULL; + } + return -1; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormat.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormat.java new file mode 100644 index 0000000..e3cdea0 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormat.java @@ -0,0 +1,64 @@ +/* + * 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.text.NumberFormat; + +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.model.TemplateNumberModel; +import org.apache.freemarker.core.valueformat.TemplateFormatUtil; +import org.apache.freemarker.core.valueformat.TemplateNumberFormat; +import org.apache.freemarker.core.valueformat.UnformattableValueException; + +final class JavaTemplateNumberFormat extends TemplateNumberFormat { + + private final String formatString; + private final NumberFormat javaNumberFormat; + + public JavaTemplateNumberFormat(NumberFormat javaNumberFormat, String formatString) { + this.formatString = formatString; + this.javaNumberFormat = javaNumberFormat; + } + + @Override + public String formatToPlainText(TemplateNumberModel numberModel) throws UnformattableValueException, TemplateModelException { + Number number = TemplateFormatUtil.getNonNullNumber(numberModel); + try { + return javaNumberFormat.format(number); + } catch (ArithmeticException e) { + throw new UnformattableValueException( + "This format can't format the " + number + " number. Reason: " + e.getMessage(), e); + } + } + + @Override + public boolean isLocaleBound() { + return true; + } + + public NumberFormat getJavaNumberFormat() { + return javaNumberFormat; + } + + @Override + public String getDescription() { + return formatString; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormatFactory.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormatFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormatFactory.java new file mode 100644 index 0000000..cf292df --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormatFactory.java @@ -0,0 +1,133 @@ +/* + * 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.text.NumberFormat; +import java.text.ParseException; +import java.util.Locale; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core._CoreLogs; +import org.apache.freemarker.core.valueformat.InvalidFormatParametersException; +import org.apache.freemarker.core.valueformat.TemplateNumberFormat; +import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory; +import org.slf4j.Logger; + +/** + * Deals with {@link TemplateNumberFormat}-s that wrap a Java {@link NumberFormat}. The parameter string is usually + * a {@link java.text.DecimalFormat} pattern, with the extensions described in the Manual (see "Extended Jav decimal + * format"). There are some names that aren't parsed as patterns: "number", "currency", "percent", which + * corresponds to the predefined formats with similar name in {@link NumberFormat}-s, and also "computer" that + * behaves like {@code someNumber?c} in templates. + * + * <p>Note that the resulting {@link java.text.DecimalFormat}-s are globally cached, and threading issues are + * addressed by cloning the cached instance before returning it. So it just makes object creation faster, but doesn't + * eliminate it. + */ +public class JavaTemplateNumberFormatFactory extends TemplateNumberFormatFactory { + + public static final JavaTemplateNumberFormatFactory INSTANCE = new JavaTemplateNumberFormatFactory(); + + private static final Logger LOG = _CoreLogs.RUNTIME; + + private static final ConcurrentHashMap<CacheKey, NumberFormat> GLOBAL_FORMAT_CACHE + = new ConcurrentHashMap<>(); + private static final int LEAK_ALERT_NUMBER_FORMAT_CACHE_SIZE = 1024; + + private JavaTemplateNumberFormatFactory() { + // Not meant to be instantiated + } + + @Override + public TemplateNumberFormat get(String params, Locale locale, Environment env) + throws InvalidFormatParametersException { + CacheKey cacheKey = new CacheKey(params, locale); + NumberFormat jFormat = GLOBAL_FORMAT_CACHE.get(cacheKey); + if (jFormat == null) { + if ("number".equals(params)) { + jFormat = NumberFormat.getNumberInstance(locale); + } else if ("currency".equals(params)) { + jFormat = NumberFormat.getCurrencyInstance(locale); + } else if ("percent".equals(params)) { + jFormat = NumberFormat.getPercentInstance(locale); + } else if ("computer".equals(params)) { + jFormat = env.getCNumberFormat(); + } else { + try { + jFormat = ExtendedDecimalFormatParser.parse(params, locale); + } catch (ParseException e) { + String msg = e.getMessage(); + throw new InvalidFormatParametersException( + msg != null ? msg : "Invalid DecimalFormat pattern", e); + } + } + + if (GLOBAL_FORMAT_CACHE.size() >= LEAK_ALERT_NUMBER_FORMAT_CACHE_SIZE) { + boolean triggered = false; + synchronized (JavaTemplateNumberFormatFactory.class) { + if (GLOBAL_FORMAT_CACHE.size() >= LEAK_ALERT_NUMBER_FORMAT_CACHE_SIZE) { + triggered = true; + GLOBAL_FORMAT_CACHE.clear(); + } + } + if (triggered) { + LOG.warn("Global Java NumberFormat cache has exceeded {} entries => cache flushed. " + + "Typical cause: Some template generates high variety of format pattern strings.", + LEAK_ALERT_NUMBER_FORMAT_CACHE_SIZE); + } + } + + NumberFormat prevJFormat = GLOBAL_FORMAT_CACHE.putIfAbsent(cacheKey, jFormat); + if (prevJFormat != null) { + jFormat = prevJFormat; + } + } // if cache miss + + // JFormat-s aren't thread-safe; must deepClone it + jFormat = (NumberFormat) jFormat.clone(); + + return new JavaTemplateNumberFormat(jFormat, params); + } + + private static final class CacheKey { + private final String pattern; + private final Locale locale; + + CacheKey(String pattern, Locale locale) { + this.pattern = pattern; + this.locale = locale; + } + + @Override + public boolean equals(Object o) { + if (o instanceof CacheKey) { + CacheKey fk = (CacheKey) o; + return fk.pattern.equals(pattern) && fk.locale.equals(locale); + } + return false; + } + + @Override + public int hashCode() { + return pattern.hashCode() ^ locale.hashCode(); + } + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormat.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormat.java new file mode 100644 index 0000000..37d64dc --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormat.java @@ -0,0 +1,94 @@ +/* + * 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; + +/** + * XML Schema format. + */ +class XSTemplateDateFormat extends ISOLikeTemplateDateFormat { + + XSTemplateDateFormat( + String settingValue, int parsingStart, + int dateType, + boolean zonelessInput, + TimeZone timeZone, + ISOLikeTemplateDateFormatFactory factory, + Environment env) + throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException { + 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.dateToXSString( + date, datePart, timePart, offsetPart, accuracy, timeZone, calendarFactory); + } + + @Override + protected Date parseDate(String s, TimeZone tz, CalendarFieldsToDateConverter calToDateConverter) + throws DateParseException { + return _DateUtil.parseXSDate(s, tz, calToDateConverter); + } + + @Override + protected Date parseTime(String s, TimeZone tz, CalendarFieldsToDateConverter calToDateConverter) + throws DateParseException { + return _DateUtil.parseXSTime(s, tz, calToDateConverter); + } + + @Override + protected Date parseDateTime(String s, TimeZone tz, + CalendarFieldsToDateConverter calToDateConverter) throws DateParseException { + return _DateUtil.parseXSDateTime(s, tz, calToDateConverter); + } + + @Override + protected String getDateDescription() { + return "W3C XML Schema date"; + } + + @Override + protected String getTimeDescription() { + return "W3C XML Schema time"; + } + + @Override + protected String getDateTimeDescription() { + return "W3C XML Schema dateTime"; + } + + @Override + protected boolean isXSMode() { + return true; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormatFactory.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormatFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormatFactory.java new file mode 100644 index 0000000..352b353 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormatFactory.java @@ -0,0 +1,51 @@ +/* + * 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 the W3C XML Schema date, time and dateTime syntax. + */ +public final class XSTemplateDateFormatFactory extends ISOLikeTemplateDateFormatFactory { + + public static final XSTemplateDateFormatFactory INSTANCE = new XSTemplateDateFormatFactory(); + + private XSTemplateDateFormatFactory() { + // 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 XSTemplateDateFormat( + params, 2, + dateType, zonelessInput, + timeZone, this, env); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/package.html ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/package.html b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/package.html new file mode 100644 index 0000000..ecfd725 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/package.html @@ -0,0 +1,26 @@ +<!-- + 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. +--> +<html> +<head> +</head> +<body> +<p>Formatting values shown in templates: Standard implementations. This package is part of the published API, that +is, user code can safely depend on it.</p> +</body> +</html> http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/package.html ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/package.html b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/package.html new file mode 100644 index 0000000..21d4c1b --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/package.html @@ -0,0 +1,25 @@ +<!-- + 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. +--> +<html> +<head> +</head> +<body> +<p>Formatting values shown in templates: Base classes/interfaces</p> +</body> +</html> http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/dom/AtAtKey.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/dom/AtAtKey.java b/freemarker-core/src/main/java/org/apache/freemarker/dom/AtAtKey.java new file mode 100644 index 0000000..ca6ac6b --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/dom/AtAtKey.java @@ -0,0 +1,58 @@ +/* + * 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.dom; + +/** + * The special hash keys that start with "@@". + */ +enum AtAtKey { + + MARKUP("@@markup"), + NESTED_MARKUP("@@nested_markup"), + ATTRIBUTES_MARKUP("@@attributes_markup"), + TEXT("@@text"), + START_TAG("@@start_tag"), + END_TAG("@@end_tag"), + QNAME("@@qname"), + NAMESPACE("@@namespace"), + LOCAL_NAME("@@local_name"), + ATTRIBUTES("@@"), + PREVIOUS_SIBLING_ELEMENT("@@previous_sibling_element"), + NEXT_SIBLING_ELEMENT("@@next_sibling_element"); + + private final String key; + + public String getKey() { + return key; + } + + AtAtKey(String key) { + this.key = key; + } + + public static boolean containsKey(String key) { + for (AtAtKey item : AtAtKey.values()) { + if (item.getKey().equals(key)) { + return true; + } + } + return false; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/dom/AttributeNodeModel.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/dom/AttributeNodeModel.java b/freemarker-core/src/main/java/org/apache/freemarker/dom/AttributeNodeModel.java new file mode 100644 index 0000000..cc510c4 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/dom/AttributeNodeModel.java @@ -0,0 +1,69 @@ +/* + * 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.dom; + +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.model.TemplateScalarModel; +import org.w3c.dom.Attr; + +class AttributeNodeModel extends NodeModel implements TemplateScalarModel { + + public AttributeNodeModel(Attr att) { + super(att); + } + + @Override + public String getAsString() { + return ((Attr) node).getValue(); + } + + @Override + public String getNodeName() { + String result = node.getLocalName(); + if (result == null || result.equals("")) { + result = node.getNodeName(); + } + return result; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + String getQualifiedName() { + String nsURI = node.getNamespaceURI(); + if (nsURI == null || nsURI.equals("")) + return node.getNodeName(); + Environment env = Environment.getCurrentEnvironment(); + String defaultNS = env.getDefaultNS(); + String prefix = null; + if (nsURI.equals(defaultNS)) { + prefix = "D"; + } else { + prefix = env.getPrefixForNamespace(nsURI); + } + if (prefix == null) { + return null; + } + return prefix + ":" + node.getLocalName(); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/dom/CharacterDataNodeModel.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/dom/CharacterDataNodeModel.java b/freemarker-core/src/main/java/org/apache/freemarker/dom/CharacterDataNodeModel.java new file mode 100644 index 0000000..264c0db --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/dom/CharacterDataNodeModel.java @@ -0,0 +1,46 @@ +/* + * 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.dom; + +import org.apache.freemarker.core.model.TemplateScalarModel; +import org.w3c.dom.CharacterData; +import org.w3c.dom.Comment; + +class CharacterDataNodeModel extends NodeModel implements TemplateScalarModel { + + public CharacterDataNodeModel(CharacterData text) { + super(text); + } + + @Override + public String getAsString() { + return ((org.w3c.dom.CharacterData) node).getData(); + } + + @Override + public String getNodeName() { + return (node instanceof Comment) ? "@comment" : "@text"; + } + + @Override + public boolean isEmpty() { + return true; + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/dom/DocumentModel.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/dom/DocumentModel.java b/freemarker-core/src/main/java/org/apache/freemarker/dom/DocumentModel.java new file mode 100644 index 0000000..876b3cf --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/dom/DocumentModel.java @@ -0,0 +1,76 @@ +/* + * 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.dom; + +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.model.TemplateHashModel; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; + +/** + * A class that wraps the root node of a parsed XML document, using + * the W3C DOM_WRAPPER API. + */ + +class DocumentModel extends NodeModel implements TemplateHashModel { + + private ElementModel rootElement; + + DocumentModel(Document doc) { + super(doc); + } + + @Override + public String getNodeName() { + return "@document"; + } + + @Override + public TemplateModel get(String key) throws TemplateModelException { + if (key.equals("*")) { + return getRootElement(); + } else if (key.equals("**")) { + NodeList nl = ((Document) node).getElementsByTagName("*"); + return new NodeListModel(nl, this); + } else if (DomStringUtil.isXMLNameLike(key)) { + ElementModel em = (ElementModel) NodeModel.wrap(((Document) node).getDocumentElement()); + if (em.matchesName(key, Environment.getCurrentEnvironment())) { + return em; + } else { + return new NodeListModel(this); + } + } + return super.get(key); + } + + ElementModel getRootElement() { + if (rootElement == null) { + rootElement = (ElementModel) wrap(((Document) node).getDocumentElement()); + } + return rootElement; + } + + @Override + public boolean isEmpty() { + return false; + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/dom/DocumentTypeModel.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/dom/DocumentTypeModel.java b/freemarker-core/src/main/java/org/apache/freemarker/dom/DocumentTypeModel.java new file mode 100644 index 0000000..3448f77 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/dom/DocumentTypeModel.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.dom; + +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.model.TemplateSequenceModel; +import org.w3c.dom.DocumentType; +import org.w3c.dom.ProcessingInstruction; + +class DocumentTypeModel extends NodeModel { + + public DocumentTypeModel(DocumentType docType) { + super(docType); + } + + public String getAsString() { + return ((ProcessingInstruction) node).getData(); + } + + public TemplateSequenceModel getChildren() throws TemplateModelException { + throw new TemplateModelException("entering the child nodes of a DTD node is not currently supported"); + } + + @Override + public TemplateModel get(String key) throws TemplateModelException { + throw new TemplateModelException("accessing properties of a DTD is not currently supported"); + } + + @Override + public String getNodeName() { + return "@document_type$" + node.getNodeName(); + } + + @Override + public boolean isEmpty() { + return true; + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/dom/DomLog.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/dom/DomLog.java b/freemarker-core/src/main/java/org/apache/freemarker/dom/DomLog.java new file mode 100644 index 0000000..a1f6f0c --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/dom/DomLog.java @@ -0,0 +1,32 @@ +/* + * 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.dom; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class DomLog { + + private DomLog() { + // + } + + public static final Logger LOG = LoggerFactory.getLogger("org.apache.freemarker.dom"); + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/dom/DomStringUtil.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/dom/DomStringUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/dom/DomStringUtil.java new file mode 100644 index 0000000..f5b58f8 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/dom/DomStringUtil.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.dom; + +/** + * For internal use only; don't depend on this, there's no backward compatibility guarantee at all! + * This class is to work around the lack of module system in Java, i.e., so that other FreeMarker packages can + * access things inside this package that users shouldn't. + */ +final class DomStringUtil { + + private DomStringUtil() { + // Not meant to be instantiated + } + + static boolean isXMLNameLike(String name) { + return isXMLNameLike(name, 0); + } + + /** + * Check if the name looks like an XML element name. + * + * @param firstCharIdx The index of the character in the string parameter that we treat as the beginning of the + * string to check. This is to spare substringing that has become more expensive in Java 7. + * + * @return whether the name is a valid XML element name. (This routine might only be 99% accurate. REVISIT) + */ + static boolean isXMLNameLike(String name, int firstCharIdx) { + int ln = name.length(); + for (int i = firstCharIdx; i < ln; i++) { + char c = name.charAt(i); + if (i == firstCharIdx && (c == '-' || c == '.' || Character.isDigit(c))) { + return false; + } + if (!Character.isLetterOrDigit(c) && c != '_' && c != '-' && c != '.') { + if (c == ':') { + if (i + 1 < ln && name.charAt(i + 1) == ':') { + // "::" is used in XPath + return false; + } + // We don't return here, as a lonely ":" is allowed. + } else { + return false; + } + } + } + return true; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/dom/ElementModel.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/dom/ElementModel.java b/freemarker-core/src/main/java/org/apache/freemarker/dom/ElementModel.java new file mode 100644 index 0000000..220f414 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/dom/ElementModel.java @@ -0,0 +1,234 @@ +/* + * 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.dom; + +import java.util.Collections; + +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.Template; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.model.TemplateScalarModel; +import org.apache.freemarker.core.model.TemplateSequenceModel; +import org.apache.freemarker.core.model.impl.SimpleScalar; +import org.apache.freemarker.core.util._StringUtil; +import org.w3c.dom.Attr; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +class ElementModel extends NodeModel implements TemplateScalarModel { + + public ElementModel(Element element) { + super(element); + } + + @Override + public boolean isEmpty() { + return false; + } + + /** + * An Element node supports various hash keys. + * Any key that corresponds to the tag name of any child elements + * returns a sequence of those elements. The special key "*" returns + * all the element's direct children. + * The "**" key return all the element's descendants in the order they + * occur in the document. + * Any key starting with '@' is taken to be the name of an element attribute. + * The special key "@@" returns a hash of all the element's attributes. + * The special key "/" returns the root document node associated with this element. + */ + @Override + public TemplateModel get(String key) throws TemplateModelException { + if (key.equals("*")) { + NodeListModel ns = new NodeListModel(this); + TemplateSequenceModel children = getChildNodes(); + for (int i = 0; i < children.size(); i++) { + NodeModel child = (NodeModel) children.get(i); + if (child.node.getNodeType() == Node.ELEMENT_NODE) { + ns.add(child); + } + } + return ns; + } else if (key.equals("**")) { + return new NodeListModel(((Element) node).getElementsByTagName("*"), this); + } else if (key.startsWith("@")) { + if (key.startsWith("@@")) { + if (key.equals(AtAtKey.ATTRIBUTES.getKey())) { + return new NodeListModel(node.getAttributes(), this); + } else if (key.equals(AtAtKey.START_TAG.getKey())) { + NodeOutputter nodeOutputter = new NodeOutputter(node); + return new SimpleScalar(nodeOutputter.getOpeningTag((Element) node)); + } else if (key.equals(AtAtKey.END_TAG.getKey())) { + NodeOutputter nodeOutputter = new NodeOutputter(node); + return new SimpleScalar(nodeOutputter.getClosingTag((Element) node)); + } else if (key.equals(AtAtKey.ATTRIBUTES_MARKUP.getKey())) { + StringBuilder buf = new StringBuilder(); + NodeOutputter nu = new NodeOutputter(node); + nu.outputContent(node.getAttributes(), buf); + return new SimpleScalar(buf.toString().trim()); + } else if (key.equals(AtAtKey.PREVIOUS_SIBLING_ELEMENT.getKey())) { + Node previousSibling = node.getPreviousSibling(); + while (previousSibling != null && !isSignificantNode(previousSibling)) { + previousSibling = previousSibling.getPreviousSibling(); + } + return previousSibling != null && previousSibling.getNodeType() == Node.ELEMENT_NODE + ? wrap(previousSibling) : new NodeListModel(Collections.emptyList(), null); + } else if (key.equals(AtAtKey.NEXT_SIBLING_ELEMENT.getKey())) { + Node nextSibling = node.getNextSibling(); + while (nextSibling != null && !isSignificantNode(nextSibling)) { + nextSibling = nextSibling.getNextSibling(); + } + return nextSibling != null && nextSibling.getNodeType() == Node.ELEMENT_NODE + ? wrap(nextSibling) : new NodeListModel(Collections.emptyList(), null); + } else { + // We don't know anything like this that's element-specific; fall back + return super.get(key); + } + } else { // Starts with "@", but not with "@@" + if (DomStringUtil.isXMLNameLike(key, 1)) { + Attr att = getAttribute(key.substring(1)); + if (att == null) { + return new NodeListModel(this); + } + return wrap(att); + } else if (key.equals("@*")) { + return new NodeListModel(node.getAttributes(), this); + } else { + // We don't know anything like this that's element-specific; fall back + return super.get(key); + } + } + } else if (DomStringUtil.isXMLNameLike(key)) { + // We interpret key as an element name + NodeListModel result = ((NodeListModel) getChildNodes()).filterByName(key); + return result.size() != 1 ? result : result.get(0); + } else { + // We don't anything like this that's element-specific; fall back + return super.get(key); + } + } + + @Override + public String getAsString() throws TemplateModelException { + NodeList nl = node.getChildNodes(); + String result = ""; + for (int i = 0; i < nl.getLength(); i++) { + Node child = nl.item(i); + int nodeType = child.getNodeType(); + if (nodeType == Node.ELEMENT_NODE) { + String msg = "Only elements with no child elements can be processed as text." + + "\nThis element with name \"" + + node.getNodeName() + + "\" has a child element named: " + child.getNodeName(); + throw new TemplateModelException(msg); + } else if (nodeType == Node.TEXT_NODE || nodeType == Node.CDATA_SECTION_NODE) { + result += child.getNodeValue(); + } + } + return result; + } + + @Override + public String getNodeName() { + String result = node.getLocalName(); + if (result == null || result.equals("")) { + result = node.getNodeName(); + } + return result; + } + + @Override + String getQualifiedName() { + String nodeName = getNodeName(); + String nsURI = getNodeNamespace(); + if (nsURI == null || nsURI.length() == 0) { + return nodeName; + } + Environment env = Environment.getCurrentEnvironment(); + String defaultNS = env.getDefaultNS(); + String prefix; + if (defaultNS != null && defaultNS.equals(nsURI)) { + prefix = ""; + } else { + prefix = env.getPrefixForNamespace(nsURI); + + } + if (prefix == null) { + return null; // We have no qualified name, because there is no prefix mapping + } + if (prefix.length() > 0) { + prefix += ":"; + } + return prefix + nodeName; + } + + private Attr getAttribute(String qname) { + Element element = (Element) node; + Attr result = element.getAttributeNode(qname); + if (result != null) + return result; + int colonIndex = qname.indexOf(':'); + if (colonIndex > 0) { + String prefix = qname.substring(0, colonIndex); + String uri; + if (prefix.equals(Template.DEFAULT_NAMESPACE_PREFIX)) { + uri = Environment.getCurrentEnvironment().getDefaultNS(); + } else { + uri = Environment.getCurrentEnvironment().getNamespaceForPrefix(prefix); + } + String localName = qname.substring(1 + colonIndex); + if (uri != null) { + result = element.getAttributeNodeNS(uri, localName); + } + } + return result; + } + + private boolean isSignificantNode(Node node) throws TemplateModelException { + return (node.getNodeType() == Node.TEXT_NODE || node.getNodeType() == Node.CDATA_SECTION_NODE) + ? !isBlankXMLText(node.getTextContent()) + : node.getNodeType() != Node.PROCESSING_INSTRUCTION_NODE && node.getNodeType() != Node.COMMENT_NODE; + } + + private boolean isBlankXMLText(String s) { + if (s == null) { + return true; + } + for (int i = 0; i < s.length(); i++) { + if (!isXMLWhiteSpace(s.charAt(i))) { + return false; + } + } + return true; + } + + /** + * White space according the XML spec. + */ + private boolean isXMLWhiteSpace(char c) { + return c == ' ' || c == '\t' || c == '\n' | c == '\r'; + } + + boolean matchesName(String name, Environment env) { + return _StringUtil.matchesQName(name, getNodeName(), getNodeNamespace(), env); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/dom/JaxenXPathSupport.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/dom/JaxenXPathSupport.java b/freemarker-core/src/main/java/org/apache/freemarker/dom/JaxenXPathSupport.java new file mode 100644 index 0000000..3e52836 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/dom/JaxenXPathSupport.java @@ -0,0 +1,243 @@ +/* + * 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.dom; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.apache.freemarker.core.CustomStateKey; +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.Template; +import org.apache.freemarker.core.TemplateException; +import org.apache.freemarker.core.model.TemplateBooleanModel; +import org.apache.freemarker.core.model.TemplateDateModel; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.model.TemplateNumberModel; +import org.apache.freemarker.core.model.TemplateScalarModel; +import org.apache.freemarker.core.util.UndeclaredThrowableException; +import org.apache.freemarker.core.util._ObjectHolder; +import org.jaxen.BaseXPath; +import org.jaxen.Function; +import org.jaxen.FunctionCallException; +import org.jaxen.FunctionContext; +import org.jaxen.JaxenException; +import org.jaxen.NamespaceContext; +import org.jaxen.Navigator; +import org.jaxen.UnresolvableException; +import org.jaxen.VariableContext; +import org.jaxen.XPathFunctionContext; +import org.jaxen.dom.DocumentNavigator; +import org.w3c.dom.Document; +import org.xml.sax.EntityResolver; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + + +/** + */ +class JaxenXPathSupport implements XPathSupport { + + private static final CustomStateKey<Map<String, BaseXPath>> XPATH_CACHE_ATTR + = new CustomStateKey<Map<String, BaseXPath>>() { + @Override + protected Map<String, BaseXPath> create() { + return new HashMap<String, BaseXPath>(); + } + }; + + // [2.4] Can't we just use Collections.emptyList()? + private final static ArrayList EMPTY_ARRAYLIST = new ArrayList(); + + @Override + public TemplateModel executeQuery(Object context, String xpathQuery) throws TemplateModelException { + try { + BaseXPath xpath; + Map<String, BaseXPath> xpathCache = Environment.getCurrentEnvironmentNotNull().getCurrentTemplateNotNull() + .getCustomState(XPATH_CACHE_ATTR); + synchronized (xpathCache) { + xpath = xpathCache.get(xpathQuery); + if (xpath == null) { + xpath = new BaseXPath(xpathQuery, FM_DOM_NAVIGATOR); + xpath.setNamespaceContext(customNamespaceContext); + xpath.setFunctionContext(FM_FUNCTION_CONTEXT); + xpath.setVariableContext(FM_VARIABLE_CONTEXT); + xpathCache.put(xpathQuery, xpath); + } + } + List result = xpath.selectNodes(context != null ? context : EMPTY_ARRAYLIST); + if (result.size() == 1) { + return NodeQueryResultItemObjectWrapper.INSTANCE.wrap(result.get(0)); + } + NodeListModel nlm = new NodeListModel(result, null); + nlm.xpathSupport = this; + return nlm; + } catch (UndeclaredThrowableException e) { + Throwable t = e.getUndeclaredThrowable(); + if (t instanceof TemplateModelException) { + throw (TemplateModelException) t; + } + throw e; + } catch (JaxenException je) { + throw new TemplateModelException(je); + } + } + + static private final NamespaceContext customNamespaceContext = new NamespaceContext() { + + @Override + public String translateNamespacePrefixToUri(String prefix) { + if (prefix.equals(Template.DEFAULT_NAMESPACE_PREFIX)) { + return Environment.getCurrentEnvironment().getDefaultNS(); + } + return Environment.getCurrentEnvironment().getNamespaceForPrefix(prefix); + } + }; + + private static final VariableContext FM_VARIABLE_CONTEXT = new VariableContext() { + @Override + public Object getVariableValue(String namespaceURI, String prefix, String localName) + throws UnresolvableException { + try { + TemplateModel model = Environment.getCurrentEnvironment().getVariable(localName); + if (model == null) { + throw new UnresolvableException("Variable \"" + localName + "\" not found."); + } + if (model instanceof TemplateScalarModel) { + return ((TemplateScalarModel) model).getAsString(); + } + if (model instanceof TemplateNumberModel) { + return ((TemplateNumberModel) model).getAsNumber(); + } + if (model instanceof TemplateDateModel) { + return ((TemplateDateModel) model).getAsDate(); + } + if (model instanceof TemplateBooleanModel) { + return Boolean.valueOf(((TemplateBooleanModel) model).getAsBoolean()); + } + } catch (TemplateModelException e) { + throw new UndeclaredThrowableException(e); + } + throw new UnresolvableException( + "Variable \"" + localName + "\" exists, but it's not a string, number, date, or boolean"); + } + }; + + private static final FunctionContext FM_FUNCTION_CONTEXT = new XPathFunctionContext() { + @Override + public Function getFunction(String namespaceURI, String prefix, String localName) + throws UnresolvableException { + try { + return super.getFunction(namespaceURI, prefix, localName); + } catch (UnresolvableException e) { + return super.getFunction(null, null, localName); + } + } + }; + + /** + * Stores the the template parsed as {@link Document} in the template itself. + */ + private static final CustomStateKey<_ObjectHolder<Document>> FM_DOM_NAVIAGOTOR_CACHED_DOM + = new CustomStateKey<_ObjectHolder<Document>>() { + @Override + protected _ObjectHolder<Document> create() { + return new _ObjectHolder<>(null); + } + }; + + private static final Navigator FM_DOM_NAVIGATOR = new DocumentNavigator() { + @Override + public Object getDocument(String uri) throws FunctionCallException { + try { + Template raw = getTemplate(uri); + _ObjectHolder<Document> docHolder = Environment.getCurrentEnvironmentNotNull() + .getCurrentTemplateNotNull().getCustomState(FM_DOM_NAVIAGOTOR_CACHED_DOM); + synchronized (docHolder) { + Document doc = docHolder.get(); + if (doc == null) { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + DocumentBuilder builder = factory.newDocumentBuilder(); + FmEntityResolver er = new FmEntityResolver(); + builder.setEntityResolver(er); + doc = builder.parse(createInputSource(null, raw)); + // If the entity resolver got called 0 times, the document + // is standalone, so we can safely cache it + if (er.getCallCount() == 0) { + docHolder.set(doc); + } + } + return doc; + } + } catch (Exception e) { + throw new FunctionCallException("Failed to parse document for URI: " + uri, e); + } + } + }; + + // [FM3] Look into this "hidden" feature + static Template getTemplate(String systemId) throws IOException { + Environment env = Environment.getCurrentEnvironment(); + String templatePath = env.getCurrentTemplate().getLookupName(); + int lastSlash = templatePath.lastIndexOf('/'); + templatePath = lastSlash == -1 ? "" : templatePath.substring(0, lastSlash + 1); + systemId = env.toFullTemplateName(templatePath, systemId); + return env.getConfiguration().getTemplate(systemId, env.getLocale()); + } + + private static InputSource createInputSource(String publicId, Template raw) throws IOException, SAXException { + StringWriter sw = new StringWriter(); + try { + raw.process(Collections.EMPTY_MAP, sw); + } catch (TemplateException e) { + throw new SAXException(e); + } + InputSource is = new InputSource(); + is.setPublicId(publicId); + is.setSystemId(raw.getLookupName()); + is.setCharacterStream(new StringReader(sw.toString())); + return is; + } + + private static class FmEntityResolver implements EntityResolver { + private int callCount = 0; + + @Override + public InputSource resolveEntity(String publicId, String systemId) + throws SAXException, IOException { + ++callCount; + return createInputSource(publicId, getTemplate(systemId)); + } + + int getCallCount() { + return callCount; + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/dom/NodeListModel.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/dom/NodeListModel.java b/freemarker-core/src/main/java/org/apache/freemarker/dom/NodeListModel.java new file mode 100644 index 0000000..333bb5c --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/dom/NodeListModel.java @@ -0,0 +1,219 @@ +/* + * 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.dom; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.freemarker.core.Configuration; +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core._UnexpectedTypeErrorExplainerTemplateModel; +import org.apache.freemarker.core.model.TemplateBooleanModel; +import org.apache.freemarker.core.model.TemplateDateModel; +import org.apache.freemarker.core.model.TemplateHashModel; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.model.TemplateNodeModel; +import org.apache.freemarker.core.model.TemplateNumberModel; +import org.apache.freemarker.core.model.TemplateScalarModel; +import org.apache.freemarker.core.model.TemplateSequenceModel; +import org.apache.freemarker.core.model.impl.SimpleScalar; +import org.apache.freemarker.core.model.impl.SimpleSequence; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * Used when the result set contains 0 or multiple nodes; shouldn't be used when you have exactly 1 node. For exactly 1 + * node, use {@link NodeModel#wrap(Node)}, because {@link NodeModel} subclasses can have extra features building on that + * restriction, like single elements with text content can be used as FTL string-s. + * <p> + * This class is not guaranteed to be thread safe, so instances of this shouldn't be used as + * {@linkplain Configuration#getSharedVariables() shared variable}. + */ +class NodeListModel extends SimpleSequence implements TemplateHashModel, _UnexpectedTypeErrorExplainerTemplateModel { + + // [2.4] make these private + NodeModel contextNode; + XPathSupport xpathSupport; + + NodeListModel(Node contextNode) { + this(NodeModel.wrap(contextNode)); + } + + NodeListModel(NodeModel contextNode) { + super(NodeQueryResultItemObjectWrapper.INSTANCE); + this.contextNode = contextNode; + } + + NodeListModel(NodeList nodeList, NodeModel contextNode) { + super(NodeQueryResultItemObjectWrapper.INSTANCE); + for (int i = 0; i < nodeList.getLength(); i++) { + list.add(nodeList.item(i)); + } + this.contextNode = contextNode; + } + + NodeListModel(NamedNodeMap nodeList, NodeModel contextNode) { + super(NodeQueryResultItemObjectWrapper.INSTANCE); + for (int i = 0; i < nodeList.getLength(); i++) { + list.add(nodeList.item(i)); + } + this.contextNode = contextNode; + } + + NodeListModel(List list, NodeModel contextNode) { + super(list, NodeQueryResultItemObjectWrapper.INSTANCE); + this.contextNode = contextNode; + } + + NodeListModel filterByName(String name) throws TemplateModelException { + NodeListModel result = new NodeListModel(contextNode); + int size = size(); + if (size == 0) { + return result; + } + Environment env = Environment.getCurrentEnvironment(); + for (int i = 0; i < size; i++) { + NodeModel nm = (NodeModel) get(i); + if (nm instanceof ElementModel) { + if (((ElementModel) nm).matchesName(name, env)) { + result.add(nm); + } + } + } + return result; + } + + @Override + public boolean isEmpty() { + return size() == 0; + } + + @Override + public TemplateModel get(String key) throws TemplateModelException { + if (size() == 1) { + NodeModel nm = (NodeModel) get(0); + return nm.get(key); + } + if (key.startsWith("@@")) { + if (key.equals(AtAtKey.MARKUP.getKey()) + || key.equals(AtAtKey.NESTED_MARKUP.getKey()) + || key.equals(AtAtKey.TEXT.getKey())) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < size(); i++) { + NodeModel nm = (NodeModel) get(i); + TemplateScalarModel textModel = (TemplateScalarModel) nm.get(key); + result.append(textModel.getAsString()); + } + return new SimpleScalar(result.toString()); + } else if (key.length() != 2 /* to allow "@@" to fall through */) { + // As @@... would cause exception in the XPath engine, we throw a nicer exception now. + if (AtAtKey.containsKey(key)) { + throw new TemplateModelException( + "\"" + key + "\" is only applicable to a single XML node, but it was applied on " + + (size() != 0 + ? size() + " XML nodes (multiple matches)." + : "an empty list of XML nodes (no matches).")); + } else { + throw new TemplateModelException("Unsupported @@ key: " + key); + } + } + } + if (DomStringUtil.isXMLNameLike(key) + || ((key.startsWith("@") + && (DomStringUtil.isXMLNameLike(key, 1) || key.equals("@@") || key.equals("@*")))) + || key.equals("*") || key.equals("**")) { + NodeListModel result = new NodeListModel(contextNode); + for (int i = 0; i < size(); i++) { + NodeModel nm = (NodeModel) get(i); + if (nm instanceof ElementModel) { + TemplateSequenceModel tsm = (TemplateSequenceModel) nm.get(key); + if (tsm != null) { + int size = tsm.size(); + for (int j = 0; j < size; j++) { + result.add(tsm.get(j)); + } + } + } + } + if (result.size() == 1) { + return result.get(0); + } + return result; + } + XPathSupport xps = getXPathSupport(); + if (xps != null) { + Object context = (size() == 0) ? null : rawNodeList(); + return xps.executeQuery(context, key); + } else { + throw new TemplateModelException( + "Can't try to resolve the XML query key, because no XPath support is available. " + + "This is either malformed or an XPath expression: " + key); + } + } + + private List rawNodeList() throws TemplateModelException { + int size = size(); + ArrayList al = new ArrayList(size); + for (int i = 0; i < size; i++) { + al.add(((NodeModel) get(i)).node); + } + return al; + } + + XPathSupport getXPathSupport() throws TemplateModelException { + if (xpathSupport == null) { + if (contextNode != null) { + xpathSupport = contextNode.getXPathSupport(); + } else if (size() > 0) { + xpathSupport = ((NodeModel) get(0)).getXPathSupport(); + } + } + return xpathSupport; + } + + @Override + public Object[] explainTypeError(Class[] expectedClasses) { + for (Class expectedClass : expectedClasses) { + if (TemplateScalarModel.class.isAssignableFrom(expectedClass) + || TemplateDateModel.class.isAssignableFrom(expectedClass) + || TemplateNumberModel.class.isAssignableFrom(expectedClass) + || TemplateBooleanModel.class.isAssignableFrom(expectedClass)) { + return newTypeErrorExplanation("string"); + } else if (TemplateNodeModel.class.isAssignableFrom(expectedClass)) { + return newTypeErrorExplanation("node"); + } + } + return null; + } + + private Object[] newTypeErrorExplanation(String type) { + return new Object[] { + "This XML query result can't be used as ", type, " because for that it had to contain exactly " + + "1 XML node, but it contains ", Integer.valueOf(size()), " nodes. " + + "That is, the constructing XML query has found ", + isEmpty() + ? "no matches." + : "multiple matches." + }; + } + +} \ No newline at end of file
