/***************************************************************************
* xsapplet
****************************************************************************
* Copyright (C) 07/2001 Ralf Schweiger
*
* 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 2
* 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, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
*
* Contact the Author:
*
* University of Giessen
* c/o Ralf Schweiger
* Heinrich-Buff-Ring 44
* 35392 Giessen
* Germany
*
* tel:    +49-641-9941370
* fax:    +49-641-9941359
* mailto: ralf.schweiger@informatik.med.uni-giessen.de
*
***************************************************************************/
import java.io.*;
import com.sun.xml.tree.XmlDocument;
import org.w3c.dom.*;
import com.jclark.xsl.dom.*;
import java.util.*;
import org.xml.sax.InputSource;
import dtd2xs;

/**
 * Document or instantiate DTD (REC-xml-19980210) or XML schema (REC-xmlschema-1-20010502)
 * @author Ralf Schweiger
 * @version 2.0, 25/07/01
 */
public class xsapplet extends java.applet.Applet {

   /**************************************************
      MODEL v SCHEMA
      --------------
      A MODEL in this context differs from a SCHEMA in several ways:
      (1) A MODEL refers to a single concept, ie group, element, attribute.
      (2) A MODEL stops at inner elements and attributes.
          Nested elements and attributes are identified in two ways: by reference and by definition (have separate identities).
      (3) A MODEL describes the definition (INNER MODEL), not the reference (OUTER MODEL) of a concept.
          Eg, the MODEL describes the occurrence of nested groups and elements, but not the occurrence of the concept itself.
      
      INTERNAL MARKUP OF XSBROWSER
      ----------------------------
      s = schema
      m = node model
      i = instance node
      s:choice|sequence|element|attribute/@_id: identifier
      s:schema/@_documentId: s:@_id of document element
      s:schema/@_selectedId: s:@_id of selected element
      s:schema/@_parentGroup: "sequence", "unlimitedChoice" (group reduction)
      m:@_id: s:reference/@_id
      m:@_defId: s:definition/@_id
      m:model/@_container: "element", "attribute", "choice", "sequence" ...
      m:documentation/@_name: tag or attribute the documentation belongs to
      m:documentation/@_value: value the documentation belongs to
      m:documentation/@_option: documentation belongs to an optional part of the document model?
      i:@_id: m:@_id (down navigation, instance list)
      i:@_defId: m:@_defId (up navigation) => unnecessary if ALL node models are stored in array model[] (performance v storage)
      i:@_mixed: mixed content?
      i:@_prev: previous text node?
      i:@_next: following text node?
      i:_choice: s:choice instance
      i:_sequence: s:sequence instance
      i:@_time: time stamp (*creation, update, selection ...) for selection of latest instance
      i:@_repeating: repeating group/element?
      i:@_optionId: m:@_id of only model option

   **************************************************/

   private Document xmlschema;
   private Document document; // xmlschema instance
   private Document xsd2mdl, mdl2htm;
   private Node selectedNode;
   private Document nodeModel; // dereferenced XML schema for selectedNode
   private final int maxNumId = 10000;
   private int maxId;
   private Document [] model; // modelOf optimization
   private final String [] builtInSimpleType = { "string", "normalizedString", "token", "byte", "unsignedByte", "base64Binary", "hexBinary", "integer", "positiveInteger", "negativeInteger", "nonNegativeInteger", "nonPositiveInteger", "int", "unsignedInt", "long", "unsignedLong", "short", "unsignedShort", "decimal", "float", "double", "boolean", "time", "dateTime", "duration", "date", "gMonth", "gYear", "gYearMonth", "gDay", "gMonthDay", "Name", "QName", "NCName", "anyURI", "language", "ID", "IDREF", "IDREFS", "ENTITY", "ENTITIES", "NOTATION", "NMTOKEN", "NMTOKENS" };
   private boolean typeOption;
   private Node nodeType, nodeMeaning, nodeEntry; // XHTML
   private StringBuffer sbLog;
   private String userFunction; // browse, edit
   private String fatalError;
   private String invalidAgainst;
   private String attributeName; // DOM does not allow to process ATTRIBUTE_NODE in the same way as ELEMENT_NODE: getParentNode()
   private final long timeOut = 1000; // modelOf
   private long loadTime;
   private int instanceNum; // number of id instances including selectedNode

   public void init() {
      xmlschema = null;
      document = null;
      xsd2mdl = null;
      mdl2htm = null;
      selectedNode = null;
      maxId = -1;
      model = new Document [maxNumId];
      sbLog = null;
   }

