http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/RegexpHelper.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/RegexpHelper.java b/freemarker-core/src/main/java/org/apache/freemarker/core/RegexpHelper.java new file mode 100644 index 0000000..3d0e853 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/RegexpHelper.java @@ -0,0 +1,207 @@ +/* + * 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; + +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.templateresolver.impl.MruCacheStorage; +import org.apache.freemarker.core.util._StringUtil; +import org.slf4j.Logger; + +/** + * Helper for language features (like built-ins) that use regular expressions. + */ +final class RegexpHelper { + + private static final Logger LOG = _CoreLogs.RUNTIME; + + private static volatile boolean flagWarningsEnabled = LOG.isWarnEnabled(); + private static final int MAX_FLAG_WARNINGS_LOGGED = 25; + private static final Object flagWarningsCntSync = new Object(); + private static int flagWarningsCnt; + + private static final MruCacheStorage patternCache = new MruCacheStorage(50, 150); + + static private long intFlagToLong(int flag) { + return flag & 0x0000FFFFL; + } + + // Standard regular expression flags converted to long: + static final long RE_FLAG_CASE_INSENSITIVE = intFlagToLong(Pattern.CASE_INSENSITIVE); + + static final long RE_FLAG_MULTILINE = intFlagToLong(Pattern.MULTILINE); + + static final long RE_FLAG_COMMENTS = intFlagToLong(Pattern.COMMENTS); + + static final long RE_FLAG_DOTALL = intFlagToLong(Pattern.DOTALL); + + // FreeMarker-specific regular expression flags (using the higher 32 bits): + static final long RE_FLAG_REGEXP = 0x100000000L; + + static final long RE_FLAG_FIRST_ONLY = 0x200000000L; + + // Can't be instantiated + private RegexpHelper() { } + + static Pattern getPattern(String patternString, int flags) + throws TemplateModelException { + PatternCacheKey patternKey = new PatternCacheKey(patternString, flags); + + Pattern result; + + synchronized (patternCache) { + result = (Pattern) patternCache.get(patternKey); + } + if (result != null) { + return result; + } + + try { + result = Pattern.compile(patternString, flags); + } catch (PatternSyntaxException e) { + throw new _TemplateModelException(e, + "Malformed regular expression: ", new _DelayedGetMessage(e)); + } + synchronized (patternCache) { + patternCache.put(patternKey, result); + } + return result; + } + + private static class PatternCacheKey { + private final String patternString; + private final int flags; + private final int hashCode; + + public PatternCacheKey(String patternString, int flags) { + this.patternString = patternString; + this.flags = flags; + hashCode = patternString.hashCode() + 31 * flags; + } + + @Override + public boolean equals(Object that) { + if (that instanceof PatternCacheKey) { + PatternCacheKey thatPCK = (PatternCacheKey) that; + return thatPCK.flags == flags + && thatPCK.patternString.equals(patternString); + } else { + return false; + } + } + + @Override + public int hashCode() { + return hashCode; + } + + } + + static long parseFlagString(String flagString) { + long flags = 0; + for (int i = 0; i < flagString.length(); i++) { + char c = flagString.charAt(i); + switch (c) { + case 'i': + flags |= RE_FLAG_CASE_INSENSITIVE; + break; + case 'm': + flags |= RE_FLAG_MULTILINE; + break; + case 'c': + flags |= RE_FLAG_COMMENTS; + break; + case 's': + flags |= RE_FLAG_DOTALL; + break; + case 'r': + flags |= RE_FLAG_REGEXP; + break; + case 'f': + flags |= RE_FLAG_FIRST_ONLY; + break; + default: + if (flagWarningsEnabled) { + // [FM3] Should be an error + RegexpHelper.logFlagWarning( + "Unrecognized regular expression flag: " + + _StringUtil.jQuote(String.valueOf(c)) + "."); + } + } // switch + } + return flags; + } + + /** + * Logs flag warning for a limited number of times. This is used to prevent + * log flooding. + */ + static void logFlagWarning(String message) { + if (!flagWarningsEnabled) return; + + int cnt; + synchronized (flagWarningsCntSync) { + cnt = flagWarningsCnt; + if (cnt < MAX_FLAG_WARNINGS_LOGGED) { + flagWarningsCnt++; + } else { + flagWarningsEnabled = false; + return; + } + } + message += " This will be an error in some later FreeMarker version!"; + if (cnt + 1 == MAX_FLAG_WARNINGS_LOGGED) { + message += " [Will not log more regular expression flag problems until restart!]"; + } + LOG.warn(message); + } + + static void checkNonRegexpFlags(String biName, long flags) throws _TemplateModelException { + checkOnlyHasNonRegexpFlags(biName, flags, false); + } + + static void checkOnlyHasNonRegexpFlags(String biName, long flags, boolean strict) + throws _TemplateModelException { + if (!strict && !flagWarningsEnabled) return; + + String flag; + if ((flags & RE_FLAG_MULTILINE) != 0) { + flag = "m"; + } else if ((flags & RE_FLAG_DOTALL) != 0) { + flag = "s"; + } else if ((flags & RE_FLAG_COMMENTS) != 0) { + flag = "c"; + } else { + return; + } + + final Object[] msg = { "?", biName ," doesn't support the \"", flag, "\" flag " + + "without the \"r\" flag." }; + if (strict) { + throw new _TemplateModelException(msg); + } else { + // Suppress error for backward compatibility + logFlagWarning(new _ErrorDescriptionBuilder(msg).toString()); + } + } + +}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/RightUnboundedRangeModel.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/RightUnboundedRangeModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/RightUnboundedRangeModel.java new file mode 100644 index 0000000..e135d18 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/RightUnboundedRangeModel.java @@ -0,0 +1,48 @@ +/* + * 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; + +abstract class RightUnboundedRangeModel extends RangeModel { + + RightUnboundedRangeModel(int begin) { + super(begin); + } + + @Override + final int getStep() { + return 1; + } + + @Override + final boolean isRightUnbounded() { + return true; + } + + @Override + final boolean isRightAdaptive() { + return true; + } + + @Override + final boolean isAffactedByStringSlicingBug() { + return false; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/SettingValueNotSetException.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/SettingValueNotSetException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/SettingValueNotSetException.java new file mode 100644 index 0000000..1ce895d --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/SettingValueNotSetException.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.freemarker.core; + +import org.apache.freemarker.core.util._StringUtil; + +public class SettingValueNotSetException extends IllegalStateException { + + private final String settingName; + + public SettingValueNotSetException(String settingName) { + super("The " + _StringUtil.jQuote(settingName) + + " setting is not set in this layer and has no default here either."); + this.settingName = settingName; + } +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/SpecialBuiltIn.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/SpecialBuiltIn.java b/freemarker-core/src/main/java/org/apache/freemarker/core/SpecialBuiltIn.java new file mode 100644 index 0000000..5d43b59 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/SpecialBuiltIn.java @@ -0,0 +1,27 @@ +/* + * 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; + + +/** + * Marker class for built-ins that has special treatment during parsing. + */ +abstract class SpecialBuiltIn extends ASTExpBuiltIn { + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/StopException.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/StopException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/StopException.java new file mode 100644 index 0000000..9706456 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/StopException.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; + +import java.io.PrintStream; +import java.io.PrintWriter; + +/** + * This exception is thrown when a <tt>#stop</tt> directive is encountered. + */ +public class StopException extends TemplateException { + + StopException(Environment env) { + super(env); + } + + StopException(Environment env, String s) { + super(s, env); + } + + @Override + public void printStackTrace(PrintWriter pw) { + synchronized (pw) { + String msg = getMessage(); + pw.print("Encountered stop instruction"); + if (msg != null && !msg.equals("")) { + pw.println("\nCause given: " + msg); + } else pw.println(); + super.printStackTrace(pw); + } + } + + @Override + public void printStackTrace(PrintStream ps) { + synchronized (ps) { + String msg = getMessage(); + ps.print("Encountered stop instruction"); + if (msg != null && !msg.equals("")) { + ps.println("\nCause given: " + msg); + } else ps.println(); + super.printStackTrace(ps); + } + } + +} + + http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java b/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java new file mode 100644 index 0000000..c4787e4 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java @@ -0,0 +1,1341 @@ +/* + * 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; + +import java.io.BufferedReader; +import java.io.FilterReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.io.Reader; +import java.io.Serializable; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.reflect.UndeclaredThrowableException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.freemarker.core.arithmetic.ArithmeticEngine; +import org.apache.freemarker.core.debug._DebuggerService; +import org.apache.freemarker.core.model.ObjectWrapper; +import org.apache.freemarker.core.model.TemplateHashModel; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateNodeModel; +import org.apache.freemarker.core.model.impl.SimpleHash; +import org.apache.freemarker.core.outputformat.OutputFormat; +import org.apache.freemarker.core.templateresolver.TemplateLoader; +import org.apache.freemarker.core.templateresolver.TemplateLookupStrategy; +import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateResolver; +import org.apache.freemarker.core.templateresolver.impl.FileTemplateLoader; +import org.apache.freemarker.core.util.BugException; +import org.apache.freemarker.core.util._NullArgumentException; +import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory; +import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +/** + * <p> + * Stores an already parsed template, ready to be processed (rendered) for unlimited times, possibly from multiple + * threads. + * + * <p> + * Typically, you will use {@link Configuration#getTemplate(String)} to invoke/get {@link Template} objects, so you + * don't construct them directly. But you can also construct a template from a {@link Reader} or a {@link String} that + * contains the template source code. But then it's important to know that while the resulting {@link Template} is + * efficient for later processing, creating a new {@link Template} itself is relatively expensive. So try to re-use + * {@link Template} objects if possible. {@link Configuration#getTemplate(String)} (and its overloads) does that + * (caching {@link Template}-s) for you, but the constructor of course doesn't, so it's up to you to solve then. + * + * <p> + * Objects of this class meant to be handled as immutable and thus thread-safe. However, it has some setter methods for + * changing FreeMarker settings. Those must not be used while the template is being processed, or if the template object + * is already accessible from multiple threads. If some templates need different settings that those coming from the + * shared {@link Configuration}, and you are using {@link Configuration#getTemplate(String)} (or its overloads), then + * use the {@link Configuration#getTemplateConfigurations() templateConfigurations} setting to achieve that. + */ +// TODO [FM3] Try to make Template serializable for distributed caching. Transient fields will have to be restored. +public class Template implements ProcessingConfiguration, CustomStateScope { + public static final String DEFAULT_NAMESPACE_PREFIX = "D"; + public static final String NO_NS_PREFIX = "N"; + + private static final int READER_BUFFER_SIZE = 8192; + + private ASTElement rootElement; + private Map macros = new HashMap(); // TODO Don't create new object if it remains empty. + private List imports = new ArrayList(); // TODO Don't create new object if it remains empty. + + // Source (TemplateLoader) related information: + private final String sourceName; + private final ArrayList lines = new ArrayList(); + + // TODO [FM3] We want to get rid of these, thenthe same Template object could be reused for different lookups. + // Template lookup parameters: + private final String lookupName; + private Locale lookupLocale; + private Serializable customLookupCondition; + + // Inherited settings: + private final transient Configuration cfg; + private final transient TemplateConfiguration tCfg; + private final transient ParsingConfiguration parsingConfiguration; + + // Values from the template content (#ftl header parameters usually), as opposed to from the TemplateConfiguration: + private transient OutputFormat outputFormat; // TODO Deserialization: use the name of the output format + private String defaultNS; + private Map prefixToNamespaceURILookup = new HashMap(); + private Map namespaceURIToPrefixLookup = new HashMap(); + private Map<String, Serializable> customAttributes; + private transient Map<Object, Object> mergedCustomAttributes; + + private Integer autoEscapingPolicy; + // Values from template content that are detected automatically: + private Charset actualSourceEncoding; + private int actualTagSyntax; + + private int actualNamingConvention; + // Custom state: + private final Object customStateMapLock = new Object(); + private final ConcurrentHashMap<CustomStateKey, Object> customStateMap = new ConcurrentHashMap<>(0); + + /** + * Same as {@link #Template(String, String, Reader, Configuration)} with {@code null} {@code sourceName} parameter. + */ + public Template(String lookupName, Reader reader, Configuration cfg) throws IOException { + this(lookupName, null, reader, cfg); + } + + /** + * Convenience constructor for {@link #Template(String, Reader, Configuration) + * Template(lookupName, new StringReader(reader), cfg)}. + * + * @since 2.3.20 + */ + public Template(String lookupName, String sourceCode, Configuration cfg) throws IOException { + this(lookupName, new StringReader(sourceCode), cfg); + } + + /** + * Convenience constructor for {@link #Template(String, String, Reader, Configuration, TemplateConfiguration, + * Charset) Template(lookupName, null, new StringReader(reader), cfg), tc, null}. + * + * @since 2.3.20 + */ + public Template(String lookupName, String sourceCode, Configuration cfg, TemplateConfiguration tc) throws IOException { + this(lookupName, null, new StringReader(sourceCode), cfg, tc, null); + } + + /** + * Convenience constructor for {@link #Template(String, String, Reader, Configuration, Charset) Template(lookupName, null, + * reader, cfg, sourceEncoding)}. + */ + public Template(String lookupName, Reader reader, Configuration cfg, Charset sourceEncoding) throws IOException { + this(lookupName, null, reader, cfg, sourceEncoding); + } + + /** + * Constructs a template from a character stream. Note that this is a relatively expensive operation; where higher + * performance matters, you should re-use (cache) {@link Template} instances instead of re-creating them from the + * same source again and again. ({@link Configuration#getTemplate(String) and its overloads already do such reuse.}) + * + * @param lookupName + * The name (path) with which the template was get (usually via + * {@link Configuration#getTemplate(String)}), after basic normalization. (Basic normalization means + * things that doesn't require accessing the backing storage, such as {@code "/a/../b/foo.ftl"} + * becomes to {@code "b/foo.ftl"}). + * This is usually the path of the template file relatively to the (virtual) directory that you use to + * store the templates (except if the {@link #getSourceName()} sourceName} differs from it). + * Shouldn't start with {@code '/'}. Should use {@code '/'}, not {@code '\'}. Check + * {@link #getLookupName()} to see how the name will be used. The name should be independent of the actual + * storage mechanism and physical location as far as possible. Even when the templates are stored + * straightforwardly in real files (they often aren't; see {@link TemplateLoader}), the name shouldn't be + * an absolute file path. Like if the template is stored in {@code "/www/templates/forum/main.ftl"}, and + * you are using {@code "/www/templates/"} as the template root directory via + * {@link FileTemplateLoader#FileTemplateLoader(java.io.File)}, then the template name will be + * {@code "forum/main.ftl"}. The name can be {@code null} (should be used for template made on-the-fly + * instead of being loaded from somewhere), in which case relative paths in it will be relative to + * the template root directory (and here again, it's the {@link TemplateLoader} that knows what that + * "physically" means). + * @param sourceName + * Often the same as the {@code lookupName}; see {@link #getSourceName()} for more. Can be + * {@code null}, in which case error messages will fall back to use {@link #getLookupName()}. + * @param reader + * The character stream to read from. The {@link Reader} is <em>not</em> closed by this method (unlike + * in FreeMarker 2.x.x), so be sure that it's closed somewhere. (Except of course, readers like + * {@link StringReader} need not be closed.) The {@link Reader} need not be buffered, because this + * method ensures that it will be read in few kilobyte chunks, not byte by byte. + * @param cfg + * The Configuration object that this Template is associated with. Can't be {@code null}. + * + * @since 2.3.22 + */ + public Template( + String lookupName, String sourceName, Reader reader, Configuration cfg) throws IOException { + this(lookupName, sourceName, reader, cfg, null); + } + + /** + * Same as {@link #Template(String, String, Reader, Configuration)}, but also specifies the template's source + * encoding. + * + * @param actualSourceEncoding + * This is the charset that was used to read the template. This can be {@code null} if the template + * was loaded from a source that returns it already as text. If this is not {@code null} and there's an + * {@code #ftl} header with {@code encoding} parameter, they must match, or else a + * {@link WrongTemplateCharsetException} is thrown. + * + * @since 2.3.22 + */ + public Template( + String lookupName, String sourceName, Reader reader, Configuration cfg, Charset actualSourceEncoding) throws + IOException { + this(lookupName, sourceName, reader, cfg, null, actualSourceEncoding); + } + + /** + * Same as {@link #Template(String, String, Reader, Configuration, Charset)}, but also specifies a + * {@link TemplateConfiguration}. This is mostly meant to be used by FreeMarker internally, but advanced users might + * still find this useful. + * + * @param templateConfiguration + * Overrides the configuration settings of the {@link Configuration} parameter; can be + * {@code null}. This is useful as the {@link Configuration} is normally a singleton shared by all + * templates, and so it's not good for specifying template-specific settings. Settings that influence + * parsing always have an effect, while settings that influence processing only have effect when the + * template is the main template of the {@link Environment}. + * @param actualSourceEncoding + * Same as in {@link #Template(String, String, Reader, Configuration, Charset)}. + * + * @since 2.3.24 + */ + public Template( + String lookupName, String sourceName, Reader reader, + Configuration cfg, TemplateConfiguration templateConfiguration, + Charset actualSourceEncoding) throws IOException { + this(lookupName, sourceName, reader, cfg, templateConfiguration, actualSourceEncoding, null); + } + + /** + * Same as {@link #Template(String, String, Reader, Configuration, TemplateConfiguration, Charset)}, but allows + * specifying the {@code streamToUnmarkWhenEncEstabd}. + * + * @param streamToUnmarkWhenEncEstabd + * If not {@code null}, when during the parsing we reach a point where we know that no {@link + * WrongTemplateCharsetException} will be thrown, {@link InputStream#mark(int) mark(0)} will be called on this. + * This is meant to be used when the reader parameter is a {@link InputStreamReader}, and this parameter is + * the underlying {@link InputStream}, and you have a mark at the beginning of the {@link InputStream} so + * that you can retry if a {@link WrongTemplateCharsetException} is thrown without extra I/O. As keeping that + * mark consumes some resources, so you may want to release it as soon as possible. + */ + public Template( + String lookupName, String sourceName, Reader reader, + Configuration cfg, TemplateConfiguration templateConfiguration, + Charset actualSourceEncoding, InputStream streamToUnmarkWhenEncEstabd) throws IOException, ParseException { + this(lookupName, sourceName, reader, + cfg, templateConfiguration, + null, null, + actualSourceEncoding, streamToUnmarkWhenEncEstabd); + } + + /** + * Same as {@link #Template(String, String, Reader, Configuration, TemplateConfiguration, Charset, InputStream)}, + * but allows specifying the output format and the auto escaping policy, with similar effect as if they were + * specified in the template content (like in the #ftl header). + * <p> + * <p>This method is currently only used internally, as it's not generalized enough and so it carries too much + * backward compatibility risk. Also, the same functionality can be achieved by constructing an appropriate + * {@link TemplateConfiguration}, only that's somewhat slower. + * + * @param contextOutputFormat + * The output format of the enclosing lexical context, used when a template snippet is parsed on runtime. If + * not {@code null}, this will override the value coming from the {@link TemplateConfiguration} or the + * {@link Configuration}. + * @param contextAutoEscapingPolicy + * Similar to {@code contextOutputFormat}; usually this and the that is set together. + */ + Template( + String lookupName, String sourceName, Reader reader, + Configuration configuration, TemplateConfiguration templateConfiguration, + OutputFormat contextOutputFormat, Integer contextAutoEscapingPolicy, + Charset actualSourceEncoding, InputStream streamToUnmarkWhenEncEstabd) throws IOException, ParseException { + _NullArgumentException.check("configuration", configuration); + this.cfg = configuration; + this.tCfg = templateConfiguration; + this.parsingConfiguration = tCfg != null ? new TemplateParsingConfigurationWithFallback(cfg, tCfg) : cfg; + this.lookupName = lookupName; + this.sourceName = sourceName; + + setActualSourceEncoding(actualSourceEncoding); + LineTableBuilder ltbReader; + try { + // Ensure that the parameter Reader is only read in bigger chunks, as we don't know if the it's buffered. + // In particular, inside the FreeMarker code, we assume that the stream stages need not be buffered. + if (!(reader instanceof BufferedReader) && !(reader instanceof StringReader)) { + reader = new BufferedReader(reader, READER_BUFFER_SIZE); + } + + ltbReader = new LineTableBuilder(reader, parsingConfiguration); + reader = ltbReader; + + try { + FMParser parser = new FMParser( + this, reader, + parsingConfiguration, contextOutputFormat, contextAutoEscapingPolicy, + streamToUnmarkWhenEncEstabd); + try { + rootElement = parser.Root(); + } catch (IndexOutOfBoundsException exc) { + // There's a JavaCC bug where the Reader throws a RuntimeExcepton and then JavaCC fails with + // IndexOutOfBoundsException. If that wasn't the case, we just rethrow. Otherwise we suppress the + // IndexOutOfBoundsException and let the real cause to be thrown later. + if (!ltbReader.hasFailure()) { + throw exc; + } + rootElement = null; + } + actualTagSyntax = parser._getLastTagSyntax(); + actualNamingConvention = parser._getLastNamingConvention(); + } catch (TokenMgrError exc) { + // TokenMgrError VS ParseException is not an interesting difference for the user, so we just convert it + // to ParseException + throw exc.toParseException(this); + } + } catch (ParseException e) { + e.setTemplate(this); + throw e; + } + + // Throws any exception that JavaCC has silently treated as EOF: + ltbReader.throwFailure(); + + _DebuggerService.registerTemplate(this); + namespaceURIToPrefixLookup = Collections.unmodifiableMap(namespaceURIToPrefixLookup); + prefixToNamespaceURILookup = Collections.unmodifiableMap(prefixToNamespaceURILookup); + } + + /** + * Same as {@link #createPlainTextTemplate(String, String, String, Configuration, Charset)} with {@code null} + * {@code sourceName} argument. + */ + static public Template createPlainTextTemplate(String lookupName, String content, Configuration config) { + return createPlainTextTemplate(lookupName, null, content, config, null); + } + + /** + * Creates a {@link Template} that only contains a single block of static text, no dynamic content. + * + * @param lookupName + * See {@link #getLookupName} for more details. + * @param sourceName + * See {@link #getSourceName} for more details. If {@code null}, it will be the same as the {@code name}. + * @param content + * the block of text that this template represents + * @param config + * the configuration to which this template belongs + * + * @param sourceEncoding The charset used to decode the template content to the {@link String} passed in with the + * {@code content} parameter. If that information is not known or irrelevant, this should be + * {@code null}. + * + * @since 2.3.22 + */ + static public Template createPlainTextTemplate(String lookupName, String sourceName, String content, Configuration config, + Charset sourceEncoding) { + Template template; + try { + template = new Template(lookupName, sourceName, new StringReader("X"), config); + } catch (IOException e) { + throw new BugException("Plain text template creation failed", e); + } + ((ASTStaticText) template.rootElement).replaceText(content); + template.setActualSourceEncoding(sourceEncoding); + + _DebuggerService.registerTemplate(template); + + return template; + } + + /** + * Executes template, using the data-model provided, writing the generated output to the supplied {@link Writer}. + * + * <p> + * For finer control over the runtime environment setup, such as per-HTTP-request configuring of FreeMarker + * settings, you may need to use {@link #createProcessingEnvironment(Object, Writer)} instead. + * + * @param dataModel + * the holder of the variables visible from the template (name-value pairs); usually a + * {@code Map<String, Object>} or a JavaBean (where the JavaBean properties will be the variables). Can + * be any object that the {@link ObjectWrapper} in use turns into a {@link TemplateHashModel}. You can + * also use an object that already implements {@link TemplateHashModel}; in that case it won't be + * wrapped. If it's {@code null}, an empty data model is used. + * @param out + * The {@link Writer} where the output of the template will go. Note that unless you have set + * {@link ProcessingConfiguration#getAutoFlush() autoFlush} to {@code false} to disable this, + * {@link Writer#flush()} will be called at the when the template processing was finished. + * {@link Writer#close()} is not called. Can't be {@code null}. + * + * @throws TemplateException + * if an exception occurs during template processing + * @throws IOException + * if an I/O exception occurs during writing to the writer. + */ + public void process(Object dataModel, Writer out) + throws TemplateException, IOException { + createProcessingEnvironment(dataModel, out, null).process(); + } + + /** + * Like {@link #process(Object, Writer)}, but also sets a (XML-)node to be recursively processed by the template. + * That node is accessed in the template with <tt>.node</tt>, <tt>#recurse</tt>, etc. See the + * <a href="http://freemarker.org/docs/xgui_declarative.html" target="_blank">Declarative XML Processing</a> as a + * typical example of recursive node processing. + * + * @param rootNode The root node for recursive processing or {@code null}. + * + * @throws TemplateException if an exception occurs during template processing + * @throws IOException if an I/O exception occurs during writing to the writer. + */ + public void process(Object dataModel, Writer out, ObjectWrapper wrapper, TemplateNodeModel rootNode) + throws TemplateException, IOException { + Environment env = createProcessingEnvironment(dataModel, out, wrapper); + if (rootNode != null) { + env.setCurrentVisitorNode(rootNode); + } + env.process(); + } + + /** + * Like {@link #process(Object, Writer)}, but overrides the {@link Configuration#getObjectWrapper()}. + * + * @param wrapper The {@link ObjectWrapper} to be used instead of what {@link Configuration#getObjectWrapper()} + * provides, or {@code null} if you don't want to override that. + */ + public void process(Object dataModel, Writer out, ObjectWrapper wrapper) + throws TemplateException, IOException { + createProcessingEnvironment(dataModel, out, wrapper).process(); + } + + /** + * Creates a {@link org.apache.freemarker.core.Environment Environment} object, using this template, the data-model provided as + * parameter. You have to call {@link Environment#process()} on the return value to set off the actual rendering. + * + * <p>Use this method if you want to do some special initialization on the {@link Environment} before template + * processing, or if you want to read the {@link Environment} after template processing. Otherwise using + * {@link Template#process(Object, Writer)} is simpler. + * + * <p>Example: + * + * <pre> + * Environment env = myTemplate.createProcessingEnvironment(root, out, null); + * env.process();</pre> + * + * <p>The above is equivalent with this: + * + * <pre> + * myTemplate.process(root, out);</pre> + * + * <p>But with <tt>createProcessingEnvironment</tt>, you can manipulate the environment + * before and after the processing: + * + * <pre> + * Environment env = myTemplate.createProcessingEnvironment(root, out); + * + * env.setLocale(myUsersPreferredLocale); + * env.setTimeZone(myUsersPreferredTimezone); + * + * env.process(); // output is rendered here + * + * TemplateModel x = env.getVariable("x"); // read back a variable set by the template</pre> + * + * @param dataModel the holder of the variables visible from all templates; see {@link #process(Object, Writer)} for + * more details. + * @param wrapper The {@link ObjectWrapper} to use to wrap objects into {@link TemplateModel} + * instances. Normally you left it {@code null}, in which case {@link MutableProcessingConfiguration#getObjectWrapper()} will be + * used. + * @param out The {@link Writer} where the output of the template will go; see {@link #process(Object, Writer)} for + * more details. + * + * @return the {@link Environment} object created for processing. Call {@link Environment#process()} to process the + * template. + * + * @throws TemplateException if an exception occurs while setting up the Environment object. + * @throws IOException if an exception occurs doing any auto-imports + */ + public Environment createProcessingEnvironment(Object dataModel, Writer out, ObjectWrapper wrapper) + throws TemplateException, IOException { + final TemplateHashModel dataModelHash; + if (dataModel instanceof TemplateHashModel) { + dataModelHash = (TemplateHashModel) dataModel; + } else { + if (wrapper == null) { + wrapper = getObjectWrapper(); + } + + if (dataModel == null) { + dataModelHash = new SimpleHash(wrapper); + } else { + TemplateModel wrappedDataModel = wrapper.wrap(dataModel); + if (wrappedDataModel instanceof TemplateHashModel) { + dataModelHash = (TemplateHashModel) wrappedDataModel; + } else if (wrappedDataModel == null) { + throw new IllegalArgumentException( + wrapper.getClass().getName() + " converted " + dataModel.getClass().getName() + " to null."); + } else { + throw new IllegalArgumentException( + wrapper.getClass().getName() + " didn't convert " + dataModel.getClass().getName() + + " to a TemplateHashModel. Generally, you want to use a Map<String, Object> or a " + + "JavaBean as the root-map (aka. data-model) parameter. The Map key-s or JavaBean " + + "property names will be the variable names in the template."); + } + } + } + return new Environment(this, dataModelHash, out); + } + + /** + * Same as {@link #createProcessingEnvironment(Object, Writer, ObjectWrapper) + * createProcessingEnvironment(dataModel, out, null)}. + */ + public Environment createProcessingEnvironment(Object dataModel, Writer out) + throws TemplateException, IOException { + return createProcessingEnvironment(dataModel, out, null); + } + + /** + * Returns a string representing the raw template + * text in canonical form. + */ + @Override + public String toString() { + StringWriter sw = new StringWriter(); + try { + dump(sw); + } catch (IOException ioe) { + throw new RuntimeException(ioe.getMessage()); + } + return sw.toString(); + } + + + /** + * The usually path-like (or URL-like) normalized identifier of the template, with which the template was get + * (usually via {@link Configuration#getTemplate(String)}), or possibly {@code null} for non-stored templates. + * It usually looks like a relative UN*X path; it should use {@code /}, not {@code \}, and shouldn't + * start with {@code /} (but there are no hard guarantees). It's not a real path in a file-system, it's just a name + * that a {@link TemplateLoader} used to load the backing resource (in simple cases; actually that name is + * {@link #getSourceName()}, but see it there). Or, it can also be a name that was never used to load the template + * (directly created with {@link #Template(String, Reader, Configuration)}). Even if the templates are stored + * straightforwardly in files, this is relative to the base directory of the {@link TemplateLoader}. So it really + * could be anything, except that it has importance in these situations: + * + * <p> + * Relative paths to other templates in this template will be resolved relatively to the directory part of this. + * Like if the template name is {@code "foo/this.ftl"}, then {@code <#include "other.ftl">} gets the template with + * name {@code "foo/other.ftl"}. + * </p> + * + * <p> + * You should not use this name to indicate error locations, or to find the actual templates in general, because + * localized lookup, acquisition and other lookup strategies can transform names before they get to the + * {@link TemplateLoader} (the template storage) mechanism. Use {@link #getSourceName()} for these purposes. + * </p> + * + * <p> + * Some frameworks use URL-like template names like {@code "someSchema://foo/bar.ftl"}. FreeMarker understands this + * notation, so an absolute path like {@code "/baaz.ftl"} in that template will be resolved too + * {@code "someSchema://baaz.ftl"}. + */ + public String getLookupName() { + return lookupName; + } + + /** + * The name that was actually used to load this template from the {@link TemplateLoader} (or from other custom + * storage mechanism). This is what should be shown in error messages as the error location. This is usually the + * same as {@link #getLookupName()}, except when localized lookup, template acquisition ({@code *} step in the + * name), or other {@link TemplateLookupStrategy} transforms the requested name ({@link #getLookupName()}) to a + * different final {@link TemplateLoader}-level name. For example, when you get a template with name {@code "foo + * .ftl"} then because of localized lookup, it's possible that something like {@code "foo_en.ftl"} will be loaded + * behind the scenes. While the template name will be still the same as the requested template name ({@code "foo + * .ftl"}), errors should point to {@code "foo_de.ftl"}. Note that relative paths are always resolved relatively + * to the {@code name}, not to the {@code sourceName}. + */ + public String getSourceName() { + return sourceName; + } + + /** + * Returns the {@linkplain #getSourceName() source name}, or if that's {@code null} then the + * {@linkplain #getLookupName() lookup name}. This name is primarily meant to be used in error messages. + */ + public String getSourceOrLookupName() { + return getSourceName() != null ? getSourceName() : getLookupName(); + } + + /** + * Returns the Configuration object associated with this template. + */ + public Configuration getConfiguration() { + return cfg; + } + + /** + * The {@link TemplateConfiguration} associated to this template, or {@code null} if there was none. + */ + public TemplateConfiguration getTemplateConfiguration() { + return tCfg; + } + + public ParsingConfiguration getParsingConfiguration() { + return parsingConfiguration; + } + + + /** + * @param actualSourceEncoding + * The sourceEncoding that was used to read this template, or {@code null} if the source of the template + * already gives back text (as opposed to binary data), so no decoding with a charset was needed. + */ + void setActualSourceEncoding(Charset actualSourceEncoding) { + this.actualSourceEncoding = actualSourceEncoding; + } + + /** + * The charset that was actually used to read this template from the binary source, or {@code null} if that + * information is not known. + * When using {@link DefaultTemplateResolver}, this is {@code null} exactly if the {@link TemplateLoader} + * returns text instead of binary content, which should only be the case for data sources that naturally return + * text (such as varchar and CLOB columns in a database). + */ + public Charset getActualSourceEncoding() { + return actualSourceEncoding; + } + + /** + * Gets the custom lookup condition with which this template was found. See the {@code customLookupCondition} + * parameter of {@link Configuration#getTemplate(String, Locale, Serializable, boolean)} for more + * explanation. + */ + public Serializable getCustomLookupCondition() { + return customLookupCondition; + } + + /** + * Mostly only used internally; setter pair of {@link #getCustomLookupCondition()}. This meant to be called directly + * after instantiating the template with its constructor, after a successfull lookup that used this condition. So + * this should only be called from code that deals with creating new {@code Template} objects, like from + * {@link DefaultTemplateResolver}. + */ + public void setCustomLookupCondition(Serializable customLookupCondition) { + this.customLookupCondition = customLookupCondition; + } + + /** + * Returns the tag syntax the parser has chosen for this template. If the syntax could be determined, it's + * {@link ParsingConfiguration#SQUARE_BRACKET_TAG_SYNTAX} or {@link ParsingConfiguration#ANGLE_BRACKET_TAG_SYNTAX}. If the syntax + * couldn't be determined (like because there was no tags in the template, or it was a plain text template), this + * returns whatever the default is in the current configuration, so it's maybe + * {@link ParsingConfiguration#AUTO_DETECT_TAG_SYNTAX}. + * + * @since 2.3.20 + */ + public int getActualTagSyntax() { + return actualTagSyntax; + } + + /** + * Returns the naming convention the parser has chosen for this template. If it could be determined, it's + * {@link ParsingConfiguration#LEGACY_NAMING_CONVENTION} or {@link ParsingConfiguration#CAMEL_CASE_NAMING_CONVENTION}. If it + * couldn't be determined (like because there no identifier that's part of the template language was used where + * the naming convention matters), this returns whatever the default is in the current configuration, so it's maybe + * {@link ParsingConfiguration#AUTO_DETECT_TAG_SYNTAX}. + * + * @since 2.3.23 + */ + public int getActualNamingConvention() { + return actualNamingConvention; + } + + /** + * Returns the output format (see {@link Configuration#getOutputFormat()}) used for this template. + * The output format of a template can come from various places, in order of increasing priority: + * {@link Configuration#getOutputFormat()}, {@link ParsingConfiguration#getOutputFormat()} (which is usually + * provided by {@link Configuration#getTemplateConfigurations()}) and the {@code #ftl} header's + * {@code output_format} option in the template. + * + * @since 2.3.24 + */ + public OutputFormat getOutputFormat() { + return outputFormat; + } + + /** + * Should be called by the parser, for example to apply the output format specified in the #ftl header. + */ + void setOutputFormat(OutputFormat outputFormat) { + this.outputFormat = outputFormat; + } + + /** + * Returns the {@link Configuration#getAutoEscapingPolicy()} autoEscapingPolicy) that this template uses. + * This is decided from these, in increasing priority: + * {@link Configuration#getAutoEscapingPolicy()}, {@link ParsingConfiguration#getAutoEscapingPolicy()}, + * {@code #ftl} header's {@code auto_esc} option in the template. + */ + public int getAutoEscapingPolicy() { + return autoEscapingPolicy != null ? autoEscapingPolicy + : tCfg != null && tCfg.isAutoEscapingPolicySet() ? tCfg.getAutoEscapingPolicy() + : cfg.getAutoEscapingPolicy(); + } + + /** + * Should be called by the parser, for example to apply the auto escaping policy specified in the #ftl header. + */ + void setAutoEscapingPolicy(int autoEscapingPolicy) { + this.autoEscapingPolicy = autoEscapingPolicy; + } + + /** + * Dump the raw template in canonical form. + */ + public void dump(PrintStream ps) { + ps.print(rootElement.getCanonicalForm()); + } + + /** + * Dump the raw template in canonical form. + */ + public void dump(Writer out) throws IOException { + out.write(rootElement != null ? rootElement.getCanonicalForm() : "Unfinished template"); + } + + void addMacro(ASTDirMacro macro) { + macros.put(macro.getName(), macro); + } + + void addImport(ASTDirImport ll) { + imports.add(ll); + } + + /** + * Returns the template source at the location specified by the coordinates given, or {@code null} if unavailable. + * A strange legacy in the behavior of this method is that it replaces tab characters with spaces according the + * value of {@link Template#getParsingConfiguration()}/{@link ParsingConfiguration#getTabSize()} (which usually + * comes from {@link Configuration#getTabSize()}), because tab characters move the column number with more than + * 1 in error messages. However, if you set the tab size to 1, this method leaves the tab characters as is. + * + * @param beginColumn the first column of the requested source, 1-based + * @param beginLine the first line of the requested source, 1-based + * @param endColumn the last column of the requested source, 1-based + * @param endLine the last line of the requested source, 1-based + * + * @see org.apache.freemarker.core.ASTNode#getSource() + */ + public String getSource(int beginColumn, + int beginLine, + int endColumn, + int endLine) { + if (beginLine < 1 || endLine < 1) return null; // dynamically ?eval-ed expressions has no source available + + // Our container is zero-based. + --beginLine; + --beginColumn; + --endColumn; + --endLine; + StringBuilder buf = new StringBuilder(); + for (int i = beginLine ; i <= endLine; i++) { + if (i < lines.size()) { + buf.append(lines.get(i)); + } + } + int lastLineLength = lines.get(endLine).toString().length(); + int trailingCharsToDelete = lastLineLength - endColumn - 1; + buf.delete(0, beginColumn); + buf.delete(buf.length() - trailingCharsToDelete, buf.length()); + return buf.toString(); + } + + @Override + public Locale getLocale() { + // TODO [FM3] Temporary hack; See comment above the locale field + if (lookupLocale != null) { + return lookupLocale; + } + + return tCfg != null && tCfg.isLocaleSet() ? tCfg.getLocale() : cfg.getLocale(); + } + + // TODO [FM3] Temporary hack; See comment above the locale field + public void setLookupLocale(Locale lookupLocale) { + this.lookupLocale = lookupLocale; + } + + @Override + public boolean isLocaleSet() { + return tCfg != null && tCfg.isLocaleSet(); + } + + @Override + public TimeZone getTimeZone() { + return tCfg != null && tCfg.isTimeZoneSet() ? tCfg.getTimeZone() : cfg.getTimeZone(); + } + + @Override + public boolean isTimeZoneSet() { + return tCfg != null && tCfg.isTimeZoneSet(); + } + + @Override + public TimeZone getSQLDateAndTimeTimeZone() { + return tCfg != null && tCfg.isSQLDateAndTimeTimeZoneSet() ? tCfg.getSQLDateAndTimeTimeZone() : cfg.getSQLDateAndTimeTimeZone(); + } + + @Override + public boolean isSQLDateAndTimeTimeZoneSet() { + return tCfg != null && tCfg.isSQLDateAndTimeTimeZoneSet(); + } + + @Override + public String getNumberFormat() { + return tCfg != null && tCfg.isNumberFormatSet() ? tCfg.getNumberFormat() : cfg.getNumberFormat(); + } + + @Override + public boolean isNumberFormatSet() { + return tCfg != null && tCfg.isNumberFormatSet(); + } + + @Override + public Map<String, TemplateNumberFormatFactory> getCustomNumberFormats() { + return tCfg != null && tCfg.isCustomNumberFormatsSet() ? tCfg.getCustomNumberFormats() + : cfg.getCustomNumberFormats(); + } + + @Override + public TemplateNumberFormatFactory getCustomNumberFormat(String name) { + if (tCfg != null && tCfg.isCustomNumberFormatsSet()) { + TemplateNumberFormatFactory value = tCfg.getCustomNumberFormats().get(name); + if (value != null) { + return value; + } + } + return cfg.getCustomNumberFormat(name); + } + + @Override + public boolean isCustomNumberFormatsSet() { + return tCfg != null && tCfg.isCustomNumberFormatsSet(); + } + + @Override + public String getBooleanFormat() { + return tCfg != null && tCfg.isBooleanFormatSet() ? tCfg.getBooleanFormat() : cfg.getBooleanFormat(); + } + + @Override + public boolean isBooleanFormatSet() { + return tCfg != null && tCfg.isBooleanFormatSet(); + } + + @Override + public String getTimeFormat() { + return tCfg != null && tCfg.isTimeFormatSet() ? tCfg.getTimeFormat() : cfg.getTimeFormat(); + } + + @Override + public boolean isTimeFormatSet() { + return tCfg != null && tCfg.isTimeFormatSet(); + } + + @Override + public String getDateFormat() { + return tCfg != null && tCfg.isDateFormatSet() ? tCfg.getDateFormat() : cfg.getDateFormat(); + } + + @Override + public boolean isDateFormatSet() { + return tCfg != null && tCfg.isDateFormatSet(); + } + + @Override + public String getDateTimeFormat() { + return tCfg != null && tCfg.isDateTimeFormatSet() ? tCfg.getDateTimeFormat() : cfg.getDateTimeFormat(); + } + + @Override + public boolean isDateTimeFormatSet() { + return tCfg != null && tCfg.isDateTimeFormatSet(); + } + + @Override + public Map<String, TemplateDateFormatFactory> getCustomDateFormats() { + return tCfg != null && tCfg.isCustomDateFormatsSet() ? tCfg.getCustomDateFormats() : cfg.getCustomDateFormats(); + } + + @Override + public TemplateDateFormatFactory getCustomDateFormat(String name) { + if (tCfg != null && tCfg.isCustomDateFormatsSet()) { + TemplateDateFormatFactory value = tCfg.getCustomDateFormats().get(name); + if (value != null) { + return value; + } + } + return cfg.getCustomDateFormat(name); + } + + @Override + public boolean isCustomDateFormatsSet() { + return tCfg != null && tCfg.isCustomDateFormatsSet(); + } + + @Override + public TemplateExceptionHandler getTemplateExceptionHandler() { + return tCfg != null && tCfg.isTemplateExceptionHandlerSet() ? tCfg.getTemplateExceptionHandler() : cfg.getTemplateExceptionHandler(); + } + + @Override + public boolean isTemplateExceptionHandlerSet() { + return tCfg != null && tCfg.isTemplateExceptionHandlerSet(); + } + + @Override + public ArithmeticEngine getArithmeticEngine() { + return tCfg != null && tCfg.isArithmeticEngineSet() ? tCfg.getArithmeticEngine() : cfg.getArithmeticEngine(); + } + + @Override + public boolean isArithmeticEngineSet() { + return tCfg != null && tCfg.isArithmeticEngineSet(); + } + + @Override + public ObjectWrapper getObjectWrapper() { + return tCfg != null && tCfg.isObjectWrapperSet() ? tCfg.getObjectWrapper() : cfg.getObjectWrapper(); + } + + @Override + public boolean isObjectWrapperSet() { + return tCfg != null && tCfg.isObjectWrapperSet(); + } + + @Override + public Charset getOutputEncoding() { + return tCfg != null && tCfg.isOutputEncodingSet() ? tCfg.getOutputEncoding() : cfg.getOutputEncoding(); + } + + @Override + public boolean isOutputEncodingSet() { + return tCfg != null && tCfg.isOutputEncodingSet(); + } + + @Override + public Charset getURLEscapingCharset() { + return tCfg != null && tCfg.isURLEscapingCharsetSet() ? tCfg.getURLEscapingCharset() : cfg.getURLEscapingCharset(); + } + + @Override + public boolean isURLEscapingCharsetSet() { + return tCfg != null && tCfg.isURLEscapingCharsetSet(); + } + + @Override + public TemplateClassResolver getNewBuiltinClassResolver() { + return tCfg != null && tCfg.isNewBuiltinClassResolverSet() ? tCfg.getNewBuiltinClassResolver() : cfg.getNewBuiltinClassResolver(); + } + + @Override + public boolean isNewBuiltinClassResolverSet() { + return tCfg != null && tCfg.isNewBuiltinClassResolverSet(); + } + + @Override + public boolean getAPIBuiltinEnabled() { + return tCfg != null && tCfg.isAPIBuiltinEnabledSet() ? tCfg.getAPIBuiltinEnabled() : cfg.getAPIBuiltinEnabled(); + } + + @Override + public boolean isAPIBuiltinEnabledSet() { + return tCfg != null && tCfg.isAPIBuiltinEnabledSet(); + } + + @Override + public boolean getAutoFlush() { + return tCfg != null && tCfg.isAutoFlushSet() ? tCfg.getAutoFlush() : cfg.getAutoFlush(); + } + + @Override + public boolean isAutoFlushSet() { + return tCfg != null && tCfg.isAutoFlushSet(); + } + + @Override + public boolean getShowErrorTips() { + return tCfg != null && tCfg.isShowErrorTipsSet() ? tCfg.getShowErrorTips() : cfg.getShowErrorTips(); + } + + @Override + public boolean isShowErrorTipsSet() { + return tCfg != null && tCfg.isShowErrorTipsSet(); + } + + @Override + public boolean getLogTemplateExceptions() { + return tCfg != null && tCfg.isLogTemplateExceptionsSet() ? tCfg.getLogTemplateExceptions() : cfg.getLogTemplateExceptions(); + } + + @Override + public boolean isLogTemplateExceptionsSet() { + return tCfg != null && tCfg.isLogTemplateExceptionsSet(); + } + + @Override + public boolean getLazyImports() { + return tCfg != null && tCfg.isLazyImportsSet() ? tCfg.getLazyImports() : cfg.getLazyImports(); + } + + @Override + public boolean isLazyImportsSet() { + return tCfg != null && tCfg.isLazyImportsSet(); + } + + @Override + public Boolean getLazyAutoImports() { + return tCfg != null && tCfg.isLazyAutoImportsSet() ? tCfg.getLazyAutoImports() : cfg.getLazyAutoImports(); + } + + @Override + public boolean isLazyAutoImportsSet() { + return tCfg != null && tCfg.isLazyAutoImportsSet(); + } + + @Override + public Map<String, String> getAutoImports() { + return tCfg != null && tCfg.isAutoImportsSet() ? tCfg.getAutoImports() : cfg.getAutoImports(); + } + + @Override + public boolean isAutoImportsSet() { + return tCfg != null && tCfg.isAutoImportsSet(); + } + + @Override + public List<String> getAutoIncludes() { + return tCfg != null && tCfg.isAutoIncludesSet() ? tCfg.getAutoIncludes() : cfg.getAutoIncludes(); + } + + @Override + public boolean isAutoIncludesSet() { + return tCfg != null && tCfg.isAutoIncludesSet(); + } + + /** + * This exists to provide the functionality required by {@link ProcessingConfiguration}, but try not call it + * too frequently as it has some overhead compared to an usual getter. + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public Map<Object, Object> getCustomAttributes() { + if (mergedCustomAttributes != null) { + return Collections.unmodifiableMap(mergedCustomAttributes); + } else if (customAttributes != null) { + return (Map) Collections.unmodifiableMap(customAttributes); + } else if (tCfg != null && tCfg.isCustomAttributesSet()) { + return tCfg.getCustomAttributes(); + } else { + return cfg.getCustomAttributes(); + } + } + + @Override + public boolean isCustomAttributesSet() { + return customAttributes != null || tCfg != null && tCfg.isCustomAttributesSet(); + } + + @Override + public Object getCustomAttribute(Object name) { + // Extra step for custom attributes specified in the #ftl header: + if (mergedCustomAttributes != null) { + Object value = mergedCustomAttributes.get(name); + if (value != null || mergedCustomAttributes.containsKey(name)) { + return value; + } + } else if (customAttributes != null) { + Object value = customAttributes.get(name); + if (value != null || customAttributes.containsKey(name)) { + return value; + } + } else if (tCfg != null && tCfg.isCustomAttributesSet()) { + Object value = tCfg.getCustomAttributes().get(name); + if (value != null || tCfg.getCustomAttributes().containsKey(name)) { + return value; + } + } + return cfg.getCustomAttribute(name); + } + + /** + * Should be called by the parser, for example to add the attributes specified in the #ftl header. + */ + void setCustomAttribute(String attName, Serializable attValue) { + if (customAttributes == null) { + customAttributes = new LinkedHashMap<>(); + } + customAttributes.put(attName, attValue); + + if (tCfg != null && tCfg.isCustomAttributesSet()) { + if (mergedCustomAttributes == null) { + mergedCustomAttributes = new LinkedHashMap<>(tCfg.getCustomAttributes()); + } + mergedCustomAttributes.put(attName, attValue); + } + } + + /** + * Reader that builds up the line table info for us, and also helps in working around JavaCC's exception + * suppression. + */ + private class LineTableBuilder extends FilterReader { + + private final int tabSize; + private final StringBuilder lineBuf = new StringBuilder(); + int lastChar; + boolean closed; + + /** Needed to work around JavaCC behavior where it silently treats any errors as EOF. */ + private Exception failure; + + /** + * @param r the character stream to wrap + */ + LineTableBuilder(Reader r, ParsingConfiguration parserConfiguration) { + super(r); + tabSize = parserConfiguration.getTabSize(); + } + + public boolean hasFailure() { + return failure != null; + } + + public void throwFailure() throws IOException { + if (failure != null) { + if (failure instanceof IOException) { + throw (IOException) failure; + } + if (failure instanceof RuntimeException) { + throw (RuntimeException) failure; + } + throw new UndeclaredThrowableException(failure); + } + } + + @Override + public int read() throws IOException { + try { + int c = in.read(); + handleChar(c); + return c; + } catch (Exception e) { + throw rememberException(e); + } + } + + private IOException rememberException(Exception e) throws IOException { + // JavaCC used to read from the Reader after it was closed. So we must not treat that as a failure. + if (!closed) { + failure = e; + } + if (e instanceof IOException) { + return (IOException) e; + } + if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } + throw new UndeclaredThrowableException(e); + } + + @Override + public int read(char cbuf[], int off, int len) throws IOException { + try { + int numchars = in.read(cbuf, off, len); + for (int i = off; i < off + numchars; i++) { + char c = cbuf[i]; + handleChar(c); + } + return numchars; + } catch (Exception e) { + throw rememberException(e); + } + } + + @Override + public void close() throws IOException { + if (lineBuf.length() > 0) { + lines.add(lineBuf.toString()); + lineBuf.setLength(0); + } + super.close(); + closed = true; + } + + private void handleChar(int c) { + if (c == '\n' || c == '\r') { + if (lastChar == '\r' && c == '\n') { // CRLF under Windoze + int lastIndex = lines.size() - 1; + String lastLine = (String) lines.get(lastIndex); + lines.set(lastIndex, lastLine + '\n'); + } else { + lineBuf.append((char) c); + lines.add(lineBuf.toString()); + lineBuf.setLength(0); + } + } else if (c == '\t' && tabSize != 1) { + int numSpaces = tabSize - (lineBuf.length() % tabSize); + for (int i = 0; i < numSpaces; i++) { + lineBuf.append(' '); + } + } else { + lineBuf.append((char) c); + } + lastChar = c; + } + } + + ASTElement getRootASTNode() { + return rootElement; + } + + Map getMacros() { + return macros; + } + + List getImports() { + return imports; + } + + void addPrefixNSMapping(String prefix, String nsURI) { + if (nsURI.length() == 0) { + throw new IllegalArgumentException("Cannot map empty string URI"); + } + if (prefix.length() == 0) { + throw new IllegalArgumentException("Cannot map empty string prefix"); + } + if (prefix.equals(NO_NS_PREFIX)) { + throw new IllegalArgumentException("The prefix: " + prefix + " cannot be registered, it's reserved for special internal use."); + } + if (prefixToNamespaceURILookup.containsKey(prefix)) { + throw new IllegalArgumentException("The prefix: '" + prefix + "' was repeated. This is illegal."); + } + if (namespaceURIToPrefixLookup.containsKey(nsURI)) { + throw new IllegalArgumentException("The namespace URI: " + nsURI + " cannot be mapped to 2 different prefixes."); + } + if (prefix.equals(DEFAULT_NAMESPACE_PREFIX)) { + defaultNS = nsURI; + } else { + prefixToNamespaceURILookup.put(prefix, nsURI); + namespaceURIToPrefixLookup.put(nsURI, prefix); + } + } + + public String getDefaultNS() { + return defaultNS; + } + + /** + * @return the NamespaceUri mapped to this prefix in this template. (Or null if there is none.) + */ + public String getNamespaceForPrefix(String prefix) { + if (prefix.equals("")) { + return defaultNS == null ? "" : defaultNS; + } + return (String) prefixToNamespaceURILookup.get(prefix); + } + + /** + * @return the prefix mapped to this nsURI in this template. (Or null if there is none.) + */ + public String getPrefixForNamespace(String nsURI) { + if (nsURI == null) { + return null; + } + if (nsURI.length() == 0) { + return defaultNS == null ? "" : NO_NS_PREFIX; + } + if (nsURI.equals(defaultNS)) { + return ""; + } + return (String) namespaceURIToPrefixLookup.get(nsURI); + } + + /** + * @return the prefixed name, based on the ns_prefixes defined + * in this template's header for the local name and node namespace + * passed in as parameters. + */ + public String getPrefixedName(String localName, String nsURI) { + if (nsURI == null || nsURI.length() == 0) { + if (defaultNS != null) { + return NO_NS_PREFIX + ":" + localName; + } else { + return localName; + } + } + if (nsURI.equals(defaultNS)) { + return localName; + } + String prefix = getPrefixForNamespace(nsURI); + if (prefix == null) { + return null; + } + return prefix + ":" + localName; + } + + @Override + @SuppressWarnings("unchecked") + @SuppressFBWarnings("AT_OPERATION_SEQUENCE_ON_CONCURRENT_ABSTRACTION") + public <T> T getCustomState(CustomStateKey<T> customStateKey) { + T customState = (T) customStateMap.get(customStateKey); + if (customState == null) { + synchronized (customStateMapLock) { + customState = (T) customStateMap.get(customStateKey); + if (customState == null) { + customState = customStateKey.create(); + if (customState == null) { + throw new IllegalStateException("CustomStateKey.create() must not return null (for key: " + + customStateKey + ")"); + } + customStateMap.put(customStateKey, customState); + } + } + } + return customState; + } + +} + http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateBooleanFormat.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateBooleanFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateBooleanFormat.java new file mode 100644 index 0000000..52f753d --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateBooleanFormat.java @@ -0,0 +1,91 @@ +/* + * 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; + +import org.apache.freemarker.core.util._StringUtil; +import org.apache.freemarker.core.valueformat.TemplateValueFormat; + +// TODO Should be public and moved over to core.valueformat? +final class TemplateBooleanFormat extends TemplateValueFormat { + + static final String C_TRUE_FALSE = "true,false"; + static final TemplateBooleanFormat C_TRUE_FALSE_FORMAT = new TemplateBooleanFormat(); + + static TemplateBooleanFormat getInstance(String format) { + return format.equals(C_TRUE_FALSE) ? C_TRUE_FALSE_FORMAT : new TemplateBooleanFormat(format); + } + + private final String formatString; + private final String trueStringValue; // deduced from booleanFormat + private final String falseStringValue; // deduced from booleanFormat + + /** + * Use for {@link #C_TRUE_FALSE} only! + */ + private TemplateBooleanFormat() { + formatString = C_TRUE_FALSE; + trueStringValue = null; + falseStringValue = null; + } + + private TemplateBooleanFormat(String formatString) { + int commaIdx = formatString.indexOf(','); + if (commaIdx == -1) { + throw new IllegalArgumentException( + "Setting value must be string that contains two comma-separated values for true and false, " + + "respectively."); + } + + this.formatString = formatString; + trueStringValue = formatString.substring(0, commaIdx); + falseStringValue = formatString.substring(commaIdx + 1); + } + + public String getFormatString() { + return formatString; + } + + /** + * Returns the string to which {@code true} is converted to for human audience, or {@code null} if automatic + * coercion to string is not allowed. The default value is {@code null}. + * + * <p>This value is deduced from the {@code "boolean_format"} setting. + * Confusingly, for backward compatibility (at least until 2.4) that defaults to {@code "true,false"}, yet this + * defaults to {@code null}. That's so because {@code "true,false"} is treated exceptionally, as that default is a + * historical mistake in FreeMarker, since it targets computer language output, not human writing. Thus it's + * ignored. + */ + public String getTrueStringValue() { + return trueStringValue; + } + + /** + * Same as {@link #getTrueStringValue()} but with {@code false}. + */ + public String getFalseStringValue() { + return falseStringValue; + } + + @Override + public String getDescription() { + return _StringUtil.jQuote(formatString); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateClassResolver.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateClassResolver.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateClassResolver.java new file mode 100644 index 0000000..c49e3fa --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateClassResolver.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.freemarker.core; + +import org.apache.freemarker.core.util._ClassUtil; + +/** + * Used by built-ins and other template language features that get a class + * based on a string. This can be handy both for implementing security + * restrictions and for working around local class-loader issues. + * + * The implementation should be thread-safe, unless an + * instance is always only used in a single {@link Environment} object. + * + * @see MutableProcessingConfiguration#setNewBuiltinClassResolver(TemplateClassResolver) + * + * @since 2.3.17 + */ +public interface TemplateClassResolver { + + /** + * Simply calls {@link _ClassUtil#forName(String)}. + */ + TemplateClassResolver UNRESTRICTED_RESOLVER = new TemplateClassResolver() { + + @Override + public Class resolve(String className, Environment env, Template template) + throws TemplateException { + try { + return _ClassUtil.forName(className); + } catch (ClassNotFoundException e) { + throw new _MiscTemplateException(e, env); + } + } + + }; + + /** + * Doesn't allow resolving any classes. + */ + TemplateClassResolver ALLOWS_NOTHING_RESOLVER = new TemplateClassResolver() { + + @Override + public Class resolve(String className, Environment env, Template template) + throws TemplateException { + throw MessageUtil.newInstantiatingClassNotAllowedException(className, env); + } + + }; + + /** + * Gets a {@link Class} based on the class name. + * + * @param className the full-qualified class name + * @param env the environment in which the template executes + * @param template the template where the operation that require the + * class resolution resides in. This is <code>null</code> if the + * call doesn't come from a template. + * + * @throws TemplateException if the class can't be found or shouldn't be + * accessed from a template for security reasons. + */ + Class resolve(String className, Environment env, Template template) throws TemplateException; + +}
