aboutsummaryrefslogtreecommitdiff
path: root/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource
diff options
context:
space:
mode:
Diffstat (limited to 'velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource')
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ContentResource.java105
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/Resource.java320
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceCache.java81
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceCacheImpl.java168
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceFactory.java59
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceManager.java81
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceManagerImpl.java607
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/ClasspathResourceLoader.java169
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/DataSourceResourceLoader.java550
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/FileResourceLoader.java373
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/JarHolder.java169
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/JarResourceLoader.java263
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/ResourceLoader.java324
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/ResourceLoaderFactory.java62
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/StringResourceLoader.java438
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/URLResourceLoader.java214
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/util/StringResource.java108
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/util/StringResourceRepository.java76
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/util/StringResourceRepositoryImpl.java104
19 files changed, 4271 insertions, 0 deletions
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ContentResource.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ContentResource.java
new file mode 100644
index 00000000..67974d1e
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ContentResource.java
@@ -0,0 +1,105 @@
+package org.apache.velocity.runtime.resource;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.util.StringBuilderWriter;
+
+import java.io.BufferedReader;
+import java.io.Writer;
+
+/**
+ * This class represent a general text resource that may have been
+ * retrieved from any number of possible sources.
+ *
+ * Also of interest is Velocity's {@link org.apache.velocity.Template}
+ * <code>Resource</code>.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public class ContentResource extends Resource
+{
+ /** Default empty constructor */
+ public ContentResource()
+ {
+ super();
+
+ setType(ResourceManager.RESOURCE_CONTENT);
+ }
+
+ /**
+ * Pull in static content and store it.
+ * @return True if everything went ok.
+ *
+ * @exception ResourceNotFoundException Resource could not be
+ * found.
+ */
+ @Override
+ public boolean process()
+ throws ResourceNotFoundException
+ {
+ BufferedReader reader = null;
+
+ try
+ {
+ Writer sw = new StringBuilderWriter();
+
+ reader = new BufferedReader(resourceLoader.getResourceReader(name, encoding));
+
+ char buf[] = new char[1024];
+ int len = 0;
+
+ while ( ( len = reader.read( buf, 0, 1024 )) != -1)
+ sw.write( buf, 0, len );
+
+ setData(sw.toString());
+
+ return true;
+ }
+ catch ( ResourceNotFoundException e )
+ {
+ // Tell the ContentManager to continue to look through any
+ // remaining configured ResourceLoaders.
+ throw e;
+ }
+ catch ( Exception e )
+ {
+ String msg = "Cannot process content resource";
+ log.error(msg, e);
+ throw new VelocityException(msg, e, rsvc.getLogContext().getStackTrace());
+ }
+ finally
+ {
+ if (reader != null)
+ {
+ try
+ {
+ reader.close();
+ }
+ catch (Exception ignored)
+ {
+ }
+ }
+ }
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/Resource.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/Resource.java
new file mode 100644
index 00000000..5922604d
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/Resource.java
@@ -0,0 +1,320 @@
+package org.apache.velocity.runtime.resource;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.resource.loader.ResourceLoader;
+import org.slf4j.Logger;
+
+/**
+ * This class represent a general text resource that
+ * may have been retrieved from any number of possible
+ * sources.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public abstract class Resource implements Cloneable
+{
+ protected RuntimeServices rsvc = null;
+ protected Logger log = null;
+
+ /**
+ * The template loader that initially loaded the input
+ * stream for this template, and knows how to check the
+ * source of the input stream for modification.
+ */
+ protected ResourceLoader resourceLoader;
+
+ /**
+ * The number of milliseconds in a minute, used to calculate the
+ * check interval.
+ */
+ protected static final long MILLIS_PER_SECOND = 1000;
+
+ /**
+ * How often the file modification time is checked (in seconds).
+ */
+ protected long modificationCheckInterval = 0;
+
+ /**
+ * The file modification time (in milliseconds) for the cached template.
+ */
+ protected long lastModified = 0;
+
+ /**
+ * The next time the file modification time will be checked (in
+ * milliseconds).
+ */
+ protected long nextCheck = 0;
+
+ /**
+ * Name of the resource
+ */
+ protected String name;
+
+ /**
+ * Character encoding of this resource
+ */
+ protected String encoding = RuntimeConstants.ENCODING_DEFAULT;
+
+ /**
+ * Resource might require ancillary storage of some kind
+ */
+ protected Object data = null;
+
+ /**
+ * Resource type (RESOURCE_TEMPLATE or RESOURCE_CONTENT)
+ */
+ protected int type;
+
+ /**
+ * Default constructor
+ */
+ public Resource()
+ {
+ }
+
+ /**
+ * @param rs
+ */
+ public void setRuntimeServices( RuntimeServices rs )
+ {
+ rsvc = rs;
+ log = rsvc.getLog("loader");
+ }
+
+ /**
+ * Perform any subsequent processing that might need
+ * to be done by a resource. In the case of a template
+ * the actual parsing of the input stream needs to be
+ * performed.
+ *
+ * @return Whether the resource could be processed successfully.
+ * For a {@link org.apache.velocity.Template} or {@link
+ * org.apache.velocity.runtime.resource.ContentResource}, this
+ * indicates whether the resource could be read.
+ * @exception ResourceNotFoundException Similar in semantics as
+ * returning <code>false</code>.
+ * @throws ParseErrorException
+ */
+ public abstract boolean process()
+ throws ResourceNotFoundException, ParseErrorException;
+
+ /**
+ * @return True if source has been modified.
+ */
+ public boolean isSourceModified()
+ {
+ return resourceLoader.isSourceModified(this);
+ }
+
+ /**
+ * Set the modification check interval.
+ * @param modificationCheckInterval The interval (in seconds).
+ */
+ public void setModificationCheckInterval(long modificationCheckInterval)
+ {
+ this.modificationCheckInterval = modificationCheckInterval;
+ }
+
+ /**
+ * Is it time to check to see if the resource
+ * source has been updated?
+ * @return True if resource must be checked.
+ */
+ public boolean requiresChecking()
+ {
+ /*
+ * short circuit this if modificationCheckInterval == 0
+ * as this means "don't check"
+ */
+
+ if (modificationCheckInterval <= 0 )
+ {
+ return false;
+ }
+
+ /*
+ * see if we need to check now
+ */
+
+ return ( System.currentTimeMillis() >= nextCheck );
+ }
+
+ /**
+ * 'Touch' this template and thereby resetting
+ * the nextCheck field.
+ */
+ public void touch()
+ {
+ nextCheck = System.currentTimeMillis() + ( MILLIS_PER_SECOND * modificationCheckInterval);
+ }
+
+ /**
+ * Set the name of this resource, for example
+ * test.vm.
+ * @param name
+ */
+ public void setName(String name)
+ {
+ this.name = name;
+ }
+
+ /**
+ * Get the name of this template.
+ * @return The name of this template.
+ */
+ public String getName()
+ {
+ return name;
+ }
+
+ /**
+ * set the encoding of this resource
+ * for example, "ISO-8859-1"
+ * @param encoding
+ */
+ public void setEncoding( String encoding )
+ {
+ this.encoding = encoding;
+ }
+
+ /**
+ * get the encoding of this resource
+ * for example, "ISO-8859-1"
+ * @return The encoding of this resource.
+ */
+ public String getEncoding()
+ {
+ return encoding;
+ }
+
+
+ /**
+ * Return the lastModifed time of this
+ * resource.
+ * @return The lastModifed time of this resource.
+ */
+ public long getLastModified()
+ {
+ return lastModified;
+ }
+
+ /**
+ * Set the last modified time for this
+ * resource.
+ * @param lastModified
+ */
+ public void setLastModified(long lastModified)
+ {
+ this.lastModified = lastModified;
+ }
+
+ /**
+ * Return the template loader that pulled
+ * in the template stream
+ * @return The resource loader for this resource.
+ */
+ public ResourceLoader getResourceLoader()
+ {
+ return resourceLoader;
+ }
+
+ /**
+ * Set the template loader for this template. Set
+ * when the Runtime determines where this template
+ * came from the list of possible sources.
+ * @param resourceLoader
+ */
+ public void setResourceLoader(ResourceLoader resourceLoader)
+ {
+ this.resourceLoader = resourceLoader;
+ }
+
+ /**
+ * Set arbitrary data object that might be used
+ * by the resource.
+ * @param data
+ */
+ public void setData(Object data)
+ {
+ this.data = data;
+ }
+
+ /**
+ * Get arbitrary data object that might be used
+ * by the resource.
+ * @return The data object for this resource.
+ */
+ public Object getData()
+ {
+ return data;
+ }
+
+ /**
+ * Sets the type of this Resource (RESOURCE_TEMPLATE or RESOURCE_CONTENT)
+ * @param type RESOURCE_TEMPLATE or RESOURCE_CONTENT
+ * @since 1.6
+ */
+ public void setType(int type)
+ {
+ this.type = type;
+ }
+
+ /**
+ * @return type code of the Resource
+ * @since 1.6
+ */
+ public int getType()
+ {
+ return type;
+ }
+
+ /**
+ * @return cloned resource
+ * @since 2.4
+ */
+ @Override
+ public Object clone() {
+ try
+ {
+ Resource clone = (Resource) super.clone();
+ clone.deepCloneData();
+ return clone;
+ }
+ catch (CloneNotSupportedException e) {
+ throw new RuntimeException("cloning not supported");
+ }
+ }
+
+ /**
+ * Deep cloning of resource data
+ * @return cloned data
+ */
+ protected void deepCloneData() throws CloneNotSupportedException
+ {
+ // default implementation does nothing
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceCache.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceCache.java
new file mode 100644
index 00000000..98570971
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceCache.java
@@ -0,0 +1,81 @@
+package org.apache.velocity.runtime.resource;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.runtime.RuntimeServices;
+
+import java.util.Iterator;
+
+/**
+ * Interface that defines the shape of a pluggable resource cache
+ * for the included ResourceManager
+ *
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public interface ResourceCache
+{
+ /**
+ * initializes the ResourceCache. Will be
+ * called before any utilization
+ *
+ * @param rs RuntimeServices to use for logging, etc
+ */
+ void initialize(RuntimeServices rs);
+
+ /**
+ * retrieves a Resource from the
+ * cache
+ *
+ * @param resourceKey key for Resource to be retrieved
+ * @return Resource specified or null if not found
+ */
+ Resource get(Object resourceKey);
+
+ /**
+ * stores a Resource in the cache
+ *
+ * @param resourceKey key to associate with the Resource
+ * @param resource Resource to be stored
+ * @return existing Resource stored under this key, or null if none
+ */
+ Resource put(Object resourceKey, Resource resource);
+
+ /**
+ * removes a Resource from the cache
+ *
+ * @param resourceKey resource to be removed
+ * @return stored under key
+ */
+ Resource remove(Object resourceKey);
+
+ /**
+ * Removes all of the resources from this cache.
+ * The cache will be empty after this call returns.
+ * @since 2.0
+ */
+ void clear();
+
+ /**
+ * returns an Iterator of Keys in the cache.
+ * @return An Iterator of Keys in the cache.
+ */
+ Iterator enumerateKeys();
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceCacheImpl.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceCacheImpl.java
new file mode 100644
index 00000000..7ad03518
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceCacheImpl.java
@@ -0,0 +1,168 @@
+package org.apache.velocity.runtime.resource;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.slf4j.Logger;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Default implementation of the resource cache for the default
+ * ResourceManager. The cache uses a <i>least recently used</i> (LRU)
+ * algorithm, with a maximum size specified via the
+ * <code>resource.manager.cache.size</code> property (identified by the
+ * {@link
+ * org.apache.velocity.runtime.RuntimeConstants#RESOURCE_MANAGER_DEFAULTCACHE_SIZE}
+ * constant). This property get be set to <code>0</code> or less for
+ * a greedy, unbounded cache (the behavior from pre-v1.5).
+ *
+ * @author <a href="mailto:geirm@apache.org">Geir Magnusson Jr.</a>
+ * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
+ * @version $Id$
+ */
+public class ResourceCacheImpl implements ResourceCache
+{
+
+ /**
+ * A simple LRU Map based on {@link LinkedHashSet}.
+ *
+ * @param <K> The key type of the map.
+ * @param <V> The value type of the map.
+ */
+ private static class LRUMap<K, V> extends LinkedHashMap<K, V>{
+
+ /**
+ * The serial version uid;
+ */
+ private static final long serialVersionUID = 5889225121697975043L;
+
+ /**
+ * The size of the cache.
+ */
+ private int cacheSize;
+
+ /**
+ * Constructor.
+ *
+ * @param cacheSize The size of the cache. After reaching this size, the
+ * eldest-accessed element will be erased.
+ */
+ public LRUMap(int cacheSize)
+ {
+ this.cacheSize = cacheSize;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean removeEldestEntry(Entry<K, V> eldest)
+ {
+ return size() > cacheSize;
+ }
+ }
+
+ /**
+ * Cache storage, assumed to be thread-safe.
+ */
+ protected Map<Object, Resource> cache = new ConcurrentHashMap<>(512, 0.5f, 30);
+
+ /**
+ * Runtime services, generally initialized by the
+ * <code>initialize()</code> method.
+ */
+ protected RuntimeServices rsvc = null;
+
+ protected Logger log;
+
+ /**
+ * @see org.apache.velocity.runtime.resource.ResourceCache#initialize(org.apache.velocity.runtime.RuntimeServices)
+ */
+ @Override
+ public void initialize(RuntimeServices rs )
+ {
+ rsvc = rs;
+
+ int maxSize =
+ rsvc.getInt(RuntimeConstants.RESOURCE_MANAGER_DEFAULTCACHE_SIZE, 89);
+ if (maxSize > 0)
+ {
+ // Create a whole new Map here to avoid hanging on to a
+ // handle to the unsynch'd LRUMap for our lifetime.
+ Map<Object, Resource> lruCache = Collections.synchronizedMap(new LRUMap<>(maxSize));
+ lruCache.putAll(cache);
+ cache = lruCache;
+ }
+ rsvc.getLog().debug("initialized ({}) with {} cache map.", this.getClass(), cache.getClass());
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.resource.ResourceCache#get(java.lang.Object)
+ */
+ @Override
+ public Resource get(Object key )
+ {
+ return cache.get( key );
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.resource.ResourceCache#put(java.lang.Object, org.apache.velocity.runtime.resource.Resource)
+ */
+ @Override
+ public Resource put(Object key, Resource value )
+ {
+ return cache.put( key, value );
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.resource.ResourceCache#remove(java.lang.Object)
+ */
+ @Override
+ public Resource remove(Object key )
+ {
+ return cache.remove( key );
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.resource.ResourceCache#clear()
+ * @since 2.0
+ */
+ @Override
+ public void clear()
+ {
+ cache.clear();
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.resource.ResourceCache#enumerateKeys()
+ */
+ @Override
+ public Iterator<Object> enumerateKeys()
+ {
+ return cache.keySet().iterator();
+ }
+}
+
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceFactory.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceFactory.java
new file mode 100644
index 00000000..e95712aa
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceFactory.java
@@ -0,0 +1,59 @@
+package org.apache.velocity.runtime.resource;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.Template;
+import org.apache.velocity.exception.VelocityException;
+
+/**
+ * Class responsible for instantiating <code>Resource</code> objects,
+ * given name and type.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public class ResourceFactory
+{
+ /**
+ * @param resourceName
+ * @param resourceType
+ * @return The resource described by name and type.
+ */
+ public static Resource getResource(String resourceName, int resourceType)
+ {
+ Resource resource = null;
+
+ switch (resourceType)
+ {
+ case ResourceManager.RESOURCE_TEMPLATE:
+ resource = new Template();
+ break;
+
+ case ResourceManager.RESOURCE_CONTENT:
+ resource = new ContentResource();
+ break;
+ default:
+ throw new VelocityException("invalide resource type");
+ }
+
+ return resource;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceManager.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceManager.java
new file mode 100644
index 00000000..a5b64395
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceManager.java
@@ -0,0 +1,81 @@
+package org.apache.velocity.runtime.resource;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.runtime.RuntimeServices;
+
+/**
+ * Class to manage the text resource for the Velocity
+ * Runtime.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:paulo.gaspar@krankikom.de">Paulo Gaspar</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public interface ResourceManager
+{
+ /**
+ * A template resources.
+ */
+ int RESOURCE_TEMPLATE = 1;
+
+ /**
+ * A static content resource.
+ */
+ int RESOURCE_CONTENT = 2;
+
+ /**
+ * Initialize the ResourceManager.
+ * @param rs
+ */
+ void initialize(RuntimeServices rs);
+
+ /**
+ * Gets the named resource. Returned class type corresponds to specified type
+ * (i.e. <code>Template</code> to <code>RESOURCE_TEMPLATE</code>).
+ *
+ * @param resourceName The name of the resource to retrieve.
+ * @param resourceType The type of resource (<code>RESOURCE_TEMPLATE</code>,
+ * <code>RESOURCE_CONTENT</code>, etc.).
+ * @param encoding The character encoding to use.
+ * @return Resource with the template parsed and ready.
+ * @throws ResourceNotFoundException if template not found
+ * from any available source.
+ * @throws ParseErrorException if template cannot be parsed due
+ * to syntax (or other) error.
+ */
+ Resource getResource(String resourceName, int resourceType, String encoding)
+ throws ResourceNotFoundException, ParseErrorException;
+
+ /**
+ * Determines is a template exists, and returns name of the loader that
+ * provides it. This is a slightly less hokey way to support
+ * the Velocity.templateExists() utility method, which was broken
+ * when per-template encoding was introduced. We can revisit this.
+ *
+ * @param resourceName Name of template or content resource
+ * @return class name of loader than can provide it
+ */
+ String getLoaderNameForResource(String resourceName);
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceManagerImpl.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceManagerImpl.java
new file mode 100644
index 00000000..82843ac1
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceManagerImpl.java
@@ -0,0 +1,607 @@
+package org.apache.velocity.runtime.resource;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.resource.loader.ResourceLoader;
+import org.apache.velocity.runtime.resource.loader.ResourceLoaderFactory;
+import org.apache.velocity.util.ClassUtils;
+import org.apache.velocity.util.ExtProperties;
+
+import org.apache.commons.lang3.StringUtils;
+
+import org.slf4j.Logger;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Vector;
+
+
+/**
+ * Class to manage the text resource for the Velocity Runtime.
+ *
+ * @author <a href="mailto:wglass@forio.com">Will Glass-Husain</a>
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:paulo.gaspar@krankikom.de">Paulo Gaspar</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
+ * @version $Id$
+ */
+public class ResourceManagerImpl
+ implements ResourceManager
+{
+
+ /** A template resources. */
+ public static final int RESOURCE_TEMPLATE = 1;
+
+ /** A static content resource. */
+ public static final int RESOURCE_CONTENT = 2;
+
+ /** Object implementing ResourceCache to be our resource manager's Resource cache. */
+ protected ResourceCache globalCache = null;
+
+ /** The List of templateLoaders that the Runtime will use to locate the InputStream source of a template. */
+ protected final List<ResourceLoader> resourceLoaders = new ArrayList<>();
+
+ /**
+ * This is a list of the template input stream source initializers, basically properties for a particular template stream
+ * source. The order in this list reflects numbering of the properties i.e.
+ *
+ * <p>resource.loader.&lt;loader-id&gt;.&lt;property&gt; = &lt;value&gt;</p>
+ */
+ private final List<ExtProperties> sourceInitializerList = new ArrayList<>();
+
+ /**
+ * Has this Manager been initialized?
+ */
+ private boolean isInit = false;
+
+ /** switch to turn off log notice when a resource is found for the first time. */
+ private boolean logWhenFound = true;
+
+ /** The internal RuntimeServices object. */
+ protected RuntimeServices rsvc = null;
+
+ /** Logging. */
+ protected Logger log = null;
+
+ /**
+ * Initialize the ResourceManager.
+ *
+ * @param rs The Runtime Services object which is associated with this Resource Manager.
+ */
+ @Override
+ public synchronized void initialize(final RuntimeServices rs)
+ {
+ if (isInit)
+ {
+ log.debug("Re-initialization of ResourceLoader attempted and ignored.");
+ return;
+ }
+
+ ResourceLoader resourceLoader = null;
+
+ rsvc = rs;
+ log = rsvc.getLog("loader");
+
+ log.trace("ResourceManager initializing: {}", this.getClass());
+
+ assembleResourceLoaderInitializers();
+
+ for (ExtProperties configuration : sourceInitializerList)
+ {
+ /*
+ * Resource loader can be loaded either via class name or be passed
+ * in as an instance.
+ */
+
+ String loaderClass = StringUtils.trim(configuration.getString(RuntimeConstants.RESOURCE_LOADER_CLASS));
+ ResourceLoader loaderInstance = (ResourceLoader) configuration.get(RuntimeConstants.RESOURCE_LOADER_INSTANCE);
+
+ if (loaderInstance != null)
+ {
+ resourceLoader = loaderInstance;
+ } else if (loaderClass != null)
+ {
+ resourceLoader = ResourceLoaderFactory.getLoader(rsvc, loaderClass);
+ } else
+ {
+ String msg = "Unable to find 'resource.loader." +
+ configuration.getString(RuntimeConstants.RESOURCE_LOADER_IDENTIFIER) +
+ ".class' specification in configuration." +
+ " This is a critical value. Please adjust configuration.";
+ log.error(msg);
+ throw new VelocityException(msg, null, rsvc.getLogContext().getStackTrace());
+ }
+
+ resourceLoader.commonInit(rsvc, configuration);
+ resourceLoader.init(configuration);
+ resourceLoaders.add(resourceLoader);
+ }
+
+ /*
+ * now see if this is overridden by configuration
+ */
+
+ logWhenFound = rsvc.getBoolean(RuntimeConstants.RESOURCE_MANAGER_LOGWHENFOUND, true);
+
+ /*
+ * now, is a global cache specified?
+ */
+
+ String cacheClassName = rsvc.getString(RuntimeConstants.RESOURCE_MANAGER_CACHE_CLASS);
+
+ Object cacheObject = null;
+
+ if (StringUtils.isNotEmpty(cacheClassName))
+ {
+ try
+ {
+ cacheObject = ClassUtils.getNewInstance(cacheClassName);
+ }
+ catch (ClassNotFoundException cnfe)
+ {
+ String msg = "The specified class for ResourceCache (" + cacheClassName +
+ ") does not exist or is not accessible to the current classloader.";
+ log.error(msg, cnfe);
+ throw new VelocityException(msg, cnfe);
+ }
+ catch (IllegalAccessException ae)
+ {
+ throw new VelocityException("Could not access class '"
+ + cacheClassName + "'", ae);
+ }
+ catch (InstantiationException ie)
+ {
+ throw new VelocityException("Could not instantiate class '"
+ + cacheClassName + "'", ie);
+ }
+
+ if (!(cacheObject instanceof ResourceCache))
+ {
+ String msg = "The specified resource cache class (" + cacheClassName +
+ ") must implement " + ResourceCache.class.getName();
+ log.error(msg);
+ throw new RuntimeException(msg);
+ }
+ }
+
+ /*
+ * if we didn't get through that, just use the default.
+ */
+ if (cacheObject == null)
+ {
+ cacheObject = new ResourceCacheImpl();
+ }
+
+ globalCache = (ResourceCache) cacheObject;
+
+ globalCache.initialize(rsvc);
+
+ isInit = true;
+
+ log.trace("Default ResourceManager initialization complete.");
+ }
+
+ /**
+ * This will produce a List of Hashtables, each hashtable contains the initialization info for a particular resource loader. This
+ * Hashtable will be passed in when initializing the the template loader.
+ */
+ private void assembleResourceLoaderInitializers()
+ {
+ Vector<String> resourceLoaderNames = rsvc.getConfiguration().getVector(RuntimeConstants.RESOURCE_LOADERS);
+
+ for (ListIterator<String> it = resourceLoaderNames.listIterator(); it.hasNext(); )
+ {
+ /*
+ * The loader id might look something like the following:
+ *
+ * resource.loader.file
+ *
+ * The loader id is the prefix used for all properties
+ * pertaining to a particular loader.
+ */
+ String loaderName = StringUtils.trim(it.next());
+ it.set(loaderName);
+ StringBuilder loaderID = new StringBuilder();
+ loaderID.append(RuntimeConstants.RESOURCE_LOADER).append('.').append(loaderName);
+
+ ExtProperties loaderConfiguration =
+ rsvc.getConfiguration().subset(loaderID.toString());
+
+ /*
+ * we can't really count on ExtProperties to give us an empty set
+ */
+ if (loaderConfiguration == null)
+ {
+ log.debug("ResourceManager : No configuration information found "+
+ "for resource loader named '{}' (id is {}). Skipping it...",
+ loaderName, loaderID);
+ continue;
+ }
+
+ /*
+ * add the loader name token to the initializer if we need it
+ * for reference later. We can't count on the user to fill
+ * in the 'name' field
+ */
+
+ loaderConfiguration.setProperty(RuntimeConstants.RESOURCE_LOADER_IDENTIFIER, loaderName);
+
+ /*
+ * Add resources to the list of resource loader
+ * initializers.
+ */
+ sourceInitializerList.add(loaderConfiguration);
+ }
+ }
+
+ /**
+ * Gets the named resource. Returned class type corresponds to specified type (i.e. <code>Template</code> to <code>
+ * RESOURCE_TEMPLATE</code>).
+ *
+ * This method is now unsynchronized which requires that ResourceCache
+ * implementations be thread safe (as the default is).
+ *
+ * @param resourceName The name of the resource to retrieve.
+ * @param resourceType The type of resource (<code>RESOURCE_TEMPLATE</code>, <code>RESOURCE_CONTENT</code>, etc.).
+ * @param encoding The character encoding to use.
+ *
+ * @return Resource with the template parsed and ready.
+ *
+ * @throws ResourceNotFoundException if template not found from any available source.
+ * @throws ParseErrorException if template cannot be parsed due to syntax (or other) error.
+ */
+ @Override
+ public Resource getResource(final String resourceName, final int resourceType, final String encoding)
+ throws ResourceNotFoundException,
+ ParseErrorException
+ {
+ /*
+ * Check to see if the resource was placed in the cache.
+ * If it was placed in the cache then we will use
+ * the cached version of the resource. If not we
+ * will load it.
+ *
+ * Note: the type is included in the key to differentiate ContentResource
+ * (static content from #include) with a Template.
+ */
+
+ String resourceKey = resourceType + resourceName;
+ Resource resource = globalCache.get(resourceKey);
+
+ if (resource != null)
+ {
+ try
+ {
+ // avoids additional method call to refreshResource
+ if (resource.requiresChecking())
+ {
+ /*
+ * both loadResource() and refreshResource() now return
+ * a new Resource instance when they are called
+ * (put in the cache when appropriate) in order to allow
+ * several threads to parse the same template simultaneously.
+ * It is redundant work and will cause more garbage collection but the
+ * benefit is that it allows concurrent parsing and processing
+ * without race conditions when multiple requests try to
+ * refresh/load the same template at the same time.
+ *
+ * Another alternative is to limit template parsing/retrieval
+ * so that only one thread can parse each template at a time
+ * but that creates a scalability bottleneck.
+ *
+ * See VELOCITY-606, VELOCITY-595 and VELOCITY-24
+ */
+ resource = refreshResource(resource, encoding);
+ }
+ }
+ catch (ResourceNotFoundException rnfe)
+ {
+ /*
+ * something exceptional happened to that resource
+ * this could be on purpose,
+ * so clear the cache and try again
+ */
+
+ globalCache.remove(resourceKey);
+
+ return getResource(resourceName, resourceType, encoding);
+ }
+ catch (RuntimeException re)
+ {
+ log.error("ResourceManager.getResource() exception", re);
+ throw re;
+ }
+ }
+ else
+ {
+ try
+ {
+ /*
+ * it's not in the cache, so load it.
+ */
+ resource = loadResource(resourceName, resourceType, encoding);
+
+ if (resource.getResourceLoader().isCachingOn())
+ {
+ globalCache.put(resourceKey, resource);
+ }
+ }
+ catch (ResourceNotFoundException rnfe)
+ {
+ log.error("ResourceManager: unable to find resource '{}' in any resource loader.", resourceName);
+ throw rnfe;
+ }
+ catch (ParseErrorException pee)
+ {
+ log.error("ResourceManager: parse exception: {}", pee.getMessage());
+ throw pee;
+ }
+ catch (RuntimeException re)
+ {
+ log.error("ResourceManager.getResource() load exception", re);
+ throw re;
+ }
+ }
+
+ return resource;
+ }
+
+ /**
+ * Create a new Resource of the specified type.
+ *
+ * @param resourceName The name of the resource to retrieve.
+ * @param resourceType The type of resource (<code>RESOURCE_TEMPLATE</code>, <code>RESOURCE_CONTENT</code>, etc.).
+ * @return new instance of appropriate resource type
+ * @since 1.6
+ */
+ protected Resource createResource(String resourceName, int resourceType)
+ {
+ return ResourceFactory.getResource(resourceName, resourceType);
+ }
+
+ /**
+ * Loads a resource from the current set of resource loaders.
+ *
+ * @param resourceName The name of the resource to retrieve.
+ * @param resourceType The type of resource (<code>RESOURCE_TEMPLATE</code>, <code>RESOURCE_CONTENT</code>, etc.).
+ * @param encoding The character encoding to use.
+ *
+ * @return Resource with the template parsed and ready.
+ *
+ * @throws ResourceNotFoundException if template not found from any available source.
+ * @throws ParseErrorException if template cannot be parsed due to syntax (or other) error.
+ */
+ protected Resource loadResource(String resourceName, int resourceType, String encoding)
+ throws ResourceNotFoundException,
+ ParseErrorException
+ {
+ Resource resource = createResource(resourceName, resourceType);
+ resource.setRuntimeServices(rsvc);
+ resource.setName(resourceName);
+ resource.setEncoding(encoding);
+
+ /*
+ * Now we have to try to find the appropriate
+ * loader for this resource. We have to cycle through
+ * the list of available resource loaders and see
+ * which one gives us a stream that we can use to
+ * make a resource with.
+ */
+
+ long howOldItWas = 0;
+
+ for (ResourceLoader resourceLoader : resourceLoaders)
+ {
+ resource.setResourceLoader(resourceLoader);
+
+ /*
+ * catch the ResourceNotFound exception
+ * as that is ok in our new multi-loader environment
+ */
+
+ try
+ {
+
+ if (resource.process())
+ {
+ /*
+ * FIXME (gmj)
+ * moved in here - technically still
+ * a problem - but the resource needs to be
+ * processed before the loader can figure
+ * it out due to to the new
+ * multi-path support - will revisit and fix
+ */
+
+ if (logWhenFound)
+ {
+ log.debug("ResourceManager: found {} with loader {}",
+ resourceName, resourceLoader.getClassName());
+ }
+
+ howOldItWas = resourceLoader.getLastModified(resource);
+
+ break;
+ }
+ }
+ catch (ResourceNotFoundException rnfe)
+ {
+ /*
+ * that's ok - it's possible to fail in
+ * multi-loader environment
+ */
+ }
+ }
+
+ /*
+ * Return null if we can't find a resource.
+ */
+ if (resource.getData() == null)
+ {
+ throw new ResourceNotFoundException("Unable to find resource '" + resourceName + "'", null, rsvc.getLogContext().getStackTrace());
+ }
+
+ /*
+ * some final cleanup
+ */
+
+ resource.setLastModified(howOldItWas);
+ resource.setModificationCheckInterval(resource.getResourceLoader().getModificationCheckInterval());
+
+ resource.touch();
+
+ return resource;
+ }
+
+ /**
+ * Takes an existing resource, and 'refreshes' it. This generally means that the source of the resource is checked for changes
+ * according to some cache/check algorithm and if the resource changed, then the resource data is reloaded and re-parsed.
+ *
+ * @param resource resource to refresh
+ * @param encoding character encoding of the resource to refresh.
+ * @return resource
+ * @throws ResourceNotFoundException if template not found from current source for this Resource
+ * @throws ParseErrorException if template cannot be parsed due to syntax (or other) error.
+ */
+ protected Resource refreshResource(Resource resource, final String encoding)
+ throws ResourceNotFoundException, ParseErrorException
+ {
+ /*
+ * The resource knows whether it needs to be checked
+ * or not, and the resource's loader can check to
+ * see if the source has been modified. If both
+ * these conditions are true then we must reload
+ * the input stream and parse it to make a new
+ * AST for the resource.
+ */
+
+ String resourceKey = resource.getType() + resource.getName();
+
+ /*
+ * touch() the resource to reset the counters
+ */
+ resource.touch();
+
+ /* check whether this can now be found in a higher priority
+ * resource loader. if so, pass the request off to loadResource.
+ */
+ ResourceLoader loader = resource.getResourceLoader();
+ if (resourceLoaders.size() > 0 && resourceLoaders.indexOf(loader) > 0)
+ {
+ String name = resource.getName();
+ if (loader != getLoaderForResource(name))
+ {
+ resource = loadResource(name, resource.getType(), encoding);
+ if (resource.getResourceLoader().isCachingOn())
+ {
+ globalCache.put(resourceKey, resource);
+ }
+ }
+ }
+
+ if (resource.isSourceModified())
+ {
+ /*
+ * now check encoding info. It's possible that the newly declared
+ * encoding is different than the encoding already in the resource
+ * this strikes me as bad...
+ */
+
+ if (!StringUtils.equals(resource.getEncoding(), encoding))
+ {
+ log.warn("Declared encoding for template '{}' is different on reload. Old = '{}' New = '{}'",
+ resource.getName(), resource.getEncoding(), encoding);
+ resource.setEncoding(encoding);
+ }
+
+ /*
+ * read how old the resource is _before_
+ * processing (=>reading) it
+ */
+ long howOldItWas = loader.getLastModified(resource);
+
+ /*
+ * we create a copy to avoid partially overwriting a
+ * template which may be in use in another thread
+ */
+
+ Resource newResource =
+ ResourceFactory.getResource(resource.getName(), resource.getType());
+
+ newResource.setRuntimeServices(rsvc);
+ newResource.setName(resource.getName());
+ newResource.setEncoding(resource.getEncoding());
+ newResource.setResourceLoader(loader);
+ newResource.setModificationCheckInterval(loader.getModificationCheckInterval());
+
+ newResource.process();
+ newResource.setLastModified(howOldItWas);
+ resource = newResource;
+ resource.touch();
+
+ globalCache.put(resourceKey, newResource);
+ }
+ return resource;
+ }
+
+ /**
+ * Determines if a template exists, and returns name of the loader that provides it. This is a slightly less hokey way to
+ * support the Velocity.templateExists() utility method, which was broken when per-template encoding was introduced. We can
+ * revisit this.
+ *
+ * @param resourceName Name of template or content resource
+ *
+ * @return class name of loader than can provide it
+ */
+ @Override
+ public String getLoaderNameForResource(String resourceName)
+ {
+ ResourceLoader loader = getLoaderForResource(resourceName);
+ if (loader == null)
+ {
+ return null;
+ }
+ return loader.getClass().toString();
+ }
+
+ /**
+ * Returns the first {@link ResourceLoader} in which the specified
+ * resource exists.
+ */
+ private ResourceLoader getLoaderForResource(String resourceName)
+ {
+ for (ResourceLoader loader : resourceLoaders)
+ {
+ if (loader.resourceExists(resourceName))
+ {
+ return loader;
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/ClasspathResourceLoader.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/ClasspathResourceLoader.java
new file mode 100644
index 00000000..9bfd0f14
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/ClasspathResourceLoader.java
@@ -0,0 +1,169 @@
+package org.apache.velocity.runtime.resource.loader;
+
+/*
+ * 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.
+ */
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.runtime.resource.Resource;
+import org.apache.velocity.util.ClassUtils;
+import org.apache.velocity.util.ExtProperties;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+
+/**
+ * ClasspathResourceLoader is a simple loader that will load
+ * templates from the classpath.
+ * <br>
+ * <br>
+ * Will load templates from from multiple instances of
+ * and arbitrary combinations of:
+ * <ul>
+ * <li> jar files
+ * <li> zip files
+ * <li> template directories (any directory containing templates)
+ * </ul>
+ * This is a configuration-free loader, in that there are no
+ * parameters to be specified in the configuration properties,
+ * other than specifying this as the loader to use. For example
+ * the following is all that the loader needs to be functional:
+ * <br>
+ * <pre><code>
+ * resource.loaders = class
+ * resource.loader.class.class =org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader
+ * </code></pre>
+ * <br>
+ * <br>
+ * To use, put your template directories, jars
+ * and zip files into the classpath or other mechanisms that make
+ * resources accessible to the classloader.
+ * <br>
+ * <br>
+ * This makes deployment trivial for web applications running in
+ * any Servlet 2.2 compliant servlet runner, such as Tomcat 3.2
+ * and others.
+ * <br>
+ * <br>
+ * For a Servlet Spec v2.2 servlet runner,
+ * just drop the jars of template files into the WEB-INF/lib
+ * directory of your webapp, and you won't have to worry about setting
+ * template paths or altering them with the root of the webapp
+ * before initializing.
+ * <br>
+ * <br>
+ * I have also tried it with a WAR deployment, and that seemed to
+ * work just fine.
+ *
+ * @author <a href="mailto:mailmur@yahoo.com">Aki Nieminen</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public class ClasspathResourceLoader extends ResourceLoader
+{
+
+ /**
+ * This is abstract in the base class, so we need it
+ * @param configuration
+ */
+ @Override
+ public void init(ExtProperties configuration)
+ {
+ log.trace("ClasspathResourceLoader: initialization complete.");
+ }
+
+ /**
+ * Get a Reader so that the Runtime can build a
+ * template with it.
+ *
+ * @param name name of template to get
+ * @param encoding asked encoding
+ * @return InputStream containing the template
+ * @throws ResourceNotFoundException if template not found
+ * in classpath.
+ * @since 2.0
+ */
+ @Override
+ public Reader getResourceReader(String name, String encoding )
+ throws ResourceNotFoundException
+ {
+ Reader result = null;
+
+ if (StringUtils.isEmpty(name))
+ {
+ throw new ResourceNotFoundException ("No template name provided");
+ }
+
+ /*
+ * look for resource in thread classloader first (e.g. WEB-INF\lib in
+ * a servlet container) then fall back to the system classloader.
+ */
+
+ InputStream rawStream = null;
+ try
+ {
+ rawStream = ClassUtils.getResourceAsStream( getClass(), name );
+ if (rawStream != null)
+ {
+ result = buildReader(rawStream, encoding);
+ }
+ }
+ catch( Exception fnfe )
+ {
+ if (rawStream != null)
+ {
+ try
+ {
+ rawStream.close();
+ }
+ catch (IOException ioe) {}
+ }
+ throw new ResourceNotFoundException("ClasspathResourceLoader problem with template: " + name, fnfe, rsvc.getLogContext().getStackTrace() );
+ }
+
+ if (result == null)
+ {
+ String msg = "ClasspathResourceLoader Error: cannot find resource " + name;
+
+ throw new ResourceNotFoundException( msg, null, rsvc.getLogContext().getStackTrace() );
+ }
+
+ return result;
+ }
+
+ /**
+ * @see ResourceLoader#isSourceModified(org.apache.velocity.runtime.resource.Resource)
+ */
+ @Override
+ public boolean isSourceModified(Resource resource)
+ {
+ return false;
+ }
+
+ /**
+ * @see ResourceLoader#getLastModified(org.apache.velocity.runtime.resource.Resource)
+ */
+ @Override
+ public long getLastModified(Resource resource)
+ {
+ return 0;
+ }
+}
+
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/DataSourceResourceLoader.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/DataSourceResourceLoader.java
new file mode 100644
index 00000000..b9cc48cc
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/DataSourceResourceLoader.java
@@ -0,0 +1,550 @@
+package org.apache.velocity.runtime.resource.loader;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.resource.Resource;
+import org.apache.velocity.util.ExtProperties;
+
+import org.apache.commons.lang3.StringUtils;
+
+import javax.naming.InitialContext;
+import javax.naming.NamingException;
+import javax.sql.DataSource;
+import java.io.FilterReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+
+/**
+ * <p>This is a simple template file loader that loads templates
+ * from a DataSource instead of plain files.</p>
+ *
+ * <p>It can be configured with a datasource name, a table name,
+ * id column (name), content column (the template body) and a
+ * datetime column (for last modification info).</p>
+ * <br>
+ * <p>Example configuration snippet for velocity.properties:</p>
+ * <br>
+ * <pre><code>
+ * resource.loaders = file, ds
+ *
+ * resource.loader.ds.description = Velocity DataSource Resource Loader <br>
+ * resource.loader.ds.class = org.apache.velocity.runtime.resource.loader.DataSourceResourceLoader <br>
+ * resource.loader.ds.resource.datasource_url = java:comp/env/jdbc/Velocity <br>
+ * resource.loader.ds.resource.table = tb_velocity_template <br>
+ * resource.loader.ds.resource.key_column = id_template <br>
+ * resource.loader.ds.resource.template_column = template_definition <br>
+ * resource.loader.ds.resource.timestamp_column = template_timestamp <br>
+ * resource.loader.ds.cache = false <br>
+ * resource.loader.ds.modification_check_interval = 60 <br>
+ * </code></pre>
+ * <p>Optionally, the developer can instantiate the DataSourceResourceLoader and set the DataSource via code in
+ * a manner similar to the following:</p>
+ * <br>
+ * <pre><code>
+ * DataSourceResourceLoader ds = new DataSourceResourceLoader();
+ * ds.setDataSource(DATASOURCE);
+ * Velocity.setProperty("resource.loader.ds.instance",ds);
+ * </code></pre>
+ * <p> The property <code>resource.loader.ds.class</code> should be left out, otherwise all the other
+ * properties in velocity.properties would remain the same.</p>
+ * <br>
+ * <p>Example WEB-INF/web.xml:</p>
+ * <br>
+ * <pre><code>
+ * &lt;resource-ref&gt;
+ * &lt;description&gt;Velocity template DataSource&lt;/description&gt;
+ * &lt;res-ref-name&gt;jdbc/Velocity&lt;/res-ref-name&gt;
+ * &lt;res-type&gt;javax.sql.DataSource&lt;/res-type&gt;
+ * &lt;res-auth&gt;Container&lt;/res-auth&gt;
+ * &lt;/resource-ref&gt;
+ * </code></pre>
+ * <br>
+ * and Tomcat 4 server.xml file: <br>
+ * <pre><code>
+ * [...]
+ * &lt;Context path="/exampleVelocity" docBase="exampleVelocity" debug="0"&gt;
+ * [...]
+ * &lt;ResourceParams name="jdbc/Velocity"&gt;
+ * &lt;parameter&gt;
+ * &lt;name&gt;driverClassName&lt;/name&gt;
+ * &lt;value&gt;org.hsql.jdbcDriver&lt;/value&gt;
+ * &lt;/parameter&gt;
+ * &lt;parameter&gt;
+ * &lt;name&gt;driverName&lt;/name&gt;
+ * &lt;value&gt;jdbc:HypersonicSQL:database&lt;/value&gt;
+ * &lt;/parameter&gt;
+ * &lt;parameter&gt;
+ * &lt;name&gt;user&lt;/name&gt;
+ * &lt;value&gt;database_username&lt;/value&gt;
+ * &lt;/parameter&gt;
+ * &lt;parameter&gt;
+ * &lt;name&gt;password&lt;/name&gt;
+ * &lt;value&gt;database_password&lt;/value&gt;
+ * &lt;/parameter&gt;
+ * &lt;/ResourceParams&gt;
+ * [...]
+ * &lt;/Context&gt;
+ * [...]
+ * </code></pre>
+ * <br>
+ * <p>Example sql script:</p>
+ * <pre><code>
+ * CREATE TABLE tb_velocity_template (
+ * id_template varchar (40) NOT NULL ,
+ * template_definition text (16) NOT NULL ,
+ * template_timestamp datetime NOT NULL
+ * );
+ * </code></pre>
+ *
+ * @author <a href="mailto:wglass@forio.com">Will Glass-Husain</a>
+ * @author <a href="mailto:matt@raibledesigns.com">Matt Raible</a>
+ * @author <a href="mailto:david.kinnvall@alertir.com">David Kinnvall</a>
+ * @author <a href="mailto:paulo.gaspar@krankikom.de">Paulo Gaspar</a>
+ * @author <a href="mailto:lachiewicz@plusnet.pl">Sylwester Lachiewicz</a>
+ * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
+ * @version $Id$
+ * @since 1.5
+ */
+public class DataSourceResourceLoader extends ResourceLoader
+{
+ private String dataSourceName;
+ private String tableName;
+ private String keyColumn;
+ private String templateColumn;
+ private String timestampColumn;
+ private InitialContext ctx;
+ private DataSource dataSource;
+
+ /*
+ Keep connection and prepared statements open. It's not just an optimization:
+ For several engines, the connection, and/or the statement, and/or the result set
+ must be kept open for the reader to be valid.
+ */
+ private Connection connection = null;
+ private PreparedStatement templatePrepStatement = null;
+ private PreparedStatement timestampPrepStatement = null;
+
+ private static class SelfCleaningReader extends FilterReader
+ {
+ private ResultSet resultSet;
+
+ public SelfCleaningReader(Reader reader, ResultSet resultSet)
+ {
+ super(reader);
+ this.resultSet = resultSet;
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ super.close();
+ try
+ {
+ resultSet.close();
+ }
+ catch (RuntimeException re)
+ {
+ throw re;
+ }
+ catch (Exception e)
+ {
+ // ignore
+ }
+ }
+ }
+
+ /**
+ * @see ResourceLoader#init(org.apache.velocity.util.ExtProperties)
+ */
+ @Override
+ public void init(ExtProperties configuration)
+ {
+ dataSourceName = StringUtils.trim(configuration.getString("datasource_url"));
+ tableName = StringUtils.trim(configuration.getString("resource.table"));
+ keyColumn = StringUtils.trim(configuration.getString("resource.key_column"));
+ templateColumn = StringUtils.trim(configuration.getString("resource.template_column"));
+ timestampColumn = StringUtils.trim(configuration.getString("resource.timestamp_column"));
+
+ if (dataSource != null)
+ {
+ log.debug("DataSourceResourceLoader: using dataSource instance with table \"{}\"", tableName);
+ log.debug("DataSourceResourceLoader: using columns \"{}\", \"{}\" and \"{}\"", keyColumn, templateColumn, timestampColumn);
+
+ log.trace("DataSourceResourceLoader initialized.");
+ }
+ else if (dataSourceName != null)
+ {
+ log.debug("DataSourceResourceLoader: using \"{}\" datasource with table \"{}\"", dataSourceName, tableName);
+ log.debug("DataSourceResourceLoader: using columns \"{}\", \"{}\" and \"{}\"", keyColumn, templateColumn, timestampColumn);
+
+ log.trace("DataSourceResourceLoader initialized.");
+ }
+ else
+ {
+ String msg = "DataSourceResourceLoader not properly initialized. No DataSource was identified.";
+ log.error(msg);
+ throw new RuntimeException(msg);
+ }
+ }
+
+ /**
+ * Set the DataSource used by this resource loader. Call this as an alternative to
+ * specifying the data source name via properties.
+ * @param dataSource The data source for this ResourceLoader.
+ */
+ public void setDataSource(final DataSource dataSource)
+ {
+ this.dataSource = dataSource;
+ }
+
+ /**
+ * @see ResourceLoader#isSourceModified(org.apache.velocity.runtime.resource.Resource)
+ */
+ @Override
+ public boolean isSourceModified(final Resource resource)
+ {
+ return (resource.getLastModified() !=
+ readLastModified(resource, "checking timestamp"));
+ }
+
+ /**
+ * @see ResourceLoader#getLastModified(org.apache.velocity.runtime.resource.Resource)
+ */
+ @Override
+ public long getLastModified(final Resource resource)
+ {
+ return readLastModified(resource, "getting timestamp");
+ }
+
+ /**
+ * Get an InputStream so that the Runtime can build a
+ * template with it.
+ *
+ * @param name name of template
+ * @param encoding asked encoding
+ * @return InputStream containing template
+ * @throws ResourceNotFoundException
+ * @since 2.0
+ */
+ @Override
+ public synchronized Reader getResourceReader(final String name, String encoding)
+ throws ResourceNotFoundException
+ {
+ if (StringUtils.isEmpty(name))
+ {
+ throw new ResourceNotFoundException("DataSourceResourceLoader: Template name was empty or null");
+ }
+
+ ResultSet rs = null;
+ try
+ {
+ checkDBConnection();
+ rs = fetchResult(templatePrepStatement, name);
+
+ if (rs.next())
+ {
+ Reader reader = getReader(rs, templateColumn, encoding);
+ if (reader == null)
+ {
+ throw new ResourceNotFoundException("DataSourceResourceLoader: "
+ + "template column for '"
+ + name + "' is null");
+ }
+ return new SelfCleaningReader(reader, rs);
+ }
+ else
+ {
+ throw new ResourceNotFoundException("DataSourceResourceLoader: "
+ + "could not find resource '"
+ + name + "'");
+
+ }
+ }
+ catch (SQLException | NamingException sqle)
+ {
+ String msg = "DataSourceResourceLoader: database problem while getting resource '"
+ + name + "': ";
+
+ log.error(msg, sqle);
+ throw new ResourceNotFoundException(msg);
+ }
+ }
+
+ /**
+ * Fetches the last modification time of the resource
+ *
+ * @param resource Resource object we are finding timestamp of
+ * @param operation string for logging, indicating caller's intention
+ *
+ * @return timestamp as long
+ */
+ private long readLastModified(final Resource resource, final String operation)
+ {
+ long timeStamp = 0;
+
+ /* get the template name from the resource */
+ String name = resource.getName();
+ if (name == null || name.length() == 0)
+ {
+ String msg = "DataSourceResourceLoader: Template name was empty or null";
+ log.error(msg);
+ throw new NullPointerException(msg);
+ }
+ else
+ {
+ ResultSet rs = null;
+
+ try
+ {
+ checkDBConnection();
+ rs = fetchResult(timestampPrepStatement, name);
+
+ if (rs.next())
+ {
+ Timestamp ts = rs.getTimestamp(timestampColumn);
+ timeStamp = ts != null ? ts.getTime() : 0;
+ }
+ else
+ {
+ String msg = "DataSourceResourceLoader: could not find resource "
+ + name + " while " + operation;
+ log.error(msg);
+ throw new ResourceNotFoundException(msg);
+ }
+ }
+ catch (SQLException | NamingException sqle)
+ {
+ String msg = "DataSourceResourceLoader: database problem while "
+ + operation + " of '" + name + "': ";
+
+ log.error(msg, sqle);
+ throw new VelocityException(msg, sqle, rsvc.getLogContext().getStackTrace());
+ }
+ finally
+ {
+ closeResultSet(rs);
+ }
+ }
+ return timeStamp;
+ }
+
+ /**
+ * Gets connection to the datasource specified through the configuration
+ * parameters.
+ *
+ */
+ private void openDBConnection() throws NamingException, SQLException
+ {
+ if (dataSource == null)
+ {
+ if (ctx == null)
+ {
+ ctx = new InitialContext();
+ }
+
+ dataSource = (DataSource) ctx.lookup(dataSourceName);
+ }
+
+ if (connection != null)
+ {
+ closeDBConnection();
+ }
+
+ connection = dataSource.getConnection();
+ templatePrepStatement = prepareStatement(connection, templateColumn, tableName, keyColumn);
+ timestampPrepStatement = prepareStatement(connection, timestampColumn, tableName, keyColumn);
+ }
+
+ /**
+ * Checks the connection is valid
+ *
+ */
+ private void checkDBConnection() throws NamingException, SQLException
+ {
+ if (connection == null || !connection.isValid(0))
+ {
+ openDBConnection();
+ }
+ }
+
+ /**
+ * Close DB connection on finalization
+ *
+ * @throws Throwable
+ */
+ @Override
+ protected void finalize()
+ throws Throwable
+ {
+ closeDBConnection();
+ }
+
+ /**
+ * Closes the prepared statements and the connection to the datasource
+ */
+ private void closeDBConnection()
+ {
+ if (templatePrepStatement != null)
+ {
+ try
+ {
+ templatePrepStatement.close();
+ }
+ catch (RuntimeException re)
+ {
+ throw re;
+ }
+ catch (SQLException e)
+ {
+ // ignore
+ }
+ finally
+ {
+ templatePrepStatement = null;
+ }
+ }
+ if (timestampPrepStatement != null)
+ {
+ try
+ {
+ timestampPrepStatement.close();
+ }
+ catch (RuntimeException re)
+ {
+ throw re;
+ }
+ catch (SQLException e)
+ {
+ // ignore
+ }
+ finally
+ {
+ timestampPrepStatement = null;
+ }
+ }
+ if (connection != null)
+ {
+ try
+ {
+ connection.close();
+ }
+ catch (RuntimeException re)
+ {
+ throw re;
+ }
+ catch (SQLException e)
+ {
+ // ignore
+ }
+ finally
+ {
+ connection = null;
+ }
+ }
+ }
+
+ /**
+ * Closes the result set.
+ */
+ private void closeResultSet(final ResultSet rs)
+ {
+ if (rs != null)
+ {
+ try
+ {
+ rs.close();
+ }
+ catch (RuntimeException re)
+ {
+ throw re;
+ }
+ catch (Exception e)
+ {
+ // ignore
+ }
+ }
+ }
+
+ /**
+ * Creates the following PreparedStatement query :
+ * <br>
+ * SELECT <i>columnNames</i> FROM <i>tableName</i> WHERE <i>keyColumn</i>
+ * = '<i>templateName</i>'
+ * <br>
+ * where <i>keyColumn</i> is a class member set in init()
+ *
+ * @param conn connection to datasource
+ * @param columnNames columns to fetch from datasource
+ * @param tableName table to fetch from
+ * @param keyColumn column whose value should match templateName
+ * @return PreparedStatement
+ * @throws SQLException
+ */
+ protected PreparedStatement prepareStatement(
+ final Connection conn,
+ final String columnNames,
+ final String tableName,
+ final String keyColumn
+ ) throws SQLException
+ {
+ PreparedStatement ps = conn.prepareStatement("SELECT " + columnNames + " FROM "+ tableName + " WHERE " + keyColumn + " = ?");
+ return ps;
+ }
+
+ /**
+ * Fetches the result for a given template name.
+ * Inherit this method if there is any calculation to perform on the template name.
+ *
+ * @param ps target prepared statement
+ * @param templateName input template name
+ * @return result set
+ * @throws SQLException
+ */
+ protected ResultSet fetchResult(
+ final PreparedStatement ps,
+ final String templateName
+ ) throws SQLException
+ {
+ ps.setString(1, templateName);
+ return ps.executeQuery();
+ }
+
+ /**
+ * Gets a reader from a result set's column
+ * @param resultSet
+ * @param column
+ * @param encoding
+ * @return reader
+ * @throws SQLException
+ */
+ protected Reader getReader(ResultSet resultSet, String column, String encoding)
+ throws SQLException
+ {
+ return resultSet.getCharacterStream(column);
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/FileResourceLoader.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/FileResourceLoader.java
new file mode 100644
index 00000000..ba9e5f98
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/FileResourceLoader.java
@@ -0,0 +1,373 @@
+package org.apache.velocity.runtime.resource.loader;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.resource.Resource;
+import org.apache.velocity.util.ExtProperties;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+
+/**
+ * A loader for templates stored on the file system. Treats the template
+ * as relative to the configured root path. If the root path is empty
+ * treats the template name as an absolute path.
+ *
+ * @author <a href="mailto:wglass@forio.com">Will Glass-Husain</a>
+ * @author <a href="mailto:mailmur@yahoo.com">Aki Nieminen</a>
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @version $Id$
+ */
+public class FileResourceLoader extends ResourceLoader
+{
+ /**
+ * The paths to search for templates.
+ */
+ private List<String> paths = new ArrayList<>();
+
+ /**
+ * Used to map the path that a template was found on
+ * so that we can properly check the modification
+ * times of the files. This is synchronizedMap
+ * instance.
+ */
+ private Map<String, String> templatePaths = Collections.synchronizedMap(new HashMap<>());
+
+ /**
+ * @see ResourceLoader#init(org.apache.velocity.util.ExtProperties)
+ */
+ @Override
+ public void init(ExtProperties configuration)
+ {
+ log.trace("FileResourceLoader: initialization starting.");
+
+ paths.addAll( configuration.getVector(RuntimeConstants.RESOURCE_LOADER_PATHS) );
+
+ // trim spaces from all paths
+ for (ListIterator<String> it = paths.listIterator(); it.hasNext(); )
+ {
+ String path = StringUtils.trim(it.next());
+ it.set(path);
+ log.debug("FileResourceLoader: adding path '{}'", path);
+ }
+ log.trace("FileResourceLoader: initialization complete.");
+ }
+
+ /**
+ * Get a Reader so that the Runtime can build a
+ * template with it.
+ *
+ * @param templateName name of template to get
+ * @return Reader containing the template
+ * @throws ResourceNotFoundException if template not found
+ * in the file template path.
+ * @since 2.0
+ */
+ @Override
+ public Reader getResourceReader(String templateName, String encoding)
+ throws ResourceNotFoundException
+ {
+ /*
+ * Make sure we have a valid templateName.
+ */
+ if (org.apache.commons.lang3.StringUtils.isEmpty(templateName))
+ {
+ /*
+ * If we don't get a properly formed templateName then
+ * there's not much we can do. So we'll forget about
+ * trying to search any more paths for the template.
+ */
+ throw new ResourceNotFoundException(
+ "Need to specify a file name or file path!");
+ }
+
+ String template = FilenameUtils.normalize( templateName, true );
+ if ( template == null || template.length() == 0 )
+ {
+ String msg = "File resource error: argument " + template +
+ " contains .. and may be trying to access " +
+ "content outside of template root. Rejected.";
+
+ log.error("FileResourceLoader: {}", msg);
+
+ throw new ResourceNotFoundException ( msg );
+ }
+
+ int size = paths.size();
+ for (String path : paths)
+ {
+ InputStream rawStream = null;
+ Reader reader = null;
+
+ try
+ {
+ rawStream = findTemplate(path, template);
+ if (rawStream != null)
+ {
+ reader = buildReader(rawStream, encoding);
+ }
+ }
+ catch (IOException ioe)
+ {
+ closeQuiet(rawStream);
+ String msg = "Exception while loading Template " + template;
+ log.error(msg, ioe);
+ throw new VelocityException(msg, ioe, rsvc.getLogContext().getStackTrace());
+ }
+ if (reader != null)
+ {
+ /*
+ * Store the path that this template came
+ * from so that we can check its modification
+ * time.
+ */
+ templatePaths.put(templateName, path);
+ return reader;
+ }
+ }
+
+ /*
+ * We have now searched all the paths for
+ * templates and we didn't find anything so
+ * throw an exception.
+ */
+ throw new ResourceNotFoundException("FileResourceLoader: cannot find " + template);
+ }
+
+ /**
+ * Overrides superclass for better performance.
+ * @since 1.6
+ */
+ @Override
+ public boolean resourceExists(String name)
+ {
+ if (name == null)
+ {
+ return false;
+ }
+ name = FilenameUtils.normalize(name);
+ if (name == null || name.length() == 0)
+ {
+ return false;
+ }
+
+ int size = paths.size();
+ for (String path : paths)
+ {
+ try
+ {
+ File file = getFile(path, name);
+ if (file.canRead())
+ {
+ return true;
+ }
+ }
+ catch (Exception ioe)
+ {
+ log.debug("Exception while checking for template {}", name);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Try to find a template given a normalized path.
+ *
+ * @param path a normalized path
+ * @param template name of template to find
+ * @return InputStream input stream that will be parsed
+ *
+ */
+ private InputStream findTemplate(final String path, final String template)
+ throws IOException
+ {
+ try
+ {
+ File file = getFile(path, template);
+
+ if (file.canRead())
+ {
+ FileInputStream fis = null;
+ try
+ {
+ fis = new FileInputStream(file.getAbsolutePath());
+ return fis;
+ }
+ catch (IOException e)
+ {
+ closeQuiet(fis);
+ throw e;
+ }
+ }
+ else
+ {
+ return null;
+ }
+ }
+ catch(FileNotFoundException fnfe)
+ {
+ /*
+ * log and convert to a general Velocity ResourceNotFoundException
+ */
+ return null;
+ }
+ }
+
+ private void closeQuiet(final InputStream is)
+ {
+ if (is != null)
+ {
+ try
+ {
+ is.close();
+ }
+ catch(IOException ioe)
+ {
+ // Ignore
+ }
+ }
+ }
+
+ /**
+ * How to keep track of all the modified times
+ * across the paths. Note that a file might have
+ * appeared in a directory which is earlier in the
+ * path; so we should search the path and see if
+ * the file we find that way is the same as the one
+ * that we have cached.
+ * @param resource
+ * @return True if the source has been modified.
+ */
+ @Override
+ public boolean isSourceModified(Resource resource)
+ {
+ /*
+ * we assume that the file needs to be reloaded;
+ * if we find the original file and it's unchanged,
+ * then we'll flip this.
+ */
+ boolean modified = true;
+
+ String fileName = resource.getName();
+ String path = templatePaths.get(fileName);
+ File currentFile = null;
+
+ for (int i = 0; currentFile == null && i < paths.size(); i++)
+ {
+ String testPath = (String) paths.get(i);
+ File testFile = getFile(testPath, fileName);
+ if (testFile.canRead())
+ {
+ currentFile = testFile;
+ }
+ }
+ File file = getFile(path, fileName);
+ if (currentFile == null || !file.exists())
+ {
+ /*
+ * noop: if the file is missing now (either the cached
+ * file is gone, or the file can no longer be found)
+ * then we leave modified alone (it's set to true); a
+ * reload attempt will be done, which will either use
+ * a new template or fail with an appropriate message
+ * about how the file couldn't be found.
+ */
+ }
+ else if (currentFile.equals(file) && file.canRead())
+ {
+ /*
+ * if only if currentFile is the same as file and
+ * file.lastModified() is the same as
+ * resource.getLastModified(), then we should use the
+ * cached version.
+ */
+ modified = (file.lastModified() != resource.getLastModified());
+ }
+
+ /*
+ * rsvc.debug("isSourceModified for " + fileName + ": " + modified);
+ */
+ return modified;
+ }
+
+ /**
+ * @see ResourceLoader#getLastModified(org.apache.velocity.runtime.resource.Resource)
+ */
+ @Override
+ public long getLastModified(Resource resource)
+ {
+ String path = templatePaths.get(resource.getName());
+ File file = getFile(path, resource.getName());
+
+ if (file.canRead())
+ {
+ return file.lastModified();
+ }
+ else
+ {
+ return 0;
+ }
+ }
+
+
+ /**
+ * Create a File based on either a relative path if given, or absolute path otherwise
+ */
+ private File getFile(String path, String template)
+ {
+
+ File file = null;
+
+ if("".equals(path))
+ {
+ file = new File( template );
+ }
+ else
+ {
+ /*
+ * if a / leads off, then just nip that :)
+ */
+ if (template.startsWith("/"))
+ {
+ template = template.substring(1);
+ }
+
+ file = new File ( path, template );
+ }
+
+ return file;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/JarHolder.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/JarHolder.java
new file mode 100644
index 00000000..3ccdc944
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/JarHolder.java
@@ -0,0 +1,169 @@
+package org.apache.velocity.runtime.resource.loader;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.RuntimeServices;
+
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.JarURLConnection;
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+/**
+ * A small wrapper around a Jar
+ *
+ * @author <a href="mailto:daveb@miceda-data.com">Dave Bryson</a>
+ * @version $Id$
+ */
+public class JarHolder
+{
+ private String urlpath = null;
+ private JarFile theJar = null;
+ private JarURLConnection conn = null;
+
+ private Logger log = null;
+
+ /**
+ * @param rs
+ * @param urlpath
+ * @param log
+ */
+ public JarHolder( RuntimeServices rs, String urlpath, Logger log )
+ {
+ this.log = log;
+
+ this.urlpath=urlpath;
+ init();
+
+ log.debug("JarHolder: initialized JAR: {}", urlpath);
+ }
+
+ /**
+ *
+ */
+ public void init()
+ {
+ try
+ {
+ log.debug("JarHolder: attempting to connect to {}", urlpath);
+
+ URL url = new URL( urlpath );
+ conn = (JarURLConnection) url.openConnection();
+ conn.setAllowUserInteraction(false);
+ conn.setDoInput(true);
+ conn.setDoOutput(false);
+ conn.connect();
+ theJar = conn.getJarFile();
+ }
+ catch (IOException ioe)
+ {
+ String msg = "JarHolder: error establishing connection to JAR at \""
+ + urlpath + "\"";
+ log.error(msg, ioe);
+ throw new VelocityException(msg, ioe);
+ }
+ }
+
+ /**
+ *
+ */
+ public void close()
+ {
+ try
+ {
+ theJar.close();
+ }
+ catch ( Exception e )
+ {
+ String msg = "JarHolder: error closing the JAR file";
+ log.error(msg, e);
+ throw new VelocityException(msg, e);
+ }
+ theJar = null;
+ conn = null;
+
+ log.trace("JarHolder: JAR file closed");
+ }
+
+ /**
+ * @param theentry
+ * @return The requested resource.
+ * @throws ResourceNotFoundException
+ */
+ public InputStream getResource( String theentry )
+ throws ResourceNotFoundException {
+ InputStream data = null;
+
+ try
+ {
+ JarEntry entry = theJar.getJarEntry( theentry );
+
+ if ( entry != null )
+ {
+ data = theJar.getInputStream( entry );
+ }
+ }
+ catch(Exception fnfe)
+ {
+ log.error("JarHolder: getResource() error", fnfe);
+ throw new ResourceNotFoundException(fnfe);
+ }
+
+ return data;
+ }
+
+ /**
+ * @return The entries of the jar as a hashtable.
+ */
+ public Map<String, String> getEntries()
+ {
+ Map<String, String> allEntries = new HashMap<>(559);
+
+ Enumeration<JarEntry> all = theJar.entries();
+ while ( all.hasMoreElements() )
+ {
+ JarEntry je = all.nextElement();
+
+ // We don't map plain directory entries
+ if ( !je.isDirectory() )
+ {
+ allEntries.put( je.getName(), this.urlpath );
+ }
+ }
+ return allEntries;
+ }
+
+ /**
+ * @return The URL path of this jar holder.
+ */
+ public String getUrlPath()
+ {
+ return urlpath;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/JarResourceLoader.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/JarResourceLoader.java
new file mode 100644
index 00000000..23db3743
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/JarResourceLoader.java
@@ -0,0 +1,263 @@
+package org.apache.velocity.runtime.resource.loader;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.resource.Resource;
+import org.apache.velocity.util.ExtProperties;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.util.HashMap;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+
+/**
+ * <p>
+ * ResourceLoader to load templates from multiple Jar files.
+ * </p>
+ * <p>
+ * The configuration of the JarResourceLoader is straightforward -
+ * You simply add the JarResourceLoader to the configuration via
+ * </p>
+ * <pre><code>
+ * resource.loaders = jar
+ * resource.loader.jar.class = org.apache.velocity.runtime.resource.loader.JarResourceLoader
+ * resource.loader.jar.path = list of JAR &lt;URL&gt;s
+ * </code></pre>
+ *
+ * <p> So for example, if you had a jar file on your local filesystem, you could simply do</p>
+ * <pre><code>
+ * resource.loader.jar.path = jar:file:/opt/myfiles/jar1.jar
+ * </code></pre>
+ * <p> Note that jar specification for the <code>.path</code> configuration property
+ * conforms to the same rules for the java.net.JarUrlConnection class.
+ * </p>
+ *
+ * <p> For a working example, see the unit test case,
+ * org.apache.velocity.test.MultiLoaderTestCase class
+ * </p>
+ *
+ * @author <a href="mailto:mailmur@yahoo.com">Aki Nieminen</a>
+ * @author <a href="mailto:daveb@miceda-data.com">Dave Bryson</a>
+ * @version $Id$
+ */
+public class JarResourceLoader extends ResourceLoader
+{
+ /**
+ * Maps entries to the parent JAR File
+ * Key = the entry *excluding* plain directories
+ * Value = the JAR URL
+ */
+ private Map<String, String> entryDirectory = new HashMap<>(559);
+
+ /**
+ * Maps JAR URLs to the actual JAR
+ * Key = the JAR URL
+ * Value = the JAR
+ */
+ private Map<String, JarHolder> jarfiles = new HashMap<>(89);
+
+ /**
+ * Called by Velocity to initialize the loader
+ * @param configuration
+ */
+ @Override
+ public void init(ExtProperties configuration)
+ {
+ log.trace("JarResourceLoader: initialization starting.");
+
+ List<String> paths = configuration.getList(RuntimeConstants.RESOURCE_LOADER_PATHS);
+
+ if (paths != null)
+ {
+ log.debug("JarResourceLoader # of paths: {}", paths.size() );
+
+ for (ListIterator<String> it = paths.listIterator(); it.hasNext(); )
+ {
+ String jar = StringUtils.trim(it.next());
+ it.set(jar);
+ loadJar(jar);
+ }
+ }
+
+ log.trace("JarResourceLoader: initialization complete.");
+ }
+
+ private void loadJar( String path )
+ {
+ log.debug("JarResourceLoader: trying to load \"{}\"", path);
+
+ // Check path information
+ if ( path == null )
+ {
+ String msg = "JarResourceLoader: can not load JAR - JAR path is null";
+ log.error(msg);
+ throw new RuntimeException(msg);
+ }
+ if ( !path.startsWith("jar:") )
+ {
+ String msg = "JarResourceLoader: JAR path must start with jar: -> see java.net.JarURLConnection for information";
+ log.error(msg);
+ throw new RuntimeException(msg);
+ }
+ if (!path.contains("!/"))
+ {
+ path += "!/";
+ }
+
+ // Close the jar if it's already open
+ // this is useful for a reload
+ closeJar( path );
+
+ // Create a new JarHolder
+ JarHolder temp = new JarHolder( rsvc, path, log );
+ // Add it's entries to the entryCollection
+ addEntries(temp.getEntries());
+ // Add it to the Jar table
+ jarfiles.put(temp.getUrlPath(), temp);
+ }
+
+ /**
+ * Closes a Jar file and set its URLConnection
+ * to null.
+ */
+ private void closeJar( String path )
+ {
+ if ( jarfiles.containsKey(path) )
+ {
+ JarHolder theJar = jarfiles.get(path);
+ theJar.close();
+ }
+ }
+
+ /**
+ * Copy all the entries into the entryDirectory
+ * It will overwrite any duplicate keys.
+ */
+ private void addEntries( Map<String, String> entries )
+ {
+ entryDirectory.putAll( entries );
+ }
+
+ /**
+ * Get a Reader so that the Runtime can build a
+ * template with it.
+ *
+ * @param source name of template to get
+ * @param encoding asked encoding
+ * @return InputStream containing the template
+ * @throws ResourceNotFoundException if template not found
+ * in the file template path.
+ * @since 2.0
+ */
+ @Override
+ public Reader getResourceReader(String source, String encoding )
+ throws ResourceNotFoundException
+ {
+ Reader result = null;
+
+ if (org.apache.commons.lang3.StringUtils.isEmpty(source))
+ {
+ throw new ResourceNotFoundException("Need to have a resource!");
+ }
+
+ String normalizedPath = FilenameUtils.normalize( source, true );
+
+ if ( normalizedPath == null || normalizedPath.length() == 0 )
+ {
+ String msg = "JAR resource error: argument " + normalizedPath +
+ " contains .. and may be trying to access " +
+ "content outside of template root. Rejected.";
+
+ log.error( "JarResourceLoader: {}", msg );
+
+ throw new ResourceNotFoundException ( msg );
+ }
+
+ /*
+ * if a / leads off, then just nip that :)
+ */
+ if ( normalizedPath.startsWith("/") )
+ {
+ normalizedPath = normalizedPath.substring(1);
+ }
+
+ if ( entryDirectory.containsKey( normalizedPath ) )
+ {
+ String jarurl = entryDirectory.get( normalizedPath );
+
+ if ( jarfiles.containsKey( jarurl ) )
+ {
+ JarHolder holder = (JarHolder)jarfiles.get( jarurl );
+ InputStream rawStream = holder.getResource( normalizedPath );
+ try
+ {
+ return buildReader(rawStream, encoding);
+ }
+ catch (Exception e)
+ {
+ if (rawStream != null)
+ {
+ try
+ {
+ rawStream.close();
+ }
+ catch (IOException ioe) {}
+ }
+ String msg = "JAR resource error: Exception while loading " + source;
+ log.error(msg, e);
+ throw new VelocityException(msg, e, rsvc.getLogContext().getStackTrace());
+ }
+ }
+ }
+
+ throw new ResourceNotFoundException( "JarResourceLoader Error: cannot find resource " +
+ source );
+
+ }
+
+ // TODO: SHOULD BE DELEGATED TO THE JARHOLDER
+
+ /**
+ * @see ResourceLoader#isSourceModified(org.apache.velocity.runtime.resource.Resource)
+ */
+ @Override
+ public boolean isSourceModified(Resource resource)
+ {
+ return true;
+ }
+
+ /**
+ * @see ResourceLoader#getLastModified(org.apache.velocity.runtime.resource.Resource)
+ */
+ @Override
+ public long getLastModified(Resource resource)
+ {
+ return 0;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/ResourceLoader.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/ResourceLoader.java
new file mode 100644
index 00000000..9a2d5643
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/ResourceLoader.java
@@ -0,0 +1,324 @@
+package org.apache.velocity.runtime.resource.loader;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.io.UnicodeInputStream;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.resource.Resource;
+import org.apache.velocity.runtime.resource.ResourceCacheImpl;
+import org.apache.velocity.util.ExtProperties;
+
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * This is abstract class the all text resource loaders should
+ * extend.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @author <a href="mailto:claude.brisson@gmail.com">Claude Brisson</a>
+ * @version $Id$
+ */
+public abstract class ResourceLoader
+{
+ /**
+ * Does this loader want templates produced with it
+ * cached in the Runtime.
+ */
+ protected boolean isCachingOn = false;
+
+ /**
+ * This property will be passed on to the templates
+ * that are created with this loader.
+ */
+ protected long modificationCheckInterval = 2;
+
+ /**
+ * Class name for this loader, for logging/debuggin
+ * purposes.
+ */
+ protected String className = null;
+
+ protected RuntimeServices rsvc = null;
+ protected Logger log = null;
+
+ /**
+ * This initialization is used by all resource
+ * loaders and must be called to set up common
+ * properties shared by all resource loaders
+ *
+ * @param rs
+ * @param configuration
+ */
+ public void commonInit(RuntimeServices rs, ExtProperties configuration)
+ {
+ this.rsvc = rs;
+ String loaderName = configuration.getString(RuntimeConstants.RESOURCE_LOADER_IDENTIFIER);
+ log = rsvc.getLog("loader." + (loaderName == null ? this.getClass().getSimpleName() : loaderName));
+
+ /*
+ * these two properties are not required for all loaders.
+ * For example, for ClasspathLoader, what would cache mean?
+ * so adding default values which I think are the safest
+ *
+ * don't cache, and modCheckInterval irrelevant...
+ */
+
+ try
+ {
+ isCachingOn = configuration.getBoolean(RuntimeConstants.RESOURCE_LOADER_CACHE, false);
+ }
+ catch (Exception e)
+ {
+ isCachingOn = false;
+ String msg = "Exception parsing cache setting: " + configuration.getString(RuntimeConstants.RESOURCE_LOADER_CACHE);
+ log.error(msg, e);
+ throw new VelocityException(msg, e);
+ }
+ try
+ {
+ modificationCheckInterval = configuration.getLong(RuntimeConstants.RESOURCE_LOADER_CHECK_INTERVAL, 0);
+ }
+ catch (Exception e)
+ {
+ modificationCheckInterval = 0;
+ String msg = "Exception parsing modificationCheckInterval setting: " + RuntimeConstants.RESOURCE_LOADER_CHECK_INTERVAL;
+ log.error(msg, e);
+ throw new VelocityException(msg, e);
+ }
+
+ /*
+ * this is a must!
+ */
+ className = ResourceCacheImpl.class.getName();
+ try
+ {
+ className = configuration.getString(RuntimeConstants.RESOURCE_LOADER_CLASS, className);
+ }
+ catch (Exception e)
+ {
+ String msg = "Exception retrieving resource cache class name";
+ log.error(msg, e);
+ throw new VelocityException(msg, e);
+ }
+ }
+
+ /**
+ * Initialize the template loader with a
+ * a resources class.
+ *
+ * @param configuration
+ */
+ public abstract void init(ExtProperties configuration);
+
+ /**
+ * Get the Reader that the Runtime will parse
+ * to create a template.
+ *
+ * @param source
+ * @param encoding
+ * @return The reader for the requested resource.
+ * @throws ResourceNotFoundException
+ * @since 2.0
+ */
+ public abstract Reader getResourceReader(String source, String encoding)
+ throws ResourceNotFoundException;
+
+ /**
+ * Given a template, check to see if the source of InputStream
+ * has been modified.
+ *
+ * @param resource
+ * @return True if the resource has been modified.
+ */
+ public abstract boolean isSourceModified(Resource resource);
+
+ /**
+ * Get the last modified time of the InputStream source
+ * that was used to create the template. We need the template
+ * here because we have to extract the name of the template
+ * in order to locate the InputStream source.
+ *
+ * @param resource
+ * @return Time in millis when the resource has been modified.
+ */
+ public abstract long getLastModified(Resource resource);
+
+ /**
+ * Return the class name of this resource Loader
+ *
+ * @return Class name of the resource loader.
+ */
+ public String getClassName()
+ {
+ return className;
+ }
+
+ /**
+ * Set the caching state. If true, then this loader
+ * would like the Runtime to cache templates that
+ * have been created with InputStreams provided
+ * by this loader.
+ *
+ * @param value
+ */
+ public void setCachingOn(boolean value)
+ {
+ isCachingOn = value;
+ }
+
+ /**
+ * The Runtime uses this to find out whether this
+ * template loader wants the Runtime to cache
+ * templates created with InputStreams provided
+ * by this loader.
+ *
+ * @return True if this resource loader caches.
+ */
+ public boolean isCachingOn()
+ {
+ return isCachingOn;
+ }
+
+ /**
+ * Set the interval at which the InputStream source
+ * should be checked for modifications.
+ *
+ * @param modificationCheckInterval
+ */
+ public void setModificationCheckInterval(long modificationCheckInterval)
+ {
+ this.modificationCheckInterval = modificationCheckInterval;
+ }
+
+ /**
+ * Get the interval at which the InputStream source
+ * should be checked for modifications.
+ *
+ * @return The modification check interval.
+ */
+ public long getModificationCheckInterval()
+ {
+ return modificationCheckInterval;
+ }
+
+ /**
+ * Check whether any given resource exists. This is not really
+ * a very efficient test and it can and should be overridden in the
+ * subclasses extending ResourceLoader2.
+ *
+ * @param resourceName The name of a resource.
+ * @return true if a resource exists and can be accessed.
+ * @since 1.6
+ */
+ public boolean resourceExists(final String resourceName)
+ {
+ Reader reader = null;
+ try
+ {
+ reader = getResourceReader(resourceName, null);
+ }
+ catch (ResourceNotFoundException e)
+ {
+ log.debug("Could not load resource '{}' from ResourceLoader {}",
+ resourceName, this.getClass().getName());
+ }
+ finally
+ {
+ try
+ {
+ if (reader != null)
+ {
+ reader.close();
+ }
+ }
+ catch (Exception e)
+ {
+ String msg = "While closing InputStream for resource '" +
+ resourceName + "' from ResourceLoader " +
+ this.getClass().getName();
+ log.error(msg, e);
+ throw new VelocityException(msg, e);
+ }
+ }
+ return (reader != null);
+ }
+
+ /**
+ * Builds a Reader given a raw InputStream and an encoding. Should be use
+ * by every subclass that whishes to accept optional BOMs in resources.
+ * This method does *not* close the given input stream whenever an exception is thrown.
+ *
+ * @param rawStream The raw input stream.
+ * @param encoding The asked encoding.
+ * @return found reader
+ * @throws IOException
+ * @throws UnsupportedEncodingException
+ * @since 2.0
+ */
+ protected Reader buildReader(InputStream rawStream, String encoding)
+ throws IOException
+ {
+ UnicodeInputStream inputStream = new UnicodeInputStream(rawStream);
+ /*
+ * Check encoding
+ */
+ String foundEncoding = inputStream.getEncodingFromStream();
+ if (foundEncoding != null && encoding != null && !UnicodeInputStream.sameEncoding(foundEncoding, encoding))
+ {
+ log.warn("Found BOM encoding '{}' differs from asked encoding: '{}' - using BOM encoding to read resource.", foundEncoding, encoding);
+ encoding = foundEncoding;
+ }
+ if (encoding == null)
+ {
+ if (foundEncoding == null)
+ {
+ encoding = rsvc.getString(RuntimeConstants.INPUT_ENCODING);
+ } else
+ {
+ encoding = foundEncoding;
+ }
+ }
+
+ try
+ {
+ return new InputStreamReader(inputStream, encoding);
+ }
+ catch (UnsupportedEncodingException uee)
+ {
+ try
+ {
+ inputStream.close();
+ }
+ catch (IOException ioe) {}
+ throw uee;
+ }
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/ResourceLoaderFactory.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/ResourceLoaderFactory.java
new file mode 100644
index 00000000..09f1c73e
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/ResourceLoaderFactory.java
@@ -0,0 +1,62 @@
+package org.apache.velocity.runtime.resource.loader;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.util.ClassUtils;
+
+/**
+ * Factory to grab a template loader.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @version $Id$
+ */
+public class ResourceLoaderFactory
+{
+ /**
+ * Gets the loader specified in the configuration file.
+ * @param rs
+ * @param loaderClassName
+ * @return TemplateLoader
+ */
+ public static ResourceLoader getLoader(RuntimeServices rs, String loaderClassName)
+ {
+ ResourceLoader loader = null;
+
+ try
+ {
+ loader = (ResourceLoader) ClassUtils.getNewInstance( loaderClassName );
+
+ rs.getLog().debug("ResourceLoader instantiated: {}", loader.getClass().getName());
+
+ return loader;
+ }
+ // The ugly three strike again: ClassNotFoundException,IllegalAccessException,InstantiationException
+ catch(Exception e)
+ {
+ String msg = "Problem instantiating the template loader: "+loaderClassName+"." + System.lineSeparator() +
+ "Look at your properties file and make sure the" + System.lineSeparator() +
+ "name of the template loader is correct.";
+ rs.getLog().error(msg, e);
+ throw new VelocityException(msg, e);
+ }
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/StringResourceLoader.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/StringResourceLoader.java
new file mode 100644
index 00000000..fd5d8c4a
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/StringResourceLoader.java
@@ -0,0 +1,438 @@
+package org.apache.velocity.runtime.resource.loader;
+
+/*
+ * 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.
+ */
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.resource.Resource;
+import org.apache.velocity.runtime.resource.util.StringResource;
+import org.apache.velocity.runtime.resource.util.StringResourceRepository;
+import org.apache.velocity.runtime.resource.util.StringResourceRepositoryImpl;
+import org.apache.velocity.util.ClassUtils;
+import org.apache.velocity.util.ExtProperties;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.UnsupportedEncodingException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Resource loader that works with Strings. Users should manually add
+ * resources to the repository that is used by the resource loader instance.
+ *
+ * Below is an example configuration for this loader.
+ * Note that 'repository.class' is not necessary;
+ * if not provided, the factory will fall back on using
+ * {@link StringResourceRepositoryImpl} as the default.
+ * <pre>
+ * resource.loaders = string
+ * resource.loader.string.description = Velocity StringResource loader
+ * resource.loader.string.class = org.apache.velocity.runtime.resource.loader.StringResourceLoader
+ * resource.loader.string.repository.name = MyRepositoryName (optional, to avoid using the default repository)
+ * resource.loader.string.repository.class = org.apache.velocity.runtime.resource.loader.StringResourceRepositoryImpl
+ * </pre>
+ * Resources can be added to the repository like this:
+ * <pre><code>
+ * StringResourceRepository repo = StringResourceLoader.getRepository();
+ *
+ * String myTemplateName = "/some/imaginary/path/hello.vm";
+ * String myTemplate = "Hi, ${username}... this is some template!";
+ * repo.putStringResource(myTemplateName, myTemplate);
+ * </code></pre>
+ *
+ * After this, the templates can be retrieved as usual.
+ * <br>
+ * <p>If there will be multiple StringResourceLoaders used in an application,
+ * you should consider specifying a 'resource.loader.string.repository.name = foo'
+ * property in order to keep you string resources in a non-default repository.
+ * This can help to avoid conflicts between different frameworks or components
+ * that are using StringResourceLoader.
+ * You can then retrieve your named repository like this:
+ * <pre><code>
+ * StringResourceRepository repo = StringResourceLoader.getRepository("foo");
+ * </code></pre>
+ * <p>and add string resources to the repo just as in the previous example.
+ * </p>
+ * <p>If you have concerns about memory leaks or for whatever reason do not wish
+ * to have your string repository stored statically as a class member, then you
+ * should set 'resource.loader.string.repository.static = false' in your properties.
+ * This will tell the resource loader that the string repository should be stored
+ * in the Velocity application attributes. To retrieve the repository, do:</p>
+ * <pre><code>
+ * StringResourceRepository repo = velocityEngine.getApplicationAttribute("foo");
+ * </code></pre>
+ * <p>If you did not specify a name for the repository, then it will be stored under the
+ * class name of the repository implementation class (for which the default is
+ * 'org.apache.velocity.runtime.resource.util.StringResourceRepositoryImpl').
+ * Incidentally, this is also true for the default statically stored repository.
+ * </p>
+ * <p>Whether your repository is stored statically or in Velocity's application
+ * attributes, you can also manually create and set it prior to Velocity
+ * initialization. For a static repository, you can do something like this:
+ * <pre><code>
+ * StringResourceRepository repo = new MyStringResourceRepository();
+ * repo.magicallyAddSomeStringResources();
+ * StringResourceLoader.setRepository("foo", repo);
+ * </code></pre>
+ * <p>Or for a non-static repository:</p>
+ * <pre><code>
+ * StringResourceRepository repo = new MyStringResourceRepository();
+ * repo.magicallyAddSomeStringResources();
+ * velocityEngine.setApplicationAttribute("foo", repo);
+ * </code></pre>
+ * <p>Then, assuming the 'resource.loader.string.repository.name' property is
+ * set to 'some.name', the StringResourceLoader will use that already created
+ * repository, rather than creating a new one.
+ * </p>
+ *
+ * @author <a href="mailto:eelco.hillenius@openedge.nl">Eelco Hillenius</a>
+ * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
+ * @author Nathan Bubna
+ * @version $Id$
+ * @since 1.5
+ */
+public class StringResourceLoader extends ResourceLoader
+{
+ /**
+ * Key to determine whether the repository should be set as the static one or not.
+ * @since 1.6
+ */
+ public static final String REPOSITORY_STATIC = "repository.static";
+
+ /**
+ * By default, repositories are stored statically (shared across the VM).
+ * @since 1.6
+ */
+ public static final boolean REPOSITORY_STATIC_DEFAULT = true;
+
+ /** Key to look up the repository implementation class. */
+ public static final String REPOSITORY_CLASS = "repository.class";
+
+ /** The default implementation class. */
+ public static final String REPOSITORY_CLASS_DEFAULT =
+ StringResourceRepositoryImpl.class.getName();
+
+ /**
+ * Key to look up the name for the repository to be used.
+ * @since 1.6
+ */
+ public static final String REPOSITORY_NAME = "repository.name";
+
+ /** The default name for string resource repositories
+ * ('org.apache.velocity.runtime.resource.util.StringResourceRepository').
+ * @since 1.6
+ */
+ public static final String REPOSITORY_NAME_DEFAULT =
+ StringResourceRepository.class.getName();
+
+ /** Key to look up the repository char encoding. */
+ public static final String REPOSITORY_ENCODING = "repository.encoding";
+
+ protected static final Map<String, StringResourceRepository> STATIC_REPOSITORIES =
+ Collections.synchronizedMap(new HashMap<>());
+
+ /**
+ * Returns a reference to the default static repository.
+ * @return default static repository
+ */
+ public static StringResourceRepository getRepository()
+ {
+ return getRepository(REPOSITORY_NAME_DEFAULT);
+ }
+
+ /**
+ * Returns a reference to the repository stored statically under the
+ * specified name.
+ * @param name
+ * @return named repository
+ * @since 1.6
+ */
+ public static StringResourceRepository getRepository(String name)
+ {
+ return (StringResourceRepository)STATIC_REPOSITORIES.get(name);
+ }
+
+ /**
+ * Sets the specified {@link StringResourceRepository} in static storage
+ * under the specified name.
+ * @param name
+ * @param repo
+ * @since 1.6
+ */
+ public static void setRepository(String name, StringResourceRepository repo)
+ {
+ STATIC_REPOSITORIES.put(name, repo);
+ }
+
+ /**
+ * Removes the {@link StringResourceRepository} stored under the specified
+ * name.
+ * @param name
+ * @return removed repository
+ * @since 1.6
+ */
+ public static StringResourceRepository removeRepository(String name)
+ {
+ return (StringResourceRepository)STATIC_REPOSITORIES.remove(name);
+ }
+
+ /**
+ * Removes all statically stored {@link StringResourceRepository}s.
+ * @since 1.6
+ */
+ public static void clearRepositories()
+ {
+ STATIC_REPOSITORIES.clear();
+ }
+
+
+ /**
+ * the repository used internally by this resource loader
+ */
+ protected StringResourceRepository repository;
+
+
+ /**
+ * @param configuration
+ * @see ResourceLoader#init(org.apache.velocity.util.ExtProperties)
+ */
+ @Override
+ public void init(final ExtProperties configuration)
+ {
+ log.trace("StringResourceLoader: initialization starting.");
+
+ // get the repository configuration info
+ String repoClass = configuration.getString(REPOSITORY_CLASS, REPOSITORY_CLASS_DEFAULT);
+ String repoName = configuration.getString(REPOSITORY_NAME, REPOSITORY_NAME_DEFAULT);
+ boolean isStatic = configuration.getBoolean(REPOSITORY_STATIC, REPOSITORY_STATIC_DEFAULT);
+ String encoding = configuration.getString(REPOSITORY_ENCODING);
+
+ // look for an existing repository of that name and isStatic setting
+ if (isStatic)
+ {
+ this.repository = getRepository(repoName);
+ if (repository != null)
+ {
+ log.debug("Loaded repository '{}' from static repo store", repoName);
+ }
+ }
+ else
+ {
+ this.repository = (StringResourceRepository)rsvc.getApplicationAttribute(repoName);
+ if (repository != null)
+ {
+ log.debug("Loaded repository '{}' from application attributes", repoName);
+ }
+ }
+
+ if (this.repository == null)
+ {
+ // since there's no repository under the repo name, create a new one
+ this.repository = createRepository(repoClass, encoding);
+
+ // and store it according to the isStatic setting
+ if (isStatic)
+ {
+ setRepository(repoName, this.repository);
+ }
+ else
+ {
+ rsvc.setApplicationAttribute(repoName, this.repository);
+ }
+ }
+ else
+ {
+ // ok, we already have a repo
+ // warn them if they are trying to change the class of the repository
+ if (!this.repository.getClass().getName().equals(repoClass))
+ {
+ log.debug("Cannot change class of string repository '{}' from {} to {}." +
+ " The change will be ignored.",
+ repoName, this.repository.getClass().getName(), repoClass);
+ }
+
+ // allow them to change the default encoding of the repo
+ if (encoding != null &&
+ !this.repository.getEncoding().equals(encoding))
+ {
+ log.debug("Changing the default encoding of string repository '{}' from {} to {}",
+ repoName, this.repository.getEncoding(), encoding);
+ this.repository.setEncoding(encoding);
+ }
+ }
+
+ log.trace("StringResourceLoader: initialization complete.");
+ }
+
+ /**
+ * @param className
+ * @param encoding
+ * @return created repository
+ * @since 1.6
+ */
+ public StringResourceRepository createRepository(final String className,
+ final String encoding)
+ {
+ log.debug("Creating string repository using class {}...", className);
+
+ StringResourceRepository repo;
+ try
+ {
+ repo = (StringResourceRepository) ClassUtils.getNewInstance(className);
+ }
+ catch (ClassNotFoundException cnfe)
+ {
+ throw new VelocityException("Could not find '" + className + "'", cnfe);
+ }
+ catch (IllegalAccessException iae)
+ {
+ throw new VelocityException("Could not access '" + className + "'", iae);
+ }
+ catch (InstantiationException ie)
+ {
+ throw new VelocityException("Could not instantiate '" + className + "'", ie);
+ }
+
+ if (encoding != null)
+ {
+ repo.setEncoding(encoding);
+ }
+ else
+ {
+ repo.setEncoding(RuntimeConstants.ENCODING_DEFAULT);
+ }
+
+ log.debug("Default repository encoding is {}", repo.getEncoding());
+ return repo;
+ }
+
+ /**
+ * Overrides superclass for better performance.
+ * @param name resource name
+ * @return whether resource exists
+ * @since 1.6
+ */
+ @Override
+ public boolean resourceExists(final String name)
+ {
+ if (name == null)
+ {
+ return false;
+ }
+ return (this.repository.getStringResource(name) != null);
+ }
+
+ /**
+ * Get a reader so that the Runtime can build a
+ * template with it.
+ *
+ * @param name name of template to get.
+ * @param encoding asked encoding
+ * @return Reader containing the template.
+ * @throws ResourceNotFoundException Ff template not found
+ * in the RepositoryFactory.
+ * @since 2.0
+ */
+ @Override
+ public Reader getResourceReader(String name, String encoding)
+ throws ResourceNotFoundException
+ {
+ if (StringUtils.isEmpty(name))
+ {
+ throw new ResourceNotFoundException("No template name provided");
+ }
+
+ StringResource resource = this.repository.getStringResource(name);
+
+ if(resource == null)
+ {
+ throw new ResourceNotFoundException("Could not locate resource '" + name + "'");
+ }
+
+ byte [] byteArray = null;
+ InputStream rawStream = null;
+
+ try
+ {
+ byteArray = resource.getBody().getBytes(resource.getEncoding());
+ rawStream = new ByteArrayInputStream(byteArray);
+ return new InputStreamReader(rawStream, resource.getEncoding());
+ }
+ catch(UnsupportedEncodingException ue)
+ {
+ if (rawStream != null)
+ {
+ try
+ {
+ rawStream.close();
+ }
+ catch (IOException ioe) {}
+ }
+ throw new VelocityException("Could not convert String using encoding " + resource.getEncoding(), ue, rsvc.getLogContext().getStackTrace());
+ }
+ }
+
+ /**
+ * @param resource
+ * @return whether resource was modified
+ * @see ResourceLoader#isSourceModified(org.apache.velocity.runtime.resource.Resource)
+ */
+ @Override
+ public boolean isSourceModified(final Resource resource)
+ {
+ StringResource original = null;
+ boolean result = true;
+
+ original = this.repository.getStringResource(resource.getName());
+
+ if (original != null)
+ {
+ result = original.getLastModified() != resource.getLastModified();
+ }
+
+ return result;
+ }
+
+ /**
+ * @param resource
+ * @return last modified timestamp
+ * @see ResourceLoader#getLastModified(org.apache.velocity.runtime.resource.Resource)
+ */
+ @Override
+ public long getLastModified(final Resource resource)
+ {
+ StringResource original = null;
+
+ original = this.repository.getStringResource(resource.getName());
+
+ return (original != null)
+ ? original.getLastModified()
+ : 0;
+ }
+
+}
+
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/URLResourceLoader.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/URLResourceLoader.java
new file mode 100644
index 00000000..81fe33b9
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/URLResourceLoader.java
@@ -0,0 +1,214 @@
+package org.apache.velocity.runtime.resource.loader;
+
+/*
+ * 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.
+ */
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.runtime.resource.Resource;
+import org.apache.velocity.util.ExtProperties;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This is a simple URL-based loader.
+ *
+ * @author <a href="mailto:geirm@apache.org">Geir Magnusson Jr.</a>
+ * @author <a href="mailto:nbubna@apache.org">Nathan Bubna</a>
+ * @version $Id: URLResourceLoader.java 191743 2005-06-21 23:22:20Z dlr $
+ * @since 1.5
+ */
+public class URLResourceLoader extends ResourceLoader
+{
+ private String[] roots = null;
+ protected Map<String, String> templateRoots = null;
+ private int timeout = -1;
+
+ /**
+ * @param configuration
+ * @see ResourceLoader#init(org.apache.velocity.util.ExtProperties)
+ */
+ @Override
+ public void init(ExtProperties configuration)
+ {
+ log.trace("URLResourceLoader: initialization starting.");
+
+ roots = configuration.getStringArray("root");
+ if (log.isDebugEnabled())
+ {
+ for (String root : roots)
+ {
+ log.debug("URLResourceLoader: adding root '{}'", root);
+ }
+ }
+
+ timeout = configuration.getInt("timeout", -1);
+
+ // init the template paths map
+ templateRoots = new HashMap<>();
+
+ log.trace("URLResourceLoader: initialization complete.");
+ }
+
+ /**
+ * Get a Reader so that the Runtime can build a
+ * template with it.
+ *
+ * @param name name of template to fetch bytestream of
+ * @param encoding asked encoding
+ * @return InputStream containing the template
+ * @throws ResourceNotFoundException if template not found
+ * in the file template path.
+ * @since 2.0
+ */
+ @Override
+ public synchronized Reader getResourceReader(String name, String encoding)
+ throws ResourceNotFoundException
+ {
+ if (StringUtils.isEmpty(name))
+ {
+ throw new ResourceNotFoundException("URLResourceLoader: No template name provided");
+ }
+
+ Reader reader = null;
+ Exception exception = null;
+ for (String root : roots)
+ {
+ InputStream rawStream = null;
+ try
+ {
+ URL u = new URL(root + name);
+ URLConnection conn = u.openConnection();
+ conn.setConnectTimeout(timeout);
+ conn.setReadTimeout(timeout);
+ rawStream = conn.getInputStream();
+ reader = buildReader(rawStream, encoding);
+
+ if (reader != null)
+ {
+ log.debug("URLResourceLoader: Found '{}' at '{}'", name, root);
+
+ // save this root for later re-use
+ templateRoots.put(name, root);
+ break;
+ }
+ }
+ catch (IOException ioe)
+ {
+ if (rawStream != null)
+ {
+ try
+ {
+ rawStream.close();
+ }
+ catch (IOException e)
+ {
+ }
+ }
+ log.debug("URLResourceLoader: Exception when looking for '{}' at '{}'", name, root, ioe);
+
+ // only save the first one for later throwing
+ if (exception == null)
+ {
+ exception = ioe;
+ }
+ }
+ }
+
+ // if we never found the template
+ if (reader == null)
+ {
+ String msg;
+ if (exception == null)
+ {
+ msg = "URLResourceLoader: Resource '" + name + "' not found.";
+ }
+ else
+ {
+ msg = exception.getMessage();
+ }
+ // convert to a general Velocity ResourceNotFoundException
+ throw new ResourceNotFoundException(msg);
+ }
+
+ return reader;
+ }
+
+ /**
+ * Checks to see if a resource has been deleted, moved or modified.
+ *
+ * @param resource Resource The resource to check for modification
+ * @return boolean True if the resource has been modified, moved, or unreachable
+ */
+ @Override
+ public boolean isSourceModified(Resource resource)
+ {
+ long fileLastModified = getLastModified(resource);
+ // if the file is unreachable or otherwise changed
+ return fileLastModified == 0 ||
+ fileLastModified != resource.getLastModified();
+ }
+
+ /**
+ * Checks to see when a resource was last modified
+ *
+ * @param resource Resource the resource to check
+ * @return long The time when the resource was last modified or 0 if the file can't be reached
+ */
+ @Override
+ public long getLastModified(Resource resource)
+ {
+ // get the previously used root
+ String name = resource.getName();
+ String root = templateRoots.get(name);
+
+ try
+ {
+ // get a connection to the URL
+ URL u = new URL(root + name);
+ URLConnection conn = u.openConnection();
+ conn.setConnectTimeout(timeout);
+ conn.setReadTimeout(timeout);
+ return conn.getLastModified();
+ }
+ catch (IOException ioe)
+ {
+ // the file is not reachable at its previous address
+ String msg = "URLResourceLoader: '"+name+"' is no longer reachable at '"+root+"'";
+ log.error(msg, ioe);
+ throw new ResourceNotFoundException(msg, ioe, rsvc.getLogContext().getStackTrace());
+ }
+ }
+
+ /**
+ * Returns the current, custom timeout setting. If negative, there is no custom timeout.
+ * @return timeout
+ * @since 1.6
+ */
+ public int getTimeout()
+ {
+ return timeout;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/util/StringResource.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/util/StringResource.java
new file mode 100644
index 00000000..e0887eb6
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/util/StringResource.java
@@ -0,0 +1,108 @@
+package org.apache.velocity.runtime.resource.util;
+
+/*
+ * 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.
+ */
+
+/**
+ * Wrapper for Strings containing templates, allowing to add additional meta
+ * data like timestamps.
+ *
+ * @author <a href="mailto:eelco.hillenius@openedge.nl">Eelco Hillenius</a>
+ * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
+ * @version $Id$
+ * @since 1.5
+ */
+public final class StringResource
+{
+ /** template body */
+ private String body;
+
+ /** encoding */
+ private String encoding;
+
+ /** last modified ts */
+ private long lastModified;
+
+ /**
+ * convenience constructor; sets body to 'body' and sets lastModified to now
+ * @param body
+ * @param encoding
+ */
+ public StringResource(final String body, final String encoding)
+ {
+ setBody(body);
+ setEncoding(encoding);
+ }
+
+ /**
+ * Sets the template body.
+ * @return String containing the template body.
+ */
+ public String getBody()
+ {
+ return body;
+ }
+
+ /**
+ * Returns the modification date of the template.
+ * @return Modification date in milliseconds.
+ */
+ public long getLastModified()
+ {
+ return lastModified;
+ }
+
+ /**
+ * Sets a new value for the template body.
+ * @param body New body value
+ */
+ public void setBody(final String body)
+ {
+ this.body = body;
+ this.lastModified = System.currentTimeMillis();
+ }
+
+ /**
+ * Changes the last modified parameter.
+ * @param lastModified The modification time in millis.
+ */
+ public void setLastModified(final long lastModified)
+ {
+ this.lastModified = lastModified;
+ }
+
+ /**
+ * Returns the encoding of this String resource.
+ *
+ * @return The encoding of this String resource.
+ */
+ public String getEncoding() {
+ return this.encoding;
+ }
+
+ /**
+ * Sets the encoding of this string resource.
+ *
+ * @param encoding The new encoding of this resource.
+ */
+ public void setEncoding(final String encoding)
+ {
+ this.encoding = encoding;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/util/StringResourceRepository.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/util/StringResourceRepository.java
new file mode 100644
index 00000000..961e72a1
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/util/StringResourceRepository.java
@@ -0,0 +1,76 @@
+package org.apache.velocity.runtime.resource.util;
+
+/*
+ * 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.
+ */
+
+/**
+ * A StringResourceRepository functions as a central repository for Velocity templates
+ * stored in Strings.
+ *
+ * @author <a href="mailto:eelco.hillenius@openedge.nl">Eelco Hillenius</a>
+ * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
+ * @version $Id$
+ * @since 1.5
+ */
+public interface StringResourceRepository
+{
+ /**
+ * get the string resource that is stored with given key
+ * @param name String name to retrieve from the repository.
+ * @return A StringResource containing the template.
+ */
+ StringResource getStringResource(String name);
+
+ /**
+ * add a string resource with given key.
+ * @param name The String name to store the template under.
+ * @param body A String containing a template.
+ */
+ void putStringResource(String name, String body);
+
+ /**
+ * add a string resource with given key.
+ * @param name The String name to store the template under.
+ * @param body A String containing a template.
+ * @param encoding The encoding of this string template
+ * @since 1.6
+ */
+ void putStringResource(String name, String body, String encoding);
+
+ /**
+ * delete a string resource with given key.
+ * @param name The string name to remove from the repository.
+ */
+ void removeStringResource(String name);
+
+ /**
+ * Sets the default encoding of the repository. Encodings can also be stored per
+ * template string. The default implementation does this correctly.
+ *
+ * @param encoding The encoding to use.
+ */
+ void setEncoding(String encoding);
+
+ /**
+ * Returns the current encoding of this repository.
+ *
+ * @return The current encoding of this repository.
+ */
+ String getEncoding();
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/util/StringResourceRepositoryImpl.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/util/StringResourceRepositoryImpl.java
new file mode 100644
index 00000000..fc93602d
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/util/StringResourceRepositoryImpl.java
@@ -0,0 +1,104 @@
+package org.apache.velocity.runtime.resource.util;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.resource.Resource;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Default implementation of StringResourceRepository.
+ * Uses a HashMap for storage
+ *
+ * @author <a href="mailto:eelco.hillenius@openedge.nl">Eelco Hillenius</a>
+ * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
+ * @version $Id$
+ * @since 1.5
+ */
+public class StringResourceRepositoryImpl implements StringResourceRepository
+{
+ /**
+ * mem store
+ */
+ protected Map<String, StringResource> resources = Collections.synchronizedMap(new HashMap<>());
+
+ /**
+ * Current Repository encoding.
+ */
+ private String encoding = RuntimeConstants.ENCODING_DEFAULT;
+
+ /**
+ * @see StringResourceRepository#getStringResource(java.lang.String)
+ */
+ @Override
+ public StringResource getStringResource(final String name)
+ {
+ return resources.get(name);
+ }
+
+ /**
+ * @see StringResourceRepository#putStringResource(java.lang.String, java.lang.String)
+ */
+ @Override
+ public void putStringResource(final String name, final String body)
+ {
+ resources.put(name, new StringResource(body, getEncoding()));
+ }
+
+ /**
+ * @see StringResourceRepository#putStringResource(java.lang.String, java.lang.String, java.lang.String)
+ * @since 1.6
+ */
+ @Override
+ public void putStringResource(final String name, final String body, final String encoding)
+ {
+ resources.put(name, new StringResource(body, encoding));
+ }
+
+ /**
+ * @see StringResourceRepository#removeStringResource(java.lang.String)
+ */
+ @Override
+ public void removeStringResource(final String name)
+ {
+ resources.remove(name);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.resource.util.StringResourceRepository#getEncoding()
+ */
+ @Override
+ public String getEncoding()
+ {
+ return encoding;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.resource.util.StringResourceRepository#setEncoding(java.lang.String)
+ */
+ @Override
+ public void setEncoding(final String encoding)
+ {
+ this.encoding = encoding;
+ }
+}