   private void log(Object item) {
      if (sbLog != null) sbLog.append(item);
   }

   private void addBuiltInSimpleTypes(Document xmlschema) {
      Element schema = xmlschema.getDocumentElement();
      for (int i = 0; i < builtInSimpleType.length; ++ i) {
         Element simpleType = xmlschema.createElement("simpleType");
         schema.appendChild(simpleType);
         simpleType.setAttribute("name", builtInSimpleType[i]);
      }
   }

   private void identify(Node x, StringBuffer elRefs) {
      if (x.getNodeType() == Node.ELEMENT_NODE && "|schema|element|attribute|choice|sequence|group|".indexOf("|" + x.getNodeName() + "|") >= 0) {
         ((Element) x).setAttribute("_id", String.valueOf(maxId));
         model[maxId ++] = null;
         // elRefs helps to find the document element
         if (x.getNodeName().equals("element")
               && ! ((Element) x).getAttribute("ref").equals("")
               && elRefs.toString().indexOf('|' + ((Element) x).getAttribute("ref") + '|') < 0)
            elRefs.append(((Element) x).getAttribute("ref")).append('|');
      }
      for (x = x.getFirstChild(); x != null; x = x.getNextSibling())
         identify(x, elRefs);
   }

   private String withoutPrefix(String markup, String prefix) {
      if (markup.startsWith(prefix))
         return markup.substring(prefix.length(), markup.length());
      else
         return markup;
   }

   private void removeNamespace(Document xmlschema) {
      Element root = xmlschema.getDocumentElement();
      int colon = root.getTagName().indexOf(':');
      if (colon > 0)
         xmlschema.replaceChild(removeNamespace(xmlschema, root, root.getTagName().substring(0, colon + 1)), root);
   }

   private Element removeNamespace(Document xmlschema, Element root, String prefix) { // remove XML Schema namespace
      Element e = xmlschema.createElement(withoutPrefix(root.getTagName(), prefix));
      // attributes
      NamedNodeMap ats = root.getAttributes();
      for (int i = 0; i < ats.getLength(); ++ i) {
         String name = ats.item(i).getNodeName();
         String value = ats.item(i).getNodeValue();
         e.setAttribute(withoutPrefix(name, prefix), withoutPrefix(value, prefix));
      }
      // elements
      for (Node x = root.getFirstChild(); x != null; x = x.getNextSibling())
         if (x.getNodeType() == Node.ELEMENT_NODE)
            e.appendChild(removeNamespace(xmlschema, (Element) x, prefix));
         else
            e.appendChild(x.cloneNode(true));
      return e;
   }

   /************************************************************************************ NOT INOVKED

   private void copyNodes(Node source, String xpathIntoSource, Node target) {
      try {
         Node t;
         if (source.getNodeType() != Node.DOCUMENT_NODE) {
            t = new XmlDocument();
            t.appendChild(source);
            source = t;
         }
         XmlDocument xslt = new XmlDocument();
         Element stylesheet = xslt.createElement("xsl:stylesheet");
         xslt.appendChild(stylesheet);
         stylesheet.setAttribute("xmlns:xsl", "http://www.w3.org/1999/XSL/Transform");
         stylesheet.setAttribute("xmlns:xt", "http://www.jclark.com/xt");
         stylesheet.setAttribute("version", "1.0");
         Element template = xslt.createElement("xsl:template");
         stylesheet.appendChild(template);
         template.setAttribute("match", "/");
         Element element = xslt.createElement("xsl:element");
         template.appendChild(element);
         element.setAttribute("name", "containerOfNodesToMove");
         Element copyOf = xslt.createElement("xsl:copy-of");
         element.appendChild(copyOf);
         copyOf.setAttribute("select", xpathIntoSource);
         XmlDocument result = new XmlDocument();
         (new XSLTransformEngine().createTransform(xslt)).transform(source, result);
         for (source = result.getDocumentElement().getFirstChild(); source != null;) {
            t = source.getNextSibling();
            ((XmlDocument) target.getOwnerDocument()).changeNodeOwner(source);
            target.appendChild(source);
            source = t;
         }
      }
      catch (Exception x) { log("\ncopyNodes: " + x); if (fatalError.equals("")) fatalError = "copyNodes: " + x; }
   }

   private void dereferenceXPointer(Node table) { // resolve <TABLE><TR><TD><A HREF="#xpointer(xpath)"/></TD></TR><TR/></TABLE>
      String duplicates = "";
      for (Node tr = table.getFirstChild(); tr != null; ) {
         Node td = tr.getFirstChild();
         Node a = td.getFirstChild();
         if (! a.getNodeName().equals("A")) { tr = tr.getNextSibling(); continue; }
         String href = ((Element) a).getAttribute("HREF").trim();
         if (! href.startsWith("#xpointer(")) { tr = tr.getNextSibling(); continue; }
         href = href.substring(10, href.length() - 1);
         if (duplicates.indexOf(href) < 0) {
            td.removeChild(a);
            copyNodes(xmlschema, href, td);
            duplicates += href;
            tr = tr.getNextSibling();
         }
         else { Node t = tr.getNextSibling(); table.removeChild(tr); tr = t; }
      }
   }

   ************************************************************************************/

