From 5213c6ac784b61b8ecf3b99814f192747aaaa4d0 Mon Sep 17 00:00:00 2001
From: Tor-Einar Skog <tor-einar.skog@nibio.no>
Date: Thu, 9 Feb 2023 10:36:34 +0100
Subject: [PATCH] SUCCESS TEST: Move Freemarker servlet parts into VIPS ->
 Refactor to Jakarta may be done!!

---
 .../cache/WebappTemplateLoader.java           |  222 +++
 .../ext/servlet/AllHttpScopesHashModel.java   |  116 ++
 .../ext/servlet/FreemarkerServlet.java        | 1672 +++++++++++++++++
 .../ext/servlet/HttpRequestHashModel.java     |  111 ++
 .../HttpRequestParametersHashModel.java       |  103 +
 .../ext/servlet/HttpSessionHashModel.java     |  113 ++
 .../freemarker/ext/servlet/IncludePage.java   |  254 +++
 .../ext/servlet/InitParamParser.java          |  265 +++
 .../ext/servlet/ServletContextHashModel.java  |   74 +
 .../nibio/freemarker/ext/servlet/package.html |   25 +
 src/main/webapp/WEB-INF/web.xml               |    2 +-
 11 files changed, 2956 insertions(+), 1 deletion(-)
 create mode 100644 src/main/java/no/nibio/freemarker/cache/WebappTemplateLoader.java
 create mode 100644 src/main/java/no/nibio/freemarker/ext/servlet/AllHttpScopesHashModel.java
 create mode 100644 src/main/java/no/nibio/freemarker/ext/servlet/FreemarkerServlet.java
 create mode 100644 src/main/java/no/nibio/freemarker/ext/servlet/HttpRequestHashModel.java
 create mode 100644 src/main/java/no/nibio/freemarker/ext/servlet/HttpRequestParametersHashModel.java
 create mode 100644 src/main/java/no/nibio/freemarker/ext/servlet/HttpSessionHashModel.java
 create mode 100644 src/main/java/no/nibio/freemarker/ext/servlet/IncludePage.java
 create mode 100644 src/main/java/no/nibio/freemarker/ext/servlet/InitParamParser.java
 create mode 100644 src/main/java/no/nibio/freemarker/ext/servlet/ServletContextHashModel.java
 create mode 100644 src/main/java/no/nibio/freemarker/ext/servlet/package.html

