You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@velocity.apache.org by cb...@apache.org on 2020/10/27 13:21:21 UTC

[velocity-engine] 01/01: Initial commit for spring-velocity-support

This is an automated email from the ASF dual-hosted git repository.

cbrisson pushed a commit to branch spring-velocity-support
in repository https://gitbox.apache.org/repos/asf/velocity-engine.git

commit 35e922975b4b48124ce2737905a215a5a6377025
Author: Claude Brisson <cl...@renegat.net>
AuthorDate: Tue Oct 27 14:19:49 2020 +0100

    Initial commit for spring-velocity-support
---
 README.md                                          |   3 +-
 pom.xml                                            |   1 +
 spring-velocity-support/pom.xml                    | 106 +++++++
 .../velocity/spring/SpringResourceLoader.java      | 140 +++++++++
 .../velocity/spring/VelocityEngineFactory.java     | 337 +++++++++++++++++++++
 .../velocity/spring/VelocityEngineFactoryBean.java |  79 +++++
 .../velocity/spring/VelocityEngineUtils.java       | 112 +++++++
 .../test/VelocityEngineFactoryBeanTests.java       |  75 +++++
 .../spring/test/VelocityEngineFactoryTests.java    |  70 +++++
 .../src/test/resources/simple.vm                   |   1 +
 .../src/test/resources/velocity.properties         |  19 ++
 11 files changed, 942 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 934b436..ccafa6b 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,7 @@ Here's a description of the top level directories:
     velocity-engine-core/       The Velocity Engine core module
     velocity-engine-examples/   Several simple examples
     velocity-engine-scripting/  JSR-223 implementation for Velocity scripting
+    spring-velocity-support     Velocity Engine factory bean for Spring framework
     src/                        Source for parent modules, mainly changelog
 
 ## REQUIREMENTS
@@ -19,7 +20,7 @@ Apache Velocity 2.2 will run with any Java runtime engine v1.8 or greater.
 
 Building from source requires Java development kit v1.8 or greater and Maven 3 (3.0.5+).
 
-At compile time, Maven should fetch all needed dependencies, which are:
+At compile time, Maven should fetch all engine needed dependencies, which are:
 
 * commons-lang v3.9
 * slf4j-api v1.7.30
diff --git a/pom.xml b/pom.xml
index 5e0065d..c1cc53a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -234,6 +234,7 @@
         <module>velocity-engine-examples</module>
         <module>velocity-engine-scripting</module>
         <module>velocity-custom-parser-example</module>
+        <module>spring-velocity-support</module>
     </modules>
 
     <!-- This project is an effort by many people. If you feel that your name