   private Document transform(Document source, Document stylesheet) {
      try {
         XmlDocument target = new XmlDocument();
         (new XSLTransformEngine().createTransform(stylesheet)).transform(source, target);
         return target;
      }
      catch (Exception x) { log("\ntransform: " + x); if (fatalError.equals("")) fatalError = "transform: " + x; return null; }
   }

   private String xpathOf(Node x) {
      String xpath = " / ";
      int up = 0, ancestor = 0, pos = 0;
      String step = "";
      boolean repeating = false;
      if (! attributeName.equals("")) { xpath += "@" + attributeName + " / "; ++ up; }
      for (; x != null; x = x.getParentNode())
         if (x.getNodeType() == Node.ELEMENT_NODE) {
            if (up > 0 && x.getNodeName().equals("_choice")) ; // ignore choice done
            else {
               if (! step.equals("")) { // save last step before it is overwritten
                  if (userFunction.equals("edit") && (repeating || pos > 0))
                     step += "[" + (pos + 1) + "]";
                  if (ancestor > 0)
                     step = "<A HREF=\"javascript: parent.schemaFrame.up(" + ancestor + ")\">" + step + "</A>";
                  xpath = " / " + step + xpath;
               }
               step = x.getNodeName();
               pos = 0;
               repeating = ((Element) x).getAttribute("_repeating").equals("true");
               ancestor = up;
            }
            for (Node y = x.getParentNode().getFirstChild(); ! y.equals(x); y = y.getNextSibling())
               if (y.getNodeName().equals(step)) ++ pos;
            ++ up;
         }
      // document element
      if (ancestor > 0)
         step = "<A HREF=\"javascript: parent.schemaFrame.up(" + ancestor + ")\">" + step + "</A>";
      xpath = " / " + step + xpath;
      return xpath;
   }

   private String pathOf(Node x) {
      String path = xpathOf(x);
      path =
         "<CENTER>XPath "
         + path
         + (invalidAgainst.equals("") ? "" : ("<EM ID=\"error\"> " + invalidAgainst + "</EM>"))
         + "</CENTER>";
      return path;
   }

   private void modelOf(String defId) {
      try {
         log("\n[modelOf"); Date date0 = new Date();
         int id = Integer.valueOf(defId).intValue();
         if (model[id] != null)
            nodeModel = model[id];
         else {
            xmlschema.getDocumentElement().setAttribute("_selectedId", defId);
            nodeModel = transform(xmlschema, xsd2mdl); // might be time consuming because of many cross-references in schema
         }
         long time = (new Date()).getTime() - date0.getTime(); // ms
         if (time > timeOut)
            model[id] = nodeModel;
         log("\n" + nodeModel.getDocumentElement());
         log("\n" + time + " modelOf]");
      }
      catch (Exception x) { log("\nmodelOf: " + x); if (fatalError.equals("")) fatalError = "modelOf: " + x; }
   }

   private boolean mixed(Document model) {
      return model.getDocumentElement().getElementsByTagName("mixed").getLength() > 0;
   }

