package com.nortal.jroad.typegen;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.URISyntaxException;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.apache.xmlbeans.XmlBeans;
import org.apache.xmlbeans.XmlException;
import org.apache.xmlbeans.XmlObject;
import org.apache.xmlbeans.XmlOptions;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import com.nortal.jroad.enums.XRoadProtocolVersion;
import com.nortal.jroad.model.XmlBeansXRoadMetadata;
import com.nortal.jroad.typegen.database.DatabaseClasses;
import com.nortal.jroad.typegen.database.DatabaseGenerator;
import com.nortal.jroad.typegen.xmlbeans.BasepackageBinder;
import com.nortal.jroad.typegen.xmlbeans.SimpleFiler;
import com.nortal.jroad.typegen.xmlbeans.XteeSchemaCodePrinter;
import com.nortal.jroad.util.SOAPUtil;
import freemarker.template.TemplateException;
/**
* XMLBeans types generator
*
* @author Dmitri Danilkin
* @author Lauri Lättemäe (lauri.lattemae@nortal.com) - protocol 4.0
*/
public class TypeGen {
private static final String WSDL_NS = "http://schemas.xmlsoap.org/wsdl/";
private static final String SCHEMA_NS = "http://www.w3.org/2001/XMLSchema";
private static final String NS_PREFIX = "xmlns";
private static final String XROAD_V4_NAMESPACE_PATTERN = "http://(.+?)\\.x-road\\.eu.*?$";
private static final String WSDL_DIR = "wsdldir";
private static final String WSDL_SUFFIX = ".wsdl";
private static final String FILENAME__DATABASE_PROPERTIES = "database.properties";
private static final String PROPERTY__DATABASE_NAME_OVERRIDE = "databaseNameOverride";
private static final String XSD_SUFFIX = ".xsd";
private static final String OUTPUT_DIR = "sourcedir";
static final String XSB_DIR = "xsbdir";
private static final String BASE_PACKAGE = "basepackage";
private static final String DB_CLASSES_PACKAGE = "dbclassespackage";
static Map<String, String> argMap = new HashMap<String, String>();
private static Map<String, XmlBeansXRoadMetadata> metadata = new HashMap<String, XmlBeansXRoadMetadata>();
private static List<XmlObject> schemas = new ArrayList<XmlObject>();
private static File curWsdl;
private static File hashFile;
private static byte[] computedHash;
private final static DatabaseDescriptor dbDesc = new DatabaseDescriptor();
public static void main(String[] args) throws Exception {
System.out.println("Starting source generation...");
Timer timer = new Timer();
Timer timer2 = new Timer();
timer2.start();
parseArgs(args);
String outputdir = argMap.get(OUTPUT_DIR);
File dirfile = new File(argMap.get(WSDL_DIR));
if (dirfile.exists()) {
File[] wsdls = getWsdls(dirfile);
if (wsdls.length > 0) {
System.out.println("Parsing " + wsdls.length + " WSDL file(s)...");
timer.start();
loadWsdlSchemasAndGenerateMetadata(wsdls);
System.out.println("WSDL files parsed, time taken: " + timer.finishStr());
System.out.println("Generating sources to " + outputdir + ", base package is: " + argMap.get(BASE_PACKAGE));
timer.start();
generateSource(outputdir, argMap.get(XSB_DIR), argMap.get(BASE_PACKAGE));
System.out.println("Sources generated, time taken: " + timer.finishStr());
System.out.println("Post-processing sources for attachment support...");
timer.start();
AttachmentPostprocessor.process(argMap.get(BASE_PACKAGE), new File(outputdir));
System.out.println("Post-processing completed, time taken: " + timer.finishStr());
System.out.println("Serializing metadata...");
timer.start();
saveMetadata(argMap.get(XSB_DIR));
System.out.println("Metadata serialized, time taken: " + timer.finishStr());
if (argMap.get(DB_CLASSES_PACKAGE) != null) {
System.out.println("Generating database classes...");
timer.start();
generateDatabaseClasses(outputdir);
System.out.println("Database classes generated, time taken: " + timer.finishStr());
}
writeHash();
}
}
System.out.println("All done, total time taken: " + timer2.finishStr());
}
private static void writeHash() throws Exception {
hashFile.delete();
hashFile.getParentFile().mkdirs();
hashFile.createNewFile();
FileOutputStream fos = new FileOutputStream(hashFile);
fos.write(computedHash);
fos.close();
}
/**
* Serializes metadata to specified directory.
*
* @param dir
*/
private static void saveMetadata(String dir) throws Exception {
File metafile = new File(dir, "xroad.metadata");
metafile.createNewFile();
FileOutputStream fos = new FileOutputStream(metafile);
ObjectOutputStream stream = new ObjectOutputStream(fos);
stream.writeObject(metadata);
stream.close();
fos.close();
}
/**
* Generates the XMLBeans source files.
*
* @param outputdir
* @param basepackage
* @throws XmlException
* @throws URISyntaxException
*/
private static void generateSource(String outputdir, String xsbdir, String basepackage)
throws XmlException, URISyntaxException {
XmlObject[] schemasarr = new XmlObject[schemas.size()];
schemas.toArray(schemasarr);
XmlOptions options = new XmlOptions();
options.setCompileDownloadUrls();
options.setGenerateJavaVersion("1.5");
options.setSchemaCodePrinter(new XteeSchemaCodePrinter(options));
XmlBeans.compileXmlBeans(null,
null,
schemasarr,
new BasepackageBinder(basepackage),
null,
new SimpleFiler(outputdir, xsbdir),
options);
}
/**
* Parses command line arguments to a map
*
* @param args
*/
private static void parseArgs(String[] args) {
for (String arg : args) {
String[] pair = arg.split("=", 2);
argMap.put(pair[0], pair[1]);
}
}
/**
* Gets all WSDL files in a directory and returns them as an array
*
* @return File array
*/
private static File[] getWsdls(File dirfile) throws Exception {
File[] allfiles = dirfile.listFiles();
List<File> files = new ArrayList<File>();
if (allfiles != null) {
MessageDigest md = MessageDigest.getInstance("MD5");
String outputDir = argMap.get(OUTPUT_DIR);
for (File file : allfiles) {
if (file.getName().endsWith(WSDL_SUFFIX)) {
files.add(file);
}
if (file.getName().endsWith(WSDL_SUFFIX) || file.getName().endsWith(XSD_SUFFIX)) {
md.update(FileUtil.getBytes(file));
}
}
computedHash = md.digest();
hashFile = new File(outputDir + File.separator + argMap.get(BASE_PACKAGE).replace('.', File.separatorChar)
+ File.separator + "hash.md5");
if (hashFile.exists()) {
byte[] readHash = FileUtil.getBytes(hashFile);
if (Arrays.equals(readHash, computedHash)) {
System.out.println("Skipping generation, files not changed.");
files.clear();
}
}
}
File[] filesarr = new File[files.size()];
files.toArray(filesarr);
return filesarr;
}
/**
* Parse WSDL files - extract types (schemas) and generate metadata for marshalling XmlBeans objects to XTee queries.
*
* @param wsdls
* @throws Exception
*/
private static void loadWsdlSchemasAndGenerateMetadata(File[] wsdls) throws Exception {
DocumentBuilderFactory fac = DocumentBuilderFactory.newInstance();
fac.setNamespaceAware(true);
fac.setValidating(false);
DocumentBuilder builder = fac.newDocumentBuilder();
for (File wsdl : wsdls) {
Document xmlWsdl = builder.parse(wsdl);
curWsdl = wsdl;
schemas.addAll(getSchemas(xmlWsdl.getElementsByTagNameNS(WSDL_NS, "types").item(0),
getNamespaces(xmlWsdl),
wsdl.getParent()));
Properties databaseProps = getDatabaseProps(wsdl.getParentFile());
String databaseNameOverride = databaseProps.getProperty(PROPERTY__DATABASE_NAME_OVERRIDE);
if (databaseNameOverride != null) {
logInfo(PROPERTY__DATABASE_NAME_OVERRIDE + " is set to '" + databaseNameOverride
+ "', will use it as database identifier.");
dbDesc.setId(databaseNameOverride, true);
}
createMetadata(xmlWsdl);
logInfo("Created metadata for database " + dbDesc.getId());
}
}
private static void logInfo(String s) {
System.out.println(s);
}
private static Properties getDatabaseProps(File wsdlLocationDir) {
logInfo("Looking for " + FILENAME__DATABASE_PROPERTIES + " at " + wsdlLocationDir);
File dbPropsFile = new File(wsdlLocationDir, FILENAME__DATABASE_PROPERTIES);
Properties result = new Properties();
if (dbPropsFile.exists()) {
try {
FileInputStream fis = new FileInputStream(dbPropsFile);
logInfo("..found, loading properties.");
result.load(fis);
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
logInfo("..not found");
}
return result;
}
/**
* Creates a map of the namespaces from the WSDL definitions element for XmlBeans
*
* @param wsdlDoc
*/
private static Map<String, String> getNamespaces(Document wsdlDoc) {
Map<String, String> additionalNs = new HashMap<String, String>();
NamedNodeMap map = wsdlDoc.getElementsByTagNameNS(WSDL_NS, "definitions").item(0).getAttributes();
for (int i = 0; i < map.getLength(); i++) {
Node attribute = map.item(i);
if (NS_PREFIX.equals(attribute.getPrefix()) && attribute.getLocalName() != null) {
additionalNs.put(attribute.getLocalName(), attribute.getNodeValue());
}
}
return additionalNs;
}
/**
* Return the schemas contained in the given wsdl:types node.
*
* @param typesNode
* @param additionalNs
* @return A collection of schemas found under the Node
* @throws Exception
*/
private static Collection<XmlObject> getSchemas(Node typesNode, Map<String, String> additionalNs, String schemaPath)
throws Exception {
List<XmlObject> schemas = new ArrayList<XmlObject>();
NodeList schemaNodes = typesNode.getChildNodes();
XmlOptions options = new XmlOptions();
options.setLoadAdditionalNamespaces(additionalNs);
for (int i = 0; i < schemaNodes.getLength(); i++) {
Node schemaNode = schemaNodes.item(i);
if (SCHEMA_NS.equals(schemaNode.getNamespaceURI()) && "schema".equals(schemaNode.getLocalName())) {
XmlObject schema = XmlObject.Factory.parse(schemaNode, options);
schema.documentProperties().setSourceName("file://" + schemaPath.replace(File.separator, "/") + "/");
schemas.add(schema);
} else if (schemaNode.getNodeType() != Node.TEXT_NODE && schemaNode.getNodeType() != Node.COMMENT_NODE) {
// TODO: Add xs:import support outside schemas.
throw new IllegalStateException("Encountered unsupported element in WSDL types definition: ({"
+ schemaNode.getNamespaceURI() + "}" + schemaNode.getLocalName() + ")!");
}
}
return schemas;
}
/**
* Creates metadata needed to marshal XmlBeans objects to valid XTee requests.
*
* @param wsdlDoc
*/
private static void createMetadata(Document wsdlDoc) {
String opNs =
wsdlDoc.getElementsByTagNameNS(WSDL_NS,
"definitions").item(0).getAttributes().getNamedItem("targetNamespace").getNodeValue().toLowerCase();
parseWsdlMetadata(opNs);
Map<String, QName> messageMap = getMessageMap(wsdlDoc);
Node binding = wsdlDoc.getElementsByTagNameNS(WSDL_NS, "binding").item(0);
NodeList bindingChildren = binding.getChildNodes();
Map<String, String> versionMap = new HashMap<String, String>();
for (int i = 0; i < bindingChildren.getLength(); i++) {
Node bindingChild = bindingChildren.item(i);
if (WSDL_NS.equals(bindingChild.getNamespaceURI()) && "operation".equals(bindingChild.getLocalName())) {
Node operation = bindingChild;
String opname = operation.getAttributes().getNamedItem("name").getNodeValue();
NodeList operationChildren = operation.getChildNodes();
String version = null;
for (int j = 0; j < operationChildren.getLength(); j++) {
Node operationChild = operationChildren.item(j);
if ("version".equals(operationChild.getLocalName())) {
version = SOAPUtil.getTextContent(operationChild);
break;
}
}
if (version == null) {
System.out.println("WARNING: Did not find version of operation \"" + opname + "\". Assuming version 1");
version = "v1";
}
versionMap.put(opname, version);
}
}
Node portType = wsdlDoc.getElementsByTagNameNS(WSDL_NS, "portType").item(0);
NodeList portTypeChildren = portType.getChildNodes();
mainLoop: for (int i = 0; i < portTypeChildren.getLength(); i++) {
Node portTypeChild = portTypeChildren.item(i);
if (WSDL_NS.equals(portTypeChild.getNamespaceURI()) && "operation".equals(portTypeChild.getLocalName())) {
Node operation = portTypeChild;
String opname = operation.getAttributes().getNamedItem("name").getNodeValue();
if (!versionMap.containsKey(opname)) {
continue;
}
String requestElementName = null;
String requestElementNs = null;
String responseElementName = null;
String responseElementNs = null;
NodeList operationChildren = operation.getChildNodes();
for (int j = 0; j < operationChildren.getLength(); j++) {
Node operationChild = operationChildren.item(j);
if (WSDL_NS.equals(operationChild.getNamespaceURI())
&& ("input".equals(operationChild.getLocalName()) || "output".equals(operationChild.getLocalName()))) {
String message = operationChild.getAttributes().getNamedItem("message").getNodeValue().split(":", 2)[1];
QName elementQName = messageMap.get(message);
if (elementQName == null) {
System.out.println("WARNING: Did not find \"keha\" part in message \"" + message + "\" (operation \""
+ opname + "\").");
continue mainLoop;
}
if ("input".equals(operationChild.getLocalName())) {
requestElementName = elementQName.getLocalPart();
requestElementNs = elementQName.getNamespaceURI();
} else if ("output".equals(operationChild.getLocalName())) {
responseElementName = elementQName.getLocalPart();
responseElementNs = elementQName.getNamespaceURI();
}
}
}
if (requestElementName == null || responseElementName == null) {
System.out.println("WARNING: Did not find \"input\" or \"output\" of operation \"" + opname + "\"");
continue mainLoop;
}
String version = versionMap.get(opname);
metadata.put(dbDesc.getId() + opname.toLowerCase(),
new XmlBeansXRoadMetadata(opname,
opNs,
requestElementName,
requestElementNs,
responseElementName,
responseElementNs,
version));
}
}
logInfo("Created metadata for operations: " + metadata.keySet());
}
private static void parseWsdlMetadata(String opNs) {
// Default to protocol 4 for now
Pattern v4 = Pattern.compile(XROAD_V4_NAMESPACE_PATTERN);
Matcher m = v4.matcher(opNs);
if (m.matches()) {
dbDesc.set(m.group(1), XRoadProtocolVersion.V4_0);
return;
}
// WSDL does not follow X-tee convention, warn and use WSDL name
// as database
System.out.println("WARNING: WSDL namespace does not match X-tee convention (found: " + opNs
+ "), setting database name from WSDL filename!");
dbDesc.set(curWsdl.getName().substring(0, curWsdl.getName().toLowerCase().indexOf(".wsdl")),
XRoadProtocolVersion.V4_0);
}
/**
* Creates a map between message names and their response elements.
*
* @param wsdlDoc
* @return
*/
private static Map<String, QName> getMessageMap(Document wsdlDoc) {
Map<String, QName> messageMap = new HashMap<String, QName>();
NodeList messages = wsdlDoc.getElementsByTagNameNS(WSDL_NS, "message");
for (int i = 0; i < messages.getLength(); i++) {
Node message = messages.item(i);
NodeList parts = message.getChildNodes();
for (int j = 0; j < parts.getLength(); j++) {
Node part = parts.item(j);
if (WSDL_NS.equals(part.getNamespaceURI()) && "part".equals(part.getLocalName())
&& !part.getAttributes().getNamedItem("name").getNodeValue().endsWith("paring")) {
Node element = part.getAttributes().getNamedItem("element");
String[] type = element == null
? part.getAttributes().getNamedItem("type").getNodeValue().split(":", 2)
: element.getNodeValue().split(":", 2);
messageMap.put(message.getAttributes().getNamedItem("name").getNodeValue(),
new QName(wsdlDoc.lookupNamespaceURI(type[0]), type[1]));
break;
}
}
}
return messageMap;
}
private static void generateDatabaseClasses(String outputdir) throws IOException, TemplateException {
DatabaseClasses classes =
new DatabaseClasses(argMap.get(XSB_DIR), argMap.get(DB_CLASSES_PACKAGE), dbDesc.getVersion());
for (Map.Entry<String, XmlBeansXRoadMetadata> entry : metadata.entrySet()) {
XmlBeansXRoadMetadata serviceMetadata = entry.getValue();
String key = entry.getKey();
String database = key.substring(0, key.lastIndexOf(serviceMetadata.getOperationName().toLowerCase()));
classes.add(database, serviceMetadata);
}
DatabaseGenerator.generate(classes, outputdir);
}
private static class Timer {
private double start;
public void start() {
start = System.currentTimeMillis();
}
public double finish() {
return System.currentTimeMillis() - start;
}
public String finishStr() {
return finish() / 1000 + " seconds.";
}
}
}