/*
* This software and supporting documentation were developed by
*
* Siemens Corporate Technology
* Competence Center Knowledge Management and Business Transformation
* D-81730 Munich, Germany
*
* Authors (representing a really great team ;-) )
* Stefan B. Augustin, Thorbj�rn Hansen, Manfred Langen
*
* This software is Open Source under GNU General Public License (GPL).
* Read the text of this license in LICENSE.TXT
* or look at www.opensource.org/licenses/
*
* Once more we emphasize, that:
* THIS SOFTWARE IS MADE AVAILABLE, AS IS, WITHOUT ANY WARRANTY
* REGARDING THE SOFTWARE, ITS PERFORMANCE OR
* FITNESS FOR ANY PARTICULAR USE, FREEDOM FROM ANY COMPUTER DISEASES OR
* ITS CONFORMITY TO ANY SPECIFICATION. THE ENTIRE RISK AS TO QUALITY AND
* PERFORMANCE OF THE SOFTWARE IS WITH THE USER.
*
*/
// XmlWithSchemaMapper
// ************ package ****************************************************
package KFM.XML;
// ************ imports ****************************************************
import org.w3c.dom.*;
import org.w3c.dom.traversal.*;
import javax.xml.parsers.*;
import java.io.*;
import java.util.*;
import KFM.Exceptions.*;
import KFM.GUI.RequestUrl;
import KFM.GUI.HttpParams;
import KFM.Converter;
import KFM.log.*;
/**
* Builds of a RequestUrl containing parameters in dotted style a XML Document
* driven by the structure of a XML schema.
*
*/
public class XmlWithSchemaMapper
{
// ************************************************************
// Variables
// ************************************************************
/** The XML Schema Document */
Document mSchema = null;
/** Location of the XML Schema file. */
String mSchemaLocation = null;
/** The result DOM Object to generate. */
Document mGeneratedDoc = null;
/** The RequestUrl containing parameters in dotted style ("context.elem$2.elem"). */
RequestUrl mParams = null;
/** Vector of Strings holding *all possible/allowed* RequestUrl parameters
* in the correct order. This Vector is built by traversing the XML schema. */
Vector mKeyOrder = new Vector();
/** Prefix of all params, e.g. "KB.community" */
String mKeyPrefix = null;
/** Build empty elements for empty parameter values. */
boolean mComplete;
TreeWalker mXSCompositorWalker = null;
TreeWalker mXSContentModelWalker = null;
// ************************************************************
// Inner classes
// ************************************************************
/**
* DOM Traversal NodeFilter instance for focussing only on compositor elements when traversing
* a XML schema. A compositor is one of the following elements:
* - xs:sequence
* - xs:all
* - xs:choice
*/
class XSCompositor implements NodeFilter
{
public short acceptNode(Node aNode)
{
String tNodeName = aNode.getNodeName();
if (tNodeName.equals("xs:sequence") || tNodeName.equals("xs:all") || tNodeName.equals("xs:choice"))
return FILTER_ACCEPT;
else
return FILTER_SKIP;
}
}
/**
* DOM Traversal NodeFilter instance for focussing only on content model elements when
* traversing a XML schema. Content model elements are all XML schema elements relevant for
* describing the schema structure:
* - xs:sequence, xs:all, xs:choice (compositors)
* - xs:complexType, xs:simpleType
* - xs:element
*
* Todo: Support for further relevant elements (xs:group, ...). Maybe for the fullest possible
* xml schema implementation in XmlWithSchemaBuilder only "xs:annotation" and "xs:documentation"
* schould be filtered.
*/
class XSContentModel implements NodeFilter
{
public short acceptNode(Node aNode)
{
String tNodeName = aNode.getNodeName();
if (tNodeName.equals("xs:sequence") || tNodeName.equals("xs:all") || tNodeName.equals("xs:choice")
|| tNodeName.equals("xs:element")
|| tNodeName.equals("xs:complexType") || tNodeName.equals("xs:simpleType"))
return FILTER_ACCEPT;
else
return FILTER_SKIP;
}
}
// ************************************************************
// Constructor
// ************************************************************
/**
* Construct an instance of XmlWithSchemaBuilder.
*/
public XmlWithSchemaMapper(String aSchemaLocation, String aKeyPrefix)
throws KFMException
{
mSchemaLocation = aSchemaLocation;
mKeyPrefix = aKeyPrefix;
// Parse the XML Schema to a DOM object
mSchema = DOM.parseToDOM(new File(mSchemaLocation), false /* Xerces can't validate XML schema */);
// Traverse the Schema and find all possible Keys
buildKeyOrder();
}
// ************************************************************
// Methods
// ************************************************************
/**
*
* @param aParams
* @param aComplete Build empty elements for empty parameter values
* @param aCdataAsText Build CDATA sections as normal text nodes (hack for Bookmarks import)
* @return
* @throws KFMException Wraps ParserConfigurationException
* @throws ProgrammerException If DOM Level 2 Traversal is not supported
*/
public Document buildXmlWithSchema(RequestUrl aParams, boolean aComplete, boolean aCdataAsText)
throws KFMException
{
mParams = aParams;
mComplete = aComplete;
// Prepare (empty) DOM that will be built up later in this class.
try {
mGeneratedDoc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
} catch (ParserConfigurationException e) {
// should never throw an exception in a correct xerces environment
throw new KFMException(e);
}
// The name of mGeneratedDoc's root element is defined in the XML schema.
// Assumption: It is exactly the the first (but maybe not the only one) "xs:element"
// under the schema root.
Element tSchemaDocElement = (Element)mSchema.getDocumentElement().getElementsByTagName("xs:element").item(0);
String tRootElemName = tSchemaDocElement.getAttribute("name");
Element tGenRootElem = mGeneratedDoc.createElement(tRootElemName);
tGenRootElem.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
tGenRootElem.setAttribute("xsi:noNamespaceSchemaLocation", mSchemaLocation);
mGeneratedDoc.appendChild(tGenRootElem);
buildTree(tGenRootElem, aCdataAsText);
return mGeneratedDoc;
}
/**
* Traverse the XML schema and find all possible RequestUrls in the right order.
* The result goes to member variable mKeyOrder.
* @param aKeyPrefix
* @throws KFMException
*/
private void buildKeyOrder()
throws KFMException
{
// The schema DOM must support DOM Level 2 Traversal
if (!mSchema.getImplementation().hasFeature("Traversal", "2.0")) {
throw new ProgrammerException("XmlWithSchemaBuilder.buildKeyOrder: DOM Level 2 Traversal not supported.");
}
// Prepare Traversal TreeWalkers for navigating through the XML schema.
DocumentTraversal tTraversal = (DocumentTraversal)mSchema;
mXSCompositorWalker = tTraversal.createTreeWalker(mSchema.getDocumentElement(),
NodeFilter.SHOW_ELEMENT, (NodeFilter)new XSCompositor(), true);
mXSContentModelWalker = tTraversal.createTreeWalker(mSchema.getDocumentElement(),
NodeFilter.SHOW_ELEMENT, (NodeFilter)new XSContentModel(), true);
// The entry point for traversing recursively the XML schema is the very first
// occurence of a compositor, i.e. the compositor of the root element.
Element tFirstCompositor=(Element)mXSCompositorWalker.nextNode();
traverse(tFirstCompositor, mKeyPrefix);
}
/**
* Public accessor to the possible RequestUrls of a Schema (result of buildKeyOrder).
*/
public Vector getKeyOrder()
{
return mKeyOrder;
}
/**
* Traverse recursively the XML schema and find all possible RequestUrls in the right order.
* The argument aXSCompositor is a Element passing the XSCompositor NodeFilter. A compositor
* is a container with an amount of "xs:element"s (children). Two cases can occur:
* 1) An Element is a simpleType. Then it defines a leaf in the tree structure and will
* result in a new parameter entry in mKeyOrder.
* 2) An Element is a complexType. Then it will contain another compositor with child
* elements of this Element. We will start the next recursion with the new compositor.
*
* @param aXSCompositor Compositor Element in the XML schema
* @param aContextPath
*/
private void traverse(Element aXSCompositor, String aContextPath)
{
// For each child Element of the compositor...
mXSContentModelWalker.setCurrentNode(aXSCompositor);
for (Element tXSCurrentElement = (Element)mXSContentModelWalker.firstChild();
tXSCurrentElement != null;
tXSCurrentElement = (Element)mXSContentModelWalker.nextSibling()) {
String tXSCurrentElemName = tXSCurrentElement.getAttribute("name"); // name of element to generate
int tMaxOccurs = getMaxOccours(tXSCurrentElement);
//System.out.println("*** " + tXSCurrentElement.getNodeName() + " - "
// + tXSCurrentElemName + " - "
// + tMaxOccurs + " - " + aContextPath);
// Find out the content model. Each Element has exactly one (can be NULL if simpleType):
NodeList tXSContentModelNL = ((Element)tXSCurrentElement).getElementsByTagName("xs:complexType");
if (tXSContentModelNL.getLength() > 0) {
// We have a complexType, i.e. a nested structure.
// Find the new child compositor Element for next recursion.
mXSCompositorWalker.setCurrentNode(tXSContentModelNL.item(0));
Element tNewXSCompositor = (Element)mXSCompositorWalker.nextNode();
// For each possible occurence of the current Element step with tNewXSCompositor
// deeper in the tree. Grow the ContextPath with tXSCurrentElemName.
String tNewContextPath;
for (int i = 1; i <= tMaxOccurs; i++) {
if (tMaxOccurs == 1) {
tNewContextPath = aContextPath + "." + tXSCurrentElemName;
} else {
tNewContextPath = aContextPath + "." + tXSCurrentElemName + "$" + i;
}
// Next recursion with the new content model
traverse(tNewXSCompositor, tNewContextPath);
// Each iteration (context$1, context$2, ...) must start at the same point.
mXSContentModelWalker.setCurrentNode(tXSCurrentElement);
}
} else {
// We have a simpleType (or no simpleType element, which means the same).
// Add parameter(s) to the vector.
for (int i = 1; i <= tMaxOccurs; i++) {
if (tMaxOccurs == 1) {
mKeyOrder.add(aContextPath + "." + tXSCurrentElemName);
} else {
mKeyOrder.add(aContextPath + "." + tXSCurrentElemName + "$" + i);
}
}
}
} /*for*/
}
/**
* Builds a complete DOM of the given request parameters using the previously computed
* Vector mKeyOrder.
*
* @param aRoot Root Element of the tree ("KnowledgeBean")
* @param aCdataAsText Build CDATA sections as normal text nodes (hack for Bookmarks import)
*/
private void buildTree(Element aRoot, boolean aCdataAsText)
{
Stack tElementStack = new Stack(); //of DOM Elements
Element tParentElement = aRoot;
StringBuffer tParentPath = new StringBuffer(mKeyPrefix);
// The currently worked parameter (called path because we use here the parameter's
// dot-syntax that represents the path in the tree from the root down to the leaf.
String tCurrentPath;
// The value (text) of the corresponding tCurrentPath.
String tCurrentValue;
// Work all possible RequestUrl parameters exactly in the given order
int i = 0;
while (i < mKeyOrder.size()) {
tCurrentPath = (String)mKeyOrder.get(i);
tCurrentValue = mParams.getParam(tCurrentPath);
//System.out.println("i = " + i + ", ParentPath:" + tParentPath +
// ", CurrentPath: " + tCurrentPath + " = " + tCurrentValue);
if (! mComplete && tCurrentValue.equals("")) {
// Only if no empty elements for empty parameter values should be built:
// The XML element is empty, do not add it. Continue with next key.
// Due to the representation in mKeyOrder, this will automatically work for
// elements that only contain empty elements, i.e. "<a><b></b></a>" will yield "".
i++;
} else if (isDescendantOf(tCurrentPath, tParentPath.toString())) {
// We have a child element, the tree is growing one Element
String tChildName = getChildName(tCurrentPath, tParentPath.toString());
tParentPath.append("." + tChildName);
// Create the child Element, without the possible $ and following number
if (tChildName.indexOf("$") != -1)
tChildName = tChildName.substring(0, tChildName.indexOf("$"));
Element tChild = mGeneratedDoc.createElement(tChildName);
tParentElement.appendChild(tChild);
// We memorize the parent Element in the stack and set the child Element as new parent Element.
// Then continue with the same i, we only have a new parent Element (deeper)
tElementStack.push(tParentElement);
tParentElement = tChild;
} else if (isEqual(tCurrentPath, tParentPath.toString())) {
// We have created all Elements that tCurrentPath requires.
// Just append the value as text or as CDATA section.
// If the text is empty (can only happen, if mComplete is true), add a "xsi:nil" attribute to
// indicate that there's an empty element. This is needed for correct validation error handling.
if (tCurrentValue.equals("")) {
tParentElement.setAttribute("xsi:nil", "true");
}
if (tCurrentValue.startsWith("<![CDATA[")) {
tCurrentValue = tCurrentValue.substring(9, tCurrentValue.length() - 3);
if (aCdataAsText) {
// Hack for Bookmarks import to prevent nested CDATA sections
tParentElement.appendChild(mGeneratedDoc.createTextNode(tCurrentValue));
} else {
tParentElement.appendChild(mGeneratedDoc.createCDATASection(tCurrentValue));
}
} else {
tParentElement.appendChild(mGeneratedDoc.createTextNode(tCurrentValue));
}
// Continue with next key.
i++;
} else if (isNotDescendantOf(tCurrentPath, tParentPath.toString())) {
// We have a tCurrentPath that points to a leaf out of tParentPath.
// Step one Element up in the tree by setting the "grandpa" Element as new parent Element
tParentElement = (Element)tElementStack.pop();
// Shorten the tParentPath of one Element
int tStart = tParentPath.toString().lastIndexOf(".");
tParentPath.delete(tStart, Integer.MAX_VALUE);
} else {
throw new ProgrammerException("XmlWithSchemaBuilder.buildTree: Unreachable code reached.");
}
} /*while*/
}
/**
* Compute the child name of aParentPath. This is the path segment of aCurrentPath,
* that follows aParentPath up to the next dot.
* e.g. if aCurrentPath="a.b.c.d.e" and aParent="a.b.c", then the name is "d".
*/
private String getChildName(String aCurrentPath, String aParentPath) {
int tStart = aParentPath.length() + 1;
int tEnd = aCurrentPath.indexOf(".", tStart);
if (tEnd == -1)
tEnd = aCurrentPath.length();
return aCurrentPath.substring(tStart, tEnd);
}
/**
* aCurrent is an descendant of aParent, if aCurrent starts with the same path as aParent.
* e.g. if aCurrent="a.b.c.d" and aParent="a.b.c", then aCurrent is a descendant of aParent.
*/
private boolean isDescendantOf(String aCurrent, String aParent)
{
return aCurrent.startsWith(aParent + ".");
}
/**
* aCurrent is equal to aParent, if the path of aCurrent is exactly the same as the path of aParent.
* e.g. if aCurrent="a.b.c.d" and aParent="a.b.c.d", then they are equal.
*/
private boolean isEqual(String aCurrent, String aParent)
{
return aCurrent.equals(aParent);
}
/**
* aCurrent is not an descendant of aParent, if aCurrent dosn't start with the same path as aParent.
* e.g. if aCurrent="a.e.f.g" and aParent="a.b.c", then aCurrent is no descendant of aParent.
*/
private boolean isNotDescendantOf(String aCurrent, String aParent)
{
return ! aCurrent.startsWith(aParent);
}
/**
* Determine the value of Attribute maxOccurs of a given Element.
* If the Attribute is missing, it will default to 1.
* We have forbidden maxOccurs="unbounded" because else we couldn't build a (finite)
* vector of all possible RequestUrl parameters.
*
* @param aElement DOM Element (of XML schema)
* @return Maximum number of occurences of this Element
*/
private int getMaxOccours(Element aElement)
{
String tMaxOccursAttr = aElement.getAttribute("maxOccurs");
if (tMaxOccursAttr.equals("")) {
// if no attribute was given, this means maxOccurs="1"
return 1;
} else if (tMaxOccursAttr.equals("unbounded")) {
throw new ProgrammerException("XmlWithSchemaBuilder.getMaxOccours: Element "
+ aElement.getNodeName() + ": unbounded not allowed.");
} else {
try {
return Integer.parseInt(tMaxOccursAttr);
} catch (NumberFormatException e) {
throw new ProgrammerException("XmlWithSchemaBuilder.getMaxOccours: Element "
+ aElement.getNodeName() + "has invalid value " + tMaxOccursAttr);
}
}
}
/**
*
* @param aDocument DOM to serialize
* @param aDocPrefix Container XML Element, where the data is in the document
* @return
* @throws KFMException
*/
public RequestUrl serializeToHttpParams(
Document aDocument,
String aDocPrefix)
throws KFMException
{
// The currently worked parameter (called path because we use here the parameter's
// dot-syntax that represents the path in the tree from the root down to the leaf.
String tCurrentPath;
RequestUrl tParams = new HttpParams();
String tElementValue = null;
String tNewPath = null;
String tXPathExpression = null;
for (int i = 0; i < mKeyOrder.size(); i++) {
tCurrentPath = (String)mKeyOrder.get(i);
// map mKeyPrefix to aDocPrefix in aCurrentPath
// e.g. "KB.community.sponsor$2.url" -> "KnowledgeBean.sponsor$2.url"
tNewPath = aDocPrefix + "." + tCurrentPath.substring(mKeyPrefix.length() + 1);
tXPathExpression = constructXPathFromParam(tNewPath);
tElementValue = DOM.applyXPath(aDocument, tXPathExpression);
// Value exists corresponding to the possible Key.
// (null means: element is missing, "" means: empty element)
if (! (tElementValue == null || "".equals(tElementValue))) {
tParams.addParam(tCurrentPath, tElementValue);
// System.out.println("* " + tCurrentPath + " = " + tElementValue);
}
}
return tParams;
}
/**
* Construct a XPath expression from a given param in dot-style.
* e.g. "KnowledgeBean.sponsor$2.url" -> "/KnowledgeBean/sponsor[position()=2]/url/text()"
*
* @param aCurrentPath
* @return XPath expression string
*/
private String constructXPathFromParam(String aCurrentPath)
{
StringBuffer tXpath = new StringBuffer();
String[] tLocationSteps = Converter.stringToArray(aCurrentPath.toString(), ".", false);
for (int i = 0; i < tLocationSteps.length; i++) {
int tIndex = tLocationSteps[i].indexOf("$");
String tStep = (tIndex != -1) ? tLocationSteps[i].substring(0, tIndex) : tLocationSteps[i];
// add a location step
tXpath.append("/" + tStep);
// when there are multiple element occurances,
// add an additional predicate to indicate its position.
try {
int tOccurance = Integer.parseInt(tLocationSteps[i].substring(tIndex + 1));
tXpath.append("[position()=" + tOccurance + "]");
} catch (NumberFormatException e) {
// there was no number to parse, this is OK.
}
}
// We are interested in the element text, and not in the whole node
tXpath.append("/text()");
//System.out.println("\n" + aCurrentPath + " => " + tXpath);
return tXpath.toString();
}
// for testing
public static void main(String[] args)
throws Exception
{
String tSchemaLocation = "O:/KFM/www-docs/public/Portal/SieMap/DTD/KB.community.xsd";
XmlWithSchemaMapper tMapper = new XmlWithSchemaMapper(tSchemaLocation, "KB.community");
// Test 1: buildXmlWithSchema
System.out.println("buildXmlWithSchema:\n-------------------");
RequestUrl tParams = new HttpParams();
tParams.addParam("KB.community.membershipPolicy", "restricted");
tParams.addParam("KB.community.communication", "FaceToFace");
tParams.addParam("KB.community.contact.name", "Hans");
//tParams.addParam("KB.community.contact.email", "a@siemens.com");
tParams.addParam("KB.community.contact.email", "a@siemens.com");
tParams.addParam("KB.community.sponsor$1.name", "ICM");
tParams.addParam("KB.community.sponsor$1.url", "http://www.icm.de");
tParams.addParam("KB.community.sponsor$2.name", "CIO");
tParams.addParam("KB.community.sponsor$2.url", "http://www.cio.com");
tParams.addParam("KB.community.hasSubCoPs$1", "subCoP1");
tParams.addParam("KB.community.hasSubCoPs$2", "subCoP2");
tParams.addParam("KB.community.hasSubCoPs$3", "subCoP3");
tParams.addParam("KB.community.since", "20x02-12-02");
Document tGenDoc = tMapper.buildXmlWithSchema(tParams, true /*complete*/, false /*CDATA as text*/);
System.out.println(DOM.serializeToString(tGenDoc, "ISO-8859-1"));
KFMSystem.log.setLogLevel(KFMLog.DEBUG_LEVEL);
XmlValidator tValidator = new XmlValidator(KFMSystem.log);
tValidator.reparseDom(tGenDoc, "KB.community");
// Test 2: serializeToHttpParams
System.out.println("\n\nserializeToHttpParams:\n----------------------");
String tXmlFile = "O:/KFM/www-docs/public/Portal/SieMap/DTD/KB_Community.xml";
Document tXmlDoc = DOM.parseToDOM(new File(tXmlFile), true);
HttpParams tParamsGen = (HttpParams)tMapper.serializeToHttpParams(
tXmlDoc,
"KnowledgeBean");
Enumeration tParamNames = tParamsGen.getParameterNames();
String tName = null;
while (tParamNames.hasMoreElements()) {
tName = (String)tParamNames.nextElement();
System.out.println(tName + " = " + tParamsGen.getParam(tName));
}
}
private static void addParamsToHashMap(HashMap aHashMap, String aName, String[] aParams)
{
Vector tV = new Vector();
for (int i = 0; i < aParams.length; i++) {
tV.add(aParams[i]);
}
aHashMap.put(aName, tV);
}
}