freemarker-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ddek...@apache.org
Subject [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
Date Sun, 14 May 2017 10:52:57 GMT
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>
+ * &lt;@capture_output var="captured"&gt;
+ *   ...
+ * &lt;/@capture_output&gt;
+ * </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 &lt;#assign&gt;), the second creates a macro-local variable (as &lt;#local&gt;),
+ * and the last creates a global variable (as &lt;#global&gt;).
+ * </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>&lt;assign x&gt;...&lt;/assign&gt;</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


Mime
View raw message