   private void styleOf(Document model) {
      try {
         log("\n[styleOf"); Date date0 = new Date();
         Element stylesheet = mdl2htm.getDocumentElement();
         Element variableOption = mdl2htm.createElement("xsl:variable"); // xslt variable option
         stylesheet.appendChild(variableOption);
         variableOption.setAttribute("name", "option");
         variableOption.appendChild(mdl2htm.createTextNode(String.valueOf(typeOption)));
         Element variableFunction = mdl2htm.createElement("xsl:variable"); // xslt variable function
         stylesheet.appendChild(variableFunction);
         variableFunction.setAttribute("name", "function");
         variableFunction.appendChild(mdl2htm.createTextNode(userFunction));
         Element variableValue = mdl2htm.createElement("xsl:variable"); // xslt variable value
         stylesheet.appendChild(variableValue);
         variableValue.setAttribute("name", "value");
         variableValue.appendChild(mdl2htm.createTextNode(
            attributeName.equals("") ?
            textOf(selectedNode, "", 0, 1, true, false, false) :
            ((Element) selectedNode).getAttribute(attributeName)));
         Node parent = selectedNode.getParentNode();
         boolean context =
            "_choice_sequence".indexOf(selectedNode.getNodeName()) < 0
            && attributeName.equals("")
            && parent != null
            && parent.getNodeType() == Node.ELEMENT_NODE
            && ((Element) parent).getAttribute("_mixed").equals("true");
         Element variableContext = mdl2htm.createElement("xsl:variable"); // xslt variable context
         stylesheet.appendChild(variableContext);
         variableContext.setAttribute("name", "context");
         variableContext.appendChild(mdl2htm.createTextNode(String.valueOf(context)));
         Element variableTextBefore = mdl2htm.createElement("xsl:variable"); // xslt variable textBefore
         stylesheet.appendChild(variableTextBefore);
         variableTextBefore.setAttribute("name", "textBefore");
         variableTextBefore.appendChild(mdl2htm.createTextNode(
            context && ((Element) selectedNode).getAttribute("_prev").equals("true") ? selectedNode.getPreviousSibling().getNodeValue() : ""));
         Element variableTextAfter = mdl2htm.createElement("xsl:variable"); // xslt variable textAfter
         stylesheet.appendChild(variableTextAfter);
         variableTextAfter.setAttribute("name", "textAfter");
         variableTextAfter.appendChild(mdl2htm.createTextNode(
            context && ((Element) selectedNode).getAttribute("_next").equals("true") ? selectedNode.getNextSibling().getNodeValue() : ""));
         Document style = transform(model, mdl2htm);
         log("\n" + style.getDocumentElement().toString());
         stylesheet.removeChild(variableOption);
         stylesheet.removeChild(variableFunction);
         stylesheet.removeChild(variableValue);
         stylesheet.removeChild(variableContext);
         stylesheet.removeChild(variableTextBefore);
         stylesheet.removeChild(variableTextAfter);
         nodeType = style.getDocumentElement().getFirstChild(); // does exist in any case
         Node xhtml = nodeType.getNextSibling();
         if (xhtml != null)
            if (xhtml.getNodeName().equals("TABLE")) {
               nodeMeaning = xhtml;
               xhtml = xhtml.getNextSibling();
               if (xhtml != null) nodeEntry = xhtml;
            }
            else nodeEntry = xhtml;
         log("\n" + ((new Date()).getTime() - date0.getTime()) + " styleOf]");
      }
      catch (Exception x) { log("\nstyleOf: " + x); if (fatalError.equals("")) fatalError = "styleOf: " + x; }
   }

   private String textOf(Node x, String separator, int stopLength, int stopDepth, boolean includeSpace, boolean includeAttribute, boolean includeContext) {
      if (x == null) return "";
      StringBuffer txt = new StringBuffer("");
      if (includeContext && x.getNodeType() == Node.ELEMENT_NODE && ((Element) x).getAttribute("_prev").equals("true")) {
         String value = x.getPreviousSibling().getNodeValue();
         if (! includeSpace) value = value.trim();
         if (! value.equals("")) txt.append(separator + value);
      }
      if (stopLength < 1) stopLength = Integer.MAX_VALUE;
      if (stopDepth < 0) stopDepth = Integer.MAX_VALUE;
      textOfRecur(x, separator, txt, stopLength, 0, stopDepth, includeSpace, includeAttribute);
      if (includeContext && x.getNodeType() == Node.ELEMENT_NODE && ((Element) x).getAttribute("_next").equals("true")) {
         String value = x.getNextSibling().getNodeValue();
         if (! includeSpace) value = value.trim();
         if (! value.equals("")) txt.append(separator + value);
      }
      if (txt.length() > stopLength) return txt.toString().substring(0, stopLength);
      else return txt.toString();
   }