diff --git a/spring-velocity-support/pom.xml b/spring-velocity-support/pom.xml
new file mode 100644
index 0000000..f4701ff
--- /dev/null
+++ b/spring-velocity-support/pom.xml
@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.velocity</groupId>
+        <artifactId>velocity-engine-parent</artifactId>
+        <version>2.3-SNAPSHOT</version>
+    </parent>
+    <artifactId>spring-velocity-support</artifactId>
+    <name>Spring framework Velocity support</name>
+    <description>Velocity Engine factory bean for Spring framework</description>
+    <properties>
+        <springframework.version>5.2.6.RELEASE</springframework.version>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.velocity</groupId>
+            <artifactId>velocity-engine-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-core</artifactId>
+            <version>${springframework.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-beans</artifactId>
+            <version>${springframework.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-context</artifactId>
+            <version>${springframework.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>${junit.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <version>${slf4j.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-source-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>${surefire.plugin.version}</version>
+                <configuration>
+                    <systemProperties>
+                        <property>
+                            <name>test.resources.dir</name>
+                            <value>${project.build.testOutputDirectory}</value>
+                        </property>
+                    </systemProperties>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>integration-test</id>
+                        <phase>integration-test</phase>
+                        <goals>
+                            <goal>test</goal>
+                        </goals>
+                        <configuration>
+                            <skip>false</skip>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/spring-velocity-support/src/main/java/org/apache/velocity/spring/SpringResourceLoader.java b/spring-velocity-support/src/main/java/org/apache/velocity/spring/SpringResourceLoader.java
new file mode 100644
index 0000000..befe4c5
--- /dev/null
+++ b/spring-velocity-support/src/main/java/org/apache/velocity/spring/SpringResourceLoader.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2002-2012 the original author or authors.
+ *
+ * Licensed 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
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.velocity.spring;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.Arrays;
+
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.runtime.resource.Resource;
+import org.apache.velocity.runtime.resource.loader.ResourceLoader;
+
+import org.apache.velocity.util.ExtProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.util.StringUtils;
+
+/**
+ * Velocity ResourceLoader adapter that loads via a Spring ResourceLoader.
+ * Used by VelocityEngineFactory for any resource loader path that cannot
+ * be resolved to a {@code java.io.File}.
+ *
+ * <p>Note that this loader does not allow for modification detection:
+ * Use Velocity's default FileResourceLoader for {@code java.io.File}
+ * resources.
+ *
+ * <p>Expects "spring.resource.loader" and "spring.resource.loader.path"
+ * application attributes in the Velocity runtime: the former of type
+ * {@code org.springframework.core.io.ResourceLoader}, the latter a String.
+ *
+ * @author Juergen Hoeller
+ * @author Claude Brisson
+ * @since 2020-05-29
+ * @see VelocityEngineFactory#setResourceLoaderPath
+ * @see org.springframework.core.io.ResourceLoader
+ * @see org.apache.velocity.runtime.resource.loader.FileResourceLoader
+ */
+public class SpringResourceLoader extends ResourceLoader {
+
+	public static final String NAME = "spring";
+
+	public static final String SPRING_RESOURCE_LOADER_CLASS = "spring.resource.loader.class";
+
+	public static final String SPRING_RESOURCE_LOADER_CACHE = "spring.resource.loader.cache";
+
+	public static final String SPRING_RESOURCE_LOADER = "spring.resource.loader";
+
+	public static final String SPRING_RESOURCE_LOADER_PATH = "spring.resource.loader.path";
+
+
+	protected final Logger logger = LoggerFactory.getLogger(getClass());
+
+	private org.springframework.core.io.ResourceLoader resourceLoader;
+
+	private String[] resourceLoaderPaths;
+
+
+	@Override
+	public void init(ExtProperties configuration) {
+		this.resourceLoader = (org.springframework.core.io.ResourceLoader)
+				this.rsvc.getApplicationAttribute(SPRING_RESOURCE_LOADER);
+		String resourceLoaderPath = (String) this.rsvc.getApplicationAttribute(SPRING_RESOURCE_LOADER_PATH);
+		if (this.resourceLoader == null) {
+			throw new IllegalArgumentException(
+					"'resourceLoader' application attribute must be present for SpringResourceLoader");
+		}
+		if (resourceLoaderPath == null) {
+			throw new IllegalArgumentException(
+					"'resourceLoaderPath' application attribute must be present for SpringResourceLoader");
+		}
+		this.resourceLoaderPaths = StringUtils.commaDelimitedListToStringArray(resourceLoaderPath);
+		for (int i = 0; i < this.resourceLoaderPaths.length; i++) {
+			String path = this.resourceLoaderPaths[i];
+			if (!path.endsWith("/")) {
+				this.resourceLoaderPaths[i] = path + "/";
+			}
+		}
+		if (logger.isInfoEnabled()) {
+			logger.info("SpringResourceLoader for Velocity: using resource loader [" + this.resourceLoader +
+					"] and resource loader paths " + Arrays.asList(this.resourceLoaderPaths));
+		}
+	}
+
+	/**
+	 * Get the Reader that the Runtime will parse
+	 * to create a template.
+	 *
+	 * @param source resource name
+	 * @param encoding resource encoding
+	 * @return The reader for the requested resource.
+	 * @throws ResourceNotFoundException
+	 * @since 2.0
+	 */
+	@Override
+	public Reader getResourceReader(String source, String encoding) throws ResourceNotFoundException {
+		if (logger.isDebugEnabled()) {
+			logger.debug("Looking for Velocity resource with name [" + source + "]");
+		}
+		for (String resourceLoaderPath : this.resourceLoaderPaths) {
+			org.springframework.core.io.Resource resource =
+					this.resourceLoader.getResource(resourceLoaderPath + source);
+			try {
+				return new InputStreamReader(resource.getInputStream(), encoding);
+			}
+			catch (IOException ex) {
+				if (logger.isDebugEnabled()) {
+					logger.debug("Could not find Velocity resource: " + resource);
+				}
+			}
+		}
+		throw new ResourceNotFoundException(
+				"Could not find resource [" + source + "] in Spring resource loader path");
+	}
+
+	@Override
+	public boolean isSourceModified(Resource resource) {
+		return false;
+	}
+
+	@Override
+	public long getLastModified(Resource resource) {
+		return 0;
+	}
+
+}
diff --git a/spring-velocity-support/src/main/java/org/apache/velocity/spring/VelocityEngineFactory.java b/spring-velocity-support/src/main/java/org/apache/velocity/spring/VelocityEngineFactory.java
new file mode 100644
index 0000000..6b3ee25
--- /dev/null
+++ b/spring-velocity-support/src/main/java/org/apache/velocity/spring/VelocityEngineFactory.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright 2002-2013 the original author or authors.
+ *
+ * Licensed 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
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.velocity.spring;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.velocity.app.VelocityEngine;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.RuntimeConstants;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.io.DefaultResourceLoader;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.core.io.support.PropertiesLoaderUtils;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Factory that configures a VelocityEngine. Can be used standalone,
+ * but typically you will either use {@link VelocityEngineFactoryBean}
+ * for preparing a VelocityEngine as bean reference, or
+ * {@link org.springframework.web.servlet.view.velocity.VelocityConfigurer}
+ * for web views.
+ *
+ * <p>The optional "configLocation" property sets the location of the Velocity
+ * properties file, within the current application. Velocity properties can be
+ * overridden via "velocityProperties", or even completely specified locally,
+ * avoiding the need for an external properties file.
+ *
+ * <p>The "resourceLoaderPath" property can be used to specify the Velocity
+ * resource loader path via Spring's Resource abstraction, possibly relative
+ * to the Spring application context.
+ *
+ * <p>The simplest way to use this class is to specify a
+ * {@link #setResourceLoaderPath(String) "resourceLoaderPath"}; the
+ * VelocityEngine typically then does not need any further configuration.
+ *
+ * @author Juergen Hoeller
+ * @author Claude Brisson
+ * @since 2020-05-29
+ * @see #setConfigLocation
+ * @see #setVelocityProperties
+ * @see #setResourceLoaderPath
+ * @see #createVelocityEngine
+ * @see VelocityEngineFactoryBean
+ * @see org.springframework.web.servlet.view.velocity.VelocityConfigurer
+ * @see org.apache.velocity.app.VelocityEngine
+ */
+public class VelocityEngineFactory {
+
+	protected static final Logger logger = LoggerFactory.getLogger(VelocityEngineFactory.class);
+
+	private Resource configLocation;
+
+	private final Map<String, Object> velocityProperties = new HashMap<String, Object>();
+
+	private String resourceLoaderPath;
+
+	private ResourceLoader resourceLoader = new DefaultResourceLoader();
+
+	private boolean preferFileSystemAccess = true;
+
+	private boolean overrideLogging = true;
+
+
+	/**
+	 * Set the location of the Velocity config file.
+	 * Alternatively, you can specify all properties locally.
+	 * @see #setVelocityProperties
+	 * @see #setResourceLoaderPath
+	 */
+	public void setConfigLocation(Resource configLocation) {
+		this.configLocation = configLocation;
+	}
+
+	/**
+	 * Set Velocity properties, like "file.resource.loader.path".
+	 * Can be used to override values in a Velocity config file,
+	 * or to specify all necessary properties locally.
+	 * <p>Note that the Velocity resource loader path also be set to any
+	 * Spring resource location via the "resourceLoaderPath" property.
+	 * Setting it here is just necessary when using a non-file-based
+	 * resource loader.
+	 * @see #setVelocityPropertiesMap
+	 * @see #setConfigLocation
+	 * @see #setResourceLoaderPath
+	 */
+	public void setVelocityProperties(Properties velocityProperties) {
+		CollectionUtils.mergePropertiesIntoMap(velocityProperties, this.velocityProperties);
+	}
+
+	/**
+	 * Set Velocity properties as Map, to allow for non-String values
+	 * like "ds.resource.loader.instance".
+	 * @see #setVelocityProperties
+	 */
+	public void setVelocityPropertiesMap(Map<String, Object> velocityPropertiesMap) {
+		if (velocityPropertiesMap != null) {
+			this.velocityProperties.putAll(velocityPropertiesMap);
+		}
+	}
+
+	/**
+	 * Set the Velocity resource loader path via a Spring resource location.
+	 * Accepts multiple locations in Velocity's comma-separated path style.
+	 * <p>When populated via a String, standard URLs like "file:" and "classpath:"
+	 * pseudo URLs are supported, as understood by ResourceLoader. Allows for
+	 * relative paths when running in an ApplicationContext.
+	 * <p>Will define a path for the default Velocity resource loader with the name
+	 * "file". If the specified resource cannot be resolved to a {@code java.io.File},
+	 * a generic SpringResourceLoader will be used under the name "spring", without
+	 * modification detection.
+	 * <p>Note that resource caching will be enabled in any case. With the file
+	 * resource loader, the last-modified timestamp will be checked on access to
+	 * detect changes. With SpringResourceLoader, the resource will be cached
+	 * forever (for example for class path resources).
+	 * <p>To specify a modification check interval for files, use Velocity's
+	 * standard "file.resource.loader.modificationCheckInterval" property. By default,
+	 * the file timestamp is checked on every access (which is surprisingly fast).
+	 * Of course, this just applies when loading resources from the file system.
+	 * <p>To enforce the use of SpringResourceLoader, i.e. to not resolve a path
+	 * as file system resource in any case, turn off the "preferFileSystemAccess"
+	 * flag. See the latter's javadoc for details.
+	 * @see #setResourceLoader
+	 * @see #setVelocityProperties
+	 * @see #setPreferFileSystemAccess
+	 * @see SpringResourceLoader
+	 * @see org.apache.velocity.runtime.resource.loader.FileResourceLoader
+	 */
+	public void setResourceLoaderPath(String resourceLoaderPath) {
+		this.resourceLoaderPath = resourceLoaderPath;
+	}
+
+	/**
+	 * Set the Spring ResourceLoader to use for loading Velocity template files.
+	 * The default is DefaultResourceLoader. Will get overridden by the
+	 * ApplicationContext if running in a context.
+	 * @see org.springframework.core.io.DefaultResourceLoader
+	 * @see org.springframework.context.ApplicationContext
+	 */
+	public void setResourceLoader(ResourceLoader resourceLoader) {
+		this.resourceLoader = resourceLoader;
+	}
+
+	/**
+	 * Return the Spring ResourceLoader to use for loading Velocity template files.
+	 */
+	protected ResourceLoader getResourceLoader() {
+		return this.resourceLoader;
+	}
+
+	/**
+	 * Set whether to prefer file system access for template loading.
+	 * File system access enables hot detection of template changes.
+	 * <p>If this is enabled, VelocityEngineFactory will try to resolve the
+	 * specified "resourceLoaderPath" as file system resource (which will work
+	 * for expanded class path resources and ServletContext resources too).
+	 * <p>Default is "true". Turn this off to always load via SpringResourceLoader
+	 * (i.e. as stream, without hot detection of template changes), which might
+	 * be necessary if some of your templates reside in an expanded classes
+	 * directory while others reside in jar files.
+	 * @see #setResourceLoaderPath
+	 */
+	public void setPreferFileSystemAccess(boolean preferFileSystemAccess) {
+		this.preferFileSystemAccess = preferFileSystemAccess;
+	}
+
+	/**
+	 * Return whether to prefer file system access for template loading.
+	 */
+	protected boolean isPreferFileSystemAccess() {
+		return this.preferFileSystemAccess;
+	}
+
+	/**
+	 * Prepare the VelocityEngine instance and return it.
+	 * @return the VelocityEngine instance
+	 * @throws IOException if the config file wasn't found
+	 * @throws VelocityException on Velocity initialization failure
+	 */
+	public VelocityEngine createVelocityEngine() throws IOException, VelocityException {
+		VelocityEngine velocityEngine = newVelocityEngine();
+		Map<String, Object> props = new HashMap<String, Object>();
+
+		// Load config file if set.
+		if (this.configLocation != null) {
+			if (logger.isInfoEnabled()) {
+				logger.info("Loading Velocity config from [" + this.configLocation + "]");
+			}
+			CollectionUtils.mergePropertiesIntoMap(PropertiesLoaderUtils.loadProperties(this.configLocation), props);
+		}
+
+		// Merge local properties if set.
+		if (!this.velocityProperties.isEmpty()) {
+			props.putAll(this.velocityProperties);
+		}
+
+		// Set a resource loader path, if required.
+		if (this.resourceLoaderPath != null) {
+			initVelocityResourceLoader(velocityEngine, this.resourceLoaderPath);
+		}
+
+		// Apply properties to VelocityEngine.
+		for (Map.Entry<String, Object> entry : props.entrySet()) {
+			velocityEngine.setProperty(entry.getKey(), entry.getValue());
+		}
+
+		postProcessVelocityEngine(velocityEngine);
+
+		// Perform actual initialization.
+		velocityEngine.init();
+
+		return velocityEngine;
+	}
+
+	/**
+	 * Return a new VelocityEngine. Subclasses can override this for
+	 * custom initialization, or for using a mock object for testing.
+	 * <p>Called by {@code createVelocityEngine()}.
+	 * @return the VelocityEngine instance
+	 * @throws IOException if a config file wasn't found
+	 * @throws VelocityException on Velocity initialization failure
+	 * @see #createVelocityEngine()
+	 */
+	protected VelocityEngine newVelocityEngine() throws IOException, VelocityException {
+		return new VelocityEngine();
+	}
+
+	/**
+	 * Initialize a Velocity resource loader for the given VelocityEngine:
+	 * either a standard Velocity FileResourceLoader or a SpringResourceLoader.
+	 * <p>Called by {@code createVelocityEngine()}.
+	 * @param velocityEngine the VelocityEngine to configure
+	 * @param resourceLoaderPath the path to load Velocity resources from
+	 * @see org.apache.velocity.runtime.resource.loader.FileResourceLoader
+	 * @see SpringResourceLoader
+	 * @see #initSpringResourceLoader
+	 * @see #createVelocityEngine()
+	 */
+	protected void initVelocityResourceLoader(VelocityEngine velocityEngine, String resourceLoaderPath) {
+		if (isPreferFileSystemAccess()) {
+			// Try to load via the file system, fall back to SpringResourceLoader
+			// (for hot detection of template changes, if possible).
+			try {
+				StringBuilder resolvedPath = new StringBuilder();
+				String[] paths = StringUtils.commaDelimitedListToStringArray(resourceLoaderPath);
+				for (int i = 0; i < paths.length; i++) {
+					String path = paths[i];
+					Resource resource = getResourceLoader().getResource(path);
+					File file = resource.getFile();  // will fail if not resolvable in the file system
+					if (logger.isDebugEnabled()) {
+						logger.debug("Resource loader path [" + path + "] resolved to file [" + file.getAbsolutePath() + "]");
+					}
+					resolvedPath.append(file.getAbsolutePath());
+					if (i < paths.length - 1) {
+						resolvedPath.append(',');
+					}
+				}
+				velocityEngine.setProperty(RuntimeConstants.RESOURCE_LOADER, "file");
+				velocityEngine.setProperty(RuntimeConstants.FILE_RESOURCE_LOADER_CACHE, "true");
+				velocityEngine.setProperty(RuntimeConstants.FILE_RESOURCE_LOADER_PATH, resolvedPath.toString());
+			}
+			catch (IOException ex) {
+				if (logger.isDebugEnabled()) {
+					logger.debug("Cannot resolve resource loader path [" + resourceLoaderPath +
+							"] to [java.io.File]: using SpringResourceLoader", ex);
+				}
+				initSpringResourceLoader(velocityEngine, resourceLoaderPath);
+			}
+		}
+		else {
+			// Always load via SpringResourceLoader
+			// (without hot detection of template changes).
+			if (logger.isDebugEnabled()) {
+				logger.debug("File system access not preferred: using SpringResourceLoader");
+			}
+			initSpringResourceLoader(velocityEngine, resourceLoaderPath);
+		}
+	}
+
+	/**
+	 * Initialize a SpringResourceLoader for the given VelocityEngine.
+	 * <p>Called by {@code initVelocityResourceLoader}.
+	 * @param velocityEngine the VelocityEngine to configure
+	 * @param resourceLoaderPath the path to load Velocity resources from
+	 * @see SpringResourceLoader
+	 * @see #initVelocityResourceLoader
+	 */
+	protected void initSpringResourceLoader(VelocityEngine velocityEngine, String resourceLoaderPath) {
+		velocityEngine.setProperty(
+				RuntimeConstants.RESOURCE_LOADER, SpringResourceLoader.NAME);
+		velocityEngine.setProperty(
+				SpringResourceLoader.SPRING_RESOURCE_LOADER_CLASS, SpringResourceLoader.class.getName());
+		velocityEngine.setProperty(
+				SpringResourceLoader.SPRING_RESOURCE_LOADER_CACHE, "true");
+		velocityEngine.setApplicationAttribute(
+				SpringResourceLoader.SPRING_RESOURCE_LOADER, getResourceLoader());
+		velocityEngine.setApplicationAttribute(
+				SpringResourceLoader.SPRING_RESOURCE_LOADER_PATH, resourceLoaderPath);
+	}
+
+	/**
+	 * To be implemented by subclasses that want to perform custom
+	 * post-processing of the VelocityEngine after this FactoryBean
+	 * performed its default configuration (but before VelocityEngine.init).
+	 * <p>Called by {@code createVelocityEngine()}.
+	 * @param velocityEngine the current VelocityEngine
+	 * @throws IOException if a config file wasn't found
+	 * @throws VelocityException on Velocity initialization failure
+	 * @see #createVelocityEngine()
+	 * @see org.apache.velocity.app.VelocityEngine#init
+	 */
+	protected void postProcessVelocityEngine(VelocityEngine velocityEngine)
+			throws IOException, VelocityException {
+	}
+
+}
diff --git a/spring-velocity-support/src/main/java/org/apache/velocity/spring/VelocityEngineFactoryBean.java b/spring-velocity-support/src/main/java/org/apache/velocity/spring/VelocityEngineFactoryBean.java
new file mode 100644
index 0000000..7705aab
--- /dev/null
+++ b/spring-velocity-support/src/main/java/org/apache/velocity/spring/VelocityEngineFactoryBean.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2002-2012 the original author or authors.
+ *
+ * Licensed 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
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.velocity.spring;
+
+import java.io.IOException;
+
+import org.apache.velocity.app.VelocityEngine;
+import org.apache.velocity.exception.VelocityException;
+
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.context.ResourceLoaderAware;
+
+/**
+ * Factory bean that configures a VelocityEngine and provides it as bean
+ * reference. This bean is intended for any kind of usage of Velocity in
+ * application code, e.g. for generating email content. For web views,
+ * VelocityConfigurer is used to set up a VelocityEngine for views.
+ *
+ * <p>The simplest way to use this class is to specify a "resourceLoaderPath";
+ * you do not need any further configuration then. For example, in a web
+ * application context:
+ *
+ * <pre class="code"> &lt;bean id="velocityEngine" class="org.springframework.ui.velocity.VelocityEngineFactoryBean"&gt;
+ *   &lt;property name="resourceLoaderPath" value="/WEB-INF/velocity/"/&gt;
+ * &lt;/bean&gt;</pre>
+ *
+ * See the base class VelocityEngineFactory for configuration details.
+ *
+ * @author Juergen Hoeller
+ * @author Claude Brisson
+ * @since 2020-05-29
+ * @see #setConfigLocation
+ * @see #setVelocityProperties
+ * @see #setResourceLoaderPath
+ * @see org.springframework.web.servlet.view.velocity.VelocityConfigurer
+ */
+public class VelocityEngineFactoryBean extends VelocityEngineFactory
+		implements FactoryBean<VelocityEngine>, InitializingBean, ResourceLoaderAware {
+
+	private VelocityEngine velocityEngine;
+
+
+	@Override
+	public void afterPropertiesSet() throws IOException, VelocityException {
+		this.velocityEngine = createVelocityEngine();
+	}
+
+
+	@Override
+	public VelocityEngine getObject() {
+		return this.velocityEngine;
+	}
+
+	@Override
+	public Class<? extends VelocityEngine> getObjectType() {
+		return VelocityEngine.class;
+	}
+
+	@Override
+	public boolean isSingleton() {
+		return true;
+	}
+
+}
diff --git a/spring-velocity-support/src/main/java/org/apache/velocity/spring/VelocityEngineUtils.java b/spring-velocity-support/src/main/java/org/apache/velocity/spring/VelocityEngineUtils.java
new file mode 100644
index 0000000..7e0be72
--- /dev/null
+++ b/spring-velocity-support/src/main/java/org/apache/velocity/spring/VelocityEngineUtils.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2002-2013 the original author or authors.
+ *
+ * Licensed 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
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.velocity.spring;
+
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.Map;
+
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.VelocityEngine;
+import org.apache.velocity.exception.VelocityException;
+
+/**
+ * Utility class for working with a VelocityEngine.
+ * Provides convenience methods to merge a Velocity template with a model.
+ *
+ * @author Juergen Hoeller
+ * @author Claude Brisson
+ * @since 2020-05-29
+ */
+public abstract class VelocityEngineUtils {
+
+	/**
+	 * Merge the specified Velocity template with the given model and write
+	 * the result to the given Writer.
+	 * @param velocityEngine VelocityEngine to work with
+	 * @param templateLocation the location of template, relative to Velocity's resource loader path
+	 * @param model the Map that contains model names as keys and model objects as values
+	 * @param writer the Writer to write the result to
+	 * @throws VelocityException if the template wasn't found or rendering failed
+	 * @deprecated Use {@link #mergeTemplate(VelocityEngine, String, String, Map, Writer)}
+	 * instead, following Velocity 1.6's corresponding deprecation in its own API.
+	 */
+	@Deprecated
+	public static void mergeTemplate(
+			VelocityEngine velocityEngine, String templateLocation, Map<String, Object> model, Writer writer)
+			throws VelocityException {
+		mergeTemplate(velocityEngine, templateLocation, null, writer);
+	}
+
+	/**
+	 * Merge the specified Velocity template with the given model and write the result
+	 * to the given Writer.
+	 * @param velocityEngine VelocityEngine to work with
+	 * @param templateLocation the location of template, relative to Velocity's resource loader path
+	 * @param encoding the encoding of the template file
+	 * @param model the Map that contains model names as keys and model objects as values
+	 * @param writer the Writer to write the result to
+	 * @throws VelocityException if the template wasn't found or rendering failed
+	 */
+	public static void mergeTemplate(
+			VelocityEngine velocityEngine, String templateLocation, String encoding,
+			Map<String, Object> model, Writer writer) throws VelocityException {
+
+		VelocityContext velocityContext = new VelocityContext(model);
+		velocityEngine.mergeTemplate(templateLocation, encoding, velocityContext, writer);
+	}
+
+	/**
+	 * Merge the specified Velocity template with the given model into a String.
+	 * <p>When using this method to prepare a text for a mail to be sent with Spring's
+	 * mail support, consider wrapping VelocityException in MailPreparationException.
+	 * @param velocityEngine VelocityEngine to work with
+	 * @param templateLocation the location of template, relative to Velocity's resource loader path
+	 * @param model the Map that contains model names as keys and model objects as values
+	 * @return the result as String
+	 * @throws VelocityException if the template wasn't found or rendering failed
+	 * @see org.springframework.mail.MailPreparationException
+	 * @deprecated Use {@link #mergeTemplateIntoString(VelocityEngine, String, String, Map)}
+	 * instead, following Velocity 1.6's corresponding deprecation in its own API.
+	 */
+	@Deprecated
+	public static String mergeTemplateIntoString(VelocityEngine velocityEngine, String templateLocation,
+			Map<String, Object> model) throws VelocityException {
+		return mergeTemplateIntoString(velocityEngine, templateLocation, null, model);
+	}
+
+	/**
+	 * Merge the specified Velocity template with the given model into a String.
+	 * <p>When using this method to prepare a text for a mail to be sent with Spring's
+	 * mail support, consider wrapping VelocityException in MailPreparationException.
+	 * @param velocityEngine VelocityEngine to work with
+	 * @param templateLocation the location of template, relative to Velocity's resource loader path
+	 * @param encoding the encoding of the template file
+	 * @param model the Map that contains model names as keys and model objects as values
+	 * @return the result as String
+	 * @throws VelocityException if the template wasn't found or rendering failed
+	 * @see org.springframework.mail.MailPreparationException
+	 */
+	public static String mergeTemplateIntoString(VelocityEngine velocityEngine, String templateLocation,
+			String encoding, Map<String, Object> model) throws VelocityException {
+
+		StringWriter result = new StringWriter();
+		mergeTemplate(velocityEngine, templateLocation, encoding, model, result);
+		return result.toString();
+	}
+
+}
diff --git a/spring-velocity-support/src/test/java/org/apache/velocity/spring/test/VelocityEngineFactoryBeanTests.java b/spring-velocity-support/src/test/java/org/apache/velocity/spring/test/VelocityEngineFactoryBeanTests.java
new file mode 100644
index 0000000..816af21
--- /dev/null
+++ b/spring-velocity-support/src/test/java/org/apache/velocity/spring/test/VelocityEngineFactoryBeanTests.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2002-2019 the original author or authors.
+ *
+ * Licensed 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
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.velocity.spring.test;
+
+import org.apache.velocity.app.VelocityEngine;
+import org.apache.velocity.spring.SpringResourceLoader;
+import org.apache.velocity.spring.VelocityEngineFactoryBean;
+import org.apache.velocity.spring.VelocityEngineUtils;
+import org.junit.Test;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.beans.factory.support.RootBeanDefinition;
+import org.springframework.core.io.*;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author Juergen Hoeller
+ * @author Issam El-atif
+ * @author Sam Brannen
+ */
+public class VelocityEngineFactoryBeanTests
+{
+	private static final String resourcesPath = System.getProperty("test.resources.dir");
+	private final VelocityEngineFactoryBean vefb = new VelocityEngineFactoryBean();
+
+	@Test
+	public void velocityFactoryBeanWithConfigLocation() throws Exception {
+		vefb.setConfigLocation(new ClassPathResource("velocity.properties"));
+		vefb.afterPropertiesSet();
+		VelocityEngine engine = vefb.getObject();
+		assertEquals("bean config location failed", "bar", engine.getProperty("foo"));
+	}
+
+	@Test
+	public void velocityFactoryBeanWithResourceLoaderPath() throws Exception {
+		vefb.setResourceLoaderPath("file:" + resourcesPath);
+		vefb.afterPropertiesSet();
+		VelocityEngine engine = vefb.getObject();
+		Map<String, Object> model = new HashMap<String, Object>();
+		model.put("foo", "bar");
+		String merged = VelocityEngineUtils.mergeTemplateIntoString(engine, "simple.vm", "utf-8", model).trim();
+		assertEquals("resource loader failed", "foo=bar", merged);
+	}
+
+	@Test  // SPR-12448
+	public void velocityConfigurationAsBean() {
+		DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
+		RootBeanDefinition loaderDef = new RootBeanDefinition(SpringResourceLoader.class);
+		loaderDef.getConstructorArgumentValues().addGenericArgumentValue(new DefaultResourceLoader());
+		loaderDef.getConstructorArgumentValues().addGenericArgumentValue("/freemarker");
+		// RootBeanDefinition configDef = new RootBeanDefinition(Configuration.class);
+		//configDef.getPropertyValues().add("templateLoader", loaderDef);
+		//beanFactory.registerBeanDefinition("freeMarkerConfig", configDef);
+		// assertThat(beanFactory.getBean(Configuration.class)).isNotNull();
+	}
+
+}
diff --git a/spring-velocity-support/src/test/java/org/apache/velocity/spring/test/VelocityEngineFactoryTests.java b/spring-velocity-support/src/test/java/org/apache/velocity/spring/test/VelocityEngineFactoryTests.java
new file mode 100644
index 0000000..68b440b
--- /dev/null
+++ b/spring-velocity-support/src/test/java/org/apache/velocity/spring/test/VelocityEngineFactoryTests.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2002-2019 the original author or authors.
+ *
+ * Licensed 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
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.velocity.spring.test;
+
+import org.apache.velocity.app.VelocityEngine;
+import org.apache.velocity.spring.VelocityEngineFactory;
+import org.apache.velocity.spring.VelocityEngineUtils;
+import org.junit.Test;
+import org.springframework.core.io.ClassPathResource;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author Juergen Hoeller
+ * @author Issam El-atif
+ * @author Sam Brannen
+ */
+public class VelocityEngineFactoryTests {
+
+	private static final String resourcesPath = System.getProperty("test.resources.dir");
+
+	@Test
+	public void testCreateEngineDefaultFileLoader() throws Exception {
+		VelocityEngineFactory factory = new VelocityEngineFactory();
+		factory.setResourceLoaderPath("."); // defaults to target/test-classes file resource loading
+		VelocityEngine engine = factory.createVelocityEngine();
+		Map<String, Object> model = new HashMap<String, Object>();
+		model.put("foo", "bar");
+		String merged = VelocityEngineUtils.mergeTemplateIntoString(engine, "simple.vm", "utf-8", model).trim();
+		assertEquals("file loader failed", "foo=bar", merged);
+	}
+
+	@Test
+	public void testCreateEngineDefaultClasspathLoader() throws Exception {
+		VelocityEngineFactory factory = new VelocityEngineFactory();
+		factory.setResourceLoaderPath("/"); // defaults to classpath resource loading
+		VelocityEngine engine = factory.createVelocityEngine();
+		Map<String, Object> model = new HashMap<String, Object>();
+		model.put("foo", "bar");
+		String merged = VelocityEngineUtils.mergeTemplateIntoString(engine, "simple.vm", "utf-8", model).trim();
+		assertEquals("classpath loader failed", "foo=bar", merged);
+	}
+
+	@Test
+	public void testCreateEngineCustomConfig() throws Exception {
+		VelocityEngineFactory factory = new VelocityEngineFactory();
+		factory.setResourceLoaderPath(".");
+		factory.setConfigLocation(new ClassPathResource("/velocity.properties"));
+		VelocityEngine engine = factory.createVelocityEngine();
+		assertEquals("custom config failed", "bar", engine.getProperty("foo"));
+	}
+
+}
diff --git a/spring-velocity-support/src/test/resources/simple.vm b/spring-velocity-support/src/test/resources/simple.vm
new file mode 100644
index 0000000..c9f453e
--- /dev/null
+++ b/spring-velocity-support/src/test/resources/simple.vm
@@ -0,0 +1 @@
+foo=$foo
diff --git a/spring-velocity-support/src/test/resources/velocity.properties b/spring-velocity-support/src/test/resources/velocity.properties
new file mode 100644
index 0000000..8e09e19
--- /dev/null
+++ b/spring-velocity-support/src/test/resources/velocity.properties
@@ -0,0 +1,19 @@
+# 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.
+
+foo = bar
+