1 /*
2 * Title: ConfigLoader
3 * Description:
4 *
5 * This software is published under the terms of the OpenSymphony Software
6 * License version 1.1, of which a copy has been included with this
7 * distribution in the LICENSE.txt file.
8 */
9
10 package com.opensymphony.module.sitemesh.mapper;
11
12 import com.opensymphony.module.sitemesh.Config;
13 import com.opensymphony.module.sitemesh.Decorator;
14 import org.w3c.dom;
15 import org.xml.sax.SAXException;
16
17 import javax.servlet.ServletException;
18 import javax.xml.parsers.DocumentBuilder;
19 import javax.xml.parsers.DocumentBuilderFactory;
20 import javax.xml.parsers.ParserConfigurationException;
21 import java.io.File;
22 import java.io.IOException;
23 import java.util.HashMap;
24 import java.util.Map;
25
26 /**
27 * The ConfigLoader reads a configuration XML file that contains Decorator definitions
28 * (name, url, init-params) and path-mappings (pattern, name).
29 *
30 * <p>These can then be accessed by the getDecoratorByName() methods and getMappedName()
31 * methods respectively.</p>
32 *
33 * <p>The DTD for the configuration file in old (deprecated) format is located at
34 * <a href="http://www.opensymphony.com/dtds/sitemesh_1_0_decorators.dtd">
35 * http://www.opensymphony.com/dtds/sitemesh_1_0_decorators.dtd
36 * </a>.</p>
37 *
38 * <p>The DTD for the configuration file in new format is located at
39 * <a href="http://www.opensymphony.com/dtds/sitemesh_1_5_decorators.dtd">
40 * http://www.opensymphony.com/dtds/sitemesh_1_5_decorators.dtd
41 * </a>.</p>
42 *
43 * <p>Editing the config file will cause it to be auto-reloaded.</p>
44 *
45 * <p>This class is used by ConfigDecoratorMapper, and uses PathMapper for pattern matching.</p>
46 *
47 * @author <a href="mailto:joe@truemesh.com">Joe Walnes</a>
48 * @author <a href="mailto:pathos@pandora.be">Mathias Bogaert</a>
49 * @version $Revision: 1.6 $
50 *
51 * @see com.opensymphony.module.sitemesh.mapper.ConfigDecoratorMapper
52 * @see com.opensymphony.module.sitemesh.mapper.PathMapper
53 */
54 public final class ConfigLoader {
55 private Map decorators = null;
56 private long configLastModified;
57
58 private File configFile = null;
59 private String configFileName = null;
60 private PathMapper pathMapper = null;
61
62 private Config config = null;
63
64 /** Create new ConfigLoader using supplied File. */
65 public ConfigLoader(File configFile) throws ServletException {
66 this.configFile = configFile;
67 this.configFileName = configFile.getName();
68 loadConfig();
69 }
70
71 /** Create new ConfigLoader using supplied filename and config. */
72 public ConfigLoader(String configFileName, Config config) throws ServletException {
73 this.config = config;
74 this.configFileName = configFileName;
75 if (config.getServletContext().getRealPath(configFileName) != null) {
76 this.configFile = new File(config.getServletContext().getRealPath(configFileName));
77 }
78 loadConfig();
79 }
80
81 /** Retrieve Decorator based on name specified in configuration file. */
82 public Decorator getDecoratorByName(String name) throws ServletException {
83 refresh();
84 return (Decorator)decorators.get(name);
85 }
86
87 /** Get name of Decorator mapped to given path. */
88 public String getMappedName(String path) throws ServletException {
89 refresh();
90 return pathMapper.get(path);
91 }
92
93 /** Load configuration from file. */
94 private synchronized void loadConfig() throws ServletException {
95 try {
96 // Build a document from the file
97 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
98 DocumentBuilder builder = factory.newDocumentBuilder();
99
100 Document document = null;
101 if (configFile != null && configFile.canRead()) {
102 // Keep time we read the file to check if the file was modified
103 configLastModified = configFile.lastModified();
104 document = builder.parse(configFile);
105 }
106 else {
107 document = builder.parse(config.getServletContext().getResourceAsStream(configFileName));
108 }
109
110 // Parse the configuration document
111 parseConfig(document);
112 }
113 catch (ParserConfigurationException e) {
114 throw new ServletException("Could not get XML parser", e);
115 }
116 catch (IOException e) {
117 throw new ServletException("Could not read the config file: " + configFileName, e);
118 }
119 catch (SAXException e) {
120 throw new ServletException("Could not parse the config file: " + configFileName, e);
121 }
122 catch (IllegalArgumentException e) {
123 throw new ServletException("Could not find the config file: " + configFileName, e);
124 }
125 }
126
127 /** Parse configuration from XML document. */
128 private synchronized void parseConfig(Document document) {
129 Element root = document.getDocumentElement();
130
131 // get the default directory for the decorators
132 String defaultDir = getAttribute(root, "defaultdir");
133 if (defaultDir == null) defaultDir = getAttribute(root, "defaultDir");
134
135 // Clear previous config
136 pathMapper = new PathMapper();
137 decorators = new HashMap();
138
139 // Get decorators
140 NodeList decoratorNodes = root.getElementsByTagName("decorator");
141 Element decoratorElement = null;
142
143 for (int i = 0; i < decoratorNodes.getLength(); i++) {
144 String name = null, page = null, uriPath = null, role = null;
145
146 // get the current decorator element
147 decoratorElement = (Element) decoratorNodes.item(i);
148
149 if (getAttribute(decoratorElement, "name") != null) {
150 // The new format is used
151 name = getAttribute(decoratorElement, "name");
152 page = getAttribute(decoratorElement, "page");
153 uriPath = getAttribute(decoratorElement, "webapp");
154 role = getAttribute(decoratorElement, "role");
155
156 // Append the defaultDir
157 if (defaultDir != null && page != null && page.length() > 0 && !page.startsWith("/")) {
158 if (page.charAt(0) == '/') page = defaultDir + page;
159 else page = defaultDir + '/' + page;
160 }
161
162 // The uriPath must begin with a slash
163 if (uriPath != null && uriPath.length() > 0) {
164 if (uriPath.charAt(0) != '/') uriPath = '/' + uriPath;
165 }
166
167 // Get all <pattern>...</pattern> and <url-pattern>...</url-pattern> nodes and add a mapping
168 populatePathMapper(decoratorElement.getElementsByTagName("pattern"), role, name);
169 populatePathMapper(decoratorElement.getElementsByTagName("url-pattern"), role, name);
170 }
171 else {
172 // NOTE: Deprecated format
173 name = getContainedText(decoratorNodes.item(i), "decorator-name");
174 page = getContainedText(decoratorNodes.item(i), "resource");
175 // We have this here because the use of jsp-file is deprecated, but we still want
176 // it to work.
177 if (page == null) page = getContainedText(decoratorNodes.item(i), "jsp-file");
178 }
179
180 Map params = new HashMap();
181
182 NodeList paramNodes = decoratorElement.getElementsByTagName("init-param");
183 for (int ii = 0; ii < paramNodes.getLength(); ii++) {
184 String paramName = getContainedText(paramNodes.item(ii), "param-name");
185 String paramValue = getContainedText(paramNodes.item(ii), "param-value");
186 params.put(paramName, paramValue);
187 }
188 storeDecorator(new DefaultDecorator(name, page, uriPath, role, params));
189 }
190
191 // Get (deprecated format) decorator-mappings
192 NodeList mappingNodes = root.getElementsByTagName("decorator-mapping");
193 for (int i = 0; i < mappingNodes.getLength(); i++) {
194 Element n = (Element)mappingNodes.item(i);
195 String name = getContainedText(mappingNodes.item(i), "decorator-name");
196
197 // Get all <url-pattern>...</url-pattern> nodes and add a mapping
198 populatePathMapper(n.getElementsByTagName("url-pattern"), null, name);
199 }
200 }
201
202 /**
203 * Extracts each URL pattern and adds it to the pathMapper map.
204 */
205 private void populatePathMapper(NodeList patternNodes, String role, String name) {
206 for (int j = 0; j < patternNodes.getLength(); j++) {
207 Element p = (Element)patternNodes.item(j);
208 Text patternText = (Text) p.getFirstChild();
209 if (patternText != null) {
210 String pattern = patternText.getData().trim();
211 if (pattern != null) {
212 if (role != null) {
213 // concatenate name and role to allow more
214 // than one decorator per role
215 pathMapper.put(name + role, pattern);
216 }
217 else {
218 pathMapper.put(name, pattern);
219 }
220 }
221 }
222 }
223 }
224
225 /** Override default behavior of element.getAttribute (returns the empty string) to return null. */
226 private static String getAttribute(Element element, String name) {
227 if (element != null && element.getAttribute(name) != null && element.getAttribute(name).trim() != "") {
228 return element.getAttribute(name).trim();
229 }
230 else {
231 return null;
232 }
233 }
234
235 /**
236 * With a given parent XML Element, find the text contents of the child element with
237 * supplied name.
238 */
239 private static String getContainedText(Node parent, String childTagName) {
240 try {
241 Node tag = ((Element)parent).getElementsByTagName(childTagName).item(0);
242 String text = ((Text)tag.getFirstChild()).getData();
243 return text;
244 }
245 catch (Exception e) {
246 return null;
247 }
248 }
249
250 /** Store Decorator in Map */
251 private void storeDecorator(Decorator d) {
252 if (d.getRole() != null) {
253 decorators.put(d.getName() + d.getRole(), d);
254 }
255 else {
256 decorators.put(d.getName(), d);
257 }
258 }
259
260 /** Check if configuration file has been updated, and if so, reload. */
261 private synchronized void refresh() throws ServletException {
262 if (configFile != null && configLastModified != configFile.lastModified()) loadConfig();
263 }
264 }