blob: d5b3977e72fd5b3dba2e4fae1325e669ab8cefdc [file] [log] [blame]
/*
* 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 freemarker.core;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateDirectiveBody;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template._TemplateAPI;
import freemarker.template.utility.ClassUtil;
/**
* 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.
*/
public class _CoreAPI {
public static final String ERROR_MESSAGE_HR = "----";
// Can't be instantiated
private _CoreAPI() { }
private static void addName(Set<String> allNames, Set<String> lcNames, Set<String> ccNames,
String commonName) {
allNames.add(commonName);
lcNames.add(commonName);
ccNames.add(commonName);
}
private static void addName(Set<String> allNames, Set<String> lcNames, Set<String> ccNames,
String lcName, String ccName) {
allNames.add(lcName);
allNames.add(ccName);
lcNames.add(lcName);
ccNames.add(ccName);
}
public static final Set<String> ALL_BUILT_IN_DIRECTIVE_NAMES;
public static final Set<String> LEGACY_BUILT_IN_DIRECTIVE_NAMES;
public static final Set<String> CAMEL_CASE_BUILT_IN_DIRECTIVE_NAMES;
static {
Set<String> allNames = new TreeSet();
Set<String> lcNames = new TreeSet();
Set<String> ccNames = new TreeSet();
addName(allNames, lcNames, ccNames, "assign");
addName(allNames, lcNames, ccNames, "attempt");
addName(allNames, lcNames, ccNames, "autoesc", "autoEsc");
addName(allNames, lcNames, ccNames, "break");
addName(allNames, lcNames, ccNames, "call");
addName(allNames, lcNames, ccNames, "case");
addName(allNames, lcNames, ccNames, "comment");
addName(allNames, lcNames, ccNames, "compress");
addName(allNames, lcNames, ccNames, "default");
addName(allNames, lcNames, ccNames, "else");
addName(allNames, lcNames, ccNames, "elseif", "elseIf");
addName(allNames, lcNames, ccNames, "escape");
addName(allNames, lcNames, ccNames, "fallback");
addName(allNames, lcNames, ccNames, "flush");
addName(allNames, lcNames, ccNames, "foreach", "forEach");
addName(allNames, lcNames, ccNames, "ftl");
addName(allNames, lcNames, ccNames, "function");
addName(allNames, lcNames, ccNames, "global");
addName(allNames, lcNames, ccNames, "if");
addName(allNames, lcNames, ccNames, "import");
addName(allNames, lcNames, ccNames, "include");
addName(allNames, lcNames, ccNames, "items");
addName(allNames, lcNames, ccNames, "list");
addName(allNames, lcNames, ccNames, "local");
addName(allNames, lcNames, ccNames, "lt");
addName(allNames, lcNames, ccNames, "macro");
addName(allNames, lcNames, ccNames, "nested");
addName(allNames, lcNames, ccNames, "noautoesc", "noAutoEsc");
addName(allNames, lcNames, ccNames, "noescape", "noEscape");
addName(allNames, lcNames, ccNames, "noparse", "noParse");
addName(allNames, lcNames, ccNames, "nt");
addName(allNames, lcNames, ccNames, "outputformat", "outputFormat");
addName(allNames, lcNames, ccNames, "recover");
addName(allNames, lcNames, ccNames, "recurse");
addName(allNames, lcNames, ccNames, "return");
addName(allNames, lcNames, ccNames, "rt");
addName(allNames, lcNames, ccNames, "sep");
addName(allNames, lcNames, ccNames, "setting");
addName(allNames, lcNames, ccNames, "stop");
addName(allNames, lcNames, ccNames, "switch");
addName(allNames, lcNames, ccNames, "t");
addName(allNames, lcNames, ccNames, "transform");
addName(allNames, lcNames, ccNames, "visit");
ALL_BUILT_IN_DIRECTIVE_NAMES = Collections.unmodifiableSet(allNames);
LEGACY_BUILT_IN_DIRECTIVE_NAMES = Collections.unmodifiableSet(lcNames);
CAMEL_CASE_BUILT_IN_DIRECTIVE_NAMES = Collections.unmodifiableSet(ccNames);
}
/**
* Returns the names of the currently supported "built-ins" ({@code expr?builtin_name}-like things).
*
* @param namingConvention
* One of {@link Configuration#AUTO_DETECT_NAMING_CONVENTION},
* {@link Configuration#LEGACY_NAMING_CONVENTION}, and
* {@link Configuration#CAMEL_CASE_NAMING_CONVENTION}. If it's
* {@link Configuration#AUTO_DETECT_NAMING_CONVENTION} then the union of the names in all the naming
* conventions is returned.
*/
public static Set<String> getSupportedBuiltInNames(int namingConvention) {
Set<String> names;
if (namingConvention == Configuration.AUTO_DETECT_NAMING_CONVENTION) {
names = BuiltIn.BUILT_INS_BY_NAME.keySet();
} else if (namingConvention == Configuration.LEGACY_NAMING_CONVENTION) {
names = BuiltIn.SNAKE_CASE_NAMES;
} else if (namingConvention == Configuration.CAMEL_CASE_NAMING_CONVENTION) {
names = BuiltIn.CAMEL_CASE_NAMES;
} else {
throw new IllegalArgumentException("Unsupported naming convention constant: " + namingConvention);
}
return Collections.unmodifiableSet(names);
}
public static void appendInstructionStackItem(TemplateElement stackEl, StringBuilder sb) {
Environment.appendInstructionStackItem(stackEl, sb);
}
public static TemplateElement[] getInstructionStackSnapshot(Environment env) {
return env.getInstructionStackSnapshot();
}
public static void outputInstructionStack(
TemplateElement[] instructionStackSnapshot, boolean terseMode, Writer pw) {
Environment.outputInstructionStack(instructionStackSnapshot, terseMode, pw);
}
public static Map<String, ?> getCustomAttributes(UnboundTemplate unboundTemplate) {
return unboundTemplate.getCustomAttributes();
}
/**
* For emulating legacy {@link Template#addMacro(Macro)}.
*/
public static void addMacro(UnboundTemplate unboundTemplate, Macro macro) {
final UnboundCallable unboundCallable = macroToUnboundCallable(macro);
unboundTemplate.addUnboundCallable(unboundCallable);
}
/**
* In 2.4 {@link Macro} was split to {@link BoundCallable} and {@link UnboundCallable}, but because of BC
* constraints sometimes we can only expect a {@link Macro}, but that can always converted to
* {@link UnboundCallable}.
*/
private static UnboundCallable macroToUnboundCallable(Macro macro) {
if (macro instanceof UnboundCallable) {
// It's coming from the AST:
return (UnboundCallable) macro;
} else if (macro instanceof BoundCallable) {
// It's coming from an FTL variable:
return ((BoundCallable) macro).getUnboundCallable();
} else if (macro == null) {
return null;
} else {
// Impossible, Macro should have only two subclasses.
throw new BugException();
}
}
public static void addImport(UnboundTemplate unboundTemplate, LibraryLoad libLoad) {
unboundTemplate.addImport(libLoad);
}
public static UnboundTemplate newUnboundTemplate(Reader reader, String sourceName,
Configuration cfg, ParserConfiguration parserCfg, String assumedEncoding) throws IOException {
return new UnboundTemplate(reader, sourceName, cfg, parserCfg, assumedEncoding);
}
public static boolean isBoundCallable(Object obj) {
return obj instanceof BoundCallable;
}
public static UnboundTemplate newPlainTextUnboundTemplate(String content, String sourceName, Configuration config) {
return UnboundTemplate.newPlainTextUnboundTemplate(content, sourceName, config);
}
/** Used for implementing the deprecated {@link Template} method with similar name. */
public static TemplateElement getRootTreeNode(UnboundTemplate unboundTemplate) {
return unboundTemplate.getRootTreeNode();
}
/** Used for implementing the deprecated {@link Template} method with similar name. */
public static Map<String, UnboundCallable> getUnboundCallables(UnboundTemplate unboundTemplate) {
return unboundTemplate.getUnboundCallables();
}
/** Used for implementing the deprecated {@link Template} method with similar name. */
public static List getImports(UnboundTemplate unboundTemplate) {
return unboundTemplate.getImports();
}
/** Used for implementing the deprecated {@link Template} method with similar name. */
public static void addPrefixNSMapping(UnboundTemplate unboundTemplate, String prefix, String nsURI) {
unboundTemplate.addPrefixToNamespaceURIMapping(prefix, nsURI);
}
/** Used for implementing the deprecated {@link Template} method with similar name. */
public static List<TemplateElement> containingElements(UnboundTemplate unboundTemplate, int column, int line) {
return unboundTemplate.containingElements(column, line);
}
/**
* ATTENTION: This is used by https://github.com/kenshoo/freemarker-online. Don't break backward
* compatibility without updating that project too!
*/
static final public void addThreadInterruptedChecks(Template template) {
try {
new ThreadInterruptionSupportTemplatePostProcessor().postProcess(template);
} catch (TemplatePostProcessorException e) {
throw new RuntimeException("Template post-processing failed", e);
}
}
static final public void checkHasNoNestedContent(TemplateDirectiveBody body)
throws NestedContentNotSupportedException {
NestedContentNotSupportedException.check(body);
}
public static Map<String, Macro> createAdapterMacroMapForUnboundCallables(UnboundTemplate unboundTemplate) {
return new AdapterMacroMap(getUnboundCallables(unboundTemplate));
}
/**
* Wraps a {@code Map<String, UnboundCallable>} as if it was a {@code Map<String, Macro>}. This is for backward
* compatibility. The important use case is being able to put any {@link Macro} subclass into this {@code Map},
* despite that the backing {@link Macro} can only store {@code UnboundCallable}. Reading works a bit strangely,
* because if you put a non-{@link UnboundCallable} value in, then get it with the same key, you get the
* corresponding {@link UnboundCallable} back instead of the original object. That's also a {@code Macro} though.
*/
private static class AdapterMacroMap implements Map<String, Macro> {
private final Map<String, UnboundCallable> adapted;
public AdapterMacroMap(Map<String, UnboundCallable> unboundCallableMap) {
this.adapted = unboundCallableMap;
}
public int size() {
return adapted.size();
}
public boolean isEmpty() {
return adapted.isEmpty();
}
public boolean containsKey(Object key) {
return adapted.containsKey(key);
}
public boolean containsValue(Object value) {
return adapted.containsValue(value);
}
public UnboundCallable get(Object key) {
return adapted.get(key);
}
public UnboundCallable put(String key, Macro value) {
return adapted.put(key, macroToUnboundCallable(value));
}
public UnboundCallable remove(Object key) {
return adapted.remove(key);
}
public void putAll(Map<? extends String, ? extends Macro> t) {
for (Map.Entry<? extends String, ? extends Macro> ent : t.entrySet()) {
put(ent.getKey(), ent.getValue());
}
}
public void clear() {
adapted.clear();
}
public Set<String> keySet() {
return adapted.keySet();
}
public Collection<Macro> values() {
// According the Map API, this Collection doesn't allows adding elements, it's safe to treat as a
// Collection<Macro>.
return (Collection) adapted.values();
}
public Set<Entry<String, Macro>> entrySet() {
// According the Map API, this set doesn't allows adding elements, but, the Map.Entry-s are still
// modifiable.
return new AdapterMacroMapEntrySet(adapted.entrySet());
}
@Override
public boolean equals(Object o) {
return adapted.equals(o);
}
@Override
public int hashCode() {
return adapted.hashCode();
}
}
/** Helper for {@link AdapterMacroMap}. */
private static class AdapterMacroMapEntrySet implements Set<Map.Entry<String, Macro>> {
private final Set<Map.Entry<String, UnboundCallable>> adapted;
public AdapterMacroMapEntrySet(Set<Entry<String, UnboundCallable>> adapted) {
this.adapted = adapted;
}
public int size() {
return adapted.size();
}
public boolean isEmpty() {
return adapted.isEmpty();
}
public boolean contains(Object o) {
return adapted.contains(o);
}
public Iterator<Entry<String, Macro>> iterator() {
return new AdapterMacroMapEntrySetIterator(adapted.iterator());
}
public Object[] toArray() {
return adapted.toArray();
}
public <T> T[] toArray(T[] a) {
return adapted.toArray(a);
}
public boolean add(Entry<String, Macro> o) {
// Won't be allowed anyway
return adapted.add((Entry) o);
}
public boolean remove(Object o) {
return adapted.remove(o);
}
public boolean containsAll(Collection<?> c) {
return adapted.containsAll(c);
}
public boolean addAll(Collection<? extends Entry<String, Macro>> c) {
// Won't be allowed anyway
return adapted.addAll((Collection) c);
}
public boolean retainAll(Collection<?> c) {
return adapted.retainAll(c);
}
public boolean removeAll(Collection<?> c) {
return adapted.removeAll(c);
}
public void clear() {
adapted.clear();
}
@Override
public boolean equals(Object o) {
return adapted.equals(o);
}
@Override
public int hashCode() {
return adapted.hashCode();
}
}
/** Helper for {@link AdapterMacroMap}. */
private static class AdapterMacroMapEntrySetIterator implements Iterator<Map.Entry<String, Macro>> {
private final Iterator<Map.Entry<String, UnboundCallable>> adapted;
public AdapterMacroMapEntrySetIterator(Iterator<Entry<String, UnboundCallable>> adapted) {
this.adapted = adapted;
}
public boolean hasNext() {
return adapted.hasNext();
}
public Entry<String, Macro> next() {
return new AdapterMacroMapEntry(adapted.next());
}
public void remove() {
adapted.remove();
}
}
/** Helper for {@link AdapterMacroMap}. */
private static class AdapterMacroMapEntry implements Map.Entry<String, Macro> {
private final Map.Entry<String, UnboundCallable> adapted;
public AdapterMacroMapEntry(Entry<String, UnboundCallable> adapted) {
this.adapted = adapted;
}
public String getKey() {
return adapted.getKey();
}
public UnboundCallable getValue() {
return adapted.getValue();
}
public UnboundCallable setValue(Macro value) {
return adapted.setValue(macroToUnboundCallable(value));
}
@Override
public boolean equals(Object o) {
return adapted.equals(o);
}
@Override
public int hashCode() {
return adapted.hashCode();
}
}
/**
* @throws IllegalArgumentException
* if the type of the some of the values isn't as expected
*/
public static void checkSettingValueItemsType(String somethingsSentenceStart, Class<?> expectedClass,
Collection<? extends Object> values) {
if (values == null) return;
for (Object value : values) {
if (!expectedClass.isInstance(value)) {
throw new IllegalArgumentException(somethingsSentenceStart + " must be instances of "
+ ClassUtil.getShortClassName(expectedClass) + ", but one of them was a(n) "
+ ClassUtil.getShortClassNameOfObject(value) + ".");
}
}
}
/**
* The work around the problematic cases where we should throw a {@link TemplateException}, but we are inside
* a {@link TemplateModel} method and so we can only throw {@link TemplateModelException}-s.
*/
public static TemplateModelException ensureIsTemplateModelException(String modelOpMsg, TemplateException e) {
if (e instanceof TemplateModelException) {
return (TemplateModelException) e;
} else {
return new _TemplateModelException(
_TemplateAPI.getBlamedExpression(e), e.getCause(), e.getEnvironment(), modelOpMsg);
}
}
public static TemplateElement getParentElement(TemplateElement te) {
return te.getParentElement();
}
public static TemplateElement getChildElement(TemplateElement te, int index) {
return te.getChild(index);
}
}