   private void textOfRecur(Node x, String separator, StringBuffer txt, int stopLength, int depth, int stopDepth, boolean includeSpace, boolean includeAttribute) {
      // NODE
      String value = x.getNodeValue();
      if (value != null && ! includeSpace) value = value.trim();
      if (value != null && ! value.equals("")) txt.append(separator + value);
      // ATTRIBUTES
      NamedNodeMap attributeList = x.getAttributes();
      if (includeAttribute && attributeList != null)
         for (int i = 0; i < attributeList.getLength() && txt.length() < stopLength; ++ i) {
            Node at = attributeList.item(i);
            if (! at.getNodeName().startsWith("_")) { // technical attributes do not count
               value = at.getNodeValue();
               if (! includeSpace) value = value.trim();
               if (! value.equals("")) txt.append(separator + value);
            }
         }
      if (depth < stopDepth)
         for (x = x.getFirstChild(); x != null && txt.length() < stopLength; x = x.getNextSibling())
            textOfRecur(x, separator, txt, stopLength, depth + 1, stopDepth, includeSpace, includeAttribute);
   }

   private Node nodeOf(String id, Node x) {
      if (x.getNodeType() == Node.ELEMENT_NODE)
         if (((Element) x).getAttribute("_id").equals(id)) return x;
      Node y = null;
      for (x = x.getFirstChild(); x != null && y == null; x = x.getNextSibling())
         y = nodeOf(id, x);
      return y;
   }

   private Node insertIntoValid(Element el, Node parent) {
      Element instance; // to create and insert
      String id = el.getAttribute("_id");
      String defId = el.getAttribute("_defId");
      if (defId.equals("")) defId = id;
      if (userFunction.equals("browse")) {
         instance = document.createElement(el.getAttribute("name"));
         instance.setAttribute("_id", id);
         instance.setAttribute("_defId", defId);
         return parent.appendChild(instance);
      }
      else { // edit
         String maxOccurs = el.getAttribute("maxOccurs").trim();
         if (maxOccurs.equals("")) maxOccurs = "1";
         int maxNum = maxOccurs.equals("unbounded") ? Integer.MAX_VALUE : Integer.valueOf(maxOccurs).intValue() - 1;
         String name = "_" + el.getTagName();
         boolean mixedGroup = false;
         // boolean repeatingGroup = false; // repeating AND group
         if ("_choice_sequence".indexOf(name) < 0) name = el.getAttribute("name");
         else {
            mixedGroup =
               parent.getNodeType() == Node.ELEMENT_NODE
               && ((Element) parent).getAttribute("_mixed").equals("true");
            // repeatingGroup = maxNum > 0;
            // needed for xsd2mdl group reduction
            Element schema = xmlschema.getDocumentElement();
            if (name.equals("_choice") && el.getAttribute("minOccurs").trim().equals("0") && maxOccurs.equals("unbounded"))
               schema.setAttribute("_parentGroup", "unlimitedChoice");
            else if (name.equals("_sequence"))
               schema.setAttribute("_parentGroup", "sequence");
            else
               schema.setAttribute("_parentGroup", "");
         }
         instance = document.createElement(name);
         instance.setAttribute("_id", id); // XML schema definition
         instance.setAttribute("_defId", defId);
         if (mixedGroup)
            instance.setAttribute("_mixed", "true");
         // if (repeatingGroup) instance.setAttribute("_repeating", "true");
         if (maxNum > 0)
            instance.setAttribute("_repeating", "true");
         int elNum = 0;
         Node lastEl = null;
         instanceNum = 0; // global
         Node latestInstance = null;
         long latestTime = 0;
         for (Node x = parent.getFirstChild(); x != null; x = x.getNextSibling())
            if (x.getNodeType() == Node.ELEMENT_NODE) {
               lastEl = x;
               if (((Element) x).getAttribute("_id").equals(id)) {
                  long time = Long.valueOf(((Element) x).getAttribute("_time")).longValue();
                  if (time > latestTime) {
                     latestInstance = x;
                     latestTime = time;
                  }    
                  ++ instanceNum;
               }
               ++ elNum;
            }
        if (instanceNum > maxNum)
            return latestInstance;
         else if (parent.getNodeName().equals("_choice") && elNum > instanceNum)
            return lastEl;
         Node before = null;
         if (el.getParentNode().getNodeName().equals("sequence") || parent.getNodeName().equals("_sequence")) {
            before = parent.getFirstChild();
            Node stop = el.getNextSibling(); // mdl has no text nodes
            for (Node x = el.getParentNode().getFirstChild(); before != null && x != stop; x = x.getNextSibling())
               while (before != null
                      && (before.getNodeType() != Node.ELEMENT_NODE
                         || ((Element) before).getAttribute("_id").equals(((Element) x).getAttribute("_id"))))
                  before = before.getNextSibling();
         }
         instance.setAttribute("_time", String.valueOf((new Date()).getTime() - loadTime));
         ++ instanceNum;
         return parent.insertBefore(instance, before);
      }
   }

