1 // Copyright 2009 Google Inc. All Rights Reserved.
3 package com
.google
.apphosting
.utils
.config
;
5 import org
.mortbay
.xml
.XmlParser
;
6 import org
.mortbay
.xml
.XmlParser
.Node
;
7 import org
.xml
.sax
.SAXException
;
10 import java
.io
.FileInputStream
;
11 import java
.io
.FileNotFoundException
;
12 import java
.io
.IOException
;
13 import java
.io
.InputStream
;
14 import java
.util
.Stack
;
15 import java
.util
.logging
.Level
;
16 import java
.util
.logging
.Logger
;
19 * Abstract class for reading the XML files to configure an application.
21 * @param <T> the type of the configuration object returned
25 public abstract class AbstractConfigXmlReader
<T
> {
28 * Callback notified as nodes are traversed in the parsed XML.
30 public interface ParserCallback
{
32 * Node handling callback.
34 * @param node the newly-entered node
35 * @param ancestors a possibly-empty (but not null) stack of parent nodes
37 * @throws AppEngineConfigException if something is wrong in the XML
39 public void newNode(XmlParser
.Node node
, Stack
<XmlParser
.Node
> ancestors
);
42 /** The path to the top level directory of the application. */
43 protected final String appDir
;
45 /** Whether the config file must exist in a correct application. */
46 protected final boolean required
;
48 /** A logger for messages. */
49 protected Logger logger
;
52 * Initializes the generic attributes of all our configuration XML readers.
54 * @param appDir pathname to the application directory
55 * @param required {@code true} if is an error for the config file not to exist.
57 public AbstractConfigXmlReader(String appDir
, boolean required
) {
58 if (appDir
.length() > 0 && appDir
.charAt(appDir
.length() - 1) != File
.separatorChar
) {
59 appDir
+= File
.separatorChar
;
62 this.required
= required
;
63 logger
= Logger
.getLogger(this.getClass().getName());
67 * Gets the absolute filename for the configuration file.
69 * @return concatenation of {@link #appDir} and {@link #getRelativeFilename()}
71 public String
getFilename() {
72 return appDir
+ getRelativeFilename();
76 * Fetches the name of the configuration file processed by this instance,
77 * relative to the application directory.
79 * @return relative pathname for a configuration file
81 protected abstract String
getRelativeFilename();
84 * Parses the input stream to compute an instance of {@code T}.
86 * @return the parsed config file
87 * @throws AppEngineConfigException if there is an error.
89 protected abstract T
processXml(InputStream is
);
92 * Does the work of reading the XML file, processing it, and either returning
93 * an object representing the result or throwing error information.
95 * @return A {@link AppEngineWebXml} config object derived from the
96 * contents of the config xml, or {@code null} if no such file is defined and
97 * the config file is optional.
99 * @throws AppEngineConfigException If the file cannot be parsed properly
101 protected T
readConfigXml() {
102 InputStream is
= null;
104 if (!required
&& !fileExists()) {
108 is
= getInputStream();
109 configXml
= processXml(is
);
110 logger
.info("Successfully processed " + getFilename());
111 } catch (Exception e
) {
112 String msg
= "Received exception processing " + getFilename();
113 logger
.log(Level
.SEVERE
, msg
, e
);
114 if (e
instanceof AppEngineConfigException
) {
115 throw (AppEngineConfigException
) e
;
117 throw new AppEngineConfigException(msg
, e
);
125 * Tests for file existence. Test clases will often override this, to lie
126 * (and thus stay small by avoiding needing the real filesystem).
128 protected boolean fileExists() {
129 return (new File(getFilename())).exists();
133 * Tests for file existence. Test clases will often override this, to lie
134 * (and thus stay small by avoiding needing the real filesystem).
136 protected boolean generatedFileExists() {
137 return getGeneratedFile().exists();
141 * Opens an input stream, or fails with an AppEngineConfigException
142 * containing helpful information. Test classes will often override this.
144 * @return an open {@link InputStream}
145 * @throws AppEngineConfigException
147 protected InputStream
getInputStream() {
149 return new FileInputStream(getFilename());
150 } catch (FileNotFoundException fnfe
) {
151 throw new AppEngineConfigException(
152 "Could not locate " + new File(getFilename()).getAbsolutePath(), fnfe
);
157 * Returns a {@code File} for the generated variant of this file, or
158 * {@code null} if no generation is possible. This is not an indication that
159 * the file exists, only of where it would be if it does exist.
161 * @return the generated file, if there might be one; {@code null} if not.
163 protected File
getGeneratedFile() {
168 * Returns an InputStream of the generated contents, or {@code null} if no
169 * generated contents are available.
171 * @return input stream, or {@code null}
173 protected InputStream
getGeneratedStream() {
174 File file
= getGeneratedFile();
175 if (file
== null || !file
.exists()) {
179 return new FileInputStream(file
);
180 } catch (FileNotFoundException ex
) {
181 throw new AppEngineConfigException("can't find generated " + file
.getPath());
186 * Creates an {@link XmlParser} to use when parsing this file.
188 protected XmlParser
createXmlParser() {
189 return new XmlParser();
193 * Given an InputStream, create a Node corresponding to the top level xml
196 * @throws AppEngineConfigException If the input stream cannot be parsed.
198 protected XmlParser
.Node
getTopLevelNode(InputStream is
) {
199 XmlParser xmlParser
= createXmlParser();
201 return xmlParser
.parse(is
);
202 } catch (IOException e
) {
203 String msg
= "Received IOException parsing the input stream for " + getFilename();
204 logger
.log(Level
.SEVERE
, msg
, e
);
205 throw new AppEngineConfigException(msg
, e
);
206 } catch (SAXException e
) {
207 String msg
= "Received SAXException parsing the input stream for " + getFilename();
208 logger
.log(Level
.SEVERE
, msg
, e
);
209 throw new AppEngineConfigException(msg
, e
);
214 * Parses the nodes of an XML file. This is <i>limited</i> XML parsing,
215 * in particular skipping any TEXT element and parsing only the nodes.
217 * @param parseCb the ParseCallback to call for each node
218 * @param is the input stream to read
219 * @throws AppEngineConfigException on any error
221 protected void parse(ParserCallback parseCb
, InputStream is
) {
222 Stack
<XmlParser
.Node
> stack
= new Stack
<XmlParser
.Node
>();
223 XmlParser
.Node top
= getTopLevelNode(is
);
224 parse(top
, stack
, parseCb
);
228 * Recursive descent helper for {@link #parse(ParserCallback, InputStream)}, calling
229 * the callback for this node and recursing for its children.
231 * @param node the node being visited
232 * @param stack the anscestors of {@code node}
233 * @param parseCb the visitor callback
234 * @throws AppEngineConfigException for any configuration errors
236 protected void parse(XmlParser
.Node node
, Stack
<XmlParser
.Node
> stack
, ParserCallback parseCb
) {
237 parseCb
.newNode(node
, stack
);
239 for (Object child
: node
) {
240 if (child
instanceof XmlParser
.Node
) {
241 parse((XmlParser
.Node
) child
, stack
, parseCb
);
248 * Closes the given input stream, converting any {@link IOException} thrown
249 * to an {@link AppEngineConfigException} if necessary.
251 * @throws AppEngineConfigException if the input stream cannot close
253 protected void close(InputStream is
) {
257 } catch (IOException e
) {
258 throw new AppEngineConfigException(e
);
264 * Gets the Node's first (index zero) content value, as a trimmed string.
266 * @param node the node to get the string from.
268 protected String
getString(Node node
) {
269 String string
= (String
) node
.get(0);
270 if (string
== null) {
273 return string
.trim();