diff options
Diffstat (limited to 'velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource')
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.<loader-id>.<property> = <value></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> + * <resource-ref> + * <description>Velocity template DataSource</description> + * <res-ref-name>jdbc/Velocity</res-ref-name> + * <res-type>javax.sql.DataSource</res-type> + * <res-auth>Container</res-auth> + * </resource-ref> + * </code></pre> + * <br> + * and Tomcat 4 server.xml file: <br> + * <pre><code> + * [...] + * <Context path="/exampleVelocity" docBase="exampleVelocity" debug="0"> + * [...] + * <ResourceParams name="jdbc/Velocity"> + * <parameter> + * <name>driverClassName</name> + * <value>org.hsql.jdbcDriver</value> + * </parameter> + * <parameter> + * <name>driverName</name> + * <value>jdbc:HypersonicSQL:database</value> + * </parameter> + * <parameter> + * <name>user</name> + * <value>database_username</value> + * </parameter> + * <parameter> + * <name>password</name> + * <value>database_password</value> + * </parameter> + * </ResourceParams> + * [...] + * </Context> + * [...] + * </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 <URL>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; + } +} |