   private int identifyDocumentElement(Document xmlschema, String elRefs) {
      Element schema = xmlschema.getDocumentElement();
      for (Node x = schema.getFirstChild(); x != null; x = x.getNextSibling())
         if (x.getNodeName().equals("element") && elRefs.indexOf('|' + ((Element) x).getAttribute("name") + '|') < 0) {
            String documentId = ((Element) x).getAttribute("_id");
            schema.setAttribute("_documentId", documentId);
            return Integer.valueOf(documentId).intValue();
         }
      return -1;
   }

   public void load(String uri, String userFunction, boolean doLog) { // load DTD, XML schema, XML document
      fatalError = "";
      if (doLog) sbLog = new StringBuffer(""); else sbLog = null;
      try {
         log("\n[load"); Date date0 = new Date();
         if (uri.indexOf(":") < 0) uri = getCodeBase() + uri; // absolute URI
         log("\n" + uri);
         if (uri.endsWith(".dtd")) {
            dtd2xs dtd2xsd = new dtd2xs(sbLog);
            String xsd = dtd2xsd.translate(uri, getCodeBase() + "stylesheet/complextype.xsl", false, false, 1000, null, "element attribute enumeration", 200, 1);
            xmlschema = XmlDocument.createXmlDocument(new InputSource(new StringReader(xsd)), false);
         }
         else
            xmlschema = XmlDocument.createXmlDocument(uri);
         removeNamespace(xmlschema);
         addBuiltInSimpleTypes(xmlschema);
         maxId = 0;
         StringBuffer elRefs = new StringBuffer("|");
         identify(xmlschema, elRefs);
         log("\nReferenced elements: " + elRefs.toString());
         int documentId = identifyDocumentElement(xmlschema, elRefs.toString());
         log("\n" + xmlschema.getDocumentElement());
         xsd2mdl = XmlDocument.createXmlDocument(getCodeBase() + "stylesheet/xsd2mdl.xsl");
         mdl2htm = XmlDocument.createXmlDocument(getCodeBase() + "stylesheet/mdl2htm.xsl");
         document = new XmlDocument();
         selectedNode = document;
         attributeName = "";
         typeOption = true;
         this.userFunction = userFunction;
         invalidAgainst = "";
         loadTime = (new Date()).getTime();
         modelOf("0");
         down(documentId);
         log("\n" + ((new Date()).getTime() - date0.getTime()) + " load]");
      }
      catch (Exception x) { log("\nload: " + x); if (fatalError.equals("")) fatalError = "load: " + x; }
   }

   private String optionOf(Document model) { // return _id of ONLY child or null
      Node onlyChild = null;
      for (Node x = model.getDocumentElement().getFirstChild(); x != null; x = x.getNextSibling()) {
         String name = x.getNodeName();
         if (name.equals("choice") || name.equals("sequence") || name.equals("element") || name.equals("attribute") || name.equals("type")) // ignore <documentation> etc.
            if (onlyChild != null)
               return null;
            else onlyChild = x;
      }
      return ((Element) onlyChild).getAttribute("_id");
   }

   private String uniquePath(Node instance, boolean userSelectedInstance) {
      log("\n<uniquePath>");
      String id = null;
      if (userFunction.equals("edit"))
         if (instance.getNodeName().equals("_choice") && instance.hasChildNodes()) { // user has chosen => collapse choice
            for (Node x = instance.getFirstChild(); id == null && x != null; x = x.getNextSibling())
               if (x.getNodeType() == Node.ELEMENT_NODE) // ignore text nodes
                  id = ((Element) x).getAttribute("_id");
         }
         else {
            if (! ((Element) instance).getAttribute("_optionId").equals(""))
               // if only model option repeats check instance options
               if (! ((Element) instance).getAttribute("_repeating").equals("true")
                     || userSelectedInstance || instanceNum < 2) // no selection problem?
                  id = ((Element) instance).getAttribute("_optionId");
         }
      log("\n</uniquePath>");
      return id;
   }

