package org.radargun.config; import java.io.File; import java.io.FileNotFoundException; import java.io.PrintWriter; import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; import java.util.HashMap; import java.util.Map; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.*; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.radargun.utils.Utils; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; /** * Base class for generators of schemas. * * @author Radim Vansa <rvansa@redhat.com> */ public abstract class SchemaGenerator { protected static final String NS_XS = "http://www.w3.org/2001/XMLSchema"; protected static final String XS_ELEMENT = "element"; protected static final String XS_COMPLEX_TYPE = "complexType"; protected static final String XS_NAME = "name"; protected static final String THIS_PREFIX = "this:"; protected static final String XS_ANY = "any"; protected static final String XS_RESTRICTION = "restriction"; protected static final String XS_PATTERN = "pattern"; protected static final String XS_ENUMERATION = "enumeration"; protected static final String XS_SIMPLE_TYPE = "simpleType"; protected static final String XS_SEQUENCE = "sequence"; protected static final String XS_UNION = "union"; protected static final String XS_MIN_OCCURS = "minOccurs"; protected static final String XS_MAX_OCCURS = "maxOccurs"; protected static final String XS_ATTRIBUTE = "attribute"; protected static final String XS_TYPE = "type"; protected static final String XS_USE = "use"; protected static final String XS_REQUIRED = "required"; protected static final String XS_STRING = "string"; protected static final XmlType STRING_TYPE = new XmlType(null, XS_STRING); protected static final String XS_INTEGER = "integer"; protected static final String XS_LONG = "long"; protected static final String XS_FLOAT = "float"; protected static final String XS_DOUBLE = "double"; protected static final String XS_BOOLEAN = "boolean"; protected static final String XS_COMPLEX_CONTENT = "complexContent"; protected static final String XS_SIMPLE_CONTENT = "simpleContent"; protected static final String XS_EXTENSION = "extension"; protected static final String XS_BASE = "base"; protected static final String XS_VALUE = "value"; protected static final String XS_ABSTRACT = "abstract"; protected static final String XS_CHOICE = "choice"; protected static final String XS_ANNOTATION = "annotation"; protected static final String XS_DOCUMENTATION = "documentation"; protected static final String XS_NAMESPACE = "namespace"; protected static final String XS_OTHER_NAMESPACE = "##other"; protected static final String XS_ANY_NAMESPACE = "##any"; protected static final String XS_INCLUDE = "include"; protected static final String XS_IMPORT = "import"; protected static final String XS_SCHEMA_LOCATION = "schemaLocation"; protected final String namespaceRoot; protected final String namespace; protected final String omitPrefix; private final Map<String, String> importedNamespaces = new HashMap<>(); private int nsCounter = 0; protected Document doc; protected Element schema; protected XmlType intType; protected Map<String, XmlType> generatedTypes = new HashMap<>(); public SchemaGenerator(String namespaceRoot, String namespace, String omitPrefix) { this.namespaceRoot = namespaceRoot; this.namespace = namespace; this.omitPrefix = omitPrefix; } /** * For reusing the generator within another document * @param doc * @param schema */ void setDocSchema(Document doc, Element schema) { this.doc = doc; this.schema = schema; } protected XmlType generateClass(Class<?> clazz) { String typeName = class2xmlId(clazz); XmlType fullName = generatedTypes.get(typeName); if (fullName != null) { return fullName; } XmlType superType = null; if (clazz.getSuperclass() != Object.class) { superType = generateClass(clazz.getSuperclass()); } NamespaceHelper.Coords coords = NamespaceHelper.getCoords(namespaceRoot, clazz, omitPrefix); if (coords == null || coords.namespace.equals(this.namespace)) { String source = Utils.getCodePath(clazz); if (source == null) source = "unknown source"; int lastSlash = source.lastIndexOf('/'); if (lastSlash > 0) source = source.substring(lastSlash + 1); schema.appendChild(doc.createComment("From " + source)); Element typeElement = createComplexType(typeName, superType == null ? null : superType.toString(), true, Modifier.isAbstract(clazz.getModifiers()), findDocumentation(clazz)); Element propertiesSequence = createSequence(typeElement); for (Map.Entry<String, Path> property : PropertyHelper.getDeclaredProperties(clazz, true, true)) { generateProperty(typeElement, propertiesSequence, property.getKey(), property.getValue(), true); } fullName = new XmlType(typeName); } else { if (PropertyHelper.getProperties(clazz, false, true, false).isEmpty()) { return null; } fullName = new XmlType(requireImport(coords.namespace), typeName); } generatedTypes.put(typeName, fullName); return fullName; } protected Element createAny(Element parent) { return createAny(parent, 0, 1, XS_OTHER_NAMESPACE); } protected Element createAny(Element parent, int minOccurs, int maxOccurs, String namespace) { Element any = doc.createElementNS(NS_XS, XS_ANY); any.setAttribute(XS_NAMESPACE, namespace); any.setAttribute(XS_MIN_OCCURS, String.valueOf(minOccurs)); any.setAttribute(XS_MAX_OCCURS, maxOccurs(maxOccurs)); parent.appendChild(any); return any; } protected abstract String findDocumentation(Class<?> clazz); private void generateProperty(Element parentType, Element parentSequence, String propertyName, Path path, boolean generateAttributes) { if (propertyName.isEmpty()) { if (path.isComplete()) { throw new IllegalArgumentException("Can't use empty property name this way."); } else { // we have to put copy all properties directly here for (Map.Entry<String, Path> property : PropertyHelper.getDeclaredProperties(path.getTargetType(), true, true)) { // do not generate attributes as these already have been generated in the parent class generateProperty(parentType, parentSequence, property.getKey(), property.getValue(), false); } return; } } String name = XmlHelper.camelCaseToDash(propertyName); XmlType type; String documentation = null; boolean createElement = true; int elementMinOccurs = 0; if (!path.isComplete()) { type = generateClass(path.getTargetType()); } else { Property propertyAnnotation = path.getTargetAnnotation(); if (propertyAnnotation == null) { throw new IllegalStateException(path.toString()); } if (propertyAnnotation.readonly()) return; String propertyDocText = propertyAnnotation.doc(); if (propertyName.equals(propertyAnnotation.deprecatedName())) { propertyDocText = "*DEPRECATED* " + propertyDocText; } boolean hasComplexConverter = propertyAnnotation.complexConverter() != ComplexConverter.Dummy.class; if (hasComplexConverter) { type = generateComplexType(path.getTargetType(), propertyAnnotation.complexConverter()); elementMinOccurs = propertyAnnotation.optional() ? 0 : 1; } else { type = generateSimpleType(path.getTargetType(), propertyAnnotation.converter()); if (generateAttributes) { // property with non-trivial path can be declared in delegated element addAttribute(parentType, name, type.toString(), propertyDocText, !propertyAnnotation.optional() && path.isTrivial()); } } // do not write elements for simple mandatory properties - these are required as attributes createElement = propertyAnnotation.optional() || hasComplexConverter; documentation = propertyAnnotation.doc(); } if (createElement && path.isTrivial()) { if (type.isLocal()) { Element propertyElement = createReference(parentSequence, name, type.toString(), elementMinOccurs, 1); addDocumentation(propertyElement, documentation); } else { createComplexElement(parentSequence, name, elementMinOccurs, 1, type.toString(), documentation); } } } protected String class2xmlId(Class<?> clazz) { return XmlHelper.camelCaseToDash(clazz.getName().replaceAll("[.$]", "-")); } private XmlType generateComplexType(Class<?> type, Class<? extends ComplexConverter<?>> complexConverterClass) { String typeName = class2xmlId(type) + "-converted-by-" + class2xmlId(complexConverterClass); XmlType fullName = generatedTypes.get(typeName); if (fullName != null) { return fullName; } generatedTypes.put(typeName, new XmlType(typeName)); Element typeElement = doc.createElementNS(NS_XS, XS_COMPLEX_TYPE); typeElement.setAttribute(XS_NAME, typeName); ComplexConverter<?> converter; try { Constructor<? extends ComplexConverter<?>> ctor = complexConverterClass.getDeclaredConstructor(); ctor.setAccessible(true); converter = ctor.newInstance(); } catch (Exception e) { throw new IllegalArgumentException("Cannot create " + complexConverterClass.getName(), e); } if (converter instanceof DefinitionElementConverter) { DefinitionElementConverter<?> dec = (DefinitionElementConverter<?>) converter; Element choice = createChoice(createSequence(typeElement), dec.minAttributes(), dec.maxAttributes()); for (Class<?> inner : dec.content()) { DefinitionElement de = inner.getAnnotation(DefinitionElement.class); if (de == null) throw new IllegalArgumentException(inner.getName()); String subtypeName = class2xmlId(inner); NamespaceHelper.Coords deCoords = NamespaceHelper.getCoords(namespaceRoot, inner, omitPrefix); if (deCoords == null || deCoords.namespace.equals(this.namespace)) { if (!generatedTypes.containsKey(subtypeName)) { generatedTypes.put(subtypeName, new XmlType(subtypeName)); Map<String, Path> subtypeProperties = PropertyHelper.getProperties(inner, true, false, true); XmlType extended = null; Path valueProperty = subtypeProperties.get(""); if (valueProperty != null && valueProperty.getTargetAnnotation().complexConverter() != ComplexConverter.Dummy.class) { // if we have complex value property, let's inherit from the value converter extended = generateComplexType(valueProperty.getTargetType(), valueProperty.getTargetAnnotation().complexConverter()); subtypeProperties.remove(""); } Element subtypeType = createComplexType(subtypeName, extended == null ? null : extended.toString(), true, false, de.doc()); Element subtypeSequence = createSequence(subtypeType); for (Map.Entry<String, Path> property : subtypeProperties.entrySet()) { if (property.getKey().isEmpty()) { throw new IllegalArgumentException("Empty property in class " + inner.getName()); } generateProperty(subtypeType, subtypeSequence, property.getKey(), property.getValue(), true); } } createReference(choice, de.name(), THIS_PREFIX + subtypeName); } else { createComplexElement(choice, de.name(), 1, 1, requireImport(deCoords.namespace) + subtypeName, de.doc()); } } } else { createAny(createSequence(typeElement), 1, -1, XS_OTHER_NAMESPACE); } schema.appendChild(typeElement); return new XmlType(typeName); } protected XmlType generateSimpleType(Class<?> type, Class<? extends Converter<?>> converterClass) { String typeName = class2xmlId(type); if (!DefaultConverter.class.equals(converterClass)) { typeName += "-converted-by-" + class2xmlId(converterClass); } XmlType fullName = generatedTypes.get(typeName); if (fullName != null) { return fullName; } Element typeElement = doc.createElementNS(NS_XS, XS_SIMPLE_TYPE); typeElement.setAttribute(XS_NAME, typeName); // do not hang the element yet - if we can't specify it well, drop that Element union = doc.createElementNS(NS_XS, XS_UNION); typeElement.appendChild(union); Element propertyType = doc.createElementNS(NS_XS, XS_SIMPLE_TYPE); union.appendChild(propertyType); Element propertyRestriction = doc.createElementNS(NS_XS, XS_RESTRICTION); propertyType.appendChild(propertyRestriction); if (!DefaultConverter.class.equals(converterClass)) { Converter<?> converter; try { Constructor<? extends Converter<?>> ctor = converterClass.getDeclaredConstructor(); ctor.setAccessible(true); converter = ctor.newInstance(); } catch (Exception e) { System.err.printf("Cannot instantiate converter service %s: %s", converterClass.getName(), e.getMessage()); return STRING_TYPE; } Element propertyPattern = doc.createElementNS(NS_XS, XS_PATTERN); propertyRestriction.appendChild(propertyPattern); propertyRestriction.setAttribute(XS_BASE, XS_STRING); propertyPattern.setAttribute(XS_VALUE, converter.allowedPattern(type)); } else if (type == Integer.class || type == int.class) { propertyRestriction.setAttribute(XS_BASE, XS_INTEGER); } else if (type == Long.class || type == long.class) { propertyRestriction.setAttribute(XS_BASE, XS_LONG); } else if (type == Boolean.class || type == boolean.class) { propertyRestriction.setAttribute(XS_BASE, XS_BOOLEAN); } else if (type == Float.class || type == float.class) { propertyRestriction.setAttribute(XS_BASE, XS_FLOAT); } else if (type == Double.class || type == double.class) { propertyRestriction.setAttribute(XS_BASE, XS_DOUBLE); } else if (type == Number.class) { propertyRestriction.setAttribute(XS_BASE, XS_DOUBLE); } else if (type.isEnum()) { propertyRestriction.setAttribute(XS_BASE, XS_STRING); for (Object e : type.getEnumConstants()) { Element enumeration = doc.createElementNS(NS_XS, XS_ENUMERATION); propertyRestriction.appendChild(enumeration); enumeration.setAttribute(XS_VALUE, e.toString()); try { DocumentedValue documentedValue = type.getField(e.toString()).getAnnotation(DocumentedValue.class); if (documentedValue != null) { Element annotation = doc.createElementNS(NS_XS, XS_ANNOTATION); enumeration.appendChild(annotation); Element documentation = doc.createElementNS(NS_XS, XS_DOCUMENTATION); annotation.appendChild(documentation); documentation.setTextContent(documentedValue.value()); } } catch (NoSuchFieldException e1) { throw new IllegalStateException("Enum should always have its constants as fields!", e1); } } } else { // all the elements are just dropped generatedTypes.put(typeName, STRING_TYPE); return STRING_TYPE; } Element expressionType = doc.createElementNS(NS_XS, XS_SIMPLE_TYPE); union.appendChild(expressionType); Element expressionRestriction = doc.createElementNS(NS_XS, XS_RESTRICTION); expressionType.appendChild(expressionRestriction); expressionRestriction.setAttribute(XS_BASE, XS_STRING); Element expressionPattern = doc.createElementNS(NS_XS, XS_PATTERN); expressionRestriction.appendChild(expressionPattern); expressionPattern.setAttribute(XS_VALUE, "[$#]\\{.*\\}"); XmlType xmlType = new XmlType(typeName); generatedTypes.put(typeName, xmlType); schema.appendChild(typeElement); return xmlType; } private String maxOccurs(int maxOccurs) { return maxOccurs < 0 ? "unbounded" : String.valueOf(maxOccurs); } protected Element createSequence(Element parentComplex) { Element sequence = doc.createElementNS(NS_XS, XS_SEQUENCE); sequence.setAttribute(XS_MIN_OCCURS, "1"); sequence.setAttribute(XS_MAX_OCCURS, "1"); parentComplex.appendChild(sequence); return sequence; } protected Element createChoice(Element sequence, int minOccurs, int maxOccurs) { Element choice = doc.createElementNS(NS_XS, XS_CHOICE); choice.setAttribute(XS_MIN_OCCURS, String.valueOf(minOccurs)); choice.setAttribute(XS_MAX_OCCURS, maxOccurs(maxOccurs)); sequence.appendChild(choice); return choice; } protected Element createComplexElement(Element parentSequence, String name, Integer minOccurs, Integer maxOccurs, String documentation) { Element element = doc.createElementNS(NS_XS, XS_ELEMENT); element.setAttribute(XS_NAME, name); if (minOccurs != null && minOccurs >= 0) element.setAttribute(XS_MIN_OCCURS, String.valueOf(minOccurs)); if (maxOccurs != null) element.setAttribute(XS_MAX_OCCURS, maxOccurs(maxOccurs)); addDocumentation(element, documentation); parentSequence.appendChild(element); Element complex = doc.createElementNS(NS_XS, XS_COMPLEX_TYPE); element.appendChild(complex); return complex; } protected Element createComplexElement(Element parentSequence, String name, Integer minOccurs, Integer maxOccurs, String extended, String documentation) { Element complexType = createComplexElement(parentSequence, name, minOccurs, maxOccurs, documentation); Element content = doc.createElementNS(NS_XS, XS_COMPLEX_CONTENT); complexType.appendChild(content); Element extension = doc.createElementNS(NS_XS, XS_EXTENSION); extension.setAttribute(XS_BASE, extended); content.appendChild(extension); return complexType; } protected Element createComplexType(String name, String extended, boolean useComplexContent, boolean isAbstract, String documentation) { Element typeElement = doc.createElementNS(NS_XS, XS_COMPLEX_TYPE); if (isAbstract) typeElement.setAttribute(XS_ABSTRACT, "true"); typeElement.setAttribute(XS_NAME, name); addDocumentation(typeElement, documentation); schema.appendChild(typeElement); if (extended == null) { return typeElement; } else { Element content = doc.createElementNS(NS_XS, useComplexContent ? XS_COMPLEX_CONTENT : XS_SIMPLE_CONTENT); typeElement.appendChild(content); Element extension = doc.createElementNS(NS_XS, XS_EXTENSION); extension.setAttribute(XS_BASE, extended); content.appendChild(extension); return extension; } } protected Element createReference(Node parent, String name, String type) { return createReference(parent, name, type, -1, -1); } protected Element createReference(Node parent, String name, String type, int minOccurs, int maxOccurs) { Element reference = doc.createElementNS(NS_XS, XS_ELEMENT); reference.setAttribute(XS_NAME, name); reference.setAttribute(XS_TYPE, type); if (minOccurs >= 0) reference.setAttribute(XS_MIN_OCCURS, String.valueOf(minOccurs)); if (maxOccurs >= 0 || minOccurs >= 0) reference.setAttribute(XS_MAX_OCCURS, maxOccurs(maxOccurs)); parent.appendChild(reference); return reference; } protected void addAttribute(Element complexTypeElement, String name, boolean required) { addAttribute(complexTypeElement, name, XS_STRING, null, required); } protected void addAttribute(Element complexTypeElement, String name, String type, String documentation, boolean required) { Element attribute = doc.createElementNS(NS_XS, XS_ATTRIBUTE); attribute.setAttribute(XS_NAME, name); attribute.setAttribute(XS_TYPE, type); if (required) { attribute.setAttribute(XS_USE, XS_REQUIRED); } complexTypeElement.appendChild(attribute); addDocumentation(attribute, documentation); } private void addDocumentation(Element element, String documentation) { if (documentation != null) { Element annotation = doc.createElementNS(NS_XS, XS_ANNOTATION); element.appendChild(annotation); Element docEl = doc.createElementNS(NS_XS, XS_DOCUMENTATION); annotation.appendChild(docEl); docEl.appendChild(doc.createTextNode(documentation)); } } protected void generate(String directory, String filename) { try { PrintWriter writer = new PrintWriter(new File(directory + File.separator + filename)); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); doc = builder.newDocument(); doc.setXmlVersion("1.0"); doc.setXmlStandalone(true); generate(); TransformerFactory tf = TransformerFactory.newInstance(); tf.setAttribute("indent-number", 3); Transformer trans = tf.newTransformer(); trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); trans.setOutputProperty(OutputKeys.INDENT, "yes"); StreamResult result = new StreamResult(writer); DOMSource source = new DOMSource(doc); trans.transform(source, result); writer.flush(); } catch (FileNotFoundException e) { e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. } catch (ParserConfigurationException e) { e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. } catch (TransformerConfigurationException e) { e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. } catch (TransformerException e) { e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. } } protected Element createSchemaElement(String namespace) { schema = doc.createElementNS(NS_XS, "schema"); schema.setAttribute("attributeFormDefault", "unqualified"); schema.setAttribute("elementFormDefault", "qualified"); schema.setAttribute("version", "1.0"); schema.setAttribute("targetNamespace", namespace); schema.setAttribute("xmlns:this", namespace); doc.appendChild(schema); return schema; } /** * Generates scheme file */ protected abstract void generate(); /** * Adds include tag to the parent * * @param parent to which include is added * @param location of include file e.g. scenario.xsd * @return modified parent */ protected Element addInclude(Element parent, String location) { Element include = doc.createElementNS(NS_XS, XS_INCLUDE); include.setAttribute(XS_SCHEMA_LOCATION, location); parent.appendChild(include); return include; } protected Element addImport(String namespace, String location, String shortNs) { Element imported = doc.createElementNS(NS_XS, XS_IMPORT); imported.setAttribute(XS_NAMESPACE, namespace); imported.setAttribute(XS_SCHEMA_LOCATION, location); Node firstChild = schema.getFirstChild(); if (firstChild == null) { schema.appendChild(imported); } else { schema.insertBefore(imported, firstChild); } schema.setAttribute("xmlns:" + shortNs, namespace); return imported; } protected String requireImport(String namespace) { String shortNs = importedNamespaces.get(namespace); if (shortNs != null) return shortNs; String jarMajorMinor = NamespaceHelper.getJarMajorMinor(namespace); if (jarMajorMinor == null) { throw new IllegalStateException("Unknown jar for " + namespace); } shortNs = jarMajorMinor.replaceAll("[^a-zA-Z]", ""); if (shortNs.startsWith("radargun")) { shortNs = shortNs.substring(8); } shortNs = String.format("%s%02d", shortNs, nsCounter++); addImport(namespace, jarMajorMinor + ".xsd", shortNs); String shortNsWithColon = shortNs + ':'; importedNamespaces.put(namespace, shortNsWithColon); return shortNsWithColon; } protected static class XmlType { public final String prefix; public final String localType; public XmlType(String prefix, String localType) { this.prefix = prefix; this.localType = localType; } public XmlType(String localType) { this.prefix = SchemaGenerator.THIS_PREFIX; this.localType = localType; } @Override public String toString() { if (prefix == null) return localType; else return prefix + localType; } public boolean isLocal() { return prefix == SchemaGenerator.THIS_PREFIX; } } }