Java tutorial
/** * This class represents an XSD schema. It controls the building of the schema * representation and population of elements in an XML structure. * * Copyright (C) 2007 Stephen Harding * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * Please send inquiries to; steve@inverse2.com * * $Revision: 1.4 $ * * $Log: XSDSchema.java,v $ * Revision 1.4 2008/09/01 11:05:22 stevewdh * Added a couple of helper methods to get the schema's root name and xsd element. * * Revision 1.3 2008/07/01 17:26:46 stevewdh * Changed logging so that user can specify the type they want (Java, Log4J or HTML). * * Revision 1.2 2008/03/05 10:47:27 stevewdh * *** empty log message *** * * Revision 1.1 2007/10/04 11:06:48 stevewdh * *** empty log message *** * * Revision 1.3 2007/09/30 13:09:05 stephen harding * Added contact details to license header. * * Revision 1.2 2007/09/22 16:38:43 stephen harding * No changes. * * Revision 1.1 2007/09/15 16:09:05 stephen harding * Added header. * * */ package com.init.octo.schema; import java.io.File; import java.io.FileReader; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Properties; import org.apache.ws.commons.schema.XmlSchemaException; import org.jdom2.Attribute; import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.Namespace; import org.jdom2.input.SAXBuilder; import com.init.octo.util.Logger; public class XSDSchema extends XMLType { public static String SCHEMA = "schema"; public static String ELEMENT = "element"; public static String COMPLEXTYPE = "complexType"; public static String ANNOTATION = "annotation"; public static String SIMPLETYPE = "simpleType"; public static String RESTRICTION = "restriction"; public static String EXTENSION = "extension"; public static String LIST = "list"; public static String SIMPLECONTENT = "simpleContent"; public static String COMPLEXCONTENT = "complexContent"; public static String GROUP = "group"; public static String ALL = "all"; public static String CHOICE = "choice"; public static String SEQUENCE = "sequence"; public static String ATTRIBUTE = "attribute"; public static String ATTRIBUTEGROUP = "attributeGroup"; public static String IMPORT = "import"; public static String INCLUDE = "include"; public static String ID_ATT = "id"; public static String NAME_ATT = "name"; public static String REF_ATT = "ref"; public static String TYPE_ATT = "type"; public static String ABSTRACT_ATT = "abstract"; public static String MIXED_ATT = "mixed"; public static String BLOCK_ATT = "block"; public static String FINAL_ATT = "final"; public static String DEFAULT_ATT = "default"; public static String FORM_ATT = "form"; public static String USE_ATT = "use"; public static String FIXED_ATT = "fixed"; public static String MAXOCCURS_ATT = "maxOccurs"; public static String MINOCCURS_ATT = "minOccurs"; public static String ATTFORMDEF_ATT = "attributeFormDefault"; public static String BLOCKDEF_ATT = "blockDefault"; public static String ELFORMDEF_ATT = "elementFormDefault"; public static String FINALDEF_ATT = "final Default"; public static String TARGNAMSP_ATT = "targetNamespace"; public static String NAMESPACE_ATT = "xmlns"; public static String VERSION_ATT = "version"; public static String XMLLANG_ATT = "xml:lang"; public static String SCHEMALOC_ATT = "schemaLocation"; public static String BASE_ATT = "base"; private Logger log; // logging object private XSDCache cache; // cache of shared elements... private XSDElement root = null; // root element of the XML structure private Map<String, XSDElement> topLevelElements;//XML elements that are defined at the top level private Map<String, XSDElementTypeComplex> topLevelTypes; //type declarations at the top level private int indent; private Document buildDocument; // document being built using the schema... private Element[] elementPath; // the path of elements that created during an add element action private XSDElement[] schemaPath; // the path through the schema that was used creating an element private boolean schemaDefined; // has an XSD schema been defined private boolean firstCall; private SAXBuilder builder; // object to build JDOM documents private String schemaRoot; private List<String> schemaFileNames; private Map<String, Boolean> newTrigger = new HashMap<String, Boolean>(); // list of elements that should always be created as new /** Attributes of the schema element.. **/ private String targetNamespace; /* private String id; private String attributeFormDefault; private String blockDefault; private String elementFormDefault; private String finalDefault; private String version; private String xml_lang; */ /** * Constructor */ public XSDSchema(Properties properties, int indent) { this.cache = new XSDCache(); this.log = Logger.getLogger(XSDSchema.class.getName()); this.root = null; this.indent = indent; this.schemaDefined = false; this.builder = new SAXBuilder(); } /** * This method builds an internal structure of the XML schema * @throws Exception */ public boolean build(String schemaFileName, Document document) throws Exception { log.debug("" + indent + ": " + "Building representation of the XML schema"); topLevelElements = new HashMap<String, XSDElement>(); topLevelTypes = new HashMap<String, XSDElementTypeComplex>(); schemaFileNames = new ArrayList<String>(); schemaFileNames.add(schemaFileName); /** Add the document to the list of documents referenced in the schema... **/ cache.putSchema("main", document); // build 1 if (privateBuild(schemaFileName, document, true) == false) { log.fatal("Error building the schema"); return (false); } cache.clearSchemaCache(); cache.putSchema("main", document); // build 2 (why?) if (privateBuild(schemaFileName, document, true) == false) { log.fatal("Error building the schema"); return (false); } if (null != schemaRoot) { root = topLevelElements.get(schemaRoot); } else { // no root specified, just choose the first one root = new XSDElement(); root.setElementType((XSDElementTypeComplex) topLevelElements.entrySet().toArray()[0]); root.setName("root"); } log.debug("" + indent + ": " + "XML Schema representation built successfully"); schemaDefined = true; return (true); } private boolean privateBuild(String schemaFileName, Document document, boolean topLevel) throws Exception { int schemaFileNamesIdx = 0; String attributeValue; XSDElement thisDocRoot; // get the root element of the XML - this should be <schema>, or we wont process the document Element xsdRoot = document.getRootElement(); String elementName = xsdRoot.getName(); if (elementName.equals(SCHEMA) == false) { throw new Exception("The root element of the XML document is not <" + SCHEMA + ">"); } id = xsdRoot.getAttributeValue(XSDSchema.ID_ATT); targetNamespace = xsdRoot.getAttributeValue(XSDSchema.TARGNAMSP_ATT); try { cache.pushNamespaces(schemaFileName); } catch (Exception ex) { log.fatal("Exception caching namespaces: " + ex.toString()); return (false); } /** Go through the document and find any include or import elements... **/ for (Element element : xsdRoot.getChildren()) { elementName = element.getName(); if (elementName.equals(IMPORT) || elementName.equals(INCLUDE)) { attributeValue = element.getAttributeValue(SCHEMALOC_ATT); if (cache.schemaCached(attributeValue) == true) { log.debug("Schema already cached... ignoring"); continue; } log.debug(elementName + " schema [" + attributeValue + "]"); String importSchemaName = null; /* If the schema is relative to the current schema then work out the absolute name... */ if (attributeValue.startsWith("..") == true) { String[] currentSchemaPath = ((String) schemaFileNames.get(schemaFileNamesIdx)) .split("[/\\\\]"); String[] importSchemaFile = attributeValue.split("[/\\\\]"); int idx = 0; int n = 0; StringBuffer realImportName = new StringBuffer(); while (importSchemaFile[n].equals("..")) { idx++; n++; } for (n = 0; n < (currentSchemaPath.length - 1) - idx; n++) { realImportName.append(currentSchemaPath[n]); realImportName.append(File.separator); } for (n = idx; n < importSchemaFile.length; n++) { realImportName.append(importSchemaFile[n]); if (n < importSchemaFile.length - 1) { realImportName.append(File.separator); } } importSchemaName = realImportName.toString(); } else { /* schema in current directory... */ File sf = new File((String) schemaFileNames.get(schemaFileNamesIdx)); String dirName = sf.getParent(); importSchemaName = dirName + File.separator + attributeValue; } Document doc = builder.build(new FileReader(importSchemaName)); schemaFileNames.add(importSchemaName); schemaFileNamesIdx++; cache.putSchema(attributeValue, document); privateBuild(importSchemaName, doc, false); schemaFileNames.remove(schemaFileNamesIdx); schemaFileNamesIdx--; } } // do it twice... so that all predefined types are picked up, no matter what order they are defined in... // @todo I know it is clunky, and when I get time I will make it scan for types (and imports etc!) first then build the definition..... for (int x = 0; x < 2; x++) { /** iterate all of the child elements of schema **/ for (Element element : xsdRoot.getChildren()) { elementName = element.getName(); log.debug("" + indent + ": " + "Element <" + elementName + ">"); if (elementName.equals(ELEMENT)) { thisDocRoot = new XSDElement(indent); if (thisDocRoot.build(element, cache, "") != true) { log.fatal("Error processing the schema"); return (false); } if (topLevel == true) { topLevelElements.put(elementName, thisDocRoot); } } else if (elementName.equals(SIMPLETYPE)) { } else if (elementName.equals(COMPLEXTYPE)) { XSDElementTypeComplex complexType = new XSDElementTypeComplex(indent); if (complexType.build(element, cache, "") != true) { log.fatal("Error building a complex type"); throw new Exception("Error building a complex type"); } log.debug("Adding type <" + complexType.getName() + "> to type cache"); cache.putType(complexType.getName(), complexType); if (topLevel == true) { topLevelTypes.put(complexType.getName(), complexType); } } else if (elementName.equals(GROUP)) { XSDGroupType group = new XSDElementGroup(indent); if (group.build(element, cache, "") != true) { log.fatal("Error building a group"); return (false); } } else if (elementName.equals(ATTRIBUTEGROUP)) { XSDAttributeGroup attGroup = new XSDAttributeGroup(); if (attGroup.build(element, cache, "") != true) { log.warn("Error building an attribute group"); continue; } } else if (elementName.equals(ATTRIBUTE)) { XSDAttribute attribute = new XSDAttribute(); if (attribute.build(element, cache, "") != true) { log.warn("Error building an attribute object"); continue; } } else if (elementName.equals(IMPORT) || elementName.equals(INCLUDE)) { // do nothing - these have already been processed... } else { log.debug("" + indent + ": " + "Unexpected element <" + elementName + "> found and ignored"); } } // end for all child elements of the schema root } cache.popNamespaces(); return (true); } // end build public XSDCache getCache() { return (cache); } /** * This method shows the internal representation of the schema */ public void show(boolean htmlForm, String rootName) { if (root != null) { if (rootName != null) { root.setName(rootName); } root.show(0, false, htmlForm); } else { log.info("No root element defined so can't show schema."); log.info("Top level types = " + topLevelTypes.size()); } } /** * This method returns a list of all of the elements and attributes in the schema, along with their indent... */ public List<SchemaItem> getListOfSchemaItems() { List<SchemaItem> list = new LinkedList<SchemaItem>(); if (root != null) { root.getListOfSchemaItems(0, false, list); } return (list); } public void initDoc() { buildDocument = new Document(); if (schemaDefined == false) { root = null; } if (root != null) { buildDocument.setRootElement(new Element(schemaRoot != null ? schemaRoot : root.getName())); } else { /* else no proper root element was defined in the schema and we make a duff one to stop */ /* a future exception... */ buildDocument.setRootElement(new Element(schemaRoot != null ? schemaRoot : "root")); } elementPath = new Element[100]; schemaPath = new XSDElement[100]; // newTrigger = new String[100]; newTrigger = new HashMap<String, Boolean>(); firstCall = true; } public void setSchemaRoot(String schemaRoot) { this.schemaRoot = schemaRoot; } /** * show me where the money is * @param elementSpec * @param value * @param appendCommand * @throws Exception */ public void populateDoc(String elementSpec, String value, String appendCommand) throws Exception { boolean valueIsNull = (null == value); String[] path = elementSpec.split("\\."); log.debug(elementSpec + " contains " + path.length + " elements"); for (int q = 0; q < path.length; q++) { log.debug("path[" + q + "] = " + path[q]); } if (path.length == 0) { log.warn("Zero length element spec [" + elementSpec + "]?"); return; } if (schemaDefined == false && firstCall == true) { if (schemaRoot != null) { /* use the element defined in the xmlselect... */ log.debug("Not using a schema - so creating the root element with the first specified element [" + schemaRoot + "]"); buildDocument.setRootElement(new Element(schemaRoot)); } else { log.debug("Not using a schema - so creating the root element with the first specified element [" + path[0] + "]"); log.debug("element spec: [" + elementSpec + "]"); buildDocument.setRootElement(new Element(path[0])); } firstCall = false; } /** Get the root element from the document and the schema... **/ elementPath[0] = buildDocument.getRootElement(); schemaPath[0] = root; if (schemaDefined && elementPath[0].getName().equals(path[0]) == false) { log.warn("The first element [" + path[0] + " is not the root element"); return; } /** Follow the element spec down the document and schema tree... **/ int idx = 1; boolean elementCreated = false; boolean xToManyElement = false; XSDElement schemaElement = null; while (idx < path.length) { xToManyElement = false; if (path[idx].startsWith("@")) { /** ... attribute specified... **/ idx++; break; } else { String childName = path[idx]; Element parentElement = elementPath[idx - 1]; Element docElement = findElement(parentElement, childName); if (schemaDefined == true) { schemaElement = schemaPath[idx - 1].getChild(path[idx]); if (schemaElement == null) { log.warn("The element [" + path[idx] + "] is not in the schema..."); return; } schemaPath[idx] = schemaElement; } if (docElement == null) { log.debug("Element [" + path[idx] + "] does not exist - will create it"); docElement = new Element(path[idx]); elementPath[idx] = docElement; addElementToDocument(schemaPath[idx - 1], elementPath[idx - 1], docElement); /** Clear any trigger new elements... **/ StringBuffer tmp = new StringBuffer(); for (int j = 0; j <= idx; j++) { if (tmp.length() != 0) { tmp.append("."); } tmp.append(path[j]); } triggerNew(tmp.toString()); elementCreated = true; } else { log.debug("Element [" + path[idx] + "] does exist... checking if we should " + "create a new one..."); /** Check for any throw new triggers... **/ StringBuffer tmp = new StringBuffer(); for (int j = 0; j <= idx; j++) { if (tmp.length() != 0) { tmp.append("."); } tmp.append(path[j]); } if (triggerNew(tmp.toString()) == true) { log.debug("New element trigger detected"); /** Create the element... **/ docElement = new Element(path[idx]); elementPath[idx] = docElement; addElementToDocument(schemaPath[idx - 1], elementPath[idx - 1], docElement); } else if (schemaDefined == true && schemaElement.xToMany() == true) { /* * Flag that a 0..m element has been found... if this is * the element that we are going to populate (the last * one in the while loop) then we will create a new one * if it has not been created a fresh already. This * enables, for example, address lines that come from a * single database row to be mapped into a repeating XML * element. Without this code the trigger mechanism * would not create the new XML element and the address * lines would be overwritten... */ xToManyElement = true; } else { /** * Element is 0..1 so we will just follow the tree * down... **/ log.debug("X..1 cardinality... just keep going..."); } } elementPath[idx] = docElement; } idx++; } // end while following the tree down... idx--; if (path[idx].startsWith("@")) { /** Set the attribute... **/ if (valueIsNull == false) { log.debug("Setting attribute [" + path[idx] + "] (on " + path[idx - 1] + ") to [" + value + "]"); elementPath[idx - 1].setAttribute(path[idx].substring(1, path[idx].length()), value); } } else { /** Set the element... **/ boolean appendValid = false; String separatorChar = null; String[] appendParts; if (appendCommand != null && appendCommand.equals("") == false) { appendParts = appendCommand.split("[()]"); if (appendParts.length != 2) { throw new Exception("append length not 2. :("); } if (appendParts[1].equals("newline")) { separatorChar = "\n"; } else if (appendParts[1].equals("comma")) { separatorChar = ", "; } else if (appendParts[1].equals("dash")) { separatorChar = " - "; } else if (appendParts[1].equals("space")) { separatorChar = " "; } appendValid = true; } if (appendValid) { if (valueIsNull == false) { log.debug("Append data to element [" + path[idx] + "] to [" + value + "]"); String data = elementPath[idx].getText(); data = data + separatorChar + value; elementPath[idx].setText(data); } } else { if (xToManyElement == true && elementCreated == false) { log.debug("Creating an 0..m element [" + path[idx] + "] with value [" + value + "]"); Element docElement = new Element(path[idx]); // @todo may need to check if element is nullable... if (valueIsNull) { docElement.setAttribute("nil", "true"); } docElement.setText(value); elementPath[idx] = docElement; addElementToDocument(schemaPath[idx - 1], elementPath[idx - 1], docElement); } else { log.debug("Setting element [" + path[idx] + "] to [" + value + "]"); elementPath[idx].setText(value); // @todo may need to check if element is nullable... if (valueIsNull) { elementPath[idx].setAttribute("nil", "true"); } } } } /***** * Useful for deep debugging... try { XMLOutputter xmlout = new * XMLOutputter(); * xmlout.setFormat(org.jdom.output.Format.getPrettyFormat()); * xmlout.output(buildDocument, System.out); } catch (Exception ex) { * System.out.println("EXCEPTION: " + ex.toString()); } *****/ } // end populateDoc() private void addElementToDocument(XSDElement schemaParent, Element documentParent, Element newChild) { /* If no schema defined or no children have been added to the parent then we can safely just add our child... */ if (schemaParent == null || documentParent.getChildren().size() == 0) { documentParent.addContent(newChild); } else { List<XSDElement> xsdChildren = schemaParent.getChildren(); Object[] children = documentParent.getChildren().toArray(); Element child = null; String newChildName = newChild.getName(); boolean afterFlag = false; int childIdx = 0; for (childIdx = 0; childIdx < children.length && afterFlag == false; childIdx++) { child = (Element) children[childIdx]; if (child.getName().equals(newChildName) == false) { /* New child so check in XSD list of children to see if our new child goes before it... */ for (XSDElement el : xsdChildren) { /* If the new child name equals the XSD name then we can add after the current position... */ if (el.getName().equals(newChildName)) { afterFlag = true; childIdx--; break; } else if (el.getName().equals(child.getName())) { break; } } } } /* If the current child name equals the new child name then we need to go to the end of set of */ /* those children before we can add our new child, i.e. new A must go after AAA<here> */ while (childIdx < children.length - 1 && child.getName().equals(newChildName)) { childIdx++; child = (Element) children[childIdx]; } /* Now add the new child... */ if (childIdx < children.length) { documentParent.addContent(childIdx, newChild); } else { documentParent.addContent(newChild); } } } /** * adds a throwNew trigger to the "newTrigger" list.... fuck me this is awful * @param elementSpec */ public void throwNew(String elementSpec) { newTrigger.put(elementSpec, new Boolean(true)); } private boolean triggerNew(String elementSpec) { log.debug("checking triggerNew [" + elementSpec + "]"); boolean found = false; Boolean trigger = null; trigger = newTrigger.get(elementSpec); if (null != trigger && trigger.booleanValue() == true) { // clear out the trigger... it's been used! found = true; trigger = false; log.debug("activate triggerNew [" + elementSpec + "]"); } return found; } /* for (int i = 0; i < newTrigger.length; i++) { if (newTrigger[i] != null) { if (newTrigger[i].equals(elementSpec)) { newTrigger[i] = null; log.debug("activate triggerNew [" + elementSpec + "] (" + i + ")"); return(true); } } } return(false); } */ /** wow * * @param parent * @param childName * @return */ private Element findElement(Element parent, String childName) { List<Element> children = parent.getChildren(); ListIterator<?> it = children.listIterator(); /** We need to go backwards through the list... **/ while (it.hasNext()) { it.next(); } while (it.hasPrevious()) { Element child = (Element) it.previous(); if (child.getName().equals(childName)) { return (child); } } return (null); } public Document getDocument() { if (targetNamespace != null) { Element e = buildDocument.getRootElement(); Namespace ns = Namespace.getNamespace("tns", targetNamespace); e.setNamespace(ns); buildDocument.setBaseURI(targetNamespace); } return (buildDocument); } public void fillDoc() { /** Add any mandatory elements that may not have been populated during the mapping phase... **/ log.debug("fillDoc()"); if (schemaDefined == true && root != null) { log.debug("fillDoc() = schema defined"); fillMandatoryChildren(root, schemaRoot != null ? schemaRoot : root.getName(), buildDocument); } } private void fillMandatoryChildren(XSDElement element, String parentSpec, Document doc) { for (XSDElement el : element.getChildren()) { String path = parentSpec + "." + el.getName(); if (el.isMandatory()) { addMandatoryElementToDocument(el, path, doc.getRootElement()); } fillMandatoryChildren(el, path, doc); } /** Add any mandatory attributes... **/ List<XSDAttributeType> attList = element.getAttributeList(); if (attList != null) { for (XSDAttributeType att : attList) { if (((XSDAttribute) att).isMandatory()) { addMandatoryElementToDocument(element, parentSpec + ".@" + att.getName(), doc.getRootElement()); } } } } /** * * @param elementSpec * @param schemaElement * @param element */ private void addMandatoryElementToDocument(XSDElement schemaElement, String elementSpec, Element element) { String[] eSpec = elementSpec.split("\\."); if (eSpec[0].equals(element.getName()) == false) { log.warn("The parent element [" + element.getName() + "] does not match the element spec [" + elementSpec + "]"); return; } int idx = 1; while (idx < eSpec.length) { if (isAttribute(eSpec[idx])) { String attname = eSpec[idx].substring(1, eSpec[idx].length()); Attribute att = element.getAttribute(attname); if (att == null) { /** Mandatory attribute does not exist so add it.. **/ element.setAttribute(attname, schemaElement.getDefaultAttValue(attname)); } return; } StringBuffer tmp = new StringBuffer(); for (int i = idx; i < eSpec.length; i++) { if (tmp.length() != 0) { tmp.append("."); } tmp.append(eSpec[i]); } Iterator<?> it = element.getChildren(eSpec[idx]).iterator(); /** * If the document does not have the specified child element and we * are at the end **/ /** * of the chain then this is a mandatory element that needs to be * added... **/ if (it.hasNext() == false && idx == eSpec.length - 1) { Element newel = new Element(eSpec[idx]); addElementToDocument(schemaElement, element, newel); return; } /** * If the document does not have the specified child element and * that element is **/ /** just part of the chain, then no need to carry on.. **/ if (it.hasNext() == false) { return; } /** * If the document does have the specified child element and we are * at the end of the chain then every thing is ok, so just return **/ if (idx == eSpec.length - 1) { return; } else { /** We have to follow every one of the elements down the tree.. **/ while (it.hasNext()) { addMandatoryElementToDocument(schemaElement, tmp.toString(), (Element) it.next()); } return; } } } private boolean isAttribute(String el) { return el.startsWith("@"); } @Override public boolean build(Element grandchild, XSDCache cache, String parentName) { // TODO Auto-generated method stub return false; } @Override public void show(int preIndent, boolean subCachedType, boolean htmlForm) { // TODO Auto-generated method stub } @Override public List<XMLType> getGroup() { // TODO Auto-generated method stub return null; } /* public XSDElement getSchemaRootXSDElement() { return(root); } public String getSchemaRootName() { return(this.schemaRoot); } */ } // end class XSDSchema