App Engine 1.8.4.
[gae.git] / java / src / main / com / google / apphosting / utils / config / AbstractConfigXmlReader.java
blobb3a53f7bea7c4042894a3a96c9cf049677b3e163
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;
9 import java.io.File;
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;
18 /**
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> {
27 /**
28 * Callback notified as nodes are traversed in the parsed XML.
30 public interface ParserCallback {
31 /**
32 * Node handling callback.
34 * @param node the newly-entered node
35 * @param ancestors a possibly-empty (but not null) stack of parent nodes
36 * of {@code node}.
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;
51 /**
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;
61 this.appDir = appDir;
62 this.required = required;
63 logger = Logger.getLogger(this.getClass().getName());
66 /**
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();
75 /**
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();
83 /**
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);
91 /**
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;
103 T configXml;
104 if (!required && !fileExists()) {
105 return null;
107 try {
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);
118 } finally {
119 close(is);
121 return configXml;
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() {
148 try {
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() {
164 return null;
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()) {
176 return null;
178 try {
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
194 * element.
196 * @throws AppEngineConfigException If the input stream cannot be parsed.
198 protected XmlParser.Node getTopLevelNode(InputStream is) {
199 XmlParser xmlParser = createXmlParser();
200 try {
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);
238 stack.push(node);
239 for (Object child : node) {
240 if (child instanceof XmlParser.Node) {
241 parse((XmlParser.Node) child, stack, parseCb);
244 stack.pop();
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) {
254 if (is != null) {
255 try {
256 is.close();
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) {
271 return null;
272 } else {
273 return string.trim();