diff --git a/src/main/java/no/nibio/freemarker/cache/WebappTemplateLoader.java b/src/main/java/no/nibio/freemarker/cache/WebappTemplateLoader.java
new file mode 100644
index 00000000..8ff7b6c7
--- /dev/null
+++ b/src/main/java/no/nibio/freemarker/cache/WebappTemplateLoader.java
@@ -0,0 +1,222 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.cache;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import javax.servlet.ServletContext;
+
+import freemarker.log.Logger;
+import freemarker.template.Configuration;
+import freemarker.template.utility.CollectionUtils;
+import freemarker.template.utility.NullArgumentException;
+import freemarker.template.utility.StringUtil;
+
+/**
+ * A {@link TemplateLoader} that uses streams reachable through {@link ServletContext#getResource(String)} as its source
+ * of templates.  
+ */
+public class WebappTemplateLoader implements TemplateLoader {
+
+    private static final Logger LOG = Logger.getLogger("freemarker.cache");
+
+    private final ServletContext servletContext;
+    private final String subdirPath;
+
+    private Boolean urlConnectionUsesCaches;
+
+    private boolean attemptFileAccess = true;
+
+    /**
+     * Creates a template loader that will use the specified servlet context to load the resources. It will use
+     * the base path of <code>"/"</code> meaning templates will be resolved relative to the servlet context root
+     * location.
+     * 
+     * @param servletContext
+     *            the servlet context whose {@link ServletContext#getResource(String)} will be used to load the
+     *            templates.
+     */
+    public WebappTemplateLoader(ServletContext servletContext) {
+        this(servletContext, "/");
+    }
+
+    /**
+     * Creates a template loader that will use the specified servlet context to load the resources. It will use the
+     * specified base path, which is interpreted relatively to the context root (does not mater if you start it with "/"
+     * or not). Path components should be separated by forward slashes independently of the separator character used by
+     * the underlying operating system.
+     * 
+     * @param servletContext
+     *            the servlet context whose {@link ServletContext#getResource(String)} will be used to load the
+     *            templates.
+     * @param subdirPath
+     *            the base path to template resources.
+     */
+    public WebappTemplateLoader(ServletContext servletContext, String subdirPath) {
+        NullArgumentException.check("servletContext", servletContext);
+        NullArgumentException.check("subdirPath", subdirPath);
+
+        subdirPath = subdirPath.replace('\\', '/');
+        if (!subdirPath.endsWith("/")) {
+            subdirPath += "/";
+        }
+        if (!subdirPath.startsWith("/")) {
+            subdirPath = "/" + subdirPath;
+        }
+        this.subdirPath = subdirPath;
+        this.servletContext = servletContext;
+    }
+
+    @Override
+    public Object findTemplateSource(String name) throws IOException {
+        String fullPath = subdirPath + name;
+
+        if (attemptFileAccess) {
+            // First try to open as plain file (to bypass servlet container resource caches).
+            try {
+                String realPath = servletContext.getRealPath(fullPath);
+                if (realPath != null) {
+                    File file = new File(realPath);
+                    if (file.canRead() && file.isFile()) {
+                        return file;
+                    }
+                }
+            } catch (SecurityException e) {
+                ;// ignore
+            }
+        }
+
+        // If it fails, try to open it with servletContext.getResource.
+        URL url = null;
+        try {
+            url = servletContext.getResource(fullPath);
+        } catch (MalformedURLException e) {
+            LOG.warn("Could not retrieve resource " + StringUtil.jQuoteNoXSS(fullPath),
+                    e);
+            return null;
+        }
+        return url == null ? null : new URLTemplateSource(url, getURLConnectionUsesCaches());
+    }
+
+    @Override
+    public long getLastModified(Object templateSource) {
+        if (templateSource instanceof File) {
+            return ((File) templateSource).lastModified();
+        } else {
+            return ((URLTemplateSource) templateSource).lastModified();
+        }
+    }
+
+    @Override
+    public Reader getReader(Object templateSource, String encoding)
+            throws IOException {
+        if (templateSource instanceof File) {
+            return new InputStreamReader(
+                    new FileInputStream((File) templateSource),
+                    encoding);
+        } else {
+            return new InputStreamReader(
+                    ((URLTemplateSource) templateSource).getInputStream(),
+                    encoding);
+        }
+    }
+
+    @Override
+    public void closeTemplateSource(Object templateSource) throws IOException {
+        if (templateSource instanceof File) {
+            // Do nothing.
+        } else {
+            ((URLTemplateSource) templateSource).close();
+        }
+    }
+
+    /**
+     * Getter pair of {@link #setURLConnectionUsesCaches(Boolean)}.
+     * 
+     * @since 2.3.21
+     */
+    public Boolean getURLConnectionUsesCaches() {
+        return urlConnectionUsesCaches;
+    }
+
+    /**
+     * It does the same as {@link URLTemplateLoader#setURLConnectionUsesCaches(Boolean)}; see there.
+     * 
+     * @since 2.3.21
+     */
+    public void setURLConnectionUsesCaches(Boolean urlConnectionUsesCaches) {
+        this.urlConnectionUsesCaches = urlConnectionUsesCaches;
+    }
+
+    /**
+     * Show class name and some details that are useful in template-not-found errors.
+     * 
+     * @since 2.3.21
+     */
+    @Override
+    public String toString() {
+        return TemplateLoaderUtils.getClassNameForToString(this)
+                + "(subdirPath=" + StringUtil.jQuote(subdirPath)
+                + ", servletContext={contextPath=" + StringUtil.jQuote(getContextPath())
+                + ", displayName=" + StringUtil.jQuote(servletContext.getServletContextName()) + "})";
+    }
+
+    /** Gets the context path if we are on Servlet 2.5+, or else returns failure description string. */
+    private String getContextPath() {
+        try {
+            Method m = servletContext.getClass().getMethod("getContextPath", CollectionUtils.EMPTY_CLASS_ARRAY);
+            return (String) m.invoke(servletContext, CollectionUtils.EMPTY_OBJECT_ARRAY);
+        } catch (Throwable e) {
+            return "[can't query before Serlvet 2.5]";
+        }
+    }
+
+    /**
+     * Getter pair of {@link #setAttemptFileAccess(boolean)}.
+     * 
+     * @since 2.3.23
+     */
+    public boolean getAttemptFileAccess() {
+        return attemptFileAccess;
+    }
+
+    /**
+     * Specifies that before loading templates with {@link ServletContext#getResource(String)}, it should try to load
+     * the template as {@link File}; default is {@code true}, though it's not always recommended anymore. This is a
+     * workaround for the case when the servlet container doesn't show template modifications after the template was
+     * already loaded earlier. But it's certainly better to counter this problem by disabling the URL connection cache
+     * with {@link #setURLConnectionUsesCaches(Boolean)}, which is also the default behavior with
+     * {@link Configuration#setIncompatibleImprovements(freemarker.template.Version) incompatible_improvements} 2.3.21
+     * and later.
+     * 
+     * @since 2.3.23
+     */
+    public void setAttemptFileAccess(boolean attemptLoadingFromFile) {
+        this.attemptFileAccess = attemptLoadingFromFile;
+    }
+
+}
\ No newline at end of file
diff --git a/src/main/java/no/nibio/freemarker/ext/servlet/AllHttpScopesHashModel.java b/src/main/java/no/nibio/freemarker/ext/servlet/AllHttpScopesHashModel.java
new file mode 100644
index 00000000..2cbbfdae
--- /dev/null
+++ b/src/main/java/no/nibio/freemarker/ext/servlet/AllHttpScopesHashModel.java
@@ -0,0 +1,116 @@
+/*
+ * 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 no.nibio.freemarker.ext.servlet;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+
+import freemarker.template.ObjectWrapper;
+import freemarker.template.SimpleHash;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.template.utility.NullArgumentException;
+
+/**
+ * An extension of SimpleHash that looks up keys in the hash, then in the
+ * request, session, and servlet context scopes. Makes "Application", "Session"
+ * and "Request" keys largely obsolete, however we keep them for backward
+ * compatibility (also, "Request" is required for proper operation of JSP
+ * taglibs).
+ * It is on purpose that we didn't override <tt>keys</tt> and <tt>values</tt>
+ * methods. That way, only those variables assigned into the hash directly by a
+ * subclass of <tt>FreemarkerServlet</tt> that overrides
+ * <tt>preTemplateProcess</tt>) are discovered as "page" variables by the FM
+ * JSP PageContext implementation.
+ */
+public class AllHttpScopesHashModel extends SimpleHash {
+    private final ServletContext context;
+    private final HttpServletRequest request;
+    private final Map unlistedModels = new HashMap();
+     
+    /**
+     * Creates a new instance of AllHttpScopesHashModel for handling a single 
+     * HTTP servlet request.
+     * @param objectWrapper the object wrapper to use; not {@code null}.
+     * @param context the servlet context of the web application
+     * @param request the HTTP servlet request being processed
+     */
+    public AllHttpScopesHashModel(ObjectWrapper objectWrapper,
+            ServletContext context, HttpServletRequest request) {
+        super(objectWrapper);
+        NullArgumentException.check("wrapper", objectWrapper);
+        this.context = context;
+        this.request = request;
+    }
+    
+    /**
+     * Stores a model in the hash so that it doesn't show up in <tt>keys()</tt>
+     * and <tt>values()</tt> methods. Used to put the Application, Session,
+     * Request, RequestParameters and JspTaglibs objects.
+     * @param key the key under which the model is stored
+     * @param model the stored model
+     */
+    public void putUnlistedModel(String key, TemplateModel model) {
+        unlistedModels.put(key, model);
+    }
+
+    @Override
+    public TemplateModel get(String key) throws TemplateModelException {
+        // Lookup in page scope
+        TemplateModel model = super.get(key);
+        if (model != null) {
+            return model;
+        }
+
+        // Look in unlisted models
+        model = (TemplateModel) unlistedModels.get(key);
+        if (model != null) {
+            return model;
+        }
+        
+        // Lookup in request scope
+        Object obj = request.getAttribute(key);
+        if (obj != null) {
+            return wrap(obj);
+        }
+
+        // Lookup in session scope
+        HttpSession session = request.getSession(false);
+        if (session != null) {
+            obj = session.getAttribute(key);
+            if (obj != null) {
+                return wrap(obj);
+            }
+        }
+
+        // Lookup in application scope
+        obj = context.getAttribute(key);
+        if (obj != null) {
+            return wrap(obj);
+        }
+
+        // return wrapper's null object (probably null).        
+        return wrap(null);
+    }
+}
diff --git a/src/main/java/no/nibio/freemarker/ext/servlet/FreemarkerServlet.java b/src/main/java/no/nibio/freemarker/ext/servlet/FreemarkerServlet.java
new file mode 100644
index 00000000..8c5fd6f7
--- /dev/null
+++ b/src/main/java/no/nibio/freemarker/ext/servlet/FreemarkerServlet.java
@@ -0,0 +1,1672 @@
+/*
+ * 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 no.nibio.freemarker.ext.servlet;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.nio.charset.Charset;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Enumeration;
+import java.util.GregorianCalendar;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import freemarker.cache.ClassTemplateLoader;
+import freemarker.cache.FileTemplateLoader;
+import freemarker.cache.MultiTemplateLoader;
+import freemarker.cache.TemplateLoader;
+import freemarker.cache.WebappTemplateLoader;
+import freemarker.core.Configurable;
+import freemarker.core.Environment;
+import freemarker.core.OutputFormat;
+import freemarker.core.UndefinedOutputFormat;
+import freemarker.ext.jsp.TaglibFactory;
+import freemarker.ext.jsp.TaglibFactory.ClasspathMetaInfTldSource;
+import freemarker.ext.jsp.TaglibFactory.ClearMetaInfTldSource;
+import freemarker.ext.jsp.TaglibFactory.MetaInfTldSource;
+import freemarker.ext.jsp.TaglibFactory.WebInfPerLibJarMetaInfTldSource;
+import freemarker.log.Logger;
+import freemarker.template.Configuration;
+import freemarker.template.ObjectWrapper;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateExceptionHandler;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateNotFoundException;
+import freemarker.template.utility.SecurityUtilities;
+import freemarker.template.utility.StringUtil;
+
+/**
+ * FreeMarker MVC View servlet that can be used similarly to JSP views. That is, you put the variables to expose into
+ * HTTP servlet request attributes, then forward to an FTL file (instead of to a JSP file) that's mapped to this servet
+ * (usually via the {@code <url-pattern>*.ftl<url-pattern>}). See web.xml example (and more) in the FreeMarker Manual!
+ * 
+ * 
+ * <p>
+ * <b>Main features</b>
+ * </p>
+ *
+ * 
+ * <ul>
+ * 
+ * <li>It makes all request, request parameters, session, and servlet context attributes available to templates through
+ * <code>Request</code>, <code>RequestParameters</code>, <code>Session</code>, and <code>Application</code> variables.
+ * 
+ * <li>The scope variables are also available via automatic scope discovery. That is, writing
+ * <code>Application.attrName</code>, <code>Session.attrName</code>, <code>Request.attrName</code> is not mandatory;
+ * it's enough to write <code>attrName</code>, and if no such variable was created in the template, it will search the
+ * variable in <code>Request</code>, and then in <code>Session</code>, and finally in <code>Application</code>.
+ * 
+ * <li>It creates a variable with name <code>JspTaglibs</code> that can be used to load JSP taglibs. For example:<br>
+ * <code>&lt;#assign dt=JspTaglibs["http://displaytag.sf.net"]&gt;</code> or
+ * <code>&lt;#assign tiles=JspTaglibs["/WEB-INF/struts-tiles.tld"]&gt;</code>.
+ * 
+ * <li>A custom directive named {@code include_page} allows you to include the output of another servlet resource from
+ * your servlet container, just as if you used {@code ServletRequest.getRequestDispatcher(path).include()}: {@code 
+ * <@include_page path="/myWebapp/somePage.jsp"/>}. You can also pass parameters to the newly included page by passing a
+ * hash named {@code params}:
+ * <code>&lt;@include_page path="/myWebapp/somePage.jsp" params= lang: "en", q="5"}/&gt;</code>. By default, the request
+ * parameters of the original request (the one being processed by FreemarkerServlet) are also inherited by the include.
+ * You can explicitly control this inheritance using the {@code inherit_params} parameter:
+ * <code>&lt;@include_page path="/myWebapp/somePage.jsp" params={lang: "en", q="5"} inherit_params=false/&gt;</code>.
+ * 
+ * </ul>
+ * 
+ * 
+ * <p>
+ * <b>Supported {@code init-param}-s</b>
+ * </p>
+ * 
+ * 
+ * <ul>
+ * 
+ * <li><strong>{@value #INIT_PARAM_TEMPLATE_PATH}</strong>: Specifies the location of the template files. By default,
+ * this is interpreted as a {@link ServletContext} resource path, which practically means a web application directory
+ * relative path, or a {@code WEB-INF/lib/*.jar/META-INF/resources}-relative path (note that this last haven't always
+ * worked before FreeMarker 2.3.23).<br>
+ * Alternatively, you can prepend it with <tt>file://</tt> to indicate a literal path in the file system (i.e.
+ * <tt>file:///var/www/project/templates/</tt>). Note that three slashes were used to specify an absolute path.<br>
+ * Also, you can prepend it with {@code classpath:}, like in <tt>classpath:com/example/templates</tt>, to indicate that
+ * you want to load templates from the specified package accessible through the Thread Context Class Loader of the
+ * thread that initializes this servlet.<br>
+ * If {@code incompatible_improvements} is set to 2.3.22 (or higher), you can specify multiple comma separated locations
+ * inside square brackets, like: {@code [ WEB-INF/templates, classpath:com/example/myapp/templates ]}. This internally
+ * creates a {@link MultiTemplateLoader}. Note again that if {@code incompatible_improvements} isn't set to at least
+ * 2.3.22, the initial {@code [} has no special meaning, and so this feature is unavailable.<br>
+ * Any of the above can have a {@code ?setting(name=value, ...)} postfix to set the JavaBeans properties of the
+ * {@link TemplateLoader} created. For example,
+ * {@code /templates?settings(attemptFileAccess=false, URLConnectionUsesCaches=true)} calls
+ * {@link WebappTemplateLoader#setAttemptFileAccess(boolean)} and
+ * {@link WebappTemplateLoader#setURLConnectionUsesCaches(Boolean)} to tune the {@link WebappTemplateLoader}. For
+ * backward compatibility (not recommended!), you can use the {@code class://} prefix, like in
+ * <tt>class://com/example/templates</tt> format, which is similar to {@code classpath:}, except that it uses the
+ * defining class loader of this servlet's class. This can cause template-not-found errors, if that class (in
+ * {@code freemarer.jar} usually) is not local to the web application, while the templates are.<br>
+ * The default value is <tt>class://</tt> (that is, the root of the class hierarchy), which is not recommended anymore,
+ * and should be overwritten with the {@value #INIT_PARAM_TEMPLATE_PATH} init-param.</li>
+ * 
+ * <li><strong>{@value #INIT_PARAM_NO_CACHE}</strong>: If set to {@code true}, generates headers in the response that
+ * advise the HTTP client not to cache the returned page. If {@code false}, the HTTP response is not modified for this
+ * purpose. The default is {@code false}.</li>
+ * 
+ * <li><strong>{@value #INIT_PARAM_CONTENT_TYPE}</strong>: The Content-type HTTP header value used in the HTTP responses
+ * when nothing else specifies the MIME type. The things that may specify the MIME type (and hence this init-param is
+ * ignored), starting with the highest precedence, are:
+ * <ol>
+ * <li>If the {@value #INIT_PARAM_OVERRIDE_RESPONSE_CONTENT_TYPE} init-param is {@value #INIT_PARAM_VALUE_NEVER} (the
+ * default is {@value #INIT_PARAM_VALUE_ALWAYS}), then the value of {@link HttpServletResponse#getContentType()} is used
+ * if that's non-{@code null}.
+ * <li>The template's <tt>content_type</tt> custom attribute, usually specified via the <tt>attributes</tt> parameter of
+ * the <tt>&lt;#ftl&gt;</tt> directive. This is a legacy feature, deprecated by the {@link OutputFormat} mechanism.
+ * <li>The {@linkplain Template#getOutputFormat() output format of the template}, if that has non-{@code null} MIME-type
+ * ({@link OutputFormat#getMimeType()}). When a template has no output format specified, {@link UndefinedOutputFormat}
+ * is used, which has {@code null} MIME-type. (The output format of a template is deduced from {@link Configuration}
+ * settings, or can be specified directly in the template, like {@code <#ftl outputFormat="HTML">}. See the FreeMarker
+ * Manual for more about the output format mechanism. Note that setting an output format may turns on auto-escaping, so
+ * it's not just about MIME types.)
+ * <li>If the {@value #INIT_PARAM_OVERRIDE_RESPONSE_CONTENT_TYPE} init-param is not {@value #INIT_PARAM_VALUE_ALWAYS}
+ * (the default is {@value #INIT_PARAM_VALUE_ALWAYS}), then the value of {@link HttpServletResponse#getContentType()} is
+ * used if that's non-{@code null}.
+ * </ol>
+ * If none of the above gives a MIME type, then this init-param does. Defaults to <tt>"text/html"</tt>. If and only if
+ * the {@value #INIT_PARAM_RESPONSE_CHARACTER_ENCODING} init-param is set to {@value #INIT_PARAM_VALUE_LEGACY} (which is
+ * the default of it), the content type may include the charset (as in <tt>"text/html; charset=utf-8"</tt>), in which
+ * case that specifies the actual charset of the output. If the the {@value #INIT_PARAM_RESPONSE_CHARACTER_ENCODING}
+ * init-param is not set to {@value #INIT_PARAM_VALUE_LEGACY}, then specifying the charset in the
+ * {@value #INIT_PARAM_CONTENT_TYPE} init-param is not allowed, and will cause servlet initialization error.</li>
+ *
+ * <li><strong>{@value #INIT_PARAM_OVERRIDE_RESPONSE_CONTENT_TYPE}</strong> (since 2.3.24): Specifies when we should
+ * override the {@code contentType} that might be already set (i.e., non-{@code null}) in the
+ * {@link HttpServletResponse}. The default is {@value #INIT_PARAM_VALUE_ALWAYS}, which means that we always set the
+ * content type. Another possible value is {@value #INIT_PARAM_VALUE_NEVER}, which means that we don't set the content
+ * type in the response, unless {@link HttpServletResponse#getContentType()} is {@code null}. The third possible value
+ * is {@value #INIT_PARAM_VALUE_WHEN_TEMPLATE_HAS_MIME_TYPE}, which means that we only set the content type if either
+ * the template has an associated {@link OutputFormat} with non-{@code null} {@link OutputFormat#getMimeType()}, or it
+ * has a custom attribute with name <tt>content_type</tt>, or {@link HttpServletResponse#getContentType()} is
+ * {@code null}. Setting this init-param allows you to specify the content type before forwarding to
+ * {@link FreemarkerServlet}.</li>
+ *
+ * <li><strong>{@value #INIT_PARAM_OVERRIDE_RESPONSE_LOCALE}</strong> (since 2.3.24): Specifies if we should override
+ * the template {@code locale} that might be already set (i.e., non-{@code null}) in the {@link HttpServletRequest}. The
+ * default is {@value #INIT_PARAM_VALUE_ALWAYS}, which means that we always deduce the template {@code locale} by
+ * invoking {@link #deduceLocale(String, HttpServletRequest, HttpServletResponse)}. Another possible value is
+ * {@value #INIT_PARAM_VALUE_NEVER}, which means that we don't deduce the template {@code locale}, unless
+ * {@link HttpServletRequest#getLocale()} is {@code null}.
+ * 
+ * <li><strong>{@value #INIT_PARAM_RESPONSE_CHARACTER_ENCODING}</strong> (since 2.3.24): Specifies how the
+ * {@link HttpServletResponse} "character encoding" (as in {@link HttpServletResponse#setCharacterEncoding(String)})
+ * will be deduced. The possible modes are:
+ * <ul>
+ * <li>{@value #INIT_PARAM_VALUE_LEGACY}: This is the default for backward compatibility; in new applications, use
+ * {@value #INIT_PARAM_VALUE_FROM_TEMPLATE} (or some of the other options) instead. {@value #INIT_PARAM_VALUE_LEGACY}
+ * will use the charset of the template file to set the charset of the servlet response. Except, if the
+ * {@value #INIT_PARAM_CONTENT_TYPE} init-param contains a charset, it will use that instead. A quirk of this legacy
+ * mode is that it's not aware of the {@link Configurable#getOutputEncoding()} FreeMarker setting, and thus never reads
+ * or writes it (though very few applications utilize that setting anyway). Also, it sets the charset of the servlet
+ * response by adding it to the response content type via calling {@link HttpServletResponse#setContentType(String)} (as
+ * that was the only way before Servlet 2.4), not via the more modern
+ * {@link HttpServletResponse#setCharacterEncoding(String)} method. Note that the charset of a template usually comes
+ * from {@link Configuration#getDefaultEncoding()} (i.e., from the {@code default_encoding} FreeMarker setting),
+ * occasionally from {@link Configuration#getEncoding(Locale)} (when FreeMarker was configured to use different charsets
+ * depending on the locale) or even more rarely from {@link Configuration#getTemplateConfigurations()} (when FreeMarker was
+ * configured to use a specific charset for certain templates).
+ * <li>{@value #INIT_PARAM_VALUE_FROM_TEMPLATE}: This should be used in most applications, but it's not the default for
+ * backward compatibility. It reads the {@link Configurable#getOutputEncoding()} setting of the template (note that the
+ * template usually just inherits that from the {@link Configuration}), and if that's not set, then reads the source
+ * charset of the template, just like {@value #INIT_PARAM_VALUE_LEGACY}. Then it passes the charset acquired this way to
+ * {@link HttpServletResponse#setCharacterEncoding(String)} and {@link Environment#setOutputEncoding(String)}. (It
+ * doesn't call the legacy {@link HttpServletResponse#setContentType(String)} API to set the charset.) (Note that if the
+ * template has a {@code content_type} template attribute (which is deprecated) that specifies a charset, it will be
+ * used as the output charset of that template.)
+ * <li>{@value #INIT_PARAM_VALUE_DO_NOT_SET}: {@link FreemarkerServlet} will not set the {@link HttpServletResponse}
+ * "character encoding". It will still call {@link Environment#setOutputEncoding(String)}, so that the running template
+ * will be aware of the charset used for the output.
+ * <li>{@value #INIT_PARAM_VALUE_FORCE_PREFIX} + charset name, for example {@code force UTF-8}: The output charset will
+ * be the one specified after "force" + space, regardless of everything. The charset specified this way is passed to
+ * {@link HttpServletResponse#setCharacterEncoding(String)} and {@link Environment#setOutputEncoding(String)}. If the
+ * charset name is not recognized by Java, the servlet initialization will fail.
+ * </ul>
+ *
+ * <li><strong>{@value #INIT_PARAM_BUFFER_SIZE}</strong>: Sets the size of the output buffer in bytes, or if "KB" or
+ * "MB" is written after the number (like {@code <param-value>256 KB</param-value>}) then in kilobytes or megabytes.
+ * This corresponds to {@link HttpServletResponse#setBufferSize(int)}. If the {@link HttpServletResponse} state doesn't
+ * allow changing the buffer size, it will silently do nothing. If this init param isn't specified, then the buffer size
+ * is not set by {@link FreemarkerServlet} in the HTTP response, which usually means that the default buffer size of the
+ * servlet container will be used.</li>
+ *
+ * <li><strong>{@value #INIT_PARAM_EXCEPTION_ON_MISSING_TEMPLATE}</strong> (since 2.3.22): If {@code false} (default,
+ * but not recommended), if a template is requested that's missing, this servlet responses with a HTTP 404 "Not found"
+ * error, and only logs the problem with debug level. If {@code true} (recommended), the servlet will log the issue with
+ * error level, then throws an exception that bubbles up to the servlet container, which usually then creates a HTTP 500
+ * "Internal server error" response (and maybe logs the event into the container log). See "Error handling" later for
+ * more!</li>
+ * 
+ * <li><strong>{@value #INIT_PARAM_META_INF_TLD_LOCATIONS}</strong> (since 2.3.22): Comma separated list of items, each
+ * is either {@value #META_INF_TLD_LOCATION_WEB_INF_PER_LIB_JARS}, or {@value #META_INF_TLD_LOCATION_CLASSPATH}
+ * optionally followed by colon and a regular expression, or {@value #META_INF_TLD_LOCATION_CLEAR}. For example {@code 
+ * <param-value>classpath:.*myoverride.*\.jar$, webInfPerLibJars, classpath:.*taglib.*\.jar$</param-value>}, or {@code 
+ * <param-value>classpath</param-value>}. (Whitespace around the commas and list items will be ignored.) See
+ * {@link TaglibFactory#setMetaInfTldSources(List)} for more information. Defaults to a list that contains
+ * {@value #META_INF_TLD_LOCATION_WEB_INF_PER_LIB_JARS} only (can be overridden with
+ * {@link #createDefaultMetaInfTldSources()}). Note that this can be also specified with the
+ * {@value #SYSTEM_PROPERTY_META_INF_TLD_SOURCES} system property. If both the init-param and the system property
+ * exists, the sources listed in the system property will be added after those specified by the init-param. This is
+ * where the special entry, {@value #META_INF_TLD_LOCATION_CLEAR} comes handy, as it will remove all previous list
+ * items. (An intended usage of the system property is setting it to {@code clear, classpath} in the Eclipse run
+ * configuration if you are running the application without putting the dependency jar-s into {@code WEB-INF/lib}.)
+ * Also, note that further {@code classpath:<pattern>} items are added automatically at the end of this list based on
+ * Jetty's {@code "org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern"} servlet context attribute.</li>
+ * 
+ * <li><strong>{@value #INIT_PARAM_CLASSPATH_TLDS}</strong> (since 2.3.22): Comma separated list of paths; see
+ * {@link TaglibFactory#setClasspathTlds(List)}. Whitespace around the list items will be ignored. Defaults to no paths
+ * (can be overidden with {@link #createDefaultClassPathTlds()}). Note that this can also be specified with the
+ * {@value #SYSTEM_PROPERTY_CLASSPATH_TLDS} system property. If both the init-param and the system property exists, the
+ * items listed in system property will be added after those specified by the init-param.</li>
+ * 
+ * <li><strong>"Debug"</strong>: Deprecated, has no effect since 2.3.22. (Earlier it has enabled/disabled sending
+ * debug-level log messages to the servlet container log, but this servlet doesn't log debug level messages into the
+ * servlet container log anymore, only into the FreeMarker log.)</li>
+ * 
+ * <li>The following init-params are supported only for backward compatibility, and their usage is discouraged:
+ * {@code TemplateUpdateInterval}, {@code DefaultEncoding}, {@code ObjectWrapper}, {@code TemplateExceptionHandler}.
+ * Instead, use init-params with the setting names documented at {@link Configuration#setSetting(String, String)}, such
+ * as {@code object_wrapper}.
+ * 
+ * <li><strong>Any other init-params</strong> will be interpreted as {@link Configuration}-level FreeMarker setting. See
+ * the possible names and values at {@link Configuration#setSetting(String, String)}. Note that these init-param names
+ * are starting with lower-case letter (upper-case init-params are used for FreemarkerSerlvet settings).</li>
+ * 
+ * </ul>
+ * 
+ * 
+ * <p>
+ * <b>Error handling</b>
+ * </p>
+ * 
+ * 
+ * <p>
+ * Notes:
+ * </p>
+ * 
+ * <ul>
+ *
+ * <li>Logging below, where not said otherwise, always refers to logging with FreeMarker's logging facility (see
+ * {@link Logger}), under the "freemarker.servlet" category.</li>
+ * <li>Throwing a {@link ServletException} to the servlet container is mentioned at a few places below. That in practice
+ * usually means HTTP 500 "Internal server error" response, and maybe a log entry in the servlet container's log.</li>
+ * </ul>
+ *
+ * <p>
+ * Errors types:
+ * </p>
+ * 
+ * <ul>
+ * 
+ * <li>If the servlet initialization fails, the servlet won't be started as usual. The cause is usually logged with
+ * error level. When it isn't, check the servlet container's log.
+ * 
+ * <li>If the requested template doesn't exist, by default the servlet returns a HTTP 404 "Not found" response, and logs
+ * the problem with <em>debug</em> level. Responding with HTTP 404 is how JSP behaves, but it's actually not a
+ * recommended setting anymore. By setting {@value #INIT_PARAM_EXCEPTION_ON_MISSING_TEMPLATE} init-param to {@code true}
+ * (recommended), it will instead log the problem with error level, then the servlet throws {@link ServletException} to
+ * the servlet container (with the proper cause exception). After all, if the visited URL had an associated "action" but
+ * the template behind it is missing, that's an internal server error, not a wrong URL.</li>
+ * 
+ * <li>If the template contains parsing errors, it will log it with error level, then the servlet throws
+ * {@link ServletException} to the servlet container (with the proper cause exception).</li>
+ * 
+ * <li>If the template throws exception during its execution, and the value of the {@code template_exception_handler}
+ * init-param is {@code rethrow} (recommended), it will log it with error level and then the servlet throws
+ * {@link ServletException} to the servlet container (with the proper cause exception). But beware, the default value of
+ * the {@code template_exception_handler} init-param is {@code html_debug}, which is for development only! Set it to
+ * {@code rethrow} for production. The {@code html_debug} (and {@code debug}) handlers will print error details to the
+ * page and then commit the HTTP response with response code 200 "OK", thus, the server wont be able roll back the
+ * response and send back an HTTP 500 page. This is so that the template developers will see the error without digging
+ * the logs.
+ * 
+ * </ul>
+ */
+public class FreemarkerServlet extends HttpServlet {
+    private static final Logger LOG = Logger.getLogger("freemarker.servlet");
+    private static final Logger LOG_RT = Logger.getLogger("freemarker.runtime");
+
+    public static final long serialVersionUID = -2440216393145762479L;
+
+    /**
+     * Init-param name - see the {@link FreemarkerServlet} class documentation about the init-params. (This init-param
+     * has existed long before 2.3.22, but this constant was only added then.)
+     * 
+     * @since 2.3.22
+     */
+    public static final String INIT_PARAM_TEMPLATE_PATH = "TemplatePath";
+    
+    /**
+     * Init-param name - see the {@link FreemarkerServlet} class documentation about the init-params. (This init-param
+     * has existed long before 2.3.22, but this constant was only added then.)
+     * 
+     * @since 2.3.22
+     */
+    public static final String INIT_PARAM_NO_CACHE = "NoCache";
+
+    /**
+     * Init-param name - see the {@link FreemarkerServlet} class documentation about the init-params. (This init-param
+     * has existed long before 2.3.22, but this constant was only added then.)
+     * 
+     * @since 2.3.22
+     */
+    public static final String INIT_PARAM_CONTENT_TYPE = "ContentType";
+
+    /**
+     * Init-param name - see the {@link FreemarkerServlet} class documentation about the init-params.
+     *
+     * @since 2.3.24
+     */
+    public static final String INIT_PARAM_OVERRIDE_RESPONSE_CONTENT_TYPE = "OverrideResponseContentType";
+
+    /**
+     * Init-param name - see the {@link FreemarkerServlet} class documentation about the init-params.
+     *
+     * @since 2.3.24
+     */
+    public static final String INIT_PARAM_RESPONSE_CHARACTER_ENCODING = "ResponseCharacterEncoding";
+
+    /**
+     * Init-param name - see the {@link FreemarkerServlet} class documentation about the init-params.
+     *
+     * @since 2.3.24
+     */
+    public static final String INIT_PARAM_OVERRIDE_RESPONSE_LOCALE = "OverrideResponseLocale";
+
+    /**
+     * Init-param name - see the {@link FreemarkerServlet} class documentation about the init-params.
+     * 
+     * @since 2.3.22
+     */
+    public static final String INIT_PARAM_BUFFER_SIZE = "BufferSize";
+    
+    /**
+     * Init-param name - see the {@link FreemarkerServlet} class documentation about the init-params.
+     * 
+     * @since 2.3.22
+     */
+    public static final String INIT_PARAM_META_INF_TLD_LOCATIONS = "MetaInfTldSources";
+
+    /**
+     * Init-param name - see the {@link FreemarkerServlet} class documentation about the init-params.
+     * 
+     * @since 2.3.22
+     */
+    public static final String INIT_PARAM_EXCEPTION_ON_MISSING_TEMPLATE = "ExceptionOnMissingTemplate";
+    
+    /**
+     * Init-param name - see the {@link FreemarkerServlet} class documentation about the init-params.
+     * 
+     * @since 2.3.22
+     */
+    public static final String INIT_PARAM_CLASSPATH_TLDS = "ClasspathTlds";
+    
+    private static final String INIT_PARAM_DEBUG = "Debug";
+
+    private static final String DEPR_INITPARAM_TEMPLATE_DELAY = "TemplateDelay";
+    private static final String DEPR_INITPARAM_ENCODING = "DefaultEncoding";
+    private static final String DEPR_INITPARAM_OBJECT_WRAPPER = "ObjectWrapper";
+    private static final String DEPR_INITPARAM_WRAPPER_SIMPLE = "simple";
+    private static final String DEPR_INITPARAM_WRAPPER_BEANS = "beans";
+    private static final String DEPR_INITPARAM_WRAPPER_JYTHON = "jython";
+    private static final String DEPR_INITPARAM_TEMPLATE_EXCEPTION_HANDLER = "TemplateExceptionHandler";
+    private static final String DEPR_INITPARAM_TEMPLATE_EXCEPTION_HANDLER_RETHROW = "rethrow";
+    private static final String DEPR_INITPARAM_TEMPLATE_EXCEPTION_HANDLER_DEBUG = "debug";
+    private static final String DEPR_INITPARAM_TEMPLATE_EXCEPTION_HANDLER_HTML_DEBUG = "htmlDebug";
+    private static final String DEPR_INITPARAM_TEMPLATE_EXCEPTION_HANDLER_IGNORE = "ignore";
+    private static final String DEPR_INITPARAM_DEBUG = "debug";
+    
+    private static final ContentType DEFAULT_CONTENT_TYPE = new ContentType("text/html");
+    
+    public static final String INIT_PARAM_VALUE_NEVER = "never";
+    public static final String INIT_PARAM_VALUE_ALWAYS = "always";
+    public static final String INIT_PARAM_VALUE_WHEN_TEMPLATE_HAS_MIME_TYPE = "whenTemplateHasMimeType";
+    public static final String INIT_PARAM_VALUE_FROM_TEMPLATE = "fromTemplate";
+    public static final String INIT_PARAM_VALUE_LEGACY = "legacy";
+    public static final String INIT_PARAM_VALUE_DO_NOT_SET = "doNotSet";
+    public static final String INIT_PARAM_VALUE_FORCE_PREFIX = "force ";
+
+    /**
+     * When set, the items defined in it will be added after those coming from the
+     * {@value #INIT_PARAM_META_INF_TLD_LOCATIONS} init-param. The value syntax is the same as of the init-param. Note
+     * that {@value #META_INF_TLD_LOCATION_CLEAR} can be used to re-start the list, rather than continue it.
+     * 
+     * @since 2.3.22
+     */
+    public static final String SYSTEM_PROPERTY_META_INF_TLD_SOURCES = "org.freemarker.jsp.metaInfTldSources";
+
+    /**
+     * When set, the items defined in it will be added after those coming from the
+     * {@value #INIT_PARAM_CLASSPATH_TLDS} init-param. The value syntax is the same as of the init-param.
+     * 
+     * @since 2.3.22
+     */
+    public static final String SYSTEM_PROPERTY_CLASSPATH_TLDS = "org.freemarker.jsp.classpathTlds";
+    
+    /**
+     * Used as part of the value of the {@value #INIT_PARAM_META_INF_TLD_LOCATIONS} init-param.
+     * 
+     * @since 2.3.22
+     */
+    public static final String META_INF_TLD_LOCATION_WEB_INF_PER_LIB_JARS = "webInfPerLibJars";
+    
+    /**
+     * Used as part of the value of the {@value #INIT_PARAM_META_INF_TLD_LOCATIONS} init-param.
+     * 
+     * @since 2.3.22
+     */
+    public static final String META_INF_TLD_LOCATION_CLASSPATH = "classpath";
+    
+    /**
+     * Used as part of the value of the {@value #INIT_PARAM_META_INF_TLD_LOCATIONS} init-param.
+     * 
+     * @since 2.3.22
+     */
+    public static final String META_INF_TLD_LOCATION_CLEAR = "clear";
+
+    public static final String KEY_REQUEST = "Request";
+    public static final String KEY_INCLUDE = "include_page";
+    public static final String KEY_REQUEST_PRIVATE = "__FreeMarkerServlet.Request__";
+    public static final String KEY_REQUEST_PARAMETERS = "RequestParameters";
+    public static final String KEY_SESSION = "Session";
+    public static final String KEY_APPLICATION = "Application";
+    public static final String KEY_APPLICATION_PRIVATE = "__FreeMarkerServlet.Application__";
+    public static final String KEY_JSP_TAGLIBS = "JspTaglibs";
+
+    // Note these names start with dot, so they're essentially invisible from
+    // a freemarker script.
+    private static final String ATTR_REQUEST_MODEL = ".freemarker.Request";
+    private static final String ATTR_REQUEST_PARAMETERS_MODEL = ".freemarker.RequestParameters";
+    private static final String ATTR_SESSION_MODEL = ".freemarker.Session";
+    
+    /** @deprecated We only keeps this attribute for backward compatibility, but actually aren't using it. */
+    @Deprecated
+    private static final String ATTR_APPLICATION_MODEL = ".freemarker.Application";
+    
+    /** @deprecated We only keeps this attribute for backward compatibility, but actually aren't using it. */
+    @Deprecated
+    private static final String ATTR_JSP_TAGLIBS_MODEL = ".freemarker.JspTaglibs";
+
+    private static final String ATTR_JETTY_CP_TAGLIB_JAR_PATTERNS
+            = "org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern";
+    
+    private static final String EXPIRATION_DATE;
+
+    static {
+        // Generate expiration date that is one year from now in the past
+        GregorianCalendar expiration = new GregorianCalendar();
+        expiration.roll(Calendar.YEAR, -1);
+        SimpleDateFormat httpDate =
+            new SimpleDateFormat(
+                "EEE, dd MMM yyyy HH:mm:ss z",
+                java.util.Locale.US);
+        EXPIRATION_DATE = httpDate.format(expiration.getTime());
+    }
+
+    // Init-param values:
+    private String templatePath;
+    private boolean noCache;
+    private Integer bufferSize;
+    private boolean exceptionOnMissingTemplate;
+    
+    /**
+     * @deprecated Not used anymore; to enable/disable debug logging, just set the logging level of the logging library
+     *             used by {@link Logger}.
+     */
+    @Deprecated
+    protected boolean debug;
+    
+
+    private Configuration config;
+
+    private ObjectWrapper wrapper;
+    private ContentType contentType;
+    private OverrideResponseContentType overrideResponseContentType = initParamValueToEnum(
+            getDefaultOverrideResponseContentType(), OverrideResponseContentType.values());
+    private ResponseCharacterEncoding responseCharacterEncoding = ResponseCharacterEncoding.LEGACY;
+    private Charset forcedResponseCharacterEncoding;
+    private OverrideResponseLocale overrideResponseLocale = OverrideResponseLocale.ALWAYS;
+    private List/*<MetaInfTldSource>*/ metaInfTldSources;
+    private List/*<String>*/ classpathTlds;
+
+    private Object lazyInitFieldsLock = new Object();
+
+    private ServletContextHashModel servletContextModel;
+    private TaglibFactory taglibFactory;
+    
+    private boolean objectWrapperMismatchWarnLogged;
+
+    /**
+     * Don't override this method to adjust FreeMarker settings! Override the protected methods for that, such as
+     * {@link #createConfiguration()}, {@link #createTemplateLoader(String)}, {@link #createDefaultObjectWrapper()},
+     * etc. Also note that lot of things can be changed with init-params instead of overriding methods, so if you
+     * override settings, usually you should only override their defaults.
+     */
+    @Override
+    public void init() throws ServletException {
+        try {
+            initialize();
+        } catch (Exception e) {
+            // At least Jetty doesn't log the ServletException itself, only its cause exception. Thus we add some
+            // message here that (re)states the obvious.
+            throw new ServletException("Error while initializing " + this.getClass().getName()
+                    + " servlet; see cause exception.", e);
+        }
+    }
+    
+    private void initialize() throws InitParamValueException, MalformedWebXmlException, ConflictingInitParamsException {
+        config = createConfiguration();
+        
+        // Only override what's coming from the config if it was explicitly specified: 
+        final String iciInitParamValue = getInitParameter(Configuration.INCOMPATIBLE_IMPROVEMENTS_KEY);
+        if (iciInitParamValue != null) {
+            try {
+                config.setSetting(Configuration.INCOMPATIBLE_IMPROVEMENTS_KEY, iciInitParamValue);
+            } catch (Exception e) {
+                throw new InitParamValueException(Configuration.INCOMPATIBLE_IMPROVEMENTS_KEY, iciInitParamValue, e);
+            }
+        }
+        
+        // Set FreemarkerServlet-specific defaults, except where createConfiguration() has already set them:
+        if (!config.isTemplateExceptionHandlerExplicitlySet()) {
+            config.setTemplateExceptionHandler(TemplateExceptionHandler.HTML_DEBUG_HANDLER);
+        }
+        if (!config.isLogTemplateExceptionsExplicitlySet()) {
+            config.setLogTemplateExceptions(false);
+        }
+        
+        contentType = DEFAULT_CONTENT_TYPE;
+        
+        // Process object_wrapper init-param out of order: 
+        wrapper = createObjectWrapper();
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("Using object wrapper: " + wrapper);
+        }
+        config.setObjectWrapper(wrapper);
+        
+        // Process TemplatePath init-param out of order:
+        templatePath = getInitParameter(INIT_PARAM_TEMPLATE_PATH);
+        if (templatePath == null && !config.isTemplateLoaderExplicitlySet()) {
+            templatePath = InitParamParser.TEMPLATE_PATH_PREFIX_CLASS;
+        }
+        if (templatePath != null) {
+            try {
+                config.setTemplateLoader(createTemplateLoader(templatePath));
+            } catch (Exception e) {
+                throw new InitParamValueException(INIT_PARAM_TEMPLATE_PATH, templatePath, e);
+            }
+        }
+        
+        metaInfTldSources = createDefaultMetaInfTldSources();
+        classpathTlds = createDefaultClassPathTlds();
+
+        // Process all other init-params:
+        Enumeration initpnames = getServletConfig().getInitParameterNames();
+        while (initpnames.hasMoreElements()) {
+            final String name = (String) initpnames.nextElement();
+            final String value = getInitParameter(name);
+            if (name == null) {
+                throw new MalformedWebXmlException(
+                        "init-param without param-name. "
+                        + "Maybe the web.xml is not well-formed?");
+            }
+            if (value == null) {
+                throw new MalformedWebXmlException(
+                        "init-param " + StringUtil.jQuote(name) + " without param-value. "
+                        + "Maybe the web.xml is not well-formed?");
+            }
+            
+            try {
+                if (name.equals(DEPR_INITPARAM_OBJECT_WRAPPER)
+                        || name.equals(Configurable.OBJECT_WRAPPER_KEY)
+                        || name.equals(INIT_PARAM_TEMPLATE_PATH)
+                        || name.equals(Configuration.INCOMPATIBLE_IMPROVEMENTS)) {
+                    // ignore: we have already processed these
+                } else if (name.equals(DEPR_INITPARAM_ENCODING)) { // BC
+                    if (getInitParameter(Configuration.DEFAULT_ENCODING_KEY) != null) {
+                        throw new ConflictingInitParamsException(
+                                Configuration.DEFAULT_ENCODING_KEY, DEPR_INITPARAM_ENCODING);
+                    }
+                    config.setDefaultEncoding(value);
+                } else if (name.equals(DEPR_INITPARAM_TEMPLATE_DELAY)) { // BC
+                    if (getInitParameter(Configuration.TEMPLATE_UPDATE_DELAY_KEY) != null) {
+                        throw new ConflictingInitParamsException(
+                                Configuration.TEMPLATE_UPDATE_DELAY_KEY, DEPR_INITPARAM_TEMPLATE_DELAY);
+                    }
+                    try {
+                        config.setTemplateUpdateDelay(Integer.parseInt(value));
+                    } catch (NumberFormatException e) {
+                        // Intentionally ignored
+                    }
+                } else if (name.equals(DEPR_INITPARAM_TEMPLATE_EXCEPTION_HANDLER)) { // BC
+                    if (getInitParameter(Configurable.TEMPLATE_EXCEPTION_HANDLER_KEY) != null) {
+                        throw new ConflictingInitParamsException(
+                                Configurable.TEMPLATE_EXCEPTION_HANDLER_KEY, DEPR_INITPARAM_TEMPLATE_EXCEPTION_HANDLER);
+                    }
+    
+                    if (DEPR_INITPARAM_TEMPLATE_EXCEPTION_HANDLER_RETHROW.equals(value)) {
+                        config.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
+                    } else if (DEPR_INITPARAM_TEMPLATE_EXCEPTION_HANDLER_DEBUG.equals(value)) {
+                        config.setTemplateExceptionHandler(TemplateExceptionHandler.DEBUG_HANDLER);
+                    } else if (DEPR_INITPARAM_TEMPLATE_EXCEPTION_HANDLER_HTML_DEBUG.equals(value)) {
+                        config.setTemplateExceptionHandler(TemplateExceptionHandler.HTML_DEBUG_HANDLER);
+                    } else if (DEPR_INITPARAM_TEMPLATE_EXCEPTION_HANDLER_IGNORE.equals(value)) {
+                        config.setTemplateExceptionHandler(TemplateExceptionHandler.IGNORE_HANDLER);
+                    } else {
+                        throw new InitParamValueException(DEPR_INITPARAM_TEMPLATE_EXCEPTION_HANDLER, value,
+                                "Not one of the supported values.");
+                    }
+                } else if (name.equals(INIT_PARAM_NO_CACHE)) {
+                    noCache = StringUtil.getYesNo(value);
+                } else if (name.equals(INIT_PARAM_BUFFER_SIZE)) {
+                    bufferSize = Integer.valueOf(parseSize(value));
+                } else if (name.equals(DEPR_INITPARAM_DEBUG)) { // BC
+                    if (getInitParameter(INIT_PARAM_DEBUG) != null) {
+                        throw new ConflictingInitParamsException(INIT_PARAM_DEBUG, DEPR_INITPARAM_DEBUG);
+                    }
+                    debug = StringUtil.getYesNo(value);
+                } else if (name.equals(INIT_PARAM_DEBUG)) {
+                    debug = StringUtil.getYesNo(value);
+                } else if (name.equals(INIT_PARAM_CONTENT_TYPE)) {
+                    contentType = new ContentType(value);
+                } else if (name.equals(INIT_PARAM_OVERRIDE_RESPONSE_CONTENT_TYPE)) {
+                    overrideResponseContentType = initParamValueToEnum(value, OverrideResponseContentType.values());
+                } else if (name.equals(INIT_PARAM_RESPONSE_CHARACTER_ENCODING)) {
+                    responseCharacterEncoding = initParamValueToEnum(value, ResponseCharacterEncoding.values());
+                    if (responseCharacterEncoding == ResponseCharacterEncoding.FORCE_CHARSET) {
+                        String charsetName = value.substring(INIT_PARAM_VALUE_FORCE_PREFIX.length()).trim();
+                        forcedResponseCharacterEncoding = Charset.forName(charsetName);
+                    }
+                } else if (name.equals(INIT_PARAM_OVERRIDE_RESPONSE_LOCALE)) {
+                    overrideResponseLocale = initParamValueToEnum(value, OverrideResponseLocale.values());
+                } else if (name.equals(INIT_PARAM_EXCEPTION_ON_MISSING_TEMPLATE)) {
+                    exceptionOnMissingTemplate = StringUtil.getYesNo(value);
+                } else if (name.equals(INIT_PARAM_META_INF_TLD_LOCATIONS)) {;
+                    metaInfTldSources = parseAsMetaInfTldLocations(value);
+                } else if (name.equals(INIT_PARAM_CLASSPATH_TLDS)) {;
+                    List newClasspathTlds = new ArrayList();
+                    if (classpathTlds != null) {
+                        newClasspathTlds.addAll(classpathTlds);
+                    }
+                    newClasspathTlds.addAll(InitParamParser.parseCommaSeparatedList(value));
+                    classpathTlds = newClasspathTlds;
+                } else {
+                    config.setSetting(name, value);
+                }
+            } catch (ConflictingInitParamsException e) {
+                throw e;
+            } catch (Exception e) {
+                throw new InitParamValueException(name, value, e);
+            }
+        } // while initpnames
+        
+        if (contentType.containsCharset && responseCharacterEncoding != ResponseCharacterEncoding.LEGACY) {
+            throw new InitParamValueException(INIT_PARAM_CONTENT_TYPE, contentType.httpHeaderValue,
+                    new IllegalStateException("You can't specify the charset in the content type, because the \"" +
+                            INIT_PARAM_RESPONSE_CHARACTER_ENCODING + "\" init-param isn't set to "
+                            + "\"" + INIT_PARAM_VALUE_LEGACY + "\"."));
+        }        
+    }
+    
+    private List/*<MetaInfTldSource>*/ parseAsMetaInfTldLocations(String value) throws ParseException {
+        List/*<MetaInfTldSource>*/ metaInfTldSources = null;
+        
+        List/*<String>*/ values = InitParamParser.parseCommaSeparatedList(value);
+        for (Iterator it = values.iterator(); it.hasNext(); ) {
+            final String itemStr = (String) it.next();
+            final MetaInfTldSource metaInfTldSource;
+            if (itemStr.equals(META_INF_TLD_LOCATION_WEB_INF_PER_LIB_JARS)) {
+                metaInfTldSource = WebInfPerLibJarMetaInfTldSource.INSTANCE;
+            } else if (itemStr.startsWith(META_INF_TLD_LOCATION_CLASSPATH)) {
+                String itemRightSide = itemStr.substring(META_INF_TLD_LOCATION_CLASSPATH.length()).trim();
+                if (itemRightSide.length() == 0) {
+                    metaInfTldSource = new ClasspathMetaInfTldSource(Pattern.compile(".*", Pattern.DOTALL));
+                } else if (itemRightSide.startsWith(":")) {
+                    final String regexpStr = itemRightSide.substring(1).trim();
+                    if (regexpStr.length() == 0) {
+                        throw new ParseException("Empty regular expression after \""
+                                + META_INF_TLD_LOCATION_CLASSPATH + ":\"", -1);
+                    }
+                    metaInfTldSource = new ClasspathMetaInfTldSource(Pattern.compile(regexpStr));   
+                } else {
+                    throw new ParseException("Invalid \"" + META_INF_TLD_LOCATION_CLASSPATH
+                            + "\" value syntax: " + value, -1);
+                }
+            } else if (itemStr.startsWith(META_INF_TLD_LOCATION_CLEAR)) {
+                metaInfTldSource = ClearMetaInfTldSource.INSTANCE;
+            } else {
+                throw new ParseException("Item has no recognized source type prefix: " + itemStr, -1);
+            }
+            if (metaInfTldSources == null) {
+                metaInfTldSources = new ArrayList();
+            }
+            metaInfTldSources.add(metaInfTldSource);
+        }
+        
+        return metaInfTldSources;
+    }
+
+    /**
+     * Create the template loader. The default implementation will create a {@link ClassTemplateLoader} if the template
+     * path starts with {@code "class://"}, a {@link FileTemplateLoader} if the template path starts with
+     * {@code "file://"}, and a {@link WebappTemplateLoader} otherwise. Also, if
+     * {@link Configuration#Configuration(freemarker.template.Version) incompatible_improvements} is 2.3.22 or higher,
+     * it will create a {@link MultiTemplateLoader} if the template path starts with {@code "["}.
+     * 
+     * @param templatePath
+     *            the template path to create a loader for
+     * @return a newly created template loader
+     */
+    protected TemplateLoader createTemplateLoader(String templatePath) throws IOException {
+        return InitParamParser.createTemplateLoader(templatePath, getConfiguration(), getClass(), getServletContext());
+    }
+    
+    @Override
+    public void doGet(HttpServletRequest request, HttpServletResponse response)
+        throws ServletException, IOException {
+        process(request, response);
+    }
+
+    @Override
+    public void doPost(
+        HttpServletRequest request,
+        HttpServletResponse response)
+        throws ServletException, IOException {
+        process(request, response);
+    }
+
+    private void process(
+        HttpServletRequest request,
+        HttpServletResponse response)
+        throws ServletException, IOException {
+        // Give chance to subclasses to perform preprocessing
+        if (preprocessRequest(request, response)) {
+            return;
+        }
+        
+        if (bufferSize != null && !response.isCommitted()) {
+            try {
+                response.setBufferSize(bufferSize.intValue());
+            } catch (IllegalStateException e) {
+                LOG.debug("Can't set buffer size any more,", e);
+            }
+        }
+
+        String templatePath = requestUrlToTemplatePath(request);
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("Requested template " + StringUtil.jQuoteNoXSS(templatePath) + ".");
+        }
+
+        Locale locale = request.getLocale();
+        if (locale == null || overrideResponseLocale != OverrideResponseLocale.NEVER) {
+            locale = deduceLocale(templatePath, request, response);
+        }
+
+        final Template template;
+        try {
+            template = config.getTemplate(templatePath, locale);
+        } catch (TemplateNotFoundException e) {
+            if (exceptionOnMissingTemplate) {
+                throw newServletExceptionWithFreeMarkerLogging(
+                        "Template not found for name " + StringUtil.jQuoteNoXSS(templatePath) + ".", e);
+            } else {
+                if (LOG.isDebugEnabled()) {
+                    LOG.debug("Responding HTTP 404 \"Not found\" for missing template "
+                            + StringUtil.jQuoteNoXSS(templatePath) + ".", e);
+                }
+                response.sendError(HttpServletResponse.SC_NOT_FOUND, "Page template not found");
+                return;
+            }
+        } catch (freemarker.core.ParseException e) {
+            throw newServletExceptionWithFreeMarkerLogging(
+                    "Parsing error with template " + StringUtil.jQuoteNoXSS(templatePath) + ".", e);
+        } catch (Exception e) {
+            throw newServletExceptionWithFreeMarkerLogging(
+                    "Unexpected error when loading template " + StringUtil.jQuoteNoXSS(templatePath) + ".", e);
+        }
+
+        boolean tempSpecContentTypeContainsCharset = false;
+        if (response.getContentType() == null || overrideResponseContentType != OverrideResponseContentType.NEVER) {
+            ContentType templateSpecificContentType = getTemplateSpecificContentType(template);
+            if (templateSpecificContentType != null) {
+                // With ResponseCharacterEncoding.LEGACY we should append the charset, but we don't do that for b. c.
+                response.setContentType(
+                        responseCharacterEncoding != ResponseCharacterEncoding.DO_NOT_SET
+                                ? templateSpecificContentType.httpHeaderValue
+                                : templateSpecificContentType.getMimeType());
+                tempSpecContentTypeContainsCharset = templateSpecificContentType.containsCharset;
+            } else if (response.getContentType() == null
+                    || overrideResponseContentType == OverrideResponseContentType.ALWAYS) {
+                if (responseCharacterEncoding == ResponseCharacterEncoding.LEGACY && !contentType.containsCharset) {
+                    // In legacy mode we don't call response.setCharacterEncoding, so the charset must be set here:
+                    response.setContentType(
+                            contentType.httpHeaderValue + "; charset=" + getTemplateSpecificOutputEncoding(template));
+                } else {
+                    response.setContentType(contentType.httpHeaderValue);
+                }
+            }
+        }
+        
+        if (responseCharacterEncoding != ResponseCharacterEncoding.LEGACY
+                && responseCharacterEncoding != ResponseCharacterEncoding.DO_NOT_SET) {
+            // Using the Servlet 2.4 way of setting character encoding.
+            if (responseCharacterEncoding != ResponseCharacterEncoding.FORCE_CHARSET) {
+                if (!tempSpecContentTypeContainsCharset) {
+                    response.setCharacterEncoding(getTemplateSpecificOutputEncoding(template));
+                }
+            } else {
+                response.setCharacterEncoding(forcedResponseCharacterEncoding.name());
+            }
+        }
+
+        setBrowserCachingPolicy(response);
+
+        ServletContext servletContext = getServletContext();
+        try {
+            logWarnOnObjectWrapperMismatch();
+            
+            TemplateModel model = createModel(wrapper, servletContext, request, response);
+
+            // Give subclasses a chance to hook into preprocessing
+            if (preTemplateProcess(request, response, template, model)) {
+                try {
+                    // Process the template
+                    Environment env = template.createProcessingEnvironment(model, response.getWriter());
+                    if (responseCharacterEncoding != ResponseCharacterEncoding.LEGACY) {
+                        String actualOutputCharset = response.getCharacterEncoding();
+                        if (actualOutputCharset != null) {
+                            env.setOutputEncoding(actualOutputCharset);
+                        }
+                    }
+                    processEnvironment(env, request, response);
+                } finally {
+                    // Give subclasses a chance to hook into postprocessing
+                    postTemplateProcess(request, response, template, model);
+                }
+            }
+        } catch (TemplateException e) {
+            final TemplateExceptionHandler teh = config.getTemplateExceptionHandler();
+            // Ensure that debug handler responses aren't rolled back:
+            if (teh == TemplateExceptionHandler.HTML_DEBUG_HANDLER || teh == TemplateExceptionHandler.DEBUG_HANDLER
+                    || teh.getClass().getName().indexOf("Debug") != -1) {
+                response.flushBuffer();
+            }
+            throw newServletExceptionWithFreeMarkerLogging("Error executing FreeMarker template", e);
+        }
+    }
+
+    /**
+     * This is the method that actually executes the template. The original implementation coming from
+     * {@link FreemarkerServlet} simply calls {@link Environment#process()}. Overriding this method allows you to
+     * prepare the {@link Environment} before the execution, or extract information from the {@link Environment} after
+     * the execution. It also allows you to capture exceptions throw by the template.
+     * 
+     * @param env
+     *            The {@link Environment} object already set up to execute the template. You only have to call
+     *            {@link Environment#process()} and the output will be produced by the template.
+     * 
+     * @since 2.3.24
+     */
+    protected void processEnvironment(Environment env, HttpServletRequest request, HttpServletResponse response)
+            throws TemplateException, IOException {
+        env.process();
+    }
+
+    private String getTemplateSpecificOutputEncoding(Template template) {
+        String outputEncoding = responseCharacterEncoding == ResponseCharacterEncoding.LEGACY ? null
+                : template.getOutputEncoding();
+        return outputEncoding != null ? outputEncoding : template.getEncoding();
+    }
+
+    private ContentType getTemplateSpecificContentType(final Template template) {
+        Object contentTypeAttr = template.getCustomAttribute("content_type");
+        if (contentTypeAttr != null) {
+            // Converted with toString() for backward compatibility.
+            return new ContentType(contentTypeAttr.toString());
+        }
+        
+        String outputFormatMimeType = template.getOutputFormat().getMimeType();
+        if (outputFormatMimeType != null) {
+            if (responseCharacterEncoding == ResponseCharacterEncoding.LEGACY) {
+                // In legacy mode we won't call serlvetResponse.setCharacterEncoding(...), so:
+                return new ContentType(outputFormatMimeType + "; charset=" + getTemplateSpecificOutputEncoding(template), true);
+            } else {
+                return new ContentType(outputFormatMimeType, false);
+            }
+        }
+            
+        return null;
+    }
+
+    private ServletException newServletExceptionWithFreeMarkerLogging(String message, Throwable cause) throws ServletException {
+        if (cause instanceof TemplateException) {
+            // For backward compatibility, we log into the same category as Environment did when
+            // log_template_exceptions was true.
+            LOG_RT.error(message, cause);
+        } else {
+            LOG.error(message, cause);
+        }
+
+        ServletException e = new ServletException(message, cause);
+        try {
+            // Prior to Servlet 2.5, the cause exception wasn't set by the above constructor.
+            // If we are on 2.5+ then this will throw an exception as the cause was already set.
+            e.initCause(cause);
+        } catch (Exception ex) {
+            // Ignored; see above
+        }
+        throw e;
+    }
+
+    private void logWarnOnObjectWrapperMismatch() {
+        // Deliberately uses double check locking.
+        if (wrapper != config.getObjectWrapper() && !objectWrapperMismatchWarnLogged && LOG.isWarnEnabled()) {
+            final boolean logWarn;
+            synchronized (this) {
+                logWarn = !objectWrapperMismatchWarnLogged;
+                if (logWarn) {
+                    objectWrapperMismatchWarnLogged = true;
+                }
+            }
+            if (logWarn) {
+                LOG.warn(
+                        this.getClass().getName()
+                        + ".wrapper != config.getObjectWrapper(); possibly the result of incorrect extension of "
+                        + FreemarkerServlet.class.getName() + ".");
+            }
+        }
+    }
+    
+    /**
+     * Returns the locale used for the {@link Configuration#getTemplate(String, Locale)} call (as far as the
+     * {@value #INIT_PARAM_OVERRIDE_RESPONSE_LOCALE} Servlet init-param allows that). The base implementation in
+     * {@link FreemarkerServlet} simply returns the {@code locale} setting of the configuration. Override this method to
+     * provide different behavior, for example, to use the locale indicated in the HTTP request.
+     * 
+     * @param templatePath
+     *            The template path (template name) as it will be passed to {@link Configuration#getTemplate(String)}.
+     *            (Not to be confused with the servlet init-param of identical name; they aren't related.)
+     * 
+     * @throws ServletException
+     *             Can be thrown since 2.3.22, if the locale can't be deduced from the URL.
+     */
+    protected Locale deduceLocale(String templatePath, HttpServletRequest request, HttpServletResponse response)
+            throws ServletException {
+        return config.getLocale();
+    }
+
+    protected TemplateModel createModel(ObjectWrapper objectWrapper,
+                                        ServletContext servletContext,
+                                        final HttpServletRequest request,
+                                        final HttpServletResponse response) throws TemplateModelException {
+        try {
+            AllHttpScopesHashModel params = new AllHttpScopesHashModel(objectWrapper, servletContext, request);
+    
+            // Create hash model wrapper for servlet context (the application)
+            final ServletContextHashModel servletContextModel;
+            final TaglibFactory taglibFactory;
+            synchronized (lazyInitFieldsLock) {
+                if (this.servletContextModel == null) {
+                    servletContextModel = new ServletContextHashModel(this, objectWrapper);
+                    taglibFactory = createTaglibFactory(objectWrapper, servletContext);
+                    
+                    // For backward compatibility only. We don't use these:
+                    servletContext.setAttribute(ATTR_APPLICATION_MODEL, servletContextModel);
+                    servletContext.setAttribute(ATTR_JSP_TAGLIBS_MODEL, taglibFactory);
+                    
+                    initializeServletContext(request, response);
+
+                    this.taglibFactory = taglibFactory;
+                    this.servletContextModel = servletContextModel;
+                } else {
+                    servletContextModel = this.servletContextModel;
+                    taglibFactory = this.taglibFactory;
+                }
+            }
+            
+            params.putUnlistedModel(KEY_APPLICATION, servletContextModel);
+            params.putUnlistedModel(KEY_APPLICATION_PRIVATE, servletContextModel);
+            params.putUnlistedModel(KEY_JSP_TAGLIBS, taglibFactory);
+            // Create hash model wrapper for session
+            HttpSessionHashModel sessionModel;
+            HttpSession session = request.getSession(false);
+            if (session != null) {
+                sessionModel = (HttpSessionHashModel) session.getAttribute(ATTR_SESSION_MODEL);
+                if (sessionModel == null || sessionModel.isOrphaned(session)) {
+                    sessionModel = new HttpSessionHashModel(session, objectWrapper);
+                    initializeSessionAndInstallModel(request, response, 
+                            sessionModel, session);
+                }
+            } else {
+                sessionModel = new HttpSessionHashModel(this, request, response, objectWrapper);
+            }
+            params.putUnlistedModel(KEY_SESSION, sessionModel);
+    
+            // Create hash model wrapper for request
+            HttpRequestHashModel requestModel =
+                (HttpRequestHashModel) request.getAttribute(ATTR_REQUEST_MODEL);
+            if (requestModel == null || requestModel.getRequest() != request) {
+                requestModel = new HttpRequestHashModel(request, response, objectWrapper);
+                request.setAttribute(ATTR_REQUEST_MODEL, requestModel);
+                request.setAttribute(
+                    ATTR_REQUEST_PARAMETERS_MODEL,
+                    createRequestParametersHashModel(request));
+            }
+            params.putUnlistedModel(KEY_REQUEST, requestModel);
+            params.putUnlistedModel(KEY_INCLUDE, new IncludePage(request, response));
+            params.putUnlistedModel(KEY_REQUEST_PRIVATE, requestModel);
+    
+            // Create hash model wrapper for request parameters
+            HttpRequestParametersHashModel requestParametersModel =
+                (HttpRequestParametersHashModel) request.getAttribute(
+                    ATTR_REQUEST_PARAMETERS_MODEL);
+            params.putUnlistedModel(KEY_REQUEST_PARAMETERS, requestParametersModel);
+            return params;
+        } catch (ServletException | IOException e) {
+            throw new TemplateModelException(e);
+        }
+    }
+
+    /**
+     * Called to create the {@link TaglibFactory} once per servlet context.
+     * The default implementation configures it based on the servlet-init parameters and various other environmental
+     * settings, so if you override this method, you should call super, then adjust the result.
+     * 
+     * @since 2.3.22
+     */
+    protected TaglibFactory createTaglibFactory(ObjectWrapper objectWrapper,
+            ServletContext servletContext) throws TemplateModelException {
+        TaglibFactory taglibFactory = new TaglibFactory(servletContext);
+        
+        taglibFactory.setObjectWrapper(objectWrapper);
+        
+        {
+            List/*<MetaInfTldSource>*/ mergedMetaInfTldSources = new ArrayList();
+
+            if (metaInfTldSources != null) {
+                mergedMetaInfTldSources.addAll(metaInfTldSources);
+            }
+            
+            String sysPropVal = SecurityUtilities.getSystemProperty(SYSTEM_PROPERTY_META_INF_TLD_SOURCES, null);
+            if (sysPropVal != null) {
+                try {
+                    List metaInfTldSourcesSysProp = parseAsMetaInfTldLocations(sysPropVal);
+                    if (metaInfTldSourcesSysProp != null) {
+                        mergedMetaInfTldSources.addAll(metaInfTldSourcesSysProp);
+                    }
+                } catch (ParseException e) {
+                    throw new TemplateModelException("Failed to parse system property \""
+                            + SYSTEM_PROPERTY_META_INF_TLD_SOURCES + "\"", e);
+                }
+            }
+
+            List/*<Pattern>*/ jettyTaglibJarPatterns = null;
+            try {
+                final String attrVal = (String) servletContext.getAttribute(ATTR_JETTY_CP_TAGLIB_JAR_PATTERNS);
+                jettyTaglibJarPatterns = attrVal != null ? InitParamParser.parseCommaSeparatedPatterns(attrVal) : null;
+            } catch (Exception e) {
+                LOG.error("Failed to parse application context attribute \""
+                        + ATTR_JETTY_CP_TAGLIB_JAR_PATTERNS + "\" - it will be ignored", e);
+            }
+            if (jettyTaglibJarPatterns != null) {
+                for (Iterator/*<Pattern>*/ it = jettyTaglibJarPatterns.iterator(); it.hasNext(); ) {
+                    Pattern pattern = (Pattern) it.next();
+                    mergedMetaInfTldSources.add(new ClasspathMetaInfTldSource(pattern));
+                }
+            }
+            
+            taglibFactory.setMetaInfTldSources(mergedMetaInfTldSources);
+        }
+        
+        {
+            List/*<String>*/ mergedClassPathTlds = new ArrayList();
+            if (classpathTlds != null) {
+                mergedClassPathTlds.addAll(classpathTlds);
+            }
+            
+            String sysPropVal = SecurityUtilities.getSystemProperty(SYSTEM_PROPERTY_CLASSPATH_TLDS, null);
+            if (sysPropVal != null) {
+                try {
+                    List/*<String>*/ classpathTldsSysProp = InitParamParser.parseCommaSeparatedList(sysPropVal);
+                    if (classpathTldsSysProp != null) {
+                        mergedClassPathTlds.addAll(classpathTldsSysProp);
+                    }
+                } catch (ParseException e) {
+                    throw new TemplateModelException("Failed to parse system property \""
+                            + SYSTEM_PROPERTY_CLASSPATH_TLDS + "\"", e);
+                }
+            }
+            
+            taglibFactory.setClasspathTlds(mergedClassPathTlds);
+        }
+        
+        return taglibFactory;        
+    }
+
+    /**
+     * Creates the default of the {@value #INIT_PARAM_CLASSPATH_TLDS} init-param; if this init-param is specified, it
+     * will be appended <em>after</em> the default, not replace it.
+     * 
+     * <p>
+     * The implementation in {@link FreemarkerServlet} returns {@link TaglibFactory#DEFAULT_CLASSPATH_TLDS}.
+     * 
+     * @return A {@link List} of {@link String}-s; not {@code null}.
+     * 
+     * @since 2.3.22
+     */
+    protected List/*<MetaInfTldSource>*/ createDefaultClassPathTlds() {
+        return TaglibFactory.DEFAULT_CLASSPATH_TLDS;
+    }
+
+    /**
+     * Creates the default of the {@value #INIT_PARAM_META_INF_TLD_LOCATIONS} init-param; if this init-param is
+     * specified, it will completelly <em>replace</em> the default value.
+     * 
+     * <p>
+     * The implementation in {@link FreemarkerServlet} returns {@link TaglibFactory#DEFAULT_META_INF_TLD_SOURCES}.
+     * 
+     * @return A {@link List} of {@link MetaInfTldSource}-s; not {@code null}.
+     * 
+     * @since 2.3.22
+     */
+    protected List/*<MetaInfTldSource>*/ createDefaultMetaInfTldSources() {
+        return TaglibFactory.DEFAULT_META_INF_TLD_SOURCES;
+    }
+    
+    void initializeSessionAndInstallModel(HttpServletRequest request,
+            HttpServletResponse response, HttpSessionHashModel sessionModel, 
+            HttpSession session)
+            throws ServletException, IOException {
+        session.setAttribute(ATTR_SESSION_MODEL, sessionModel);
+        initializeSession(request, response);
+    }
+
+    /**
+     * Maps the request URL to a template path (template name) that is passed to
+     * {@link Configuration#getTemplate(String, Locale)}. You can override it (i.e. to provide advanced rewriting
+     * capabilities), but you are strongly encouraged to call the overridden method first, then only modify its return
+     * value.
+     * 
+     * @param request
+     *            The currently processed HTTP request
+     * @return The template path (template name); can't be {@code null}. This is what's passed to
+     *         {@link Configuration#getTemplate(String)} later. (Not to be confused with the {@code templatePath}
+     *         servlet init-param of identical name; that basically specifies the "virtual file system" to which this
+     *         will be relative to.)
+     * 
+     * @throws ServletException
+     *             Can be thrown since 2.3.22, if the template path can't be deduced from the URL.
+     */
+    protected String requestUrlToTemplatePath(HttpServletRequest request) throws ServletException {
+        // First, see if it's an included request
+        String includeServletPath  = (String) request.getAttribute("javax.servlet.include.servlet_path");
+        if (includeServletPath != null) {
+            // Try path info; only if that's null (servlet is mapped to an
+            // URL extension instead of to prefix) use servlet path.
+            String includePathInfo = (String) request.getAttribute("javax.servlet.include.path_info");
+            return includePathInfo == null ? includeServletPath : includePathInfo;
+        } 
+        // Seems that the servlet was not called as the result of a 
+        // RequestDispatcher.include(...). Try pathInfo then servletPath again,
+        // only now directly on the request object:
+        String path = request.getPathInfo();
+        if (path != null) return path;
+        path = request.getServletPath();
+        if (path != null) return path;
+        // Seems that it's a servlet mapped with prefix, and there was no extra path info.
+        return "";
+    }
+
+    /**
+     * Called as the first step in request processing, before the templating mechanism
+     * is put to work. By default does nothing and returns false. This method is
+     * typically overridden to manage serving of non-template resources (i.e. images)
+     * that reside in the template directory.
+     * @param request the HTTP request
+     * @param response the HTTP response
+     * @return true to indicate this method has processed the request entirely,
+     * and that the further request processing should not take place.
+     */
+    protected boolean preprocessRequest(
+        HttpServletRequest request,
+        HttpServletResponse response)
+            throws ServletException, IOException {
+        return false;
+    }
+
+    /**
+     * Creates the FreeMarker {@link Configuration} singleton and (when overidden) maybe sets its defaults. Servlet
+     * init-params will be applied later, and thus can overwrite the settings specified here.
+     * 
+     * <p>
+     * By overriding this method you can set your preferred {@link Configuration} setting defaults, as only the settings
+     * for which an init-param was specified will be overwritten later. (Note that {@link FreemarkerServlet} also has
+     * its own defaults for a few settings, but since 2.3.22, the servlet detects if those settings were already set
+     * here and then it won't overwrite them.)
+     * 
+     * <p>
+     * The default implementation simply creates a new instance with {@link Configuration#Configuration()} and returns
+     * it.
+     */
+    protected Configuration createConfiguration() {
+        // We can only set incompatible_improvements later, so ignore the deprecation warning here.
+        return new Configuration();
+    }
+    
+    /**
+     * Sets the defaults of the configuration that are specific to the {@link FreemarkerServlet} subclass.
+     * This is called after the common (wired in) {@link FreemarkerServlet} setting defaults was set, also the 
+     */
+    protected void setConfigurationDefaults() {
+        // do nothing
+    }
+    
+    /**
+     * Called from {@link #init()} to create the {@link ObjectWrapper}; to customzie this aspect, in most cases you
+     * should override {@link #createDefaultObjectWrapper()} instead. Overriding this method is necessary when you want
+     * to customize how the {@link ObjectWrapper} is created <em>from the init-param values</em>, or you want to do some
+     * post-processing (like checking) on the created {@link ObjectWrapper}. To customize init-param interpretation,
+     * call {@link #getInitParameter(String)} with {@link Configurable#OBJECT_WRAPPER_KEY} as argument, and see if it
+     * returns a value that you want to interpret yourself. If was {@code null} or you don't want to interpret the
+     * value, fall back to the super method.
+     * 
+     * <p>
+     * The default implementation interprets the {@code object_wrapper} servlet init-param with
+     * calling {@link Configurable#setSetting(String, String)} (see valid values there), or if there's no such servlet
+     * init-param, then it calls {@link #createDefaultObjectWrapper()}.
+     * 
+     * @return The {@link ObjectWrapper} that will be used for adapting request, session, and servlet context attributes
+     *         to {@link TemplateModel}-s, and also as the object wrapper setting of {@link Configuration}.
+     */
+    protected ObjectWrapper createObjectWrapper() {
+        String wrapper = getServletConfig().getInitParameter(DEPR_INITPARAM_OBJECT_WRAPPER);
+        if (wrapper != null) { // BC
+            if (getInitParameter(Configurable.OBJECT_WRAPPER_KEY) != null) {
+                throw new RuntimeException("Conflicting init-params: "
+                        + Configurable.OBJECT_WRAPPER_KEY + " and "
+                        + DEPR_INITPARAM_OBJECT_WRAPPER);
+            }
+            if (DEPR_INITPARAM_WRAPPER_BEANS.equals(wrapper)) {
+                return ObjectWrapper.BEANS_WRAPPER;
+            }
+            if (DEPR_INITPARAM_WRAPPER_SIMPLE.equals(wrapper)) {
+                return ObjectWrapper.SIMPLE_WRAPPER;
+            }
+            if (DEPR_INITPARAM_WRAPPER_JYTHON.equals(wrapper)) {
+                // Avoiding compile-time dependency on Jython package
+                try {
+                    return (ObjectWrapper) Class.forName("freemarker.ext.jython.JythonWrapper")
+                            .newInstance();
+                } catch (InstantiationException e) {
+                    throw new InstantiationError(e.getMessage());
+                } catch (IllegalAccessException e) {
+                    throw new IllegalAccessError(e.getMessage());
+                } catch (ClassNotFoundException e) {
+                    throw new NoClassDefFoundError(e.getMessage());
+                }
+            }
+            return createDefaultObjectWrapper();
+        } else {
+            wrapper = getInitParameter(Configurable.OBJECT_WRAPPER_KEY);
+            if (wrapper == null) {
+                if (!config.isObjectWrapperExplicitlySet()) {
+                    return createDefaultObjectWrapper();
+                } else {
+                    return config.getObjectWrapper();
+                }
+            } else {
+                try {
+                    config.setSetting(Configurable.OBJECT_WRAPPER_KEY, wrapper);
+                } catch (TemplateException e) {
+                    throw new RuntimeException("Failed to set " + Configurable.OBJECT_WRAPPER_KEY, e);
+                }
+                return config.getObjectWrapper();
+            }
+        }
+    }
+
+    /**
+     * Override this to specify what the default {@link ObjectWrapper} will be when the
+     * {@code object_wrapper} Servlet init-param wasn't specified. Note that this is called by
+     * {@link #createConfiguration()}, and so if that was also overidden but improperly then this method might won't be
+     * ever called. Also note that if you set the {@code object_wrapper} in {@link #createConfiguration()}, then this
+     * won't be called, since then that has already specified the default.
+     * 
+     * <p>
+     * The default implementation calls {@link Configuration#getDefaultObjectWrapper(freemarker.template.Version)}. You
+     * should also pass in the version paramter when creating an {@link ObjectWrapper} that supports that. You can get
+     * the version by calling {@link #getConfiguration()} and then {@link Configuration#getIncompatibleImprovements()}.
+     * 
+     * @since 2.3.22
+     */
+    protected ObjectWrapper createDefaultObjectWrapper() {
+        return Configuration.getDefaultObjectWrapper(config.getIncompatibleImprovements());
+    }
+    
+    /**
+     * Should be final; don't override it. Override {@link #createObjectWrapper()} instead.
+     */
+    // [2.4] Make it final
+    protected ObjectWrapper getObjectWrapper() {
+        return wrapper;
+    }
+    
+    /**
+     * The value of the {@code TemplatePath} init-param. {@code null} if the {@code template_loader} setting was set in
+     * a custom {@link #createConfiguration()}.
+     * 
+     * @deprecated Not called by FreeMarker code, and there's no point to override this (unless to cause confusion).
+     */
+    @Deprecated
+    protected final String getTemplatePath() {
+        return templatePath;
+    }
+    
+    protected HttpRequestParametersHashModel createRequestParametersHashModel(HttpServletRequest request) {
+        return new HttpRequestParametersHashModel(request);
+    }
+
+    /**
+     * Called when servlet detects in a request processing that
+     * application-global (that is, ServletContext-specific) attributes are not yet
+     * set.
+     * This is a generic hook you might use in subclasses to perform a specific
+     * action on first request in the context. By default it does nothing.
+     * @param request the actual HTTP request
+     * @param response the actual HTTP response
+     */
+    protected void initializeServletContext(
+        HttpServletRequest request,
+        HttpServletResponse response)
+            throws ServletException, IOException {
+    }
+
+    /**
+     * Called when servlet detects in a request processing that session-global 
+     * (that is, HttpSession-specific) attributes are not yet set.
+     * This is a generic hook you might use in subclasses to perform a specific
+     * action on first request in the session. By default it does nothing. It
+     * is only invoked on newly created sessions; it's not invoked when a
+     * replicated session is reinstantiated in another servlet container.
+     * 
+     * @param request the actual HTTP request
+     * @param response the actual HTTP response
+     */
+    protected void initializeSession(
+        HttpServletRequest request,
+        HttpServletResponse response)
+        throws ServletException, IOException {
+    }
+
+    /**
+     * Called before the execution is passed to {@link Template#process(Object, java.io.Writer)}. This is a
+     * generic hook you might use in subclasses to perform a specific action before the template is processed.
+     *
+     * @param request
+     *            The HTTP request that we will response to.
+     * @param response
+     *            The HTTP response. The HTTP headers are already initialized here, such as the {@code conteType} and
+     *            the {@code responseCharacterEncoding} are already set, but you can do the final adjustments here. The
+     *            response {@link Writer} isn't created yet, so changing HTTP headers and buffering parameters works.
+     * @param template
+     *            The template that will get executed
+     * @param model
+     *            The data model that will be passed to the template. By default this will be an
+     *            {@link AllHttpScopesHashModel} (which is a {@link freemarker.template.SimpleHash} subclass). Thus, you
+     *            can add new variables to the data-model with the
+     *            {@link freemarker.template.SimpleHash#put(String, Object)} subclass) method. However, to adjust the
+     *            data-model, overriding
+     *            {@link #createModel(ObjectWrapper, ServletContext, HttpServletRequest, HttpServletResponse)} is
+     *            probably a more appropriate place.
+     * 
+     * @return true to process the template, false to suppress template processing.
+     */
+    protected boolean preTemplateProcess(
+        HttpServletRequest request,
+        HttpServletResponse response,
+        Template template,
+        TemplateModel model)
+        throws ServletException, IOException {
+        return true;
+    }
+
+    /**
+     * Called after the execution returns from {@link Template#process(Object, java.io.Writer)}.
+     * This is a generic hook you might use in subclasses to perform a specific
+     * action after the template is processed. It will be invoked even if the
+     * template processing throws an exception. By default does nothing.
+     * @param request the actual HTTP request
+     * @param response the actual HTTP response
+     * @param template the template that was executed
+     * @param data the data that was passed to the template
+     */
+    protected void postTemplateProcess(
+        HttpServletRequest request,
+        HttpServletResponse response,
+        Template template,
+        TemplateModel data)
+        throws ServletException, IOException {
+    }
+    
+    /**
+     * Returns the {@link freemarker.template.Configuration} object used by this servlet.
+     * Please don't forget that {@link freemarker.template.Configuration} is not thread-safe
+     * when you modify it.
+     */
+    protected Configuration getConfiguration() {
+        return config;
+    }
+
+    /**
+     * Returns the default value of the {@value #INIT_PARAM_OVERRIDE_RESPONSE_CONTENT_TYPE} Servlet init-param.
+     * The method inherited from {@link FreemarkerServlet} returns {@value #INIT_PARAM_VALUE_ALWAYS}; subclasses my
+     * override this.
+     * 
+     * @since 2.3.24
+     */
+    protected String getDefaultOverrideResponseContentType() {
+        return INIT_PARAM_VALUE_ALWAYS;
+    }
+
+    /**
+     * If the parameter "nocache" was set to true, generate a set of headers
+     * that will advise the HTTP client not to cache the returned page.
+     */
+    private void setBrowserCachingPolicy(HttpServletResponse res) {
+        if (noCache) {
+            // HTTP/1.1 + IE extensions
+            res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, "
+                    + "post-check=0, pre-check=0");
+            // HTTP/1.0
+            res.setHeader("Pragma", "no-cache");
+            // Last resort for those that ignore all of the above
+            res.setHeader("Expires", EXPIRATION_DATE);
+        }
+    }
+    
+    private int parseSize(String value) throws ParseException {
+        int lastDigitIdx;
+        for (lastDigitIdx = value.length() - 1; lastDigitIdx >= 0; lastDigitIdx--) {
+            char c = value.charAt(lastDigitIdx);
+            if (c >= '0' && c <= '9') {
+                break;
+            }
+        }
+        
+        final int n = Integer.parseInt(value.substring(0, lastDigitIdx + 1).trim());
+        
+        final String unitStr = value.substring(lastDigitIdx + 1).trim().toUpperCase();
+        final int unit;
+        if (unitStr.length() == 0 || unitStr.equals("B")) {
+            unit = 1;
+        } else if (unitStr.equals("K") || unitStr.equals("KB") || unitStr.equals("KIB")) {
+            unit = 1024;
+        } else if (unitStr.equals("M") || unitStr.equals("MB") || unitStr.equals("MIB")) {
+            unit = 1024 * 1024;
+        } else {
+            throw new ParseException("Unknown unit: " + unitStr, lastDigitIdx + 1);
+        }
+        
+        long size = (long) n * unit;
+        if (size < 0) {
+            throw new IllegalArgumentException("Buffer size can't be negative");
+        }
+        if (size > Integer.MAX_VALUE) {
+            throw new IllegalArgumentException("Buffer size can't bigger than " + Integer.MAX_VALUE);
+        }
+        return (int) size;
+    }
+
+    private static class InitParamValueException extends Exception {
+        
+        InitParamValueException(String initParamName, String initParamValue, Throwable casue) {
+            super("Failed to set the " + StringUtil.jQuote(initParamName) + " servlet init-param to "
+                    + StringUtil.jQuote(initParamValue) + "; see cause exception.",
+                    casue);
+        }
+
+        public InitParamValueException(String initParamName, String initParamValue, String cause) {
+            super("Failed to set the " + StringUtil.jQuote(initParamName) + " servlet init-param to "
+                    + StringUtil.jQuote(initParamValue) + ": " + cause);
+        }
+        
+    }
+    
+    private static class ConflictingInitParamsException extends Exception {
+        
+        ConflictingInitParamsException(String recommendedName, String otherName) {
+            super("Conflicting servlet init-params: "
+                    + StringUtil.jQuote(recommendedName) + " and " + StringUtil.jQuote(otherName)
+                    + ". Only use " + StringUtil.jQuote(recommendedName) + ".");
+        }
+    }
+
+    private static class MalformedWebXmlException extends Exception {
+
+        MalformedWebXmlException(String message) {
+            super(message);
+        }
+        
+    }
+    
+    private static class ContentType {
+        private final String httpHeaderValue;
+        private final boolean containsCharset;
+        
+        public ContentType(String httpHeaderValue) {
+            this(httpHeaderValue, contentTypeContainsCharset(httpHeaderValue));
+        }
+
+        public ContentType(String httpHeaderValue, boolean containsCharset) {
+            this.httpHeaderValue = httpHeaderValue;
+            this.containsCharset = containsCharset;
+        }
+        
+        private static boolean contentTypeContainsCharset(String contentType) {
+            int charsetIdx = contentType.toLowerCase().indexOf("charset=");
+            if (charsetIdx != -1) {
+                char c = 0;
+                charsetIdx--;
+                while (charsetIdx >= 0) {
+                    c = contentType.charAt(charsetIdx);
+                    if (!Character.isWhitespace(c)) break;
+                    charsetIdx--;
+                }
+                if (charsetIdx == -1 || c == ';') {
+                    return true;
+                }
+            }
+            return false;
+        }
+        
+        /**
+         * Extracts the MIME type without the charset specifier or other such extras.
+         */
+        private String getMimeType() {
+            int scIdx = httpHeaderValue.indexOf(';');
+            return (scIdx == -1 ? httpHeaderValue : httpHeaderValue.substring(0, scIdx)).trim();
+        }
+        
+    }
+    
+    private <T extends InitParamValueEnum> T initParamValueToEnum(String initParamValue, T[] enumValues) {
+        for (T enumValue : enumValues) {
+            String enumInitParamValue = enumValue.getInitParamValue();
+            if (initParamValue.equals(enumInitParamValue)
+                    || enumInitParamValue.endsWith("}") && initParamValue.startsWith(
+                            enumInitParamValue.substring(0, enumInitParamValue.indexOf("${")))) {
+                return enumValue;
+            }
+        }
+        
+        StringBuilder sb = new StringBuilder();
+        sb.append(StringUtil.jQuote(initParamValue));
+        sb.append(" is not a one of the enumeration values: ");
+        boolean first = true;
+        for (T value : enumValues) {
+            if (!first) {
+                sb.append(", ");
+            } else {
+                first = false;
+            }
+            sb.append(StringUtil.jQuote(value.getInitParamValue()));
+        }
+        throw new IllegalArgumentException(sb.toString());
+    }
+
+    /**
+     * Superclass of all (future) init-param value enums.
+     * 
+     * @see #initParamValueToEnum
+     */
+    private interface InitParamValueEnum {
+        String getInitParamValue();
+    }
+    
+    private enum OverrideResponseContentType implements InitParamValueEnum {
+        ALWAYS(INIT_PARAM_VALUE_ALWAYS),
+        NEVER(INIT_PARAM_VALUE_NEVER),
+        WHEN_TEMPLATE_HAS_MIME_TYPE(INIT_PARAM_VALUE_WHEN_TEMPLATE_HAS_MIME_TYPE);
+
+        private final String initParamValue;
+        
+        OverrideResponseContentType(String initParamValue) {
+            this.initParamValue = initParamValue;
+        }
+
+        @Override
+        public String getInitParamValue() {
+            return initParamValue;
+        }
+    }
+    
+    private enum ResponseCharacterEncoding implements InitParamValueEnum {
+        LEGACY(INIT_PARAM_VALUE_LEGACY),
+        FROM_TEMPLATE(INIT_PARAM_VALUE_FROM_TEMPLATE),
+        DO_NOT_SET(INIT_PARAM_VALUE_DO_NOT_SET),
+        FORCE_CHARSET(INIT_PARAM_VALUE_FORCE_PREFIX + "${charsetName}");
+
+        private final String initParamValue;
+        
+        ResponseCharacterEncoding(String initParamValue) {
+            this.initParamValue = initParamValue;
+        }
+
+        @Override
+        public String getInitParamValue() {
+            return initParamValue;
+        }
+    }
+
+    private enum OverrideResponseLocale implements InitParamValueEnum {
+        ALWAYS(INIT_PARAM_VALUE_ALWAYS),
+        NEVER(INIT_PARAM_VALUE_NEVER);
+
+        private final String initParamValue;
+
+        OverrideResponseLocale(String initParamValue) {
+            this.initParamValue = initParamValue;
+        }
+
+        @Override
+        public String getInitParamValue() {
+            return initParamValue;
+        }
+    }
+
+}
diff --git a/src/main/java/no/nibio/freemarker/ext/servlet/HttpRequestHashModel.java b/src/main/java/no/nibio/freemarker/ext/servlet/HttpRequestHashModel.java
new file mode 100644
index 00000000..589de0b9
--- /dev/null
+++ b/src/main/java/no/nibio/freemarker/ext/servlet/HttpRequestHashModel.java
@@ -0,0 +1,111 @@
+/*
+ * 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 no.nibio.freemarker.ext.servlet;
+
+import java.util.ArrayList;
+import java.util.Enumeration;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import freemarker.template.ObjectWrapper;
+import freemarker.template.ObjectWrapperAndUnwrapper;
+import freemarker.template.SimpleCollection;
+import freemarker.template.TemplateCollectionModel;
+import freemarker.template.TemplateHashModelEx;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+
+/**
+ * TemplateHashModel wrapper for a HttpServletRequest attributes.
+ */
+public final class HttpRequestHashModel implements TemplateHashModelEx {
+    private final HttpServletRequest request;
+    private final HttpServletResponse response;
+    private final ObjectWrapper wrapper;
+
+    /**
+     * @param wrapper
+     *            Should be an {@link ObjectWrapperAndUnwrapper}, or else some features might won't work properly. (It's
+     *            declared as {@link ObjectWrapper} only for backward compatibility.)
+     */
+    public HttpRequestHashModel(
+        HttpServletRequest request, ObjectWrapper wrapper) {
+        this(request, null, wrapper);
+    }
+
+    public HttpRequestHashModel(
+        HttpServletRequest request, HttpServletResponse response, 
+        ObjectWrapper wrapper) {
+        this.request = request;
+        this.response = response;
+        this.wrapper = wrapper;
+    }
+    
+    @Override
+    public TemplateModel get(String key) throws TemplateModelException {
+        return wrapper.wrap(request.getAttribute(key));
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return !request.getAttributeNames().hasMoreElements();
+    }
+    
+    @Override
+    public int size() {
+        int result = 0;
+        for (Enumeration enumeration = request.getAttributeNames(); enumeration.hasMoreElements(); ) {
+            enumeration.nextElement();
+            ++result;
+        }
+        return result;
+    }
+    
+    @Override
+    public TemplateCollectionModel keys() {
+        ArrayList keys = new ArrayList();
+        for (Enumeration enumeration = request.getAttributeNames(); enumeration.hasMoreElements(); ) {
+            keys.add(enumeration.nextElement());
+        }
+        return new SimpleCollection(keys.iterator());
+    }
+    
+    @Override
+    public TemplateCollectionModel values() {
+        ArrayList values = new ArrayList();
+        for (Enumeration enumeration = request.getAttributeNames(); enumeration.hasMoreElements(); ) {
+            values.add(request.getAttribute((String) enumeration.nextElement()));
+        }
+        return new SimpleCollection(values.iterator(), wrapper);
+    }
+
+    public HttpServletRequest getRequest() {
+        return request;
+    }
+    
+    public HttpServletResponse getResponse() {
+        return response;
+    }
+    
+    public ObjectWrapper getObjectWrapper() {
+        return wrapper;
+    }
+}
diff --git a/src/main/java/no/nibio/freemarker/ext/servlet/HttpRequestParametersHashModel.java b/src/main/java/no/nibio/freemarker/ext/servlet/HttpRequestParametersHashModel.java
new file mode 100644
index 00000000..f98844ec
--- /dev/null
+++ b/src/main/java/no/nibio/freemarker/ext/servlet/HttpRequestParametersHashModel.java
@@ -0,0 +1,103 @@
+/*
+ * 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 no.nibio.freemarker.ext.servlet;
+
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+
+import freemarker.template.SimpleCollection;
+import freemarker.template.SimpleScalar;
+import freemarker.template.TemplateCollectionModel;
+import freemarker.template.TemplateHashModelEx;
+import freemarker.template.TemplateModel;
+
+/**
+ * TemplateHashModel wrapper for a HttpServletRequest parameters.
+ */
+
+public class HttpRequestParametersHashModel
+    implements
+    TemplateHashModelEx {
+    private final HttpServletRequest request;
+    private List keys;
+        
+    public HttpRequestParametersHashModel(HttpServletRequest request) {
+        this.request = request;
+    }
+
+    @Override
+    public TemplateModel get(String key) {
+        String value = request.getParameter(key);
+        return value == null ? null : new SimpleScalar(value);
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return !request.getParameterNames().hasMoreElements();
+    }
+    
+    @Override
+    public int size() {
+        return getKeys().size();
+    }
+    
+    @Override
+    public TemplateCollectionModel keys() {
+        return new SimpleCollection(getKeys().iterator());
+    }
+    
+    @Override
+    public TemplateCollectionModel values() {
+        final Iterator iter = getKeys().iterator();
+        return new SimpleCollection(
+            new Iterator() {
+                @Override
+                public boolean hasNext() {
+                    return iter.hasNext();
+                }
+                @Override
+                public Object next() {
+                    return request.getParameter((String) iter.next()); 
+                }
+                @Override
+                public void remove() {
+                    throw new UnsupportedOperationException();
+                }
+            });
+    }
+
+    protected String transcode(String string) {
+        return string;
+    }
+
+    private synchronized List getKeys() {
+        if (keys == null) {
+            keys = new ArrayList();
+            for (Enumeration enumeration = request.getParameterNames(); enumeration.hasMoreElements(); ) {
+                keys.add(enumeration.nextElement());
+            }
+        }
+        return keys;
+    }
+}
diff --git a/src/main/java/no/nibio/freemarker/ext/servlet/HttpSessionHashModel.java b/src/main/java/no/nibio/freemarker/ext/servlet/HttpSessionHashModel.java
new file mode 100644
index 00000000..da1492a1
--- /dev/null
+++ b/src/main/java/no/nibio/freemarker/ext/servlet/HttpSessionHashModel.java
@@ -0,0 +1,113 @@
+/*
+ * 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 no.nibio.freemarker.ext.servlet;
+
+import java.io.Serializable;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import freemarker.template.ObjectWrapper;
+import freemarker.template.TemplateHashModel;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+
+/**
+ * TemplateHashModel wrapper for a HttpSession attributes.
+ */
+
+public final class HttpSessionHashModel implements TemplateHashModel, Serializable {
+    private static final long serialVersionUID = 1L;
+    private transient HttpSession session;
+    private transient final ObjectWrapper wrapper;
+
+    // These are required for lazy initializing session
+    private transient final FreemarkerServlet servlet;
+    private transient final HttpServletRequest request;
+    private transient final HttpServletResponse response;
+    
+    /**
+     * Use this constructor when the session already exists.
+     * @param session the session
+     * @param wrapper an object wrapper used to wrap session attributes
+     */
+    public HttpSessionHashModel(HttpSession session, ObjectWrapper wrapper) {
+        this.session = session;
+        this.wrapper = wrapper;
+
+        this.servlet = null;
+        this.request = null;
+        this.response = null;
+    }
+
+    /**
+     * Use this constructor when the session isn't already created. It is passed
+     * enough parameters so that the session can be properly initialized after
+     * it's detected that it was created.
+     * @param servlet the FreemarkerServlet that created this model. If the
+     * model is not created through FreemarkerServlet, leave this argument as
+     * null.
+     * @param request the actual request
+     * @param response the actual response
+     * @param wrapper an object wrapper used to wrap session attributes
+     */
+    public HttpSessionHashModel(FreemarkerServlet servlet, HttpServletRequest request, HttpServletResponse response, ObjectWrapper wrapper) {
+        this.wrapper = wrapper;
+        
+        this.servlet = servlet;
+        this.request = request;
+        this.response = response;
+    }
+
+    @Override
+    public TemplateModel get(String key) throws TemplateModelException {
+        checkSessionExistence();
+        return wrapper.wrap(session != null ? session.getAttribute(key) : null);
+    }
+
+    private void checkSessionExistence() throws TemplateModelException {
+        if (session == null && request != null) {
+            session = request.getSession(false);
+            if (session != null && servlet != null) {
+                try {
+                    servlet.initializeSessionAndInstallModel(request, response, 
+                            this, session);
+                } catch (RuntimeException e) {
+                    throw e;
+                } catch (Exception e) {
+                    throw new TemplateModelException(e);
+                }
+            }
+        }
+    }
+
+    boolean isOrphaned(HttpSession currentSession) {
+        return (session != null && session != currentSession) || 
+            (session == null && request == null);
+    }
+    
+    @Override
+    public boolean isEmpty()
+    throws TemplateModelException {
+        checkSessionExistence();
+        return session == null || !session.getAttributeNames().hasMoreElements();
+    }
+}
diff --git a/src/main/java/no/nibio/freemarker/ext/servlet/IncludePage.java b/src/main/java/no/nibio/freemarker/ext/servlet/IncludePage.java
new file mode 100644
index 00000000..af5fa373
--- /dev/null
+++ b/src/main/java/no/nibio/freemarker/ext/servlet/IncludePage.java
@@ -0,0 +1,254 @@
+/*
+ * 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 no.nibio.freemarker.ext.servlet;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.lang.reflect.Array;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
+
+import freemarker.core.Environment;
+import freemarker.core._DelayedFTLTypeDescription;
+import freemarker.core._MiscTemplateException;
+import freemarker.template.TemplateBooleanModel;
+import freemarker.template.TemplateDirectiveBody;
+import freemarker.template.TemplateDirectiveModel;
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateScalarModel;
+import freemarker.template.utility.DeepUnwrap;
+
+
+/**
+ * A model that when invoked with a 'path' parameter will perform a servlet 
+ * include. It also support an optional hash named 'params' which specifies
+ * request parameters for the include - its keys are strings, its values
+ * should be either strings or sequences of strings (for multiple valued 
+ * parameters). A third optional parameter 'inherit_params' should be a boolean
+ * when specified, and it defaults to true when not specified. A value of true
+ * means that the include inherits the request parameters from the current 
+ * request. In this case values in 'params' will get prepended to the existing
+ * values of parameters.
+ */
+public class IncludePage implements TemplateDirectiveModel {
+    private final HttpServletRequest request;
+    private final HttpServletResponse response;
+    
+    public IncludePage(HttpServletRequest request, HttpServletResponse response) {
+        this.request = request;
+        this.response = response;
+    }
+    
+    @Override
+    public void execute(final Environment env, Map params,
+            TemplateModel[] loopVars, TemplateDirectiveBody body)
+    throws TemplateException, IOException {
+        // Determine the path
+        final TemplateModel path = (TemplateModel) params.get("path");
+        if (path == null) {
+            throw new _MiscTemplateException(env, "Missing required parameter \"path\"");
+        }
+        if (!(path instanceof TemplateScalarModel)) {
+            throw new _MiscTemplateException(env,
+                    "Expected a scalar model. \"path\" is instead ", new _DelayedFTLTypeDescription(path));
+        }
+        final String strPath = ((TemplateScalarModel) path).getAsString();
+        if (strPath == null) {
+            throw new _MiscTemplateException(env, "String value of \"path\" parameter is null");
+        }
+        
+        // See whether we need to use a custom response (if we're inside a TTM
+        // or TDM or macro nested body, we'll need to as then the current 
+        // FM environment writer is not identical to HTTP servlet response 
+        // writer. 
+        final Writer envOut = env.getOut(); 
+        final HttpServletResponse wrappedResponse;
+        if (envOut == response.getWriter()) {
+            // Don't bother wrapping if environment's writer is same as 
+            // response writer
+            wrappedResponse = response;
+        } else {
+            final PrintWriter printWriter = (envOut instanceof PrintWriter) ?
+                (PrintWriter) envOut :
+                new PrintWriter(envOut); 
+            // Otherwise, create a response wrapper that will pass the
+            // env writer, potentially first wrapping it in a print
+            // writer when it ain't one already.
+            wrappedResponse = new HttpServletResponseWrapper(response) {
+                @Override
+                public PrintWriter getWriter() {
+                    return printWriter;
+                }
+            };
+        }
+
+        // Determine inherit_params value
+        final boolean inheritParams;
+        final TemplateModel inheritParamsModel = (TemplateModel) params.get("inherit_params");
+        if (inheritParamsModel == null) {
+            // defaults to true when not specified
+            inheritParams = true; 
+        } else {
+            if (!(inheritParamsModel instanceof TemplateBooleanModel)) {
+                throw new _MiscTemplateException(env,
+                        "\"inherit_params\" should be a boolean but it's a(n) ",
+                        inheritParamsModel.getClass().getName(), " instead");
+            }
+            inheritParams = ((TemplateBooleanModel) inheritParamsModel).getAsBoolean();
+        }
+        
+        // Get explicit params, if any
+        final TemplateModel paramsModel = (TemplateModel) params.get("params");
+        
+        // Determine whether we need to wrap the request
+        final HttpServletRequest wrappedRequest;
+        if (paramsModel == null && inheritParams) {
+            // Inherit original request params & no params explicitly 
+            // specified, so use the original request
+            wrappedRequest = request;
+        } else {
+            // In any other case, use a custom request wrapper
+            final Map paramsMap;
+            if (paramsModel != null) {
+                // Convert params to a Map
+                final Object unwrapped = DeepUnwrap.unwrap(paramsModel);
+                if (!(unwrapped instanceof Map)) {
+                    throw new _MiscTemplateException(env,
+                            "Expected \"params\" to unwrap into a java.util.Map. It unwrapped into ",
+                            unwrapped.getClass().getName(), " instead.");
+                }
+                paramsMap = (Map) unwrapped;
+            } else {
+                paramsMap = Collections.EMPTY_MAP;
+            }
+            wrappedRequest = new CustomParamsRequest(request, paramsMap, 
+                    inheritParams);
+        }
+        
+        // Finally, do the include
+        try {
+            request.getRequestDispatcher(strPath).include(wrappedRequest, 
+                    wrappedResponse);
+        } catch (ServletException e) {
+            throw new _MiscTemplateException(e, env);
+        }
+    }
+
+    private static final class CustomParamsRequest extends HttpServletRequestWrapper {
+        private final HashMap paramsMap;
+
+        private CustomParamsRequest(HttpServletRequest request, Map paramMap, 
+                boolean inheritParams) {
+            super(request);
+            paramsMap = inheritParams ? new HashMap(request.getParameterMap()) : new HashMap();
+            for (Iterator it = paramMap.entrySet().iterator(); it.hasNext(); ) {
+                Map.Entry entry = (Map.Entry) it.next();
+                String name = String.valueOf(entry.getKey());
+                Object value = entry.getValue();
+                final String[] valueArray;
+                if (value == null) {
+                    // Null values are explicitly added (so, among other 
+                    // things, we can hide inherited param values).
+                    valueArray = new String[] { null };
+                } else if (value instanceof String[]) {
+                    // String[] arrays are just passed through
+                    valueArray = (String[]) value;
+                } else if (value instanceof Collection) {
+                    // Collections are converted to String[], with 
+                    // String.valueOf() used on elements
+                    Collection col = (Collection) value;
+                    valueArray = new String[col.size()];
+                    int i = 0;
+                    for (Iterator it2 = col.iterator(); it2.hasNext(); ) {
+                        valueArray[i++] = String.valueOf(it2.next());
+                    }
+                } else if (value.getClass().isArray()) {
+                    // Other array types are too converted to String[], with 
+                    // String.valueOf() used on elements
+                    int len = Array.getLength(value);
+                    valueArray = new String[len];
+                    for (int i = 0; i < len; ++i) {
+                        valueArray[i] = String.valueOf(Array.get(value, i));
+                    }
+                } else {
+                    // All other values (including strings) are converted to a
+                    // single-element String[], with String.valueOf applied to
+                    // the value.
+                    valueArray = new String[] { String.valueOf(value) };
+                }
+                String[] existingParams = (String[]) paramsMap.get(name);
+                int el = existingParams == null ? 0 : existingParams.length;
+                if (el == 0) {
+                    // No original params, just put our array
+                    paramsMap.put(name, valueArray);
+                } else {
+                    int vl = valueArray.length;
+                    if (vl > 0) {
+                        // Both original params and new params, prepend our
+                        // params to original params
+                        String[] newValueArray = new String[el + vl];
+                        System.arraycopy(valueArray, 0, newValueArray, 0, vl);
+                        System.arraycopy(existingParams, 0, newValueArray, vl, el);
+                        paramsMap.put(name, newValueArray);
+                    }
+                }
+            }
+        }
+
+        @Override
+        public String[] getParameterValues(String name) {
+            String[] value = ((String[]) paramsMap.get(name));
+            return value != null ? value.clone() : null;
+        }
+
+        @Override
+        public String getParameter(String name) {
+            String[] values = (String[]) paramsMap.get(name);
+            return values != null && values.length > 0 ? values[0] : null;
+        }
+
+        @Override
+        public Enumeration getParameterNames() {
+            return Collections.enumeration(paramsMap.keySet());
+        }
+
+        @Override
+        public Map getParameterMap() {
+            HashMap clone = (HashMap) paramsMap.clone();
+            for (Iterator it = clone.entrySet().iterator(); it.hasNext(); ) {
+                Map.Entry entry = (Map.Entry) it.next();
+                entry.setValue(((String[]) entry.getValue()).clone());
+            }
+            return Collections.unmodifiableMap(clone);
+        }
+    }
+}
diff --git a/src/main/java/no/nibio/freemarker/ext/servlet/InitParamParser.java b/src/main/java/no/nibio/freemarker/ext/servlet/InitParamParser.java
new file mode 100644
index 00000000..1d55a50d
--- /dev/null
+++ b/src/main/java/no/nibio/freemarker/ext/servlet/InitParamParser.java
@@ -0,0 +1,265 @@
+/*
+ * 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 no.nibio.freemarker.ext.servlet;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import javax.servlet.ServletContext;
+
+import freemarker.cache.ClassTemplateLoader;
+import freemarker.cache.FileTemplateLoader;
+import freemarker.cache.MultiTemplateLoader;
+import freemarker.cache.TemplateLoader;
+import freemarker.cache.WebappTemplateLoader;
+import freemarker.core._ObjectBuilderSettingEvaluator;
+import freemarker.core._SettingEvaluationEnvironment;
+import freemarker.log.Logger;
+import freemarker.template.Configuration;
+import freemarker.template.utility.StringUtil;
+
+
+final class InitParamParser {
+
+    static final String TEMPLATE_PATH_PREFIX_CLASS = "class://";
+    static final String TEMPLATE_PATH_PREFIX_CLASSPATH = "classpath:";
+    static final String TEMPLATE_PATH_PREFIX_FILE = "file://";
+    static final String TEMPLATE_PATH_SETTINGS_BI_NAME = "settings";
+    
+    private static final Logger LOG = Logger.getLogger("freemarker.servlet");
+    
+    private InitParamParser() {
+        // Not to be instantiated
+    }
+
+    static TemplateLoader createTemplateLoader(
+            String templatePath, Configuration cfg, Class classLoaderClass, ServletContext srvCtx)
+            throws IOException {
+        final int settingAssignmentsStart = findTemplatePathSettingAssignmentsStart(templatePath);
+        String pureTemplatePath = (settingAssignmentsStart == -1 ? templatePath : templatePath.substring(0, settingAssignmentsStart))
+                .trim();
+        
+        final TemplateLoader templateLoader;
+        if (pureTemplatePath.startsWith(TEMPLATE_PATH_PREFIX_CLASS)) {
+            String packagePath = pureTemplatePath.substring(TEMPLATE_PATH_PREFIX_CLASS.length());
+            packagePath = normalizeToAbsolutePackagePath(packagePath);
+            templateLoader = new ClassTemplateLoader(classLoaderClass, packagePath);
+        } else if (pureTemplatePath.startsWith(TEMPLATE_PATH_PREFIX_CLASSPATH)) {
+            // To be similar to Spring resource paths, we don't require "//":
+            String packagePath = pureTemplatePath.substring(TEMPLATE_PATH_PREFIX_CLASSPATH.length());
+            packagePath = normalizeToAbsolutePackagePath(packagePath);
+            
+            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
+            if (classLoader == null) {
+                LOG.warn("No Thread Context Class Loader was found. Falling back to the class loader of "
+                        + classLoaderClass.getName() + ".");
+                classLoader = classLoaderClass.getClassLoader();
+            }
+            
+            templateLoader = new ClassTemplateLoader(classLoader, packagePath);
+        } else if (pureTemplatePath.startsWith(TEMPLATE_PATH_PREFIX_FILE)) {
+            String filePath = pureTemplatePath.substring(TEMPLATE_PATH_PREFIX_FILE.length());
+            templateLoader = new FileTemplateLoader(new File(filePath));
+        } else if (pureTemplatePath.startsWith("[")
+                ) {
+            if (!pureTemplatePath.endsWith("]")) {
+                // B.C. constraint: Can't throw any checked exceptions.
+                throw new TemplatePathParsingException("Failed to parse template path; closing \"]\" is missing.");
+            }
+            String commaSepItems = pureTemplatePath.substring(1, pureTemplatePath.length() - 1).trim();
+            List listItems = parseCommaSeparatedTemplatePaths(commaSepItems);
+            TemplateLoader[] templateLoaders = new TemplateLoader[listItems.size()];
+            for (int i = 0; i < listItems.size(); i++) {
+                String pathItem = (String) listItems.get(i);
+                templateLoaders[i] = createTemplateLoader(pathItem, cfg, classLoaderClass, srvCtx);
+            }
+            templateLoader = new MultiTemplateLoader(templateLoaders);
+        } else if (pureTemplatePath.startsWith("{")
+                ) {
+            throw new TemplatePathParsingException("Template paths starting with \"{\" are reseved for future purposes");
+        } else {
+            templateLoader = new WebappTemplateLoader(srvCtx, pureTemplatePath);
+        }
+        
+        if (settingAssignmentsStart != -1) {
+            try {
+                int nextPos = _ObjectBuilderSettingEvaluator.configureBean(
+                        templatePath, templatePath.indexOf('(', settingAssignmentsStart) + 1, templateLoader,
+                        _SettingEvaluationEnvironment.getCurrent());
+                if (nextPos != templatePath.length()) {
+                    throw new TemplatePathParsingException("Template path should end after the setting list in: "
+                + templatePath);
+                }
+            } catch (Exception e) {
+                throw new TemplatePathParsingException("Failed to set properties in: " + templatePath, e);
+            }
+        }
+        
+        return templateLoader;
+    }
+
+    static String normalizeToAbsolutePackagePath(String path) {
+        while (path.startsWith("/")) {
+            path = path.substring(1);
+        }
+        return "/" + path;
+    }
+
+    static List/*<String>*/ parseCommaSeparatedList(String value) throws ParseException {
+        List/*<String>*/ valuesList = new ArrayList();
+        String[] values = StringUtil.split(value, ',');
+        for (int i = 0; i < values.length; i++) {
+            final String s = values[i].trim();
+            if (s.length() != 0) {
+                valuesList.add(s);
+            } else if (i != values.length - 1) {
+                throw new ParseException("Missing list item berfore a comma", -1);
+            }
+        }
+        return valuesList;
+    }
+
+    static List parseCommaSeparatedPatterns(String value) throws ParseException {
+        List/*<String>*/ values = parseCommaSeparatedList(value);
+        List/*<Pattern>*/ patterns = new ArrayList(values.size());
+        for (int i = 0; i < values.size(); i++) {
+            patterns.add(Pattern.compile((String) values.get(i)));
+        }
+        return patterns;
+    }
+    
+    /**
+     * This is like {@link #parseCommaSeparatedList(String)}, but is not confused by commas inside
+     * {@code ?settings(...)} parts at the end of the items.
+     */
+    static List parseCommaSeparatedTemplatePaths(String commaSepItems) {
+        List listItems;
+        listItems = new ArrayList();
+        while (commaSepItems.length() != 0) {
+            int itemSettingAssignmentsStart = findTemplatePathSettingAssignmentsStart(commaSepItems);
+            int pureItemEnd = itemSettingAssignmentsStart != -1 ? itemSettingAssignmentsStart : commaSepItems.length(); 
+            int prevComaIdx = commaSepItems.lastIndexOf(',', pureItemEnd - 1);
+            int itemStart = prevComaIdx != -1 ? prevComaIdx + 1 : 0;
+            final String item = commaSepItems.substring(itemStart).trim();
+            if (item.length() != 0) {
+                listItems.add(0, item);
+            } else if (listItems.size() > 0) {
+                throw new TemplatePathParsingException("Missing list item before a comma");
+            }
+            commaSepItems = prevComaIdx != -1 ? commaSepItems.substring(0, prevComaIdx).trim() : "";
+        }
+        return listItems;
+    }
+
+    /**
+     * @return -1 if there's no setting assignment.
+     */
+    static int findTemplatePathSettingAssignmentsStart(String s) {
+        int pos = s.length() - 1;
+        
+        // Skip WS
+        while (pos >= 0 && Character.isWhitespace(s.charAt(pos))) {
+            pos--;
+        }
+        
+        // Skip `)`
+        if (pos < 0 || s.charAt(pos) != ')') return -1;
+        pos--;
+        
+        // Skip `(...`
+        int parLevel = 1;
+        int mode = 0;
+        while (parLevel > 0) {
+            if (pos < 0) return -1;
+            char c = s.charAt(pos);
+            switch (mode) {
+            case 0:  // 0: outside string literal
+                switch (c) {
+                case '(': parLevel--; break;
+                case ')': parLevel++; break;
+                case '\'': mode = 1; break;
+                case '"': mode = 2; break;
+                }
+                break;
+            case 1:  // 1: inside '...'
+                if (c == '\'' && !(pos > 0 && s.charAt(pos - 1) == '\\')) {
+                    mode = 0;
+                }
+                break;
+            case 2:  // 2: inside "..."
+                if (c == '"' && !(pos > 0 && s.charAt(pos - 1) == '\\')) {
+                    mode = 0;
+                }
+                break;
+            }
+            pos--;
+        }
+
+        // Skip WS
+        while (pos >= 0 && Character.isWhitespace(s.charAt(pos))) {
+            pos--;
+        }
+        
+        int biNameEnd = pos + 1;
+        
+        // Skip name chars
+        while (pos >= 0 && Character.isJavaIdentifierPart(s.charAt(pos))) {
+            pos--;
+        }
+        
+        int biNameStart = pos + 1;
+        if (biNameStart == biNameEnd) {
+            return -1;
+        }
+        String biName = s.substring(biNameStart, biNameEnd);
+        
+        // Skip WS
+        while (pos >= 0 && Character.isWhitespace(s.charAt(pos))) {
+            pos--;
+        }
+        
+        // Skip `?`
+        if (pos < 0 || s.charAt(pos) != '?') return -1;
+        
+        if (!biName.equals(TEMPLATE_PATH_SETTINGS_BI_NAME)) {
+            throw new TemplatePathParsingException(
+                    StringUtil.jQuote(biName) + " is unexpected after the \"?\". "
+                    + "Expected \"" + TEMPLATE_PATH_SETTINGS_BI_NAME + "\".");
+        }
+        
+        return pos;
+    }
+    
+    private static final class TemplatePathParsingException extends RuntimeException {
+
+        public TemplatePathParsingException(String message, Throwable cause) {
+            super(message, cause);
+        }
+
+        public TemplatePathParsingException(String message) {
+            super(message);
+        }
+        
+    }
+    
+}
diff --git a/src/main/java/no/nibio/freemarker/ext/servlet/ServletContextHashModel.java b/src/main/java/no/nibio/freemarker/ext/servlet/ServletContextHashModel.java
new file mode 100644
index 00000000..57205a98
--- /dev/null
+++ b/src/main/java/no/nibio/freemarker/ext/servlet/ServletContextHashModel.java
@@ -0,0 +1,74 @@
+/*
+ * 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 no.nibio.freemarker.ext.servlet;
+
+import javax.servlet.GenericServlet;
+import javax.servlet.ServletContext;
+
+import freemarker.template.ObjectWrapper;
+import freemarker.template.TemplateHashModel;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+
+/**
+ * TemplateHashModel wrapper for a ServletContext attributes.
+ */
+public final class ServletContextHashModel implements TemplateHashModel {
+    private final GenericServlet servlet;
+    private final ServletContext servletctx;
+    private final ObjectWrapper wrapper;
+
+    public ServletContextHashModel(
+        GenericServlet servlet, ObjectWrapper wrapper) {
+        this.servlet = servlet;
+        this.servletctx = servlet.getServletContext();
+        this.wrapper = wrapper;
+    }
+    
+    /**
+     * @deprecated use 
+     * {@link #ServletContextHashModel(GenericServlet, ObjectWrapper)} instead.
+     */
+    @Deprecated
+    public ServletContextHashModel(
+        ServletContext servletctx, ObjectWrapper wrapper) {
+        this.servlet = null;
+        this.servletctx = servletctx;
+        this.wrapper = wrapper;
+    }
+
+    @Override
+    public TemplateModel get(String key) throws TemplateModelException {
+        return wrapper.wrap(servletctx.getAttribute(key));
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return !servletctx.getAttributeNames().hasMoreElements();
+    }
+    
+    /**
+     * Returns the underlying servlet. Can return null if this object was
+     * created using the deprecated constructor.
+     */
+    public GenericServlet getServlet() {
+        return servlet;
+    }
+}
diff --git a/src/main/java/no/nibio/freemarker/ext/servlet/package.html b/src/main/java/no/nibio/freemarker/ext/servlet/package.html
new file mode 100644
index 00000000..97902bb4
--- /dev/null
+++ b/src/main/java/no/nibio/freemarker/ext/servlet/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>
+<body>
+<p>Servlet for legacy "Model 2" frameworks that allows using FreeMarker
+templates instead of JSP as the MVC View
+(see <a href="https://freemarker.apache.org/docs/pgui_misc_servlet.html" target="_blank">in the Manual</a>).</p>
+</body>
+</html>
diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml
index 39d9ae91..5ad2b90d 100755
--- a/src/main/webapp/WEB-INF/web.xml
+++ b/src/main/webapp/WEB-INF/web.xml
@@ -245,7 +245,7 @@
     <!-- FreeMarker configuration -->
     <servlet>
         <servlet-name>freemarker</servlet-name>
-        <servlet-class>freemarker.ext.servlet.FreemarkerServlet</servlet-class>
+        <servlet-class>no.nibio.freemarker.ext.servlet.FreemarkerServlet</servlet-class>
         <!-- FreemarkerServlet settings: -->
         <init-param>
             <param-name>TemplatePath</param-name>
-- 
GitLab