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 java.io.BufferedInputStream;
23 import java.io.File;
24 import java.io.FileInputStream;
25 import java.io.FileNotFoundException;
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.util.ArrayList;
29 import java.util.Collections;
30 import java.util.HashMap;
31 import java.util.List;
32 import java.util.Map;
33
34 import org.apache.commons.collections.ExtendedProperties;
35 import org.apache.velocity.exception.ResourceNotFoundException;
36 import org.apache.velocity.exception.VelocityException;
37 import org.apache.velocity.io.UnicodeInputStream;
38 import org.apache.velocity.runtime.resource.Resource;
39 import org.apache.velocity.util.StringUtils;
40
41 /**
42 * A loader for templates stored on the file system. Treats the template
43 * as relative to the configured root path. If the root path is empty
44 * treats the template name as an absolute path.
45 *
46 * @author <a href="mailto:wglass@forio.com">Will Glass-Husain</a>
47 * @author <a href="mailto:mailmur@yahoo.com">Aki Nieminen</a>
48 * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
49 * @version $Id: FileResourceLoader.java 1043077 2010-12-07 15:01:25Z apetrelli $
50 */
51 public class FileResourceLoader extends ResourceLoader
52 {
53 /**
54 * The paths to search for templates.
55 */
56 private List paths = new ArrayList();
57
58 /**
59 * Used to map the path that a template was found on
60 * so that we can properly check the modification
61 * times of the files. This is synchronizedMap
62 * instance.
63 */
64 private Map templatePaths = Collections.synchronizedMap(new HashMap());
65
66 /** Shall we inspect unicode files to see what encoding they contain?. */
67 private boolean unicode = false;
68
69 /**
70 * @see org.apache.velocity.runtime.resource.loader.ResourceLoader#init(org.apache.commons.collections.ExtendedProperties)
71 */
72 public void init( ExtendedProperties configuration)
73 {
74 if (log.isTraceEnabled())
75 {
76 log.trace("FileResourceLoader : initialization starting.");
77 }
78
79 paths.addAll( configuration.getVector("path") );
80
81 // unicode files may have a BOM marker at the start, but Java
82 // has problems recognizing the UTF-8 bom. Enabling unicode will
83 // recognize all unicode boms.
84 unicode = configuration.getBoolean("unicode", false);
85
86 if (log.isDebugEnabled())
87 {
88 log.debug("Do unicode file recognition: " + unicode);
89 }
90
91 if (log.isDebugEnabled())
92 {
93 // trim spaces from all paths
94 StringUtils.trimStrings(paths);
95
96 // this section lets tell people what paths we will be using
97 int sz = paths.size();
98 for( int i=0; i < sz; i++)
99 {
100 log.debug("FileResourceLoader : adding path '" + (String) paths.get(i) + "'");
101 }
102 log.trace("FileResourceLoader : initialization complete.");
103 }
104 }
105
106 /**
107 * Get an InputStream so that the Runtime can build a
108 * template with it.
109 *
110 * @param templateName name of template to get
111 * @return InputStream containing the template
112 * @throws ResourceNotFoundException if template not found
113 * in the file template path.
114 */
115 public InputStream getResourceStream(String templateName)
116 throws ResourceNotFoundException
117 {
118 /*
119 * Make sure we have a valid templateName.
120 */
121 if (org.apache.commons.lang.StringUtils.isEmpty(templateName))
122 {
123 /*
124 * If we don't get a properly formed templateName then
125 * there's not much we can do. So we'll forget about
126 * trying to search any more paths for the template.
127 */
128 throw new ResourceNotFoundException(
129 "Need to specify a file name or file path!");
130 }
131
132 String template = StringUtils.normalizePath(templateName);
133 if ( template == null || template.length() == 0 )
134 {
135 String msg = "File resource error : argument " + template +
136 " contains .. and may be trying to access " +
137 "content outside of template root. Rejected.";
138
139 log.error("FileResourceLoader : " + msg);
140
141 throw new ResourceNotFoundException ( msg );
142 }
143
144 int size = paths.size();
145 for (int i = 0; i < size; i++)
146 {
147 String path = (String) paths.get(i);
148 InputStream inputStream = null;
149
150 try
151 {
152 inputStream = findTemplate(path, template);
153 }
154 catch (IOException ioe)
155 {
156 String msg = "Exception while loading Template " + template;
157 log.error(msg, ioe);
158 throw new VelocityException(msg, ioe);
159 }
160
161 if (inputStream != null)
162 {
163 /*
164 * Store the path that this template came
165 * from so that we can check its modification
166 * time.
167 */
168 templatePaths.put(templateName, path);
169 return inputStream;
170 }
171 }
172
173 /*
174 * We have now searched all the paths for
175 * templates and we didn't find anything so
176 * throw an exception.
177 */
178 throw new ResourceNotFoundException("FileResourceLoader : cannot find " + template);
179 }
180
181 /**
182 * Overrides superclass for better performance.
183 * @since 1.6
184 */
185 public boolean resourceExists(String name)
186 {
187 if (name == null)
188 {
189 return false;
190 }
191 name = StringUtils.normalizePath(name);
192 if (name == null || name.length() == 0)
193 {
194 return false;
195 }
196
197 int size = paths.size();
198 for (int i = 0; i < size; i++)
199 {
200 String path = (String)paths.get(i);
201 try
202 {
203 File file = getFile(path, name);
204 if (file.canRead())
205 {
206 return true;
207 }
208 }
209 catch (Exception ioe)
210 {
211 String msg = "Exception while checking for template " + name;
212 log.debug(msg, ioe);
213 }
214 }
215 return false;
216 }
217
218 /**
219 * Try to find a template given a normalized path.
220 *
221 * @param path a normalized path
222 * @param template name of template to find
223 * @return InputStream input stream that will be parsed
224 *
225 */
226 private InputStream findTemplate(final String path, final String template)
227 throws IOException
228 {
229 try
230 {
231 File file = getFile(path,template);
232
233 if (file.canRead())
234 {
235 FileInputStream fis = null;
236 try
237 {
238 fis = new FileInputStream(file.getAbsolutePath());
239
240 if (unicode)
241 {
242 UnicodeInputStream uis = null;
243
244 try
245 {
246 uis = new UnicodeInputStream(fis, true);
247
248 if (log.isDebugEnabled())
249 {
250 log.debug("File Encoding for " + file + " is: " + uis.getEncodingFromStream());
251 }
252
253 return new BufferedInputStream(uis);
254 }
255 catch(IOException e)
256 {
257 closeQuiet(uis);
258 throw e;
259 }
260 }
261 else
262 {
263 return new BufferedInputStream(fis);
264 }
265 }
266 catch (IOException e)
267 {
268 closeQuiet(fis);
269 throw e;
270 }
271 }
272 else
273 {
274 return null;
275 }
276 }
277 catch(FileNotFoundException fnfe)
278 {
279 /*
280 * log and convert to a general Velocity ResourceNotFoundException
281 */
282 return null;
283 }
284 }
285
286 private void closeQuiet(final InputStream is)
287 {
288 if (is != null)
289 {
290 try
291 {
292 is.close();
293 }
294 catch(IOException ioe)
295 {
296 // Ignore
297 }
298 }
299 }
300
301 /**
302 * How to keep track of all the modified times
303 * across the paths. Note that a file might have
304 * appeared in a directory which is earlier in the
305 * path; so we should search the path and see if
306 * the file we find that way is the same as the one
307 * that we have cached.
308 * @param resource
309 * @return True if the source has been modified.
310 */
311 public boolean isSourceModified(Resource resource)
312 {
313 /*
314 * we assume that the file needs to be reloaded;
315 * if we find the original file and it's unchanged,
316 * then we'll flip this.
317 */
318 boolean modified = true;
319
320 String fileName = resource.getName();
321 String path = (String) templatePaths.get(fileName);
322 File currentFile = null;
323
324 for (int i = 0; currentFile == null && i < paths.size(); i++)
325 {
326 String testPath = (String) paths.get(i);
327 File testFile = getFile(testPath, fileName);
328 if (testFile.canRead())
329 {
330 currentFile = testFile;
331 }
332 }
333 File file = getFile(path, fileName);
334 if (currentFile == null || !file.exists())
335 {
336 /*
337 * noop: if the file is missing now (either the cached
338 * file is gone, or the file can no longer be found)
339 * then we leave modified alone (it's set to true); a
340 * reload attempt will be done, which will either use
341 * a new template or fail with an appropriate message
342 * about how the file couldn't be found.
343 */
344 }
345 else if (currentFile.equals(file) && file.canRead())
346 {
347 /*
348 * if only if currentFile is the same as file and
349 * file.lastModified() is the same as
350 * resource.getLastModified(), then we should use the
351 * cached version.
352 */
353 modified = (file.lastModified() != resource.getLastModified());
354 }
355
356 /*
357 * rsvc.debug("isSourceModified for " + fileName + ": " + modified);
358 */
359 return modified;
360 }
361
362 /**
363 * @see org.apache.velocity.runtime.resource.loader.ResourceLoader#getLastModified(org.apache.velocity.runtime.resource.Resource)
364 */
365 public long getLastModified(Resource resource)
366 {
367 String path = (String) templatePaths.get(resource.getName());
368 File file = getFile(path, resource.getName());
369
370 if (file.canRead())
371 {
372 return file.lastModified();
373 }
374 else
375 {
376 return 0;
377 }
378 }
379
380
381 /**
382 * Create a File based on either a relative path if given, or absolute path otherwise
383 */
384 private File getFile(String path, String template)
385 {
386
387 File file = null;
388
389 if("".equals(path))
390 {
391 file = new File( template );
392 }
393 else
394 {
395 /*
396 * if a / leads off, then just nip that :)
397 */
398 if (template.startsWith("/"))
399 {
400 template = template.substring(1);
401 }
402
403 file = new File ( path, template );
404 }
405
406 return file;
407 }
408 }