Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / apphosting / utils / config / EarHelper.java
blob6c16b20c5b7397e4f65d1b75d25ce25b67068f3d
1 package com.google.apphosting.utils.config;
3 import com.google.common.base.Preconditions;
4 import com.google.common.collect.ImmutableList;
6 import org.xml.sax.SAXException;
8 import java.io.File;
9 import java.io.FileInputStream;
10 import java.io.FileNotFoundException;
11 import java.io.IOException;
12 import java.io.InputStream;
13 import java.util.logging.Logger;
15 import javax.annotation.Nullable;
16 import javax.xml.XMLConstants;
17 import javax.xml.transform.stream.StreamSource;
18 import javax.xml.validation.SchemaFactory;
20 /**
21 * Utility for server discovery within an EAR directory.
24 public class EarHelper {
25 private static final Logger LOGGER = Logger.getLogger(EarHelper.class.getName());
26 private static final AppEngineApplicationXmlReader APP_ENGINE_APPLICATION_XML_READER =
27 new AppEngineApplicationXmlReader();
28 private static final ApplicationXmlReader APPLICATION_XML_READER = new ApplicationXmlReader();
30 private static final String META_INF = "META-INF";
31 private static final String APPENGINE_APPLICATION_XML_NAME = "appengine-application.xml";
32 private static final String APPLICATION_XML_NAME = "application.xml";
34 /**
35 * Returns true if passed a path for an EAR directory.
36 * <p>
37 * This is a convenience fuction that simply calls
38 * {@link #isEar(String, boolean)} for {@code directory} with {@code logWhyNot}
39 * set to true.
41 public static boolean isEar(String dir) {
42 return isEar(dir, true);
45 /**
46 * Returns true if {@code directory} contains a {@link #META_INF} sub-directory
47 * with the required {@link #APPENGINE_APPLICATION_XML_NAME} and
48 * {@link #APPLICATION_XML_NAME} files.
49 * <p>
50 * Note this test is a spot check to distinguish an EAR directory as
51 * opposed to a WAR directory. Passing this test does not guarantee that a
52 * directory is a fully valid EAR directory.
54 * @param directory The path for the directory to check or null
55 * @param logWhyNot true means to log the reason this returns false if
56 * it does so.
58 public static boolean isEar(String directory, boolean logWhyNot) {
59 if (directory == null) {
60 if (logWhyNot) {
61 LOGGER.fine("Directory 'null' is not an EAR directory. ");
63 return false;
66 File metaInf = new File(directory, META_INF);
67 if (!hasFile(metaInf, APPENGINE_APPLICATION_XML_NAME)) {
68 logNotAnEar(directory.toString(), APPENGINE_APPLICATION_XML_NAME, logWhyNot);
69 return false;
72 if (!hasFile(metaInf, APPLICATION_XML_NAME)) {
73 logNotAnEar(directory.toString(), APPLICATION_XML_NAME, logWhyNot);
74 return false;
77 return true;
80 private static void logNotAnEar(String dir, String missingFile,
81 boolean withLogging) {
82 if (withLogging) {
83 LOGGER.fine("Directory '" + dir + "' is not an EAR directory. File "
84 + new File(dir, missingFile) + " not detected.");
88 private static void reportConfigException(String message) {
89 LOGGER.info(message);
90 throw new AppEngineConfigException(message);
93 /**
94 * Reads an {@link EarInfo from} the provided EAR directory.
95 * Validation is done via the xsd schemaFile.
96 * @throws AppEngineConfigException if the provided EAR directory is invalid.
98 public static EarInfo readEarInfo(String earDirectoryPath, File schemaFile)
99 throws AppEngineConfigException {
100 if (!isEar(earDirectoryPath)) {
101 throw new IllegalArgumentException("earDir '" + earDirectoryPath
102 + "' is not a valid EAR directory.");
104 File earDirectory = new File(earDirectoryPath).getAbsoluteFile();
105 File metaInf = new File(earDirectory, META_INF);
106 validateXml(new File(metaInf, APPENGINE_APPLICATION_XML_NAME), schemaFile);
107 AppEngineApplicationXml appEngineApplicationXml = APP_ENGINE_APPLICATION_XML_READER.processXml(
108 getInputStream(metaInf, APPENGINE_APPLICATION_XML_NAME));
109 ApplicationXml applicationXml = APPLICATION_XML_READER.processXml(
110 getInputStream(metaInf, APPLICATION_XML_NAME));
111 String applicationId = appEngineApplicationXml.getApplicationId();
112 ImmutableList.Builder<WebModule> moduleListBuilder = ImmutableList.builder();
113 for (ApplicationXml.Modules.Web web : applicationXml.getModules().getWeb()) {
114 File applicationDirectory = getApplicationDirectory(earDirectory, web.getWebUri());
115 WebModule webModule = readWebModule(web.getContextRoot(), applicationDirectory,
116 null, null, "");
117 if (!applicationId.equals(webModule.getAppEngineWebXml().getAppId())) {
118 LOGGER.info("Application id '" + appEngineApplicationXml.getApplicationId() + "' from '"
119 + new File(metaInf, APPENGINE_APPLICATION_XML_NAME) + "' is overriding "
120 + " application id '" + webModule.getAppEngineWebXml().getAppId() + "' from '"
121 + new File(applicationDirectory, AppEngineWebXmlReader.DEFAULT_RELATIVE_FILENAME)
122 + "'");
123 webModule.getAppEngineWebXml().setAppId(applicationId);
125 moduleListBuilder.add(webModule);
127 ImmutableList<WebModule> webModules = moduleListBuilder.build();
128 if (webModules.size() == 0) {
129 reportConfigException("At least one web module is required in '"
130 + new File(metaInf, APPLICATION_XML_NAME) + "'");
132 return new EarInfo(earDirectory, appEngineApplicationXml, applicationXml, webModules);
136 * Reads a {@link WebModule} from the provided application directory.
137 * <p>
138 * If the application directory contains a WEB-INF/app.yaml file this will call
139 * {@link AppYamlProcessor#convert} to generate WEB-INF/appengine-web.xml and
140 * WEB-INF/web.xml files.
142 * @param contextRoot if this web module is part of an EAR supply the
143 * context-root element from the web module's specification in
144 * ear/META-INF/application.xml otherwise supply null to indicate the web
145 * module has no context root.
146 * @param applicationDirectory the application directory.
147 * @param appengineWebXmlFile the appengine-web.xml to read or null to use the default.
148 * @param webXmlFile the web.xml to read or null to use the default.
149 * @param appIdPrefix a string to prepend to the app id read from appengine-web.xml. May be
150 * empty but not null.
151 * @throws AppEngineConfigException if applicationDirectory is not correct.
153 public static WebModule readWebModule(
154 @Nullable String contextRoot, File applicationDirectory, @Nullable File appengineWebXmlFile,
155 @Nullable File webXmlFile, String appIdPrefix) throws AppEngineConfigException {
156 Preconditions.checkNotNull(appIdPrefix);
157 AppEngineWebXmlReader appEngineWebXmlReader =
158 newAppEngineWebXmlReader(applicationDirectory, appengineWebXmlFile);
159 WebXmlReader webXmlReader = newWebXmlReader(applicationDirectory, webXmlFile);
161 AppYamlProcessor.convert(new File(applicationDirectory, "WEB-INF"),
162 appEngineWebXmlReader.getFilename(), webXmlReader.getFilename());
164 AppEngineWebXml appEngineWebXml;
165 try {
166 appEngineWebXml = appEngineWebXmlReader.readAppEngineWebXml();
167 } catch (AppEngineConfigException aece) {
168 throw new AppEngineConfigException(String.format("Invalid appengine-web.xml(%s) - %s",
169 appEngineWebXmlReader.getFilename(), aece.getMessage()));
171 String appId = appEngineWebXml.getAppId();
172 if (appId != null) {
173 appEngineWebXml.setAppId(appIdPrefix + appEngineWebXml.getAppId());
176 WebXml webXml = webXmlReader.readWebXml();
178 WebModule webModule =
179 new WebModule(applicationDirectory, appEngineWebXml,
180 new File(appEngineWebXmlReader.getFilename()), webXml,
181 new File(webXmlReader.getFilename()), contextRoot);
182 return webModule;
185 private static File getApplicationDirectory(File earDirectory, String contextRoot) {
186 File applicationDirectory = new File(earDirectory, contextRoot);
187 if (!applicationDirectory.exists() || !applicationDirectory.isDirectory()) {
188 reportConfigException("Application directory '" + applicationDirectory
189 + "' must exist and be a directory.");
191 return applicationDirectory;
194 private static AppEngineWebXmlReader newAppEngineWebXmlReader(File applicationDirectory,
195 File appEngineWebXmlFile) {
196 return appEngineWebXmlFile == null ?
197 new AppEngineWebXmlReader(applicationDirectory.getAbsolutePath()) :
198 new AppEngineWebXmlReader(appEngineWebXmlFile.getParent(),
199 appEngineWebXmlFile.getName());
202 private static WebXmlReader newWebXmlReader(File applicationDirectory, File webXmlFile) {
203 return webXmlFile == null ?
204 new WebXmlReader(applicationDirectory.getAbsolutePath()) :
205 new WebXmlReader(webXmlFile.getParent(), webXmlFile.getName());
209 * Returns an input stream for an existing file.
211 private static InputStream getInputStream(File parent, String fileName) {
212 File file = new File(parent, fileName);
213 try {
214 return new FileInputStream(file);
215 } catch (FileNotFoundException fnfe) {
216 throw new IllegalStateException("File should exist - '" + file + "'");
220 private static boolean hasFile(File parent, String child) {
221 File file = new File(parent, child);
222 if (!file.isFile()) {
223 return false;
226 return true;
230 * Validates a given XML document against a given schema.
232 * @param xml file with XML document
233 * @param schema XSD schema to validate with
235 * @throws AppEngineConfigException for malformed XML, or IO errors
237 private static void validateXml(File xml, File schema) {
238 if (!xml.exists()) {
239 return;
241 StreamSource ss = null;
242 try {
243 SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
244 try {
245 ss = new StreamSource(new FileInputStream(xml));
246 factory.newSchema(schema).newValidator().validate(ss);
247 } catch (SAXException ex) {
248 throw new AppEngineConfigException("XML error validating " +
249 xml.getPath() + " against " + schema.getPath(), ex);
251 } catch (IOException ex) {
252 throw new AppEngineConfigException("IO error validating " +
253 xml.getPath() + " against " + schema.getPath(), ex);
254 } finally {
255 if (ss != null) {
256 try {
257 ss.getInputStream().close();
258 } catch (IOException e) {