   public int descend(int id) { return descend(String.valueOf(id)); } // javascript interface

   private int descend(String id) { // return number of steps descended
      log("\n<descend>");
      int numSteps = 0;
      while (id != null) {
         if (! selectedNode.equals(document) || ! document.hasChildNodes()) { // create child node
            Element el = (Element) nodeOf(id, nodeModel); // de-referenced with respect to _id, minOccurs, maxOccurs, ... (transfer from reference to definition)
            String concept = el.getTagName();
            if ("element|sequence|choice".indexOf(concept) >= 0)
               selectedNode = insertIntoValid(el, selectedNode);
            else if (concept.equals("attribute"))
               attributeName = el.getAttribute("name");
            String defId = el.getAttribute("_defId");
            if (! defId.equals("")) id = defId;
         }
         else
            selectedNode = document.getDocumentElement();
         modelOf(id); // nodeModel
         if (attributeName.equals("")) {
            if (mixed(nodeModel))
               ((Element) selectedNode).setAttribute("_mixed", "true");
            String idOnlyOption = optionOf(nodeModel);
            if (idOnlyOption != null)
               ((Element) selectedNode).setAttribute("_optionId", idOnlyOption);
         }
         ++ numSteps;
         id = uniquePath(selectedNode, false);
      }
      log("\nsteps descended: " + numSteps + "\n</descend>");
      return numSteps;
   }

   public boolean down(int id) { // return success
      fatalError = "";
      try {
         nodeType = null;
         nodeMeaning = null;
         nodeEntry = null;
         descend(String.valueOf(id));
         styleOf(nodeModel); // nodeType, nodeMeaning, nodeEntry
         return true;
      }
      catch (Exception x) { log("\ndown: " + x); if (fatalError.equals("")) fatalError = "down: " + x; return false; }
   }

   private boolean removeEmpty(Node x) {
      boolean remove = textOf(x, "", 1, -1, false, true, true).equals("");
      if (remove) {
         Node parent = x.getParentNode();
         if (((Element) x).getAttribute("_prev").equals("true")) parent.removeChild(x.getPreviousSibling());
         if (((Element) x).getAttribute("_next").equals("true")) parent.removeChild(x.getNextSibling());
         parent.removeChild(x);         
      }
      return remove;
   }

   public boolean up(int numSteps) { // return success
      if (! invalidAgainst.equals("")) return false;
      fatalError = "";
      try {
         nodeType = null;
         nodeMeaning = null;
         nodeEntry = null;
         for (; numSteps > 0; -- numSteps)
            if (attributeName.equals("")) {
               Node parent = selectedNode.getParentNode();
               removeEmpty(selectedNode);
               selectedNode = parent;
            }
            else attributeName = "";
         String id = ((Element) selectedNode).getAttribute("_id");
         String defId = ((Element) selectedNode).getAttribute("_defId");
         modelOf(defId); // nodeModel
         instanceNum = 0; // define for uniquePath
         for (Node x = selectedNode.getParentNode().getFirstChild(); instanceNum < 2 && x != null; x = x.getNextSibling())
            if (x.getNodeType() == Node.ELEMENT_NODE && ((Element) x).getAttribute("_id").equals(id))
               ++ instanceNum;
         descend(uniquePath(selectedNode, false));
         styleOf(nodeModel); // nodeType, nodeEntry, nodeMeaning
         return true;
      }
      catch (Exception x) { log("\nup: " + x); if (fatalError.equals("")) fatalError = "up: " + x; return false; }
   }

   public void turnOption() {
      fatalError = "";
      nodeType = null;
      nodeMeaning = null;
      typeOption = ! typeOption;
      styleOf(nodeModel); // nodeType
   }

   public String getPath() { return pathOf(selectedNode); }

   public String getMeaning() {
      if (nodeMeaning != null)
         return nodeMeaning.toString(); // dereferenceXPointer(meaning);
      else
         return "";
   }

   public String getType() { return nodeType.toString(); }

   public void selectInstance(int pos) {
      if (! invalidAgainst.equals("")) { /* error management */ }
      NodeList siblings = selectedNode.getParentNode().getChildNodes();
      Node selected = siblings.item(pos);
      if (removeEmpty(selectedNode)) -- instanceNum;
      selectedNode = selected;
      descend(uniquePath(selectedNode, true)); // instanceNum NOT relevant in this case (uniquePath)
      styleOf(nodeModel); // nodeEntry
   }

