/**
* Copyright 2013 Google Inc. All Rights Reserved.
*/
package com.google.appengine.endpoints;
import com.google.api.server.spi.config.Api;
import com.google.appengine.repackaged.com.google.common.io.Files;
import com.google.common.base.Joiner;
import eu.infomas.annotation.AnnotationDetector;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.project.MavenProject;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
/**
* Process Endpoints annotations and change web.xml accordingly.
*
*/
public class WebXmlProcessing {
Log log;
String webXmlSourcePath;
String outputDirectory;
MavenProject project;
String userSpecifiedServiceClassNames;
public WebXmlProcessing(Log log, String webXmlSourcePath,
String outputDirectory, MavenProject project,
String userSpecifiedServiceClassNames) {
this.log = log;
this.webXmlSourcePath = webXmlSourcePath;
this.outputDirectory = outputDirectory;
this.project = project;
this.userSpecifiedServiceClassNames = userSpecifiedServiceClassNames;
}
private Log getLog() {
return log;
}
public List<String> getAPIServicesClasses() {
List<String> classes;
if (userSpecifiedServiceClassNames != null) {
classes = Arrays.asList(userSpecifiedServiceClassNames.split(","));
} else {
ApiReporter reporter = new ApiReporter();
String targetDir = project.getBuild().getOutputDirectory();
final AnnotationDetector cf = new AnnotationDetector(reporter);
try {
cf.detect(new File(targetDir));
} catch (IOException ex) {
getLog().info(ex);
}
classes = reporter.getClasses();
}
XmlUtil util = new XmlUtil();
try {
util.updateWebXml(classes, webXmlSourcePath);
} catch (Exception ex) {
getLog().info("Error: " + ex);
}
return classes;
}
class ApiReporter implements AnnotationDetector.TypeReporter {
private List<String> classes = new ArrayList<String>();
@SuppressWarnings("unchecked")
@Override
public Class<? extends Annotation>[] annotations() {
return new Class[]{Api.class};
}
@Override
public void reportTypeAnnotation(Class<? extends Annotation> annotation, String className) {
classes.add(className);
}
public List<String> getClasses() {
return classes;
}
}
/**
* Xml Manipulation Utility Class for modifying web.xml for generated APIs.
*/
class XmlUtil {
private static final String COMMA = ",";
private static final String WEB_APP = "web-app";
private static final String INIT_PARAM = "init-param";
private static final String SERVLET = "servlet";
private static final String SERVLET_NAME = "servlet-name";
private static final String SERVLET_MAPPING = "servlet-mapping";
private static final String SERVLET_CLASS = "servlet-class";
private static final String URL_PATTERN = "url-pattern";
private static final String SPI_URL_PATTERN = "/_ah/spi/*";
private static final String PARAM_NAME = "param-name";
private static final String PARAM_VALUE = "param-value";
private static final String SERVICES = "services";
private static final String SYSTEM_SERVICE_SERVLET = "SystemServiceServlet";
private static final String SYSTEM_SERVICE_SERVLET_CLASS =
"com.google.api.server.spi.SystemServiceServlet";
/**
* Finds the WebApp node in web.xml document. Then tries to find
* SystemServiceServlet node and returns it. If not found, returns WebApp
* node. The returned type is of type Element
*
* @return SystemServiceServlet node if found, else WebApp node.
*/
private Node findSystemServiceServlet(Document doc) {
Node webAppNode;
for (webAppNode = doc.getFirstChild(); webAppNode != null;
webAppNode = webAppNode.getNextSibling()) {
if (isElementAndNamed(webAppNode, WEB_APP)) {
break;
}
}
if (webAppNode == null) {
getLog().info("Not a valid web.xml document");
return null;
}
Node systemServiceServletNode;
for (systemServiceServletNode = webAppNode.getFirstChild();
systemServiceServletNode != null;
systemServiceServletNode = systemServiceServletNode.getNextSibling()) {
if (isElementAndNamed(systemServiceServletNode, SERVLET)) {
for (Node n3 = systemServiceServletNode.getFirstChild();
n3 != null; n3 = n3.getNextSibling()) {
if (isElementAndNamed(n3, SERVLET_NAME)
&& n3.getTextContent().equals(SYSTEM_SERVICE_SERVLET)) {
return systemServiceServletNode;
}
}
}
}
return webAppNode;
}
/**
* Insert a SystemServiceServlet node in web.xml inside webApp node.
*
* @return The inserted SystemServiceServlet node.
*/
private Node insertSystemServiceServlet(Document doc, Node webAppNode, String spc,
String delimiter) {
Node n2, n3, n4, n5;
n5 = doc.createTextNode(spc);
webAppNode.appendChild(n5);
n2 = doc.createElement(SERVLET);
webAppNode.appendChild(n2);
n5 = doc.createTextNode(delimiter + spc);
webAppNode.appendChild(n5);
n3 = doc.createElement(SERVLET_MAPPING);
webAppNode.appendChild(n3);
n5 = doc.createTextNode(delimiter);
webAppNode.appendChild(n5);
n5 = doc.createTextNode("\n" + spc + spc);
n2.appendChild(n5);
n5 = doc.createElement(SERVLET_NAME);
n5.setTextContent(SYSTEM_SERVICE_SERVLET);
n2.appendChild(n5);
n5 = doc.createTextNode("\n" + spc + spc);
n2.appendChild(n5);
n5 = doc.createElement(SERVLET_CLASS);
n5.setTextContent(SYSTEM_SERVICE_SERVLET_CLASS);
n2.appendChild(n5);
n5 = doc.createTextNode("\n" + spc + spc);
n2.appendChild(n5);
n4 = doc.createElement(INIT_PARAM);
n2.appendChild(n4);
n5 = doc.createTextNode("\n" + spc);
n2.appendChild(n5);
n5 = doc.createTextNode("\n" + spc + spc);
n3.appendChild(n5);
n5 = doc.createElement(SERVLET_NAME);
n5.setTextContent(SYSTEM_SERVICE_SERVLET);
n3.appendChild(n5);
n5 = doc.createTextNode("\n" + spc + spc);
n3.appendChild(n5);
n5 = doc.createElement(URL_PATTERN);
n5.setTextContent(SPI_URL_PATTERN);
n3.appendChild(n5);
n5 = doc.createTextNode("\n" + spc);
n3.appendChild(n5);
n5 = doc.createTextNode("\n" + spc + spc + spc);
n4.appendChild(n5);
n5 = doc.createElement(PARAM_NAME);
n5.setTextContent(SERVICES);
n4.appendChild(n5);
n5 = doc.createTextNode("\n" + spc + spc + spc);
n4.appendChild(n5);
n5 = doc.createElement(PARAM_VALUE);
n5.setTextContent("");
n4.appendChild(n5);
n5 = doc.createTextNode("\n" + spc + spc);
n4.appendChild(n5);
return n2;
}
/**
* Checks if a node is an XML element and checks if it has a specific name
*
* @param node
* @param name
* @return true if matching element, false if name doesn't match OR if node
* type isn't ELEMENT
*/
private boolean isElementAndNamed(Node node, String name) {
if (node == null || name == null) {
throw new IllegalArgumentException();
}
return (node.getNodeType() == Node.ELEMENT_NODE && node.getNodeName().equals(name));
}
private void saveFile(Document doc, String filePath)
throws TransformerFactoryConfigurationError, TransformerException,
IOException {
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.transform(new DOMSource(doc), new StreamResult(new File(filePath)));
}
/**
* Update the SystemServiceServlet parameter in web.xml, it doesn't make
* changes if nothing new is to be added. If changes are required, it will
* modify the file and save
*
* @param document
* @param systemServiceServletNode
* @param services
* @return
*/
private boolean updateSystemServiceServletParam(Document doc,
Node systemServiceServletNode, List<String> services) {
Node initParamNode;
for (initParamNode = systemServiceServletNode.getFirstChild();
initParamNode != null;
initParamNode = initParamNode.getNextSibling()) {
if (isElementAndNamed(initParamNode, INIT_PARAM)) {
break;
}
}
if (initParamNode == null) {
getLog().info("Not a valid web.xml document");
return false;
}
Node paramValueNode;
for (paramValueNode = initParamNode.getFirstChild();
paramValueNode != null;
paramValueNode = paramValueNode.getNextSibling()) {
if (isElementAndNamed(paramValueNode, PARAM_VALUE)) {
break;
}
}
if (paramValueNode == null) {
getLog().info("Not a valid web.xml document");
return false;
}
// get all services the file currently lists,
// put it in a treeset for sorted order, also removes duplicates
String serviceXMLString = paramValueNode.getTextContent();
Set<String> servicesOnFile = new TreeSet<String>();
if (serviceXMLString != null && !serviceXMLString.trim().isEmpty()) {
String[] servicesArray = serviceXMLString.split(",");
for (String s : servicesArray) {
servicesOnFile.add(s.trim());
}
}
// find all services we need to remove
List<String> servicesToRemove = new ArrayList<String>();
for (String s : servicesOnFile) {
if (!services.contains(s)) {
servicesToRemove.add(s);
}
}
// find all services we need to add
List<String> servicesToAdd = new ArrayList<String>();
for (String s : services) {
if (!servicesOnFile.contains(s)) {
servicesToAdd.add(s);
}
}
// if we don't need to make any changes, then return false
if (servicesToAdd.isEmpty() && servicesToRemove.isEmpty()) {
return false;
}
// remove those marked for removal
for (String s : servicesToRemove) {
servicesOnFile.remove(s);
}
// add those marked for adding
for (String s : servicesToAdd) {
servicesOnFile.add(s);
}
// write the appropriate data to the file
if (servicesOnFile.isEmpty()) {
paramValueNode.setTextContent("");
} else {
Joiner joiner = Joiner.on(COMMA);
paramValueNode.setTextContent(joiner.join(servicesOnFile));
}
// indicate that a save is required
return true;
}
public void updateWebXml(List<String> services, String webXmlPath)
throws ParserConfigurationException, SAXException, IOException,
TransformerFactoryConfigurationError, TransformerException {
boolean saveRequired;
String spc = " ";
String delimiter = "\n";
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
Document document = docBuilder.parse(webXmlPath);
Node systemServiceServletNode = findSystemServiceServlet(document);
if (systemServiceServletNode == null) {
getLog().info("Not a valid web.xml document");
return;
}
if (isElementAndNamed(systemServiceServletNode, WEB_APP)) {
systemServiceServletNode = insertSystemServiceServlet(document,
systemServiceServletNode, spc, delimiter);
saveRequired = true;
}
saveRequired = updateSystemServiceServletParam(document,
systemServiceServletNode, services);
String generatedWebInf = outputDirectory + "/WEB-INF";
new File(generatedWebInf).mkdirs();
saveFile(document, generatedWebInf + "/web.xml");
Files.copy(
new File(new File(webXmlPath).getParentFile(), "appengine-web.xml"),
new File(generatedWebInf, "appengine-web.xml"));
}
}
}