1 package org.apache.velocity.runtime.resource.loader; 2 3 /* 4 * Licensed to the Apache Software Foundation (ASF) under one 5 * or more contributor license agreements. See the NOTICE file 6 * distributed with this work for additional information 7 * regarding copyright ownership. The ASF licenses this file 8 * to you under the Apache License, Version 2.0 (the 9 * "License"); you may not use this file except in compliance 10 * with the License. You may obtain a copy of the License at 11 * 12 * http://www.apache.org/licenses/LICENSE-2.0 13 * 14 * Unless required by applicable law or agreed to in writing, 15 * software distributed under the License is distributed on an 16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 * KIND, either express or implied. See the License for the 18 * specific language governing permissions and limitations 19 * under the License. 20 */ 21 22 import org.apache.velocity.exception.ResourceNotFoundException; 23 import org.apache.velocity.exception.VelocityException; 24 import org.apache.velocity.runtime.RuntimeConstants; 25 import org.apache.velocity.runtime.resource.Resource; 26 import org.apache.velocity.util.ExtProperties; 27 28 import org.apache.commons.io.FilenameUtils; 29 import org.apache.commons.lang3.StringUtils; 30 31 import java.io.IOException; 32 import java.io.InputStream; 33 import java.io.Reader; 34 import java.util.HashMap; 35 import java.util.List; 36 import java.util.ListIterator; 37 import java.util.Map; 38 39 /** 40 * <p> 41 * ResourceLoader to load templates from multiple Jar files. 42 * </p> 43 * <p> 44 * The configuration of the JarResourceLoader is straightforward - 45 * You simply add the JarResourceLoader to the configuration via 46 * </p> 47 * <pre><code> 48 * resource.loaders = jar 49 * resource.loader.jar.class = org.apache.velocity.runtime.resource.loader.JarResourceLoader 50 * resource.loader.jar.path = list of JAR <URL>s 51 * </code></pre> 52 * 53 * <p> So for example, if you had a jar file on your local filesystem, you could simply do</p> 54 * <pre><code> 55 * resource.loader.jar.path = jar:file:/opt/myfiles/jar1.jar 56 * </code></pre> 57 * <p> Note that jar specification for the <code>.path</code> configuration property 58 * conforms to the same rules for the java.net.JarUrlConnection class. 59 * </p> 60 * 61 * <p> For a working example, see the unit test case, 62 * org.apache.velocity.test.MultiLoaderTestCase class 63 * </p> 64 * 65 * @author <a href="mailto:mailmur@yahoo.com">Aki Nieminen</a> 66 * @author <a href="mailto:daveb@miceda-data.com">Dave Bryson</a> 67 * @version $Id$ 68 */ 69 public class JarResourceLoader extends ResourceLoader 70 { 71 /** 72 * Maps entries to the parent JAR File 73 * Key = the entry *excluding* plain directories 74 * Value = the JAR URL 75 */ 76 private Map<String, String> entryDirectory = new HashMap<>(559); 77 78 /** 79 * Maps JAR URLs to the actual JAR 80 * Key = the JAR URL 81 * Value = the JAR 82 */ 83 private Map<String, JarHolder> jarfiles = new HashMap<>(89); 84 85 /** 86 * Called by Velocity to initialize the loader 87 * @param configuration 88 */ 89 @Override init(ExtProperties configuration)90 public void init(ExtProperties configuration) 91 { 92 log.trace("JarResourceLoader: initialization starting."); 93 94 List<String> paths = configuration.getList(RuntimeConstants.RESOURCE_LOADER_PATHS); 95 96 if (paths != null) 97 { 98 log.debug("JarResourceLoader # of paths: {}", paths.size() ); 99 100 for (ListIterator<String> it = paths.listIterator(); it.hasNext(); ) 101 { 102 String jar = StringUtils.trim(it.next()); 103 it.set(jar); 104 loadJar(jar); 105 } 106 } 107 108 log.trace("JarResourceLoader: initialization complete."); 109 } 110 loadJar( String path )111 private void loadJar( String path ) 112 { 113 log.debug("JarResourceLoader: trying to load \"{}\"", path); 114 115 // Check path information 116 if ( path == null ) 117 { 118 String msg = "JarResourceLoader: can not load JAR - JAR path is null"; 119 log.error(msg); 120 throw new RuntimeException(msg); 121 } 122 if ( !path.startsWith("jar:") ) 123 { 124 String msg = "JarResourceLoader: JAR path must start with jar: -> see java.net.JarURLConnection for information"; 125 log.error(msg); 126 throw new RuntimeException(msg); 127 } 128 if (!path.contains("!/")) 129 { 130 path += "!/"; 131 } 132 133 // Close the jar if it's already open 134 // this is useful for a reload 135 closeJar( path ); 136 137 // Create a new JarHolder 138 JarHolder temp = new JarHolder( rsvc, path, log ); 139 // Add it's entries to the entryCollection 140 addEntries(temp.getEntries()); 141 // Add it to the Jar table 142 jarfiles.put(temp.getUrlPath(), temp); 143 } 144 145 /** 146 * Closes a Jar file and set its URLConnection 147 * to null. 148 */ closeJar( String path )149 private void closeJar( String path ) 150 { 151 if ( jarfiles.containsKey(path) ) 152 { 153 JarHolder theJar = jarfiles.get(path); 154 theJar.close(); 155 } 156 } 157 158 /** 159 * Copy all the entries into the entryDirectory 160 * It will overwrite any duplicate keys. 161 */ addEntries( Map<String, String> entries )162 private void addEntries( Map<String, String> entries ) 163 { 164 entryDirectory.putAll( entries ); 165 } 166 167 /** 168 * Get a Reader so that the Runtime can build a 169 * template with it. 170 * 171 * @param source name of template to get 172 * @param encoding asked encoding 173 * @return InputStream containing the template 174 * @throws ResourceNotFoundException if template not found 175 * in the file template path. 176 * @since 2.0 177 */ 178 @Override getResourceReader(String source, String encoding )179 public Reader getResourceReader(String source, String encoding ) 180 throws ResourceNotFoundException 181 { 182 Reader result = null; 183 184 if (org.apache.commons.lang3.StringUtils.isEmpty(source)) 185 { 186 throw new ResourceNotFoundException("Need to have a resource!"); 187 } 188 189 String normalizedPath = FilenameUtils.normalize( source, true ); 190 191 if ( normalizedPath == null || normalizedPath.length() == 0 ) 192 { 193 String msg = "JAR resource error: argument " + normalizedPath + 194 " contains .. and may be trying to access " + 195 "content outside of template root. Rejected."; 196 197 log.error( "JarResourceLoader: {}", msg ); 198 199 throw new ResourceNotFoundException ( msg ); 200 } 201 202 /* 203 * if a / leads off, then just nip that :) 204 */ 205 if ( normalizedPath.startsWith("/") ) 206 { 207 normalizedPath = normalizedPath.substring(1); 208 } 209 210 if ( entryDirectory.containsKey( normalizedPath ) ) 211 { 212 String jarurl = entryDirectory.get( normalizedPath ); 213 214 if ( jarfiles.containsKey( jarurl ) ) 215 { 216 JarHolder holder = (JarHolder)jarfiles.get( jarurl ); 217 InputStream rawStream = holder.getResource( normalizedPath ); 218 try 219 { 220 return buildReader(rawStream, encoding); 221 } 222 catch (Exception e) 223 { 224 if (rawStream != null) 225 { 226 try 227 { 228 rawStream.close(); 229 } 230 catch (IOException ioe) {} 231 } 232 String msg = "JAR resource error: Exception while loading " + source; 233 log.error(msg, e); 234 throw new VelocityException(msg, e, rsvc.getLogContext().getStackTrace()); 235 } 236 } 237 } 238 239 throw new ResourceNotFoundException( "JarResourceLoader Error: cannot find resource " + 240 source ); 241 242 } 243 244 // TODO: SHOULD BE DELEGATED TO THE JARHOLDER 245 246 /** 247 * @see ResourceLoader#isSourceModified(org.apache.velocity.runtime.resource.Resource) 248 */ 249 @Override isSourceModified(Resource resource)250 public boolean isSourceModified(Resource resource) 251 { 252 return true; 253 } 254 255 /** 256 * @see ResourceLoader#getLastModified(org.apache.velocity.runtime.resource.Resource) 257 */ 258 @Override getLastModified(Resource resource)259 public long getLastModified(Resource resource) 260 { 261 return 0; 262 } 263 } 264