   public String getInstance() {
      if (userFunction.equals("browse") /* || instanceNum == 1 && textOf(selectedNode, "", 1, -1, false, true, true).equals("") */)
         return "";
      StringBuffer tbl = new StringBuffer("<TABLE WIDTH=\"100%\"><TR><TH>Instance</TH></TR>");
      String id = ((Element) selectedNode).getAttribute("_id");
      NodeList siblings = selectedNode.getParentNode().getChildNodes();
      int num = siblings.getLength();
      int i, j;
      for (i = 0, j = 1; i < num; ++ i) {
         Node x = siblings.item(i);
         if (x.getNodeType() == Node.ELEMENT_NODE && ((Element) x).getAttribute("_id").equals(id))
            tbl.append("<TR><TD>")
               .append(j ++)
               .append(" - ")
               .append("<A HREF=\"javascript: parent.schemaFrame.selectInstance(").append(i).append(")\">")
               .append(textOf(x, "/", 0, -1, false, true, true))
               .append("</A>")
               .append(x.equals(selectedNode) ? "<EM ID=\"label\"> selected</EM>" : "")
               .append("</TD></TR>");
      }
      tbl.append("</TABLE>");
      return tbl.toString();
   }

   public String getEntry() { if (nodeEntry != null) return nodeEntry.toString(); else return ""; }

   public String getLog() { if (sbLog != null) return sbLog.toString(); else return ""; }

   public String getError() { return fatalError; }

   private void validate(String text) {
      if (text.equals("invalid"))
         invalidAgainst = "invalid!";
      else
         invalidAgainst = "";
   }

   public void setText(String text) {
      validate(text); // invalidAgainst
      if (attributeName.equals("")) // Node.ELEMENT_NODE
         if (selectedNode.hasChildNodes())
            selectedNode.getFirstChild().setNodeValue(text);
         else 
            selectedNode.appendChild(document.createTextNode(text));
      else
         ((Element) selectedNode).setAttribute(attributeName, text);
   }

   public void setTextBefore(String text) {
      Node prev = selectedNode.getPreviousSibling();
      if (! ((Element) selectedNode).getAttribute("_prev").equals("true"))
         prev = selectedNode.getParentNode().insertBefore(document.createTextNode(""), selectedNode);
      prev.setNodeValue(text);
      ((Element) selectedNode).setAttribute("_prev", "true");
   }

   public void setTextAfter(String text) {
      Node next = selectedNode.getNextSibling();
      if (! ((Element) selectedNode).getAttribute("_next").equals("true"))
         next = selectedNode.getParentNode().insertBefore(document.createTextNode(""), next);
      next.setNodeValue(text);
      ((Element) selectedNode).setAttribute("_next", "true");
   }

   private String printDocument(Document source, boolean stripTechnicalMarkup) {
      try {
         XmlDocument target = new XmlDocument();
         if (stripTechnicalMarkup)
            stripTechnicalMarkupRecur(source, target, target);
         else
            target = (XmlDocument) source;
         StringWriter sw = new StringWriter();
         target.write(sw);
         String s= sw.toString();
         sw.close();
         return s;
      }
      catch (Exception x) { log("\nstripTechnicalMarkup: " + x); if (fatalError.equals("")) fatalError = "stripTechnicalMarkup: " + x; return ""; }
   }

   private void stripTechnicalMarkupRecur(Node source, Document target, Node root) {
      switch (source.getNodeType()) {
         case Node.ELEMENT_NODE:
            if (! source.getNodeName().startsWith( "_")) {
               Element x = (Element) source;
               Element y = target.createElement(x.getTagName());
               root.appendChild(y);
               NamedNodeMap as = x.getAttributes();
               if (as != null)
                  for (int i = 0; i < as.getLength(); ++ i) {
                     Node a = as.item(i);
                     if (! a.getNodeName().startsWith("_"))
                        y.setAttribute(a.getNodeName(), a.getNodeValue());
                  }
               root = y;
            }
            break;
         case Node.TEXT_NODE:
            root.appendChild(target.createTextNode(source.getNodeValue()));
            break;
      }
      for (source = source.getFirstChild(); source != null; source = source.getNextSibling())
         stripTechnicalMarkupRecur(source, target, root);
   }

   public String store() {
      fatalError = "";
      try {
         return printDocument(document, false);
      }
      catch (Exception x) { log("\nstore: " + x); fatalError = "store: " + x; return ""; }
   }

}

