Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
URLTemplateProvider |
|
| 5.846153846153846;5.846 |
1 | /* | |
2 | * Copyright (C) 1998-2000 Semiotek Inc. All Rights Reserved. | |
3 | * | |
4 | * Redistribution and use in source and binary forms, with or without | |
5 | * modification, are permitted under the terms of either of the following | |
6 | * Open Source licenses: | |
7 | * | |
8 | * The GNU General Public License, version 2, or any later version, as | |
9 | * published by the Free Software Foundation | |
10 | * (http://www.fsf.org/copyleft/gpl.html); | |
11 | * | |
12 | * or | |
13 | * | |
14 | * The Semiotek Public License (http://webmacro.org/LICENSE.) | |
15 | * | |
16 | * This software is provided "as is", with NO WARRANTY, not even the | |
17 | * implied warranties of fitness to purpose, or merchantability. You | |
18 | * assume all risks and liabilities associated with its use. | |
19 | * | |
20 | * See www.webmacro.org for more information on the WebMacro project. | |
21 | */ | |
22 | ||
23 | package org.webmacro.resource; | |
24 | ||
25 | import java.io.FileNotFoundException; | |
26 | import java.io.IOException; | |
27 | import java.io.InputStream; | |
28 | import java.net.MalformedURLException; | |
29 | import java.net.URL; | |
30 | import java.util.HashMap; | |
31 | import java.util.StringTokenizer; | |
32 | ||
33 | import org.slf4j.Logger; | |
34 | import org.slf4j.LoggerFactory; | |
35 | ||
36 | import org.webmacro.Broker; | |
37 | import org.webmacro.InitException; | |
38 | import org.webmacro.NotFoundException; | |
39 | import org.webmacro.ResourceException; | |
40 | import org.webmacro.Template; | |
41 | import org.webmacro.util.Settings; | |
42 | ||
43 | /** | |
44 | * | |
45 | * | |
46 | * This is a "drop-in" replacement for the standard TemplateProvider in the | |
47 | * WebMacro distribution. The primary benefit is to allow a template to be loaded | |
48 | * by a variety of means, without requiring an absolute path. This should make | |
49 | * applications more portable. | |
50 | * | |
51 | * <h3>TemplatePath</h3> | |
52 | * | |
53 | * <i>TemplatePath=path1[;path2;....]</i> | |
54 | * <p> | |
55 | * Each path should be a full URL specification or one of the special cases | |
56 | * listed below. If the path cannot be interpreted, it will be tried as file:path | |
57 | * | |
58 | * <h3>Special cases</h3> | |
59 | * | |
60 | * <ul> | |
61 | * <li><i>TemplatePath=classpath:/templates</i> | |
62 | * Template will be loaded from the classpath. Each directory on the | |
63 | * classpath will be tried in turn to locate the template. | |
64 | * | |
65 | * <li><i>TemplatePath=ignore:</i> | |
66 | * Don't use the template path. All templates will be referenced | |
67 | * explicitly as full URL's. A common case would be to generate a base URL | |
68 | * at runtime (e.g., from ServletContext.getResource("/") in JSDK 2.2+) and to | |
69 | * prepend this to the template path. | |
70 | * | |
71 | * <li><i>TemplatePath=context:</i> | |
72 | * reserved for future use (specifically for finding templates with a | |
73 | * servlet context) | |
74 | * </ul> | |
75 | * | |
76 | * <h3>Locale support</h3> | |
77 | * | |
78 | * There is a limited locale based implemention here. Template paths can contain | |
79 | * a string of the form {_aaa_bbb....} which will usually correspond to a | |
80 | * locale such as en_GB. | |
81 | * Thus <i>load("template{_en_GB}.wm")</i> will look for | |
82 | * <ul> | |
83 | * <li>template_en_GB.wm</li> | |
84 | * <li>template_en.wm</li> | |
85 | * <li>template.wm</li> | |
86 | * </ul> | |
87 | * | |
88 | * in that order, returning the first one that exists. This is implemented so that | |
89 | * requests for template_en_GB.wm & template_en_US.wm will both return the same template | |
90 | * template_en.wm assuming only the latter exists. In other words, the template is | |
91 | * only parsed once. | |
92 | * | |
93 | * @see org.webmacro.resource.CachingProvider | |
94 | * @see org.webmacro.resource.TemplateProvider | |
95 | * | |
96 | * @since before 0.96 | |
97 | * @author fergus | |
98 | */ | |
99 | 0 | final public class URLTemplateProvider extends CachingProvider |
100 | { | |
101 | 0 | static Logger _log = LoggerFactory.getLogger(URLTemplateProvider.class); |
102 | ||
103 | /** CVS Revision tag. */ | |
104 | public static final String RCS = "@(#) $Id: org.webmacro.resource.URLTemplateProvider.html,v 1.1 2010/03/04 23:00:05 timp Exp $"; | |
105 | ||
106 | // INITIALIZATION | |
107 | ||
108 | 0 | private Broker _broker = null; |
109 | ||
110 | /** The default separator for TemplathPath | |
111 | */ | |
112 | 0 | private static String _pathSeparator = ";"; |
113 | ||
114 | 0 | private String[] _templateDirectory = null; |
115 | ||
116 | /** | |
117 | * URLs can contain strings like "{_AAA_BBB...}" to mimic | |
118 | * locale handling in ResourceBundles. | |
119 | * I think this can be improved upon - one idea would be to | |
120 | * fix the wm extension and then ask for (resource)+(locale_string).wm | |
121 | * We still have the problem of how to pass in the locale info, just as to | |
122 | * pass in the encoding. | |
123 | * | |
124 | */ | |
125 | ||
126 | private static final String _OPEN = "{"; | |
127 | private static final String _CLOSE = "}"; | |
128 | ||
129 | private static final String CLASSPATH_PREFIX = "classpath:"; | |
130 | private static final String CONTEXT_PREFIX = "context:"; | |
131 | private static final String IGNORE_PREFIX = "ignore:"; | |
132 | ||
133 | 0 | private static final int CLASSPATH_PREFIX_LENGTH |
134 | = CLASSPATH_PREFIX.length(); | |
135 | //private static final int CONTEXT_PREFIX_LENGTH | |
136 | // = CONTEXT_PREFIX.length(); | |
137 | ||
138 | ||
139 | private static final String _TYPE = "template"; | |
140 | ||
141 | ||
142 | /** | |
143 | * _baseURL is just a placeholder for the moment. | |
144 | * My hope is that we can pass in a context base at construction | |
145 | * time (e.g., the servlet context base) | |
146 | */ | |
147 | 0 | private URL _baseURL = null; |
148 | ||
149 | 0 | private final HashMap templateNameCache = new HashMap(); |
150 | ||
151 | ||
152 | /** | |
153 | * The value of TemplatePath in WebMacro inititialization file. | |
154 | * This is a semicolon separated string with individual values like | |
155 | * <ol> | |
156 | * <li> ignore: - ignore completely. All request will be given by a full URL</li> | |
157 | * <li> classpath:[path]. Look for values on the system classpath, optionally | |
158 | * with some relative path. E.g., classpath:/templates/</li> | |
159 | * <li> *TODO* context:[path] load relative to some path in the current context | |
160 | * (typically a ServletContext) </li> | |
161 | * <li> [url] - look for templates relative to this location</li> | |
162 | * <li> [path] - Equivalent to file:[path]</li> | |
163 | * </ol> | |
164 | */ | |
165 | ||
166 | private String _templatePath; | |
167 | ||
168 | /** | |
169 | * Supports the "template" type. This is a straight replacement for the | |
170 | * default TemplateProvider | |
171 | * @return the template type. Always the String "template" | |
172 | */ | |
173 | ||
174 | final public String getType () | |
175 | { | |
176 | 0 | return _TYPE; |
177 | } | |
178 | ||
179 | ||
180 | /** | |
181 | * Create a new TemplateProvider that uses the specified directory | |
182 | * as the source for Template objects that it will return. | |
183 | * | |
184 | * @param b A broker | |
185 | * @param config Settings from the webmacro initialization file | |
186 | * @exception InitException thrown when the provider fails to initialize | |
187 | */ | |
188 | ||
189 | public void init (Broker b, Settings config) throws InitException | |
190 | { | |
191 | 0 | super.init(b, config); |
192 | 0 | _broker = b; |
193 | ||
194 | try | |
195 | { | |
196 | 0 | _templatePath = config.getSetting("TemplatePath"); |
197 | 0 | StringTokenizer st = |
198 | new StringTokenizer(_templatePath, _pathSeparator); | |
199 | 0 | _templateDirectory = new String[st.countTokens()]; |
200 | int i; | |
201 | 0 | for (i = 0; i < _templateDirectory.length; i++) |
202 | { | |
203 | 0 | String dir = st.nextToken(); |
204 | 0 | _templateDirectory[i] = dir; |
205 | } | |
206 | ||
207 | } | |
208 | 0 | catch (Exception e) |
209 | { | |
210 | 0 | throw new InitException("Could not initialize", e); |
211 | 0 | } |
212 | 0 | } |
213 | ||
214 | ||
215 | /** | |
216 | * Grab a template based on its name, setting the request event to | |
217 | * contain it if we found it. | |
218 | * @param name The name of the template to load | |
219 | * @throws NotFoundException if no matching template can be found | |
220 | * @throws ResourceException if template cannot be loaded | |
221 | * @return the requested resource | |
222 | */ | |
223 | ||
224 | final public Object load (String name, CacheElement ce) | |
225 | throws ResourceException | |
226 | { | |
227 | 0 | return load(name, _baseURL); |
228 | } | |
229 | ||
230 | /** | |
231 | * Find the specified template in the directory managed by this | |
232 | * template store. Any path specified in the filename is relative | |
233 | * to the directory managed by the template store. | |
234 | * <p> | |
235 | * @param name relative to the current directory fo the store | |
236 | * @return a template matching that name, or null if one cannot be found | |
237 | */ | |
238 | ||
239 | final public Object load (String name, URL base) | |
240 | throws ResourceException | |
241 | { | |
242 | 0 | _log.debug("Load URLTemplate: (" + base + "," + name + ")"); |
243 | try | |
244 | { | |
245 | 0 | Template _tmpl = null; |
246 | ||
247 | 0 | if (base == null) |
248 | { | |
249 | 0 | _tmpl = getTemplate(name); |
250 | } | |
251 | else | |
252 | { | |
253 | 0 | _tmpl = getTemplate(new URL(base, name)); |
254 | } | |
255 | ||
256 | 0 | if (_tmpl == null) |
257 | { | |
258 | 0 | throw new NotFoundException( |
259 | this + " could not locate " + name + " on path " + _templatePath); | |
260 | } | |
261 | 0 | templateNameCache.put(name, _tmpl); |
262 | 0 | return _tmpl; |
263 | } | |
264 | 0 | catch (IOException e) |
265 | { | |
266 | 0 | _log.debug(e.getClass().getName() + " " + e.getMessage()); |
267 | 0 | throw new ResourceException(e.getMessage()); |
268 | } | |
269 | } | |
270 | ||
271 | /** | |
272 | * Always return false. It is not possible to decide if an object | |
273 | * fetched from a URL should be reloaded or not. Returning false | |
274 | * will cause the CachingProvider to load() only when it's cache | |
275 | * has expired. | |
276 | * @param name The name of the template to test | |
277 | * @return always returns false. | |
278 | * | |
279 | * ** TO DO ** | |
280 | * Can do better than this for file: and jar: URLs | |
281 | * | |
282 | * jar urls are of the form | |
283 | * jar:<url-of-jar>!<path-within-jar> | |
284 | * e.g., | |
285 | * jar:file:/path/my.jar!/templates/test.wm | |
286 | * | |
287 | * However, this might be an expensive operation. | |
288 | */ | |
289 | ||
290 | // IMPLEMENTATION | |
291 | ||
292 | /** | |
293 | * | |
294 | */ | |
295 | ||
296 | final public boolean shouldReload (String name) | |
297 | { | |
298 | 0 | URLTemplate tmpl = (URLTemplate) templateNameCache.get(name); |
299 | 0 | return (tmpl == null) ? false : tmpl.shouldReload(); |
300 | } | |
301 | ||
302 | private final boolean exists (URL url) | |
303 | { | |
304 | 0 | InputStream _is = null; |
305 | try | |
306 | { | |
307 | 0 | _is = url.openStream(); |
308 | 0 | System.out.println(url + " exists"); |
309 | 0 | return true; |
310 | } | |
311 | 0 | catch (IOException e) |
312 | { | |
313 | 0 | System.out.println(url + " does not exist"); |
314 | 0 | return false; |
315 | } | |
316 | finally | |
317 | { | |
318 | 0 | if (_is != null) |
319 | { | |
320 | try | |
321 | { | |
322 | 0 | _is.close(); |
323 | } | |
324 | 0 | catch (Exception ignore) |
325 | { | |
326 | 0 | } |
327 | } | |
328 | } | |
329 | } | |
330 | ||
331 | /** | |
332 | * Load a template relative to the base. | |
333 | * @param path the relative or absolute URL-path of the template | |
334 | */ | |
335 | ||
336 | final private Template getTemplate (URL path) | |
337 | { | |
338 | 0 | _log.debug("get:" + path); |
339 | URLTemplate t; | |
340 | 0 | String pre = null; |
341 | 0 | String mid = null; |
342 | 0 | String post = null; |
343 | 0 | String pathStr = path.toExternalForm(); |
344 | try | |
345 | { | |
346 | 0 | String[] parts = parseLocalePath(pathStr); |
347 | ||
348 | 0 | String urlPath = null; |
349 | 0 | URL url = path; |
350 | 0 | if (parts == null) |
351 | { | |
352 | 0 | urlPath = pathStr; |
353 | } | |
354 | else | |
355 | { | |
356 | 0 | pre = parts[0]; |
357 | 0 | mid = parts[1]; |
358 | 0 | post = parts[2]; |
359 | 0 | urlPath = pre + ((mid == null) ? "" : mid) + post; |
360 | 0 | url = new URL(urlPath); |
361 | } | |
362 | ||
363 | 0 | if (urlPath.length() > 512) |
364 | { | |
365 | 0 | throw new IllegalArgumentException("URL path too long: " + urlPath); |
366 | } | |
367 | 0 | _log.debug("URLTemplateProvider: loading " + url + |
368 | "(" + path + "," + urlPath + ")"); | |
369 | ||
370 | 0 | t = new URLTemplate(_broker, url); |
371 | 0 | _log.debug("**PARSING " + url); |
372 | 0 | t.parse(); |
373 | 0 | return t; |
374 | } | |
375 | 0 | catch (IOException e) |
376 | { | |
377 | 0 | _log.debug(e.getClass().getName() + " " + e.getMessage()); |
378 | // try the next locale | |
379 | 0 | if (mid != null) |
380 | { | |
381 | try | |
382 | { | |
383 | 0 | String p = buildPath(pre, mid, post); |
384 | 0 | return (Template) _broker.get(_TYPE, p); |
385 | } | |
386 | 0 | catch (Exception ex) |
387 | { | |
388 | 0 | _log.debug(ex.getClass().getName() + " " + ex.getMessage()); |
389 | // ignore | |
390 | } | |
391 | } | |
392 | 0 | _log.error("URLTemplateProvider(1): Could not load template: " + path, e); |
393 | } | |
394 | 0 | catch (Exception e) |
395 | { | |
396 | 0 | _log.error("URLTemplateProvider(2): Could not load template: " + path, e); |
397 | 0 | } |
398 | 0 | _log.debug("URLTemplateProvider: " + path + " not found."); |
399 | 0 | return null; |
400 | } | |
401 | ||
402 | /** | |
403 | * Get the URL for a specified template. | |
404 | */ | |
405 | ||
406 | private Template getTemplate (String path) | |
407 | throws IOException | |
408 | { | |
409 | 0 | _log.debug("getTemplate: " + path); |
410 | 0 | for (int i = 0; i < _templateDirectory.length; i++) |
411 | { | |
412 | 0 | String tPart = _templateDirectory[i]; |
413 | 0 | URL url = null; |
414 | 0 | if (tPart.startsWith(CLASSPATH_PREFIX)) |
415 | { | |
416 | 0 | tPart = tPart.substring(CLASSPATH_PREFIX_LENGTH); |
417 | 0 | url = searchClasspath(join(tPart, path)); |
418 | ||
419 | 0 | if (url == null) |
420 | { | |
421 | 0 | throw new FileNotFoundException("Unable to locate " + path + " on classpath"); |
422 | } | |
423 | } | |
424 | 0 | else if (tPart.startsWith(CONTEXT_PREFIX)) |
425 | { | |
426 | 0 | throw new IllegalStateException("Not implemented"); |
427 | } | |
428 | 0 | else if (tPart.startsWith(IGNORE_PREFIX)) |
429 | { | |
430 | 0 | url = new URL(path); |
431 | } | |
432 | else | |
433 | { | |
434 | 0 | String s = join(tPart, path); |
435 | try | |
436 | { | |
437 | 0 | url = new URL(s); |
438 | } | |
439 | 0 | catch (MalformedURLException e) |
440 | { | |
441 | 0 | url = new URL("file", null, s); |
442 | 0 | } |
443 | } | |
444 | 0 | if (exists(url)) |
445 | { | |
446 | 0 | return getTemplate(url); |
447 | } | |
448 | } | |
449 | 0 | return null; |
450 | } | |
451 | ||
452 | // Utility Methods | |
453 | ||
454 | /** | |
455 | * The URL path separator. Used by join(). | |
456 | */ | |
457 | private static final String _SEP = "/"; | |
458 | ||
459 | /** | |
460 | * Join two parts of a URL string together, without duplicating | |
461 | * any "/" between them; | |
462 | */ | |
463 | ||
464 | private final String join (String pre, String post) | |
465 | { | |
466 | 0 | _log.debug("Joining <" + pre + "> + <" + post + ">"); |
467 | 0 | if ((pre == null) || (pre.length() == 0)) return post; |
468 | 0 | if ((post == null) || (post.length() == 0)) return pre; |
469 | 0 | boolean first = pre.endsWith(_SEP); |
470 | 0 | boolean second = post.startsWith(_SEP); |
471 | 0 | if (first ^ second) return pre + post; |
472 | 0 | if (first) return pre.substring(0, pre.length() - 1) + post; |
473 | 0 | if (second) return first + post.substring(1); |
474 | 0 | return pre + _SEP + post; |
475 | } | |
476 | ||
477 | /** | |
478 | * Searches the SYSTEM classpath for a resource. | |
479 | * Probably Class.getSytemResource() might be better. | |
480 | * <p> | |
481 | * Ideally would like to be able to search the application | |
482 | * classpath, which is not necessarily the same (e.g., in servlet 2.2+) | |
483 | * so could pass in the application classloader. But we have the same | |
484 | * problem as in other places where there is no easy way to pass extra | |
485 | * information with the request. | |
486 | * | |
487 | */ | |
488 | private final URL searchClasspath (String resource) | |
489 | { | |
490 | 0 | _log.debug("Searching classpath for " + resource); |
491 | 0 | URL url = null; |
492 | 0 | ClassLoader cl = this.getClass().getClassLoader(); |
493 | 0 | if (cl != null) |
494 | { | |
495 | 0 | url = cl.getResource(resource); |
496 | } | |
497 | else | |
498 | { | |
499 | 0 | url = ClassLoader.getSystemResource(resource); |
500 | } | |
501 | ||
502 | 0 | if (url != null) |
503 | { | |
504 | 0 | return url; |
505 | } | |
506 | /* | |
507 | * look for locale specific resources AAAA{_xxxx_yyyyy_....}BBBBB | |
508 | */ | |
509 | 0 | String[] parts = parseLocalePath(resource); |
510 | 0 | if (parts != null) |
511 | { | |
512 | 0 | if (parts[1] != null) |
513 | { | |
514 | 0 | resource = buildPath(parts[0], parts[1], parts[2]); |
515 | 0 | return searchClasspath(resource); |
516 | } | |
517 | } | |
518 | 0 | return null; |
519 | } | |
520 | ||
521 | /** | |
522 | * Removes the last locale part from a string | |
523 | * <p> | |
524 | * e.g. "_en_GB" => "_en" | |
525 | */ | |
526 | private final String stripLast (String s) | |
527 | { | |
528 | 0 | if (s == null) return null; |
529 | 0 | int p = s.lastIndexOf("_"); |
530 | 0 | if (p < 0) |
531 | { | |
532 | 0 | return null; |
533 | } | |
534 | 0 | String ret = s.substring(0, p); |
535 | 0 | return ("".equals(ret)) ? null : ret; |
536 | } | |
537 | ||
538 | /** | |
539 | * Builds up a new path from the form AAAAA{_B1_B2...._Bn-1}CCCCC | |
540 | * when given arguments AAAA, _B1_B2...._Bn, CCCC | |
541 | * <p> | |
542 | * e.g., "path/myfile","_en_GB",".wm" => "path/myfile{_en}.wm" | |
543 | */ | |
544 | private final String buildPath (String pre, String mid, String post) | |
545 | { | |
546 | 0 | StringBuffer sb = new StringBuffer(pre); |
547 | 0 | String stripped = stripLast(mid); |
548 | 0 | if (stripped != null) |
549 | { | |
550 | 0 | sb.append(_OPEN); |
551 | 0 | sb.append(stripped); |
552 | 0 | sb.append(_CLOSE); |
553 | } | |
554 | 0 | sb.append(post); |
555 | 0 | return sb.toString(); |
556 | } | |
557 | ||
558 | /** | |
559 | * Looks for a string of the form AAA{BBB}CCC. | |
560 | * If found, returns [AAA,BBB,CCC], null otherwise | |
561 | * | |
562 | * This is used to strip out the "Locale" part of a resource name | |
563 | */ | |
564 | private String[] parseLocalePath (String path) | |
565 | { | |
566 | 0 | int p1 = path.indexOf(_OPEN); |
567 | 0 | int p2 = path.indexOf(_CLOSE); |
568 | 0 | if ((p1 < 0) || (p2 < 0) || (p2 < p1)) |
569 | { | |
570 | 0 | return null; |
571 | } | |
572 | 0 | String pre = path.substring(0, p1); |
573 | 0 | String mid = path.substring(p1 + 1, p2); |
574 | 0 | String post = path.substring(p2 + 1); |
575 | 0 | return new String[]{pre, mid, post}; |
576 | } | |
577 | } |