1 /*
2 * Title: DefaultFactory
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.factory;
11
12 import com.opensymphony.module.sitemesh.Config;
13 import com.opensymphony.module.sitemesh.DecoratorMapper;
14 import com.opensymphony.module.sitemesh.PageParser;
15
16 import org.w3c.dom.Document;
17 import org.w3c.dom.Element;
18 import org.w3c.dom.NodeList;
19 import org.w3c.dom.Text;
20 import org.xml.sax.SAXException;
21
22 import javax.xml.parsers.DocumentBuilder;
23 import javax.xml.parsers.DocumentBuilderFactory;
24 import javax.xml.parsers.ParserConfigurationException;
25 import java.io.File;
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.util;
29
30 /**
31 * DefaultFactory, reads configuration from <code>/WEB-INF/sitemesh.xml</code>, or uses the
32 * default configuration if <code>sitemesh.xml</code> does not exist.
33 *
34 * @author <a href="mailto:joe@truemesh.com">Joe Walnes</a>
35 * @author <a href="mailto:pathos@pandora.be">Mathias Bogaert</a>
36 * @version $Revision: 1.3 $
37 */
38 public class DefaultFactory extends BaseFactory {
39 String configFileName = "/WEB-INF/sitemesh.xml";
40 File configFile;
41 long configLastModified;
42 Map configProps = new HashMap();
43
44 String excludesFileName;
45 File excludesFile;
46
47 public DefaultFactory(Config config) {
48 super(config);
49
50 // configFilePath is null if loaded from war file
51 String configFilePath = config.getServletContext().getRealPath(configFileName);
52
53 if (configFilePath != null) { // disable config auto reloading for .war files
54 configFile = new File(configFilePath);
55 }
56
57 loadConfig();
58 }
59
60 /** Refresh config before delegating to superclass. */
61 public DecoratorMapper getDecoratorMapper() {
62 refresh();
63 return super.getDecoratorMapper();
64 }
65
66 /** Refresh config before delegating to superclass. */
67 public PageParser getPageParser(String contentType) {
68 refresh();
69 return super.getPageParser(contentType);
70 }
71
72 /** Refresh config before delegating to superclass. */
73 public boolean shouldParsePage(String contentType) {
74 refresh();
75 return super.shouldParsePage(contentType);
76 }
77
78 /**
79 * Returns <code>true</code> if the supplied path matches one of the exclude
80 * URLs specified in sitemesh.xml, otherwise returns <code>false</code>. This
81 * method refreshes the config before delgating to the superclass.
82 */
83 public boolean isPathExcluded(String path) {
84 refresh();
85 return super.isPathExcluded(path);
86 }
87
88 /** Load configuration from file. */
89 private synchronized void loadConfig() {
90 try {
91 // Load and parse the sitemesh.xml file
92 Element root = loadSitemeshXML();
93
94 NodeList sections = root.getChildNodes();
95 // Loop through child elements of root node
96 for (int i = 0; i < sections.getLength(); i++) {
97 if (sections.item(i) instanceof Element) {
98 Element curr = (Element)sections.item(i);
99 NodeList children = curr.getChildNodes();
100
101 if ("property".equalsIgnoreCase(curr.getTagName())) {
102 String name = curr.getAttribute("name");
103 String value = curr.getAttribute("value");
104 if (!"".equals(name) && !"".equals(value)) {
105 configProps.put("${" + name + "}", value);
106 }
107 }
108 else if ("page-parsers".equalsIgnoreCase(curr.getTagName())) {
109 // handle <page-parsers>
110 loadPageParsers(children);
111 }
112 else if ("decorator-mappers".equalsIgnoreCase(curr.getTagName())) {
113 // handle <decorator-mappers>
114 loadDecoratorMappers(children);
115 }
116 else if ("excludes".equalsIgnoreCase(curr.getTagName())) {
117 // handle <excludes>
118 String fileName = replaceProperties(curr.getAttribute("file"));
119 if (!"".equals(fileName)) {
120 excludesFileName = fileName;
121 loadExcludes();
122 }
123 }
124 }
125 }
126 }
127 catch (ParserConfigurationException e) {
128 report("Could not get XML parser", e);
129 }
130 catch (IOException e) {
131 report("Could not read config file : " + configFileName, e);
132 }
133 catch (SAXException e) {
134 report("Could not parse config file : " + configFileName, e);
135 }
136 }
137
138 private Element loadSitemeshXML()
139 throws ParserConfigurationException, IOException, SAXException
140 {
141 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
142 DocumentBuilder builder = factory.newDocumentBuilder();
143
144 InputStream is = null;
145
146 if (configFile == null) {
147 is = config.getServletContext().getResourceAsStream(configFileName);
148 }
149 else if (configFile.exists() && configFile.canRead()) {
150 is = configFile.toURL().openStream();
151 }
152
153 if (is == null){ // load the default sitemesh configuration
154 is = getClass().getClassLoader().getResourceAsStream("com/opensymphony/module/sitemesh/factory/sitemesh-default.xml");
155 }
156
157 if (is == null){ // load the default sitemesh configuration using another classloader
158 is = Thread.currentThread().getContextClassLoader().getResourceAsStream("com/opensymphony/module/sitemesh/factory/sitemesh-default.xml");
159 }
160
161 if (is == null){
162 throw new IllegalStateException("Cannot load default configuration from jar");
163 }
164
165 if (configFile != null) configLastModified = configFile.lastModified();
166
167 Document doc = builder.parse(is);
168 Element root = doc.getDocumentElement();
169 // Verify root element
170 if (!"sitemesh".equalsIgnoreCase(root.getTagName())) {
171 report("Root element of sitemesh configuration file not <sitemesh>", null);
172 }
173 return root;
174 }
175
176 private void loadExcludes()
177 throws ParserConfigurationException, IOException, SAXException
178 {
179 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
180 DocumentBuilder builder = factory.newDocumentBuilder();
181
182 InputStream is = null;
183
184 if (excludesFile == null) {
185 is = config.getServletContext().getResourceAsStream(excludesFileName);
186 }
187 else if (excludesFile.exists() && excludesFile.canRead()) {
188 is = excludesFile.toURL().openStream();
189 }
190
191 if (is == null){
192 throw new IllegalStateException("Cannot load excludes configuration file from jar");
193 }
194
195 Document document = builder.parse(is);
196 Element root = document.getDocumentElement();
197 NodeList sections = root.getChildNodes();
198
199 // Loop through child elements of root node looking for the <excludes> block
200 for (int i = 0; i < sections.getLength(); i++) {
201 if (sections.item(i) instanceof Element) {
202 Element curr = (Element)sections.item(i);
203 if ("excludes".equalsIgnoreCase(curr.getTagName())) {
204 loadExcludeUrls(curr.getChildNodes());
205 }
206 }
207 }
208 }
209
210 /** Loop through children of 'page-parsers' element and add all 'parser' mappings. */
211 private void loadPageParsers(NodeList nodes) {
212 clearParserMappings();
213 for (int i = 0; i < nodes.getLength(); i++) {
214 if (nodes.item(i) instanceof Element) {
215 Element curr = (Element)nodes.item(i);
216
217 if ("parser".equalsIgnoreCase(curr.getTagName())) {
218 String className = curr.getAttribute("class");
219 String contentType = curr.getAttribute("content-type");
220 mapParser(contentType, className);
221 }
222 }
223 }
224 }
225
226 private void loadDecoratorMappers(NodeList nodes) {
227 clearDecoratorMappers();
228 Properties emptyProps = new Properties();
229
230 pushDecoratorMapper("com.opensymphony.module.sitemesh.mapper.NullDecoratorMapper", emptyProps);
231
232 // note, this works from the bottom node up.
233 for (int i = nodes.getLength() - 1; i > 0; i--) {
234 if (nodes.item(i) instanceof Element) {
235 Element curr = (Element)nodes.item(i);
236 if ("mapper".equalsIgnoreCase(curr.getTagName())) {
237 String className = curr.getAttribute("class");
238 Properties props = new Properties();
239 // build properties from <param> tags.
240 NodeList children = curr.getChildNodes();
241 for (int j = 0; j < children.getLength(); j++) {
242 if (children.item(j) instanceof Element) {
243 Element currC = (Element)children.item(j);
244 if ("param".equalsIgnoreCase(currC.getTagName())) {
245 String value = currC.getAttribute("value");
246 props.put(currC.getAttribute("name"), replaceProperties(value));
247 }
248 }
249 }
250 // add mapper
251 pushDecoratorMapper(className, props);
252 }
253 }
254 }
255
256 pushDecoratorMapper("com.opensymphony.module.sitemesh.mapper.InlineDecoratorMapper", emptyProps);
257 }
258
259 /**
260 * Reads in all the url patterns to exclude from decoration.
261 */
262 private void loadExcludeUrls(NodeList nodes) {
263 clearExcludeUrls();
264 for (int i = 0; i < nodes.getLength(); i++) {
265 if (nodes.item(i) instanceof Element) {
266 Element p = (Element) nodes.item(i);
267 if ("pattern".equalsIgnoreCase(p.getTagName()) || "url-pattern".equalsIgnoreCase(p.getTagName())) {
268 Text patternText = (Text) p.getFirstChild();
269 if (patternText != null) {
270 String pattern = patternText.getData().trim();
271 if (pattern != null) {
272 addExcludeUrl(pattern);
273 }
274 }
275 }
276 }
277 }
278 }
279
280 /** Check if configuration file has been modified, and if so reload it. */
281 private void refresh() {
282 if (configFile != null && configLastModified != configFile.lastModified()) loadConfig();
283 }
284
285 /**
286 * Replaces any properties that appear in the supplied string
287 * with their actual values
288 *
289 * @param str the string to replace the properties in
290 * @return the same string but with any properties expanded out to their
291 * actual values
292 */
293 private String replaceProperties(String str) {
294 Set props = configProps.entrySet();
295 for (Iterator it = props.iterator(); it.hasNext();)
296 {
297 Map.Entry entry = (Map.Entry) it.next();
298 String key = (String) entry.getKey();
299 int idx;
300 while ((idx = str.indexOf(key)) >= 0) {
301 StringBuffer buf = new StringBuffer(100);
302 buf.append(str.substring(0, idx));
303 buf.append(entry.getValue());
304 buf.append(str.substring(idx + key.length()));
305 str = buf.toString();
306 }
307 }
308 return str;
309 }
310 }