You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2017/05/14 10:52:57 UTC
[14/51] [partial] incubator-freemarker git commit: Migrated from Ant
to Gradle, and modularized the project. This is an incomplete migration;
there are some TODO-s in the build scripts, and release related tasks are
still missing. What works: Building th
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/StringTemplateLoader.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/StringTemplateLoader.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/StringTemplateLoader.java
new file mode 100644
index 0000000..887bfce
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/StringTemplateLoader.java
@@ -0,0 +1,199 @@
+/*
+ * 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.templateresolver.impl;
+
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.io.StringReader;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.apache.freemarker.core.templateresolver.CacheStorage;
+import org.apache.freemarker.core.templateresolver.TemplateLoader;
+import org.apache.freemarker.core.templateresolver.TemplateLoaderSession;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingResult;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingSource;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * A {@link TemplateLoader} that uses a {@link Map} with {@code String} as its source of templates. This is similar to
+ * {@link StringTemplateLoader}, but uses {@code String} instead of {@link String}; see more details there.
+ *
+ * <p>Note that {@link StringTemplateLoader} can't be used with a distributed (cluster-wide) {@link CacheStorage},
+ * as it produces {@link TemplateLoadingSource}-s that deliberately throw exception on serialization (because the
+ * content is only accessible within a single JVM, and is also volatile).
+ */
+// TODO JUnit tests
+public class StringTemplateLoader implements TemplateLoader {
+
+ private static final AtomicLong INSTANCE_COUNTER = new AtomicLong();
+
+ private final long instanceId = INSTANCE_COUNTER.get();
+ private final AtomicLong templatesRevision = new AtomicLong();
+ private final ConcurrentMap<String, ContentHolder> templates = new ConcurrentHashMap<>();
+
+ /**
+ * Puts a template into the template loader. The name can contain slashes to denote logical directory structure, but
+ * must not start with a slash. Each template will get an unique revision number, thus replacing a template will
+ * cause the template cache to reload it (when the update delay expires).
+ *
+ * <p>This method is thread-safe.
+ *
+ * @param name
+ * the name of the template.
+ * @param content
+ * the source code of the template.
+ */
+ public void putTemplate(String name, String content) {
+ templates.put(
+ name,
+ new ContentHolder(content, new Source(instanceId, name), templatesRevision.incrementAndGet()));
+ }
+
+ /**
+ * Removes the template with the specified name if it was added earlier.
+ *
+ * <p>
+ * This method is thread-safe.
+ *
+ * @param name
+ * Exactly the key with which the template was added.
+ *
+ * @return Whether a template was found with the given key (and hence was removed now)
+ */
+ public boolean removeTemplate(String name) {
+ return templates.remove(name) != null;
+ }
+
+ @Override
+ public TemplateLoaderSession createSession() {
+ return null;
+ }
+
+ @Override
+ public TemplateLoadingResult load(String name, TemplateLoadingSource ifSourceDiffersFrom,
+ Serializable ifVersionDiffersFrom, TemplateLoaderSession session) throws IOException {
+ ContentHolder contentHolder = templates.get(name);
+ if (contentHolder == null) {
+ return TemplateLoadingResult.NOT_FOUND;
+ } else if (ifSourceDiffersFrom != null && ifSourceDiffersFrom.equals(contentHolder.source)
+ && Objects.equals(ifVersionDiffersFrom, contentHolder.version)) {
+ return TemplateLoadingResult.NOT_MODIFIED;
+ } else {
+ return new TemplateLoadingResult(
+ contentHolder.source, contentHolder.version,
+ new StringReader(contentHolder.content),
+ null);
+ }
+ }
+
+ @Override
+ public void resetState() {
+ // Do nothing
+ }
+
+ /**
+ * Show class name and some details that are useful in template-not-found errors.
+ */
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(_TemplateLoaderUtils.getClassNameForToString(this));
+ sb.append("(Map { ");
+ int cnt = 0;
+ for (String name : templates.keySet()) {
+ cnt++;
+ if (cnt != 1) {
+ sb.append(", ");
+ }
+ if (cnt > 10) {
+ sb.append("...");
+ break;
+ }
+ sb.append(_StringUtil.jQuote(name));
+ sb.append("=...");
+ }
+ if (cnt != 0) {
+ sb.append(' ');
+ }
+ sb.append("})");
+ return sb.toString();
+ }
+
+ private static class ContentHolder {
+ private final String content;
+ private final Source source;
+ private final long version;
+
+ public ContentHolder(String content, Source source, long version) {
+ this.content = content;
+ this.source = source;
+ this.version = version;
+ }
+
+ }
+
+ @SuppressWarnings("serial")
+ private static class Source implements TemplateLoadingSource {
+
+ private final long instanceId;
+ private final String name;
+
+ public Source(long instanceId, String name) {
+ this.instanceId = instanceId;
+ this.name = name;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (int) (instanceId ^ (instanceId >>> 32));
+ result = prime * result + ((name == null) ? 0 : name.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (getClass() != obj.getClass()) return false;
+ Source other = (Source) obj;
+ if (instanceId != other.instanceId) return false;
+ if (name == null) {
+ if (other.name != null) return false;
+ } else if (!name.equals(other.name)) {
+ return false;
+ }
+ return true;
+ }
+
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ throw new IOException(StringTemplateLoader.class.getName()
+ + " sources can't be serialized, as they don't support clustering.");
+ }
+
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/StrongCacheStorage.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/StrongCacheStorage.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/StrongCacheStorage.java
new file mode 100644
index 0000000..1d0533b
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/StrongCacheStorage.java
@@ -0,0 +1,70 @@
+/*
+ * 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.templateresolver.impl;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.templateresolver.CacheStorage;
+import org.apache.freemarker.core.templateresolver.CacheStorageWithGetSize;
+
+/**
+ * Strong cache storage is a cache storage that simply wraps a {@link Map}. It holds a strong reference to all objects
+ * it was passed, therefore prevents the cache from being purged during garbage collection. This class is always
+ * thread-safe since 2.3.24, before that if we are running on Java 5 or later.
+ *
+ * @see Configuration#getCacheStorage()
+ */
+public class StrongCacheStorage implements CacheStorage, CacheStorageWithGetSize {
+
+ private final ConcurrentMap map = new ConcurrentHashMap();
+
+ @Override
+ public Object get(Object key) {
+ return map.get(key);
+ }
+
+ @Override
+ public void put(Object key, Object value) {
+ map.put(key, value);
+ }
+
+ @Override
+ public void remove(Object key) {
+ map.remove(key);
+ }
+
+ /**
+ * Returns a close approximation of the number of cache entries.
+ *
+ * @since 2.3.21
+ */
+ @Override
+ public int getSize() {
+ return map.size();
+ }
+
+ @Override
+ public void clear() {
+ map.clear();
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/TemplateLoaderBasedTemplateLookupContext.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/TemplateLoaderBasedTemplateLookupContext.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/TemplateLoaderBasedTemplateLookupContext.java
new file mode 100644
index 0000000..e9e1c00
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/TemplateLoaderBasedTemplateLookupContext.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.freemarker.core.templateresolver.impl;
+
+import java.io.Serializable;
+import java.util.Locale;
+
+import org.apache.freemarker.core.templateresolver.TemplateLoader;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingResult;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingSource;
+import org.apache.freemarker.core.templateresolver.TemplateLookupContext;
+
+/**
+ * Base class for implementing a {@link TemplateLookupContext} that works with {@link TemplateLoader}-s.
+ */
+public abstract class TemplateLoaderBasedTemplateLookupContext
+ extends TemplateLookupContext<TemplateLoaderBasedTemplateLookupResult> {
+
+ private final TemplateLoadingSource cachedResultSource;
+ private final Serializable cachedResultVersion;
+
+ protected TemplateLoaderBasedTemplateLookupContext(String templateName, Locale templateLocale,
+ Object customLookupCondition, TemplateLoadingSource cachedResultSource, Serializable cachedResultVersion) {
+ super(templateName, templateLocale, customLookupCondition);
+ this.cachedResultSource = cachedResultSource;
+ this.cachedResultVersion = cachedResultVersion;
+ }
+
+ protected TemplateLoadingSource getCachedResultSource() {
+ return cachedResultSource;
+ }
+
+ protected Serializable getCachedResultVersion() {
+ return cachedResultVersion;
+ }
+
+ @Override
+ public final TemplateLoaderBasedTemplateLookupResult createNegativeLookupResult() {
+ return TemplateLoaderBasedTemplateLookupResult.getNegativeResult();
+ }
+
+ /**
+ * Creates a positive or negative lookup result depending on {@link TemplateLoadingResult#getStatus()}.
+ */
+ protected final TemplateLoaderBasedTemplateLookupResult createLookupResult(
+ String templateSourceName, TemplateLoadingResult templateLoaderResult) {
+ return TemplateLoaderBasedTemplateLookupResult.from(templateSourceName, templateLoaderResult);
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/TemplateLoaderBasedTemplateLookupResult.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/TemplateLoaderBasedTemplateLookupResult.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/TemplateLoaderBasedTemplateLookupResult.java
new file mode 100644
index 0000000..fe7a54c
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/TemplateLoaderBasedTemplateLookupResult.java
@@ -0,0 +1,124 @@
+/*
+ * 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.templateresolver.impl;
+
+import org.apache.freemarker.core.templateresolver.TemplateLoader;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingResult;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingResultStatus;
+import org.apache.freemarker.core.templateresolver.TemplateLookupResult;
+import org.apache.freemarker.core.util._NullArgumentException;
+
+/**
+ * Class of {@link TemplateLookupResult} instances created by {@link TemplateLoaderBasedTemplateLookupContext}. To
+ * invoke instances of this inside your own {@link TemplateLoaderBasedTemplateLookupContext} subclass, call
+ * {@link TemplateLoaderBasedTemplateLookupContext#createLookupResult(String, TemplateLoadingResult)} and
+ * {@link TemplateLoaderBasedTemplateLookupContext#createNegativeLookupResult()}. You should not try to invoke instances
+ * anywhere else. Also, this class deliberately can't be subclassed (except inside FreeMarker).
+ */
+public abstract class TemplateLoaderBasedTemplateLookupResult extends TemplateLookupResult {
+
+ /** Used internally to get a not-found result (currently just a static singleton). */
+ static TemplateLoaderBasedTemplateLookupResult getNegativeResult() {
+ return NegativeTemplateLookupResult.INSTANCE;
+ }
+
+ /** Used internally to invoke the appropriate kind of result from the parameters. */
+ static TemplateLoaderBasedTemplateLookupResult from(String templateSourceName, TemplateLoadingResult templateLoaderResult) {
+ return templateLoaderResult.getStatus() != TemplateLoadingResultStatus.NOT_FOUND
+ ? new PositiveTemplateLookupResult(templateSourceName, templateLoaderResult)
+ : getNegativeResult();
+ }
+
+ private TemplateLoaderBasedTemplateLookupResult() {
+ //
+ }
+
+ /**
+ * Used internally to extract the {@link TemplateLoadingResult}; {@code null} if {@link #isPositive()} is
+ * {@code false}.
+ */
+ public abstract TemplateLoadingResult getTemplateLoaderResult();
+
+ private static final class PositiveTemplateLookupResult extends TemplateLoaderBasedTemplateLookupResult {
+
+ private final String templateSourceName;
+ private final TemplateLoadingResult templateLoaderResult;
+
+ /**
+ * @param templateSourceName
+ * The name of the matching template found. This is not necessarily the same as the template name
+ * with which the template was originally requested. For example, one may gets a template for the
+ * {@code "foo.ftl"} name, but due to localized lookup the template is actually loaded from
+ * {@code "foo_de.ftl"}. Then this parameter must be {@code "foo_de.ftl"}, not {@code "foo.ftl"}. Not
+ * {@code null}.
+ *
+ * @param templateLoaderResult
+ * See {@link TemplateLoader#load} to understand what that means. Not
+ * {@code null}.
+ */
+ private PositiveTemplateLookupResult(String templateSourceName, TemplateLoadingResult templateLoaderResult) {
+ _NullArgumentException.check("templateName", templateSourceName);
+ _NullArgumentException.check("templateLoaderResult", templateLoaderResult);
+
+ this.templateSourceName = templateSourceName;
+ this.templateLoaderResult = templateLoaderResult;
+ }
+
+ @Override
+ public String getTemplateSourceName() {
+ return templateSourceName;
+ }
+
+ @Override
+ public TemplateLoadingResult getTemplateLoaderResult() {
+ return templateLoaderResult;
+ }
+
+ @Override
+ public boolean isPositive() {
+ return true;
+ }
+ }
+
+ private static final class NegativeTemplateLookupResult extends TemplateLoaderBasedTemplateLookupResult {
+
+ private static final TemplateLoaderBasedTemplateLookupResult.NegativeTemplateLookupResult INSTANCE = new NegativeTemplateLookupResult();
+
+ private NegativeTemplateLookupResult() {
+ // nop
+ }
+
+ @Override
+ public String getTemplateSourceName() {
+ return null;
+ }
+
+ @Override
+ public TemplateLoadingResult getTemplateLoaderResult() {
+ return null;
+ }
+
+ @Override
+ public boolean isPositive() {
+ 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/core/templateresolver/impl/URLTemplateLoader.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/URLTemplateLoader.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/URLTemplateLoader.java
new file mode 100644
index 0000000..dcb9222
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/URLTemplateLoader.java
@@ -0,0 +1,229 @@
+/*
+ * 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.templateresolver.impl;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Serializable;
+import java.net.JarURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.Objects;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core._CoreLogs;
+import org.apache.freemarker.core.templateresolver.TemplateLoader;
+import org.apache.freemarker.core.templateresolver.TemplateLoaderSession;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingResult;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingSource;
+import org.apache.freemarker.core.templateresolver.TemplateLookupStrategy;
+import org.slf4j.Logger;
+
+/**
+ * This is an abstract template loader that can load templates whose location can be described by an URL. Subclasses
+ * only need to override the {@link #getURL(String)}, {@link #extractNegativeResult(URLConnection)}, and perhaps the
+ * {@link #prepareConnection(URLConnection)} method.
+ */
+// TODO JUnit test (including implementing a HTTP-based template loader to test the new protected methods)
+public abstract class URLTemplateLoader implements TemplateLoader {
+
+ private static final Logger LOG = _CoreLogs.TEMPLATE_RESOLVER;
+
+ private Boolean urlConnectionUsesCaches = false;
+
+ /**
+ * Getter pair of {@link #setURLConnectionUsesCaches(Boolean)}.
+ *
+ * @since 2.3.21
+ */
+ public Boolean getURLConnectionUsesCaches() {
+ return urlConnectionUsesCaches;
+ }
+
+ /**
+ * Sets if {@link URLConnection#setUseCaches(boolean)} will be called, and with what value. By default this is
+ * {@code false}, because FreeMarker has its own template cache with its own update delay setting
+ * ({@link Configuration#getTemplateUpdateDelayMilliseconds()}). If this is set to {@code null},
+ * {@link URLConnection#setUseCaches(boolean)} won't be called.
+ */
+ public void setURLConnectionUsesCaches(Boolean urlConnectionUsesCaches) {
+ this.urlConnectionUsesCaches = urlConnectionUsesCaches;
+ }
+
+ @Override
+ public TemplateLoaderSession createSession() {
+ return null;
+ }
+
+ @Override
+ public TemplateLoadingResult load(String name, TemplateLoadingSource ifSourceDiffersFrom,
+ Serializable ifVersionDiffersFrom, TemplateLoaderSession session) throws IOException {
+ URL url = getURL(name);
+ if (url == null) {
+ return TemplateLoadingResult.NOT_FOUND;
+ }
+
+ URLConnection conn = url.openConnection();
+ Boolean urlConnectionUsesCaches = getURLConnectionUsesCaches();
+ if (urlConnectionUsesCaches != null) {
+ conn.setUseCaches(urlConnectionUsesCaches);
+ }
+
+ prepareConnection(conn);
+ conn.connect();
+
+ InputStream inputStream = null;
+ Long version;
+ URLTemplateLoadingSource source;
+ try {
+ TemplateLoadingResult negativeResult = extractNegativeResult(conn);
+ if (negativeResult != null) {
+ return negativeResult;
+ }
+
+ // To prevent clustering issues, getLastModified(fallbackToJarLMD=false)
+ long lmd = getLastModified(conn, false);
+ version = lmd != -1 ? lmd : null;
+
+ source = new URLTemplateLoadingSource(url);
+
+ if (ifSourceDiffersFrom != null && ifSourceDiffersFrom.equals(source)
+ && Objects.equals(ifVersionDiffersFrom, version)) {
+ return TemplateLoadingResult.NOT_MODIFIED;
+ }
+
+ inputStream = conn.getInputStream();
+ } catch (Throwable e) {
+ try {
+ if (inputStream == null) {
+ // There's no URLConnection.close(), so we do this hack. In case of HttpURLConnection we could call
+ // disconnect(), but that's perhaps too aggressive.
+ conn.getInputStream().close();
+ }
+ } catch (IOException e2) {
+ LOG.debug("Failed to close connection inputStream", e2);
+ }
+ throw e;
+ }
+ return new TemplateLoadingResult(source, version, inputStream, null);
+ }
+
+ @Override
+ public void resetState() {
+ // Do nothing
+ }
+
+ /**
+ * {@link URLConnection#getLastModified()} with JDK bug workarounds. Because of JDK-6956385, for files inside a jar,
+ * it returns the last modification time of the jar itself, rather than the last modification time of the file
+ * inside the jar.
+ *
+ * @param fallbackToJarLMD
+ * Tells if the file is in side jar, then we should return the last modification time of the jar itself,
+ * or -1 (to work around JDK-6956385).
+ */
+ public static long getLastModified(URLConnection conn, boolean fallbackToJarLMD) throws IOException {
+ if (conn instanceof JarURLConnection) {
+ // There is a bug in sun's jar url connection that causes file handle leaks when calling getLastModified()
+ // (see https://bugs.openjdk.java.net/browse/JDK-6956385).
+ // Since the time stamps of jar file contents can't vary independent from the jar file timestamp, just use
+ // the jar file timestamp
+ if (fallbackToJarLMD) {
+ URL jarURL = ((JarURLConnection) conn).getJarFileURL();
+ if (jarURL.getProtocol().equals("file")) {
+ // Return the last modified time of the underlying file - saves some opening and closing
+ return new File(jarURL.getFile()).lastModified();
+ } else {
+ // Use the URL mechanism
+ URLConnection jarConn = null;
+ try {
+ jarConn = jarURL.openConnection();
+ return jarConn.getLastModified();
+ } finally {
+ try {
+ if (jarConn != null) {
+ jarConn.getInputStream().close();
+ }
+ } catch (IOException e) {
+ LOG.warn("Failed to close URL connection for: {}", conn, e);
+ }
+ }
+ }
+ } else {
+ return -1;
+ }
+ } else {
+ return conn.getLastModified();
+ }
+ }
+
+ /**
+ * Given a template name (plus potential locale decorations) retrieves an URL that points the template source.
+ *
+ * @param name
+ * the name of the sought template (including the locale decorations, or other decorations the
+ * {@link TemplateLookupStrategy} uses).
+ *
+ * @return An URL that points to the template source, or null if it can be determined that the template source does
+ * not exist. For many implementations the existence of the template can't be decided at this point, and you
+ * rely on {@link #extractNegativeResult(URLConnection)} instead.
+ */
+ protected abstract URL getURL(String name);
+
+ /**
+ * Called before the resource if read, checks if we can immediately return a {@link TemplateLoadingResult#NOT_FOUND}
+ * or {@link TemplateLoadingResult#NOT_MODIFIED}, or throw an {@link IOException}. For example, for a HTTP-based
+ * storage, the HTTP response status 404 could result in {@link TemplateLoadingResult#NOT_FOUND}, 304 in
+ * {@link TemplateLoadingResult#NOT_MODIFIED}, and some others, like 500 in throwing an {@link IOException}.
+ *
+ * <p>Some
+ * implementations rely on {@link #getURL(String)} returning {@code null} when a template is missing, in which case
+ * this method is certainly not applicable.
+ */
+ protected abstract TemplateLoadingResult extractNegativeResult(URLConnection conn) throws IOException;
+
+ /**
+ * Called before anything that causes the connection to actually build up. This is where
+ * {@link URLConnection#setIfModifiedSince(long)} and such can be called if someone overrides this.
+ * The default implementation in {@link URLTemplateLoader} does nothing.
+ */
+ protected void prepareConnection(URLConnection conn) {
+ // Does nothing
+ }
+
+ /**
+ * Can be used by subclasses to canonicalize URL path prefixes.
+ * @param prefix the path prefix to canonicalize
+ * @return the canonicalized prefix. All backslashes are replaced with
+ * forward slashes, and a trailing slash is appended if the original
+ * prefix wasn't empty and didn't already end with a slash.
+ */
+ protected static String canonicalizePrefix(String prefix) {
+ // make it foolproof
+ prefix = prefix.replace('\\', '/');
+ // ensure there's a trailing slash
+ if (prefix.length() > 0 && !prefix.endsWith("/")) {
+ prefix += "/";
+ }
+ return prefix;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/URLTemplateLoadingSource.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/URLTemplateLoadingSource.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/URLTemplateLoadingSource.java
new file mode 100644
index 0000000..cbc1080
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/URLTemplateLoadingSource.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.core.templateresolver.impl;
+
+import java.net.URL;
+
+import org.apache.freemarker.core.templateresolver.TemplateLoadingSource;
+import org.apache.freemarker.core.util._NullArgumentException;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+@SuppressWarnings("serial")
+public class URLTemplateLoadingSource implements TemplateLoadingSource {
+
+ private final URL url;
+
+ public URLTemplateLoadingSource(URL url) {
+ _NullArgumentException.check("url", url);
+ this.url = url;
+ }
+
+ public URL getUrl() {
+ return url;
+ }
+
+ @Override
+ public int hashCode() {
+ return url.hashCode();
+ }
+
+ @Override
+ @SuppressFBWarnings("EQ_CHECK_FOR_OPERAND_NOT_COMPATIBLE_WITH_THIS")
+ public boolean equals(Object obj) {
+ return url.equals(obj);
+ }
+
+ @Override
+ public String toString() {
+ return url.toString();
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/_TemplateLoaderUtils.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/_TemplateLoaderUtils.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/_TemplateLoaderUtils.java
new file mode 100644
index 0000000..7511fa3
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/_TemplateLoaderUtils.java
@@ -0,0 +1,43 @@
+/*
+ * 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.templateresolver.impl;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.templateresolver.TemplateLoader;
+
+/**
+ * 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 final class _TemplateLoaderUtils {
+
+ private _TemplateLoaderUtils() {
+ // Not meant to be instantiated
+ }
+
+ public static String getClassNameForToString(TemplateLoader templateLoader) {
+ final Class<? extends TemplateLoader> tlClass = templateLoader.getClass();
+ final Package tlPackage = tlClass.getPackage();
+ return tlPackage == Configuration.class.getPackage() || tlPackage == TemplateLoader.class.getPackage()
+ ? tlClass.getSimpleName() : tlClass.getName();
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/package.html
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/package.html b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/package.html
new file mode 100644
index 0000000..c595bd8
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/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>Template lookup, loading and caching: 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/templateresolver/package.html
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/package.html b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/package.html
new file mode 100644
index 0000000..dd01586
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/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>Template lookup, loading, and caching: 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/core/util/BugException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/BugException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/BugException.java
new file mode 100644
index 0000000..399778a
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/BugException.java
@@ -0,0 +1,52 @@
+/*
+ * 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.util;
+
+/**
+ * An unexpected state was reached that is certainly caused by a bug in FreeMarker.
+ *
+ * @since 2.3.21
+ */
+public class BugException extends RuntimeException {
+
+ private static final String COMMON_MESSAGE
+ = "A bug was detected in FreeMarker; please report it with stack-trace";
+
+ public BugException() {
+ this((Throwable) null);
+ }
+
+ public BugException(String message) {
+ this(message, null);
+ }
+
+ public BugException(Throwable cause) {
+ super(COMMON_MESSAGE, cause);
+ }
+
+ public BugException(String message, Throwable cause) {
+ super(COMMON_MESSAGE + ": " + message, cause);
+ }
+
+ public BugException(int value) {
+ this(String.valueOf(value));
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/CaptureOutput.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/CaptureOutput.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/CaptureOutput.java
new file mode 100644
index 0000000..93109e0
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/CaptureOutput.java
@@ -0,0 +1,147 @@
+/*
+ * 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.util;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Map;
+
+import org.apache.freemarker.core.Environment;
+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.TemplateTransformModel;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+
+/**
+ * A transform that captures the output of a block of FTL code and stores that in a variable.
+ *
+ * <p>As this transform is initially present in the shared variable set, you can always
+ * access it from the templates:</p>
+ *
+ * <pre>
+ * <@capture_output var="captured">
+ * ...
+ * </@capture_output>
+ * </pre>
+ *
+ * <p>And later in the template you can use the captured output:</p>
+ *
+ * ${captured}
+ *
+ * <p>This transform requires one of three parameters: <code>var</code>, <code>local</code>, or <code>global</code>.
+ * Each of them specifies the name of the variable that stores the captured output, but the first creates a
+ * variable in a name-space (as <#assign>), the second creates a macro-local variable (as <#local>),
+ * and the last creates a global variable (as <#global>).
+ * </p>
+ * <p>In the case of an assignment within a namespace, there is an optional parameter
+ * <code>namespace</code> that indicates in which namespace to do the assignment.
+ * if this is omitted, the current namespace is used, and this will be, by far, the most
+ * common usage pattern.</p>
+ *
+ * @deprecated Use block-assignments instead, like <code><assign x>...</assign></code>.
+ */
+@Deprecated
+public class CaptureOutput implements TemplateTransformModel {
+
+ @Override
+ public Writer getWriter(final Writer out, final Map args) throws TemplateModelException {
+ String errmsg = "Must specify the name of the variable in "
+ + "which to capture the output with the 'var' or 'local' or 'global' parameter.";
+ if (args == null) throw new TemplateModelException(errmsg);
+
+ boolean local = false, global = false;
+ final TemplateModel nsModel = (TemplateModel) args.get("namespace");
+ Object varNameModel = args.get("var");
+ if (varNameModel == null) {
+ varNameModel = args.get("local");
+ if (varNameModel == null) {
+ varNameModel = args.get("global");
+ global = true;
+ } else {
+ local = true;
+ }
+ if (varNameModel == null) {
+ throw new TemplateModelException(errmsg);
+ }
+ }
+ if (args.size() == 2) {
+ if (nsModel == null) {
+ throw new TemplateModelException("Second parameter can only be namespace");
+ }
+ if (local) {
+ throw new TemplateModelException("Cannot specify namespace for a local assignment");
+ }
+ if (global) {
+ throw new TemplateModelException("Cannot specify namespace for a global assignment");
+ }
+ if (!(nsModel instanceof Environment.Namespace)) {
+ throw new TemplateModelException("namespace parameter does not specify a namespace. It is a " + nsModel.getClass().getName());
+ }
+ } else if (args.size() != 1) throw new TemplateModelException(
+ "Bad parameters. Use only one of 'var' or 'local' or 'global' parameters.");
+
+ if (!(varNameModel instanceof TemplateScalarModel)) {
+ throw new TemplateModelException("'var' or 'local' or 'global' parameter doesn't evaluate to a string");
+ }
+ final String varName = ((TemplateScalarModel) varNameModel).getAsString();
+ if (varName == null) {
+ throw new TemplateModelException("'var' or 'local' or 'global' parameter evaluates to null string");
+ }
+
+ final StringBuilder buf = new StringBuilder();
+ final Environment env = Environment.getCurrentEnvironment();
+ final boolean localVar = local;
+ final boolean globalVar = global;
+
+ return new Writer() {
+
+ @Override
+ public void write(char cbuf[], int off, int len) {
+ buf.append(cbuf, off, len);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ out.flush();
+ }
+
+ @Override
+ public void close() throws IOException {
+ SimpleScalar result = new SimpleScalar(buf.toString());
+ try {
+ if (localVar) {
+ env.setLocalVariable(varName, result);
+ } else if (globalVar) {
+ env.setGlobalVariable(varName, result);
+ } else {
+ if (nsModel == null) {
+ env.setVariable(varName, result);
+ } else {
+ ((Environment.Namespace) nsModel).put(varName, result);
+ }
+ }
+ } catch (java.lang.IllegalStateException ise) { // if somebody uses 'local' outside a macro
+ throw new IOException("Could not set variable " + varName + ": " + ise.getMessage());
+ }
+ }
+ };
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/CommonBuilder.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/CommonBuilder.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/CommonBuilder.java
new file mode 100644
index 0000000..11aab33
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/CommonBuilder.java
@@ -0,0 +1,35 @@
+/*
+ * 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.util;
+
+import org.apache.freemarker.core.ConfigurationException;
+
+/**
+ * Interface of builders (used for implementing the builder pattern).
+ */
+public interface CommonBuilder<ProductT> {
+
+ /**
+ * Creates an instance of the product class. This is usually a new instance, though if the product is stateless,
+ * it's possibly a shared object instead of a new one.
+ */
+ ProductT build() throws ConfigurationException;
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/DeepUnwrap.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/DeepUnwrap.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/DeepUnwrap.java
new file mode 100644
index 0000000..d2e361e
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/DeepUnwrap.java
@@ -0,0 +1,153 @@
+/*
+ * 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.util;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.model.AdapterTemplateModel;
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+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.WrapperTemplateModel;
+
+/**
+ * Utility methods for unwrapping {@link TemplateModel}-s.
+ */
+public class DeepUnwrap {
+ private static final Class OBJECT_CLASS = Object.class;
+ /**
+ * Unwraps {@link TemplateModel}-s recursively.
+ * The converting of the {@link TemplateModel} object happens with the following rules:
+ * <ol>
+ * <li>If the object implements {@link AdapterTemplateModel}, then the result
+ * of {@link AdapterTemplateModel#getAdaptedObject(Class)} for <tt>Object.class</tt> is returned.
+ * <li>If the object implements {@link WrapperTemplateModel}, then the result
+ * of {@link WrapperTemplateModel#getWrappedObject()} is returned.
+ * <li>If the object is identical to the null model of the current object
+ * wrapper, null is returned.
+ * <li>If the object implements {@link TemplateScalarModel}, then the result
+ * of {@link TemplateScalarModel#getAsString()} is returned.
+ * <li>If the object implements {@link TemplateNumberModel}, then the result
+ * of {@link TemplateNumberModel#getAsNumber()} is returned.
+ * <li>If the object implements {@link TemplateDateModel}, then the result
+ * of {@link TemplateDateModel#getAsDate()} is returned.
+ * <li>If the object implements {@link TemplateBooleanModel}, then the result
+ * of {@link TemplateBooleanModel#getAsBoolean()} is returned.
+ * <li>If the object implements {@link TemplateSequenceModel} or
+ * {@link TemplateCollectionModel}, then a <code>java.util.ArrayList</code> is
+ * constructed from the subvariables, and each subvariable is unwrapped with
+ * the rules described here (recursive unwrapping).
+ * <li>If the object implements {@link TemplateHashModelEx}, then a
+ * <code>java.util.HashMap</code> is constructed from the subvariables, and each
+ * subvariable is unwrapped with the rules described here (recursive unwrapping).
+ * <li>Throw a <code>TemplateModelException</code>, because it doesn't know how to
+ * unwrap the object.
+ * </ol>
+ */
+ public static Object unwrap(TemplateModel model) throws TemplateModelException {
+ return unwrap(model, false);
+ }
+
+ /**
+ * Same as {@link #unwrap(TemplateModel)}, but it doesn't throw exception
+ * if it doesn't know how to unwrap the model, but rather returns it as-is.
+ * @since 2.3.14
+ */
+ public static Object permissiveUnwrap(TemplateModel model) throws TemplateModelException {
+ return unwrap(model, true);
+ }
+
+ private static Object unwrap(TemplateModel model, boolean permissive) throws TemplateModelException {
+ Environment env = Environment.getCurrentEnvironment();
+ TemplateModel nullModel = null;
+ if (env != null) {
+ ObjectWrapper wrapper = env.getObjectWrapper();
+ if (wrapper != null) {
+ nullModel = wrapper.wrap(null);
+ }
+ }
+ return unwrap(model, nullModel, permissive);
+ }
+
+ private static Object unwrap(TemplateModel model, TemplateModel nullModel, boolean permissive) throws TemplateModelException {
+ if (model instanceof AdapterTemplateModel) {
+ return ((AdapterTemplateModel) model).getAdaptedObject(OBJECT_CLASS);
+ }
+ if (model instanceof WrapperTemplateModel) {
+ return ((WrapperTemplateModel) model).getWrappedObject();
+ }
+ if (model == nullModel) {
+ return null;
+ }
+ 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());
+ }
+ if (model instanceof TemplateSequenceModel) {
+ TemplateSequenceModel seq = (TemplateSequenceModel) model;
+ ArrayList list = new ArrayList(seq.size());
+ for (int i = 0; i < seq.size(); ++i) {
+ list.add(unwrap(seq.get(i), nullModel, permissive));
+ }
+ return list;
+ }
+ if (model instanceof TemplateCollectionModel) {
+ TemplateCollectionModel coll = (TemplateCollectionModel) model;
+ ArrayList list = new ArrayList();
+ TemplateModelIterator it = coll.iterator();
+ while (it.hasNext()) {
+ list.add(unwrap(it.next(), nullModel, permissive));
+ }
+ return list;
+ }
+ if (model instanceof TemplateHashModelEx) {
+ TemplateHashModelEx hash = (TemplateHashModelEx) model;
+ HashMap map = new HashMap();
+ TemplateModelIterator keys = hash.keys().iterator();
+ while (keys.hasNext()) {
+ String key = (String) unwrap(keys.next(), nullModel, permissive);
+ map.put(key, unwrap(hash.get(key), nullModel, permissive));
+ }
+ return map;
+ }
+ if (permissive) {
+ return model;
+ }
+ throw new TemplateModelException("Cannot deep-unwrap model of type " + model.getClass().getName());
+ }
+}
\ No newline at end of file