View Javadoc

1   /*
2    * $Id: OAST.java,v 1.100 2005/07/13 18:48:56 jlerner Exp $
3    *
4    * Copyright (c) 1999-2004, BBN Technologies, LLC.
5    * All rights reserved.
6    * http://www.daml.org/legal/opensource/bbn_license.html
7    */
8   
9   package com.bbn.swede.core.dom;
10  
11  import java.io.BufferedInputStream;
12  import java.io.IOException;
13  import java.io.StringReader;
14  import java.util.ArrayList;
15  import java.util.Arrays;
16  import java.util.Collections;
17  import java.util.Comparator;
18  import java.util.Hashtable;
19  import java.util.Iterator;
20  import java.util.List;
21  import java.util.Map;
22  import java.util.StringTokenizer;
23  
24  import javax.xml.parsers.FactoryConfigurationError;
25  import javax.xml.parsers.ParserConfigurationException;
26  import javax.xml.parsers.SAXParser;
27  import javax.xml.parsers.SAXParserFactory;
28  
29  import org.eclipse.core.resources.IFile;
30  import org.eclipse.core.resources.IResource;
31  import org.eclipse.core.runtime.CoreException;
32  import org.eclipse.core.runtime.QualifiedName;
33  import org.eclipse.jface.text.IRegion;
34  import org.eclipse.jface.text.Region;
35  import org.eclipse.ui.PartInitException;
36  import org.xml.sax.InputSource;
37  import org.xml.sax.SAXException;
38  import org.xml.sax.helpers.DefaultHandler;
39  
40  import com.bbn.swede.core.IOWLDocument;
41  import com.bbn.swede.core.IOWLElement;
42  import com.bbn.swede.core.IOWLElementVisitor;
43  import com.bbn.swede.core.IOWLProject;
44  import com.bbn.swede.core.OWLCore;
45  import com.bbn.swede.core.dom.OASTEvent.OASTNodeRef;
46  import com.bbn.swede.core.dom.rdf.Rdf;
47  import com.bbn.swede.core.resources.SWResourceManager;
48  import com.hp.hpl.jena.ontology.OntModel;
49  import com.hp.hpl.jena.ontology.OntModelSpec;
50  import com.hp.hpl.jena.rdf.model.ModelFactory;
51  import com.hp.hpl.jena.rdf.model.Resource;
52  import com.hp.hpl.jena.rdf.model.Statement;
53  
54  /***
55   * This implementation of {@link com.bbn.swede.core.dom.IOWLAbstractSyntaxTree}
56   * manages the actual OASTNode structure of the tree.  OAST is responsible for 
57   * parsing (both full and partial), node insertion, removal, and replacement, 
58   * sending notifications to OAST change listeners, and adapting the tree into a 
59   * Jena model.
60   * @author jlerner
61   */
62  public class OAST implements IOWLAbstractSyntaxTree
63  {
64     private static final String S_UNPARSEABLE = "com.bbn.swede.ui.unparseablemarker";
65     private static final QualifiedName QNAME_UNPARSEABLE =
66        new QualifiedName("com.bbn.swede.core", "unparseable");
67     
68     private DocumentRoot _root;
69     private ArrayList _alListeners = new ArrayList();
70     private ArrayList _alPrenotifiedListeners = new ArrayList();
71     private IFile _file;
72     private OntModel _model;
73     private Map _mapResourceToNode;
74     private Map _mapStatementToNode;
75  
76     /***
77      * Returns the root node of the OAST.
78      * @return The root node.
79      */
80     public DocumentRoot getRoot()
81     {
82        return _root;
83     }
84     
85     /***
86      * Translates full URIs into qualified names based on the namespace 
87      * abbreviations defined within the document.
88      * @param uri The URI to translate
89      * @return If a namespace abbreviation can be found for the URI, or it
90      *         exists in the default namespace, a qualified name for the URI.
91      *         Otherwise, the URI itself is returned.
92      */
93     public String getQNameForURI(String uri)
94     {
95        String toReturn = uri;
96        OASTNode[] namespaceNodes = null;
97        String temp = null;
98        Namespace namespace = null;
99        String fragment = null;
100       String baseURI = null;
101 
102       if(uri.indexOf("#") != -1)
103       {
104          fragment = uri.substring(uri.lastIndexOf("#") + 1);
105          baseURI = uri.substring(0, uri.lastIndexOf("#") + 1);
106          namespaceNodes = _root.getNodesOfType(OASTNode.NAMESPACE);
107          for (int i = 0; i < namespaceNodes.length; i++)
108          {
109             namespace = (Namespace)namespaceNodes[i];
110             if(baseURI.equals(getBaseURI() + "#"))
111             {
112                toReturn = fragment;
113                break;
114             }
115             else
116             {
117                if(namespace.getValue().equals(baseURI))
118                {
119                   temp = namespace.getQName();
120                   if(temp.lastIndexOf(":") != -1)
121                   {
122                      temp = temp.substring(temp.lastIndexOf(":") + 1);
123                      toReturn = temp + ":" + fragment;
124                   }
125                   else
126                   {
127                      toReturn = fragment;
128                   }
129                   break;
130                }
131             }
132          }
133       }
134       
135       return toReturn;
136    }
137    
138    /***
139     * Translates qualified names into full URIs based on the namespace 
140     * abbreviations found within the document.
141     * @param qName The qualified name to translate
142     * @return If the namespace abbreviation in the specified qname exists, or
143     *         if it refers to the default namespace, the full URI.  Otherwise,
144     *         the qualified name itself is returned.
145     */
146    public String getURIForQName(String qName)
147    {
148       String toReturn = qName;
149       OASTNode[] namespaceNodes = null;
150       String temp = null;
151       Namespace namespace = null;
152       String fragment = null;
153       String baseURI = null;
154 
155       if(qName.indexOf(":") != -1)
156       {
157          fragment = qName.substring(qName.lastIndexOf(":") + 1);
158          baseURI = qName.substring(0, qName.lastIndexOf(":") + 1);
159          namespaceNodes = _root.getNodesOfType(OASTNode.NAMESPACE);
160          for (int i = 0; i < namespaceNodes.length; i++)
161          {
162             namespace = (Namespace)namespaceNodes[i];
163             temp = namespace.getQName();
164             temp = temp.substring(temp.lastIndexOf(":") + 1);
165             if(temp.equals(baseURI))
166             {
167                toReturn = namespace.getValue() + "#" + fragment;
168                break;
169             }
170          }
171       }
172       if(qName.startsWith("#"))
173       {
174          toReturn = getBaseURI() + qName;
175       }
176       
177       return toReturn;
178    }
179 
180    /***
181     * Produces a list of unparseable regions in the document, as it is currently
182     * stored in the project metadata.  For information on unparseability in
183     * the current, possibly unsaved, document state, use 
184     * getNodesOfType(OASTNode.UNPARSEABLE)
185     * @return An array of IRegions representing all the unparseable sections
186     *         of the document
187     */
188    public IRegion[] getUnparseableRegions()
189    {
190       String s = null;
191       try
192       {
193          s = _file.getPersistentProperty(QNAME_UNPARSEABLE);
194       }
195       catch (CoreException e)
196       {
197          OWLCore.logWarning(
198             OWLCore.getID(),
199             "Error retrieving persistant property",
200             e);
201       }
202       if (s == null)
203       {
204          return new IRegion[0];
205       }
206       int iOffset, iLength;
207       StringTokenizer st = new StringTokenizer(s, " ");
208       ArrayList al = new ArrayList();
209       while (st.hasMoreTokens())
210       {
211          iOffset = Integer.parseInt(st.nextToken());
212          iLength = Integer.parseInt(st.nextToken());
213          al.add(new Region(iOffset, iLength));
214       }
215       return (IRegion[]) al.toArray(new IRegion[0]);
216    }
217 
218    /***
219     * Locates XML comments in a string.  The comments will be blanked out
220     * in the string and returend as XMLComment objects specifying the positions
221     * where they were found.
222     * @param sb The StringBuffer to be cleansed of XML comments
223     * @return An ArrayList of positioned XMLComments with correct offsets
224     *         and lengths that are not attached to the tree
225     */
226    protected ArrayList filterComments(StringBuffer sb)
227    {
228       ArrayList al = new ArrayList();
229       for (int iPos = sb.toString().indexOf("<!--");
230          iPos >= 0;
231          iPos = sb.toString().indexOf("<!--"))
232       {
233          int iEnd = sb.toString().indexOf("-->", iPos + 4);
234          if (iEnd < 0)
235          {
236             return al;
237          }
238          String s = sb.toString().substring(iPos + 4, iEnd);
239          XMLComment xc = new XMLComment(s);
240          xc.setOffset(iPos);
241          xc.setLength(iEnd - iPos + 3);
242          al.add(xc);
243          for (int i = iPos; i < iEnd + 3; i++)
244          {
245             sb.setCharAt(i, ' ');
246          }
247       }
248       return al;
249    }
250 
251    /***
252     * Attaches a list of already-positioned XMLComment nodes to the OAST.
253     * @param alComments The list of XMLComments to attach
254     * @param root Root of the tree that will receive the comments
255     */
256    protected void attachComments(ArrayList alComments, OASTNode root)
257    {
258       Iterator it = alComments.iterator();
259       while (it.hasNext())
260       {
261          XMLComment xc = (XMLComment) it.next();
262          //TODO: should insert be doing this anyway?
263          OASTNode node = root.getContainingChild(xc.getOffset());
264          try
265          {
266             node.insert(xc, false);
267          }
268          catch (OASTException e) 
269          {
270             //Do nothing - attachComments only gets called during partial
271             //parse, which operates on unattached node structures that won't
272             //cause any problems.
273          }
274       }
275    }
276    
277    private class NodeLevelComparator implements Comparator
278    {
279       public int compare(Object o1, Object o2)
280       {
281          if (!(o1 instanceof UnparseableNode)
282              || !(o2 instanceof UnparseableNode))
283          {
284             return 0;
285          }
286          UnparseableNode u1 = (UnparseableNode)o1;
287          UnparseableNode u2 = (UnparseableNode)o2;
288          if (u1.getLevel() < u2.getLevel())
289          {
290             return -1;
291          }
292          else if (u1.getLevel() > u2.getLevel())
293          {
294             return 1;
295          }
296          return 0;
297       }
298 
299       public boolean equals(Object arg0)
300       {
301          return false;
302       }
303    }
304    
305    /***
306     * Parses an entire document into an OWL Abstract Syntax Tree.  The result
307     * of the parse is returned as a dummy root node, suitable for building
308     * an OAST event.
309     * @param file The file resource to be parsed
310     * @return A dummy node whose children represent one or more node structures 
311     *         resulting from the parse, or <code>null</code> if the parse fails.
312     */
313    private OASTNode doParseDocument(IFile file)
314    {
315       if (!file.isDerived() && !file.isSynchronized(IResource.DEPTH_ZERO)
316          && !file.getWorkspace().isTreeLocked())
317       {
318          try
319          {
320             file.refreshLocal(IResource.DEPTH_ZERO, null);
321          }
322          catch (CoreException e1)
323          {
324             OWLCore.logWarning(OWLCore.getID(), 
325                              "Unable to refresh file resource.", e1);
326          }
327       }
328       SAXParser parser;
329       try
330       {
331          parser = SAXParserFactory.newInstance().newSAXParser();
332 
333          IRegion[] regions = getUnparseableRegions();
334          StringBuffer sb = new StringBuffer();
335          BufferedInputStream bis =
336             new BufferedInputStream(
337                new UnparseableFilterStream(file.getContents(), regions));
338          for (int i = bis.read(); i >= 0; i = bis.read())
339          {
340             sb.append((char) i);
341          }
342          bis.close();
343          OASTNode dummy = partialParse(_root, null, sb.toString(), 0);
344          return dummy;
345       }
346       catch (Exception e)
347       {
348          OWLCore.logWarning(
349             OWLCore.getID(),
350             "Unable to parse " + file.getName(),
351             e);
352          return null;
353       }
354    }
355 
356    /***
357     * Generates unparseable nodes based on document metadata and attaches them
358     * to a node tree.  The tree must already have space for the nodes to be
359     * created.
360     * @param file The file represented by the node tree
361     * @param root The root of the node tree to receive the unparseable nodes
362     */
363    private void restorePersistedUnparseables(IFile file, OASTNode root)
364    {
365       IRegion[] regions = getUnparseableRegions();
366       //sort the region list by tree level to ensure unparseables within
367       //unparseables get attached in the correct order
368       Arrays.sort(regions, new Comparator()
369       {
370          public int compare(Object arg0, Object arg1)
371          {
372             if (arg0 instanceof IRegion && arg1 instanceof IRegion)
373             {
374                IRegion r1 = (IRegion)arg0;
375                IRegion r2 = (IRegion)arg1;
376                if (r1.getOffset() < r2.getOffset())
377                {
378                   return -1;
379                }
380                else if (r1.getOffset() > r2.getOffset())
381                {
382                   return 1;
383                }
384                else
385                {
386                   if (r1.getLength() < r2.getLength())
387                   {
388                      //r2 contains r1 ==> r2 comes first
389                      return 1;
390                   }
391                   else if (r1.getLength() > r2.getLength())
392                   {
393                      //r1 contains r2 ==> r1 comes first
394                      return -1;
395                   }
396                }
397             }
398             return 0;
399          }
400 
401          public boolean equals(Object arg0)
402          {
403             return this.equals(arg0);
404          }
405       });
406       
407       try
408       {
409          //attach stored unparseable nodes to the OAST
410          if(file.getRawLocation() != null)
411          {
412             UnparseableFilterRandomAccessFile ufraf =
413                new UnparseableFilterRandomAccessFile(
414                   file.getRawLocation().toFile(),
415                   "r",
416                   regions);
417             for (int i = 0; i < regions.length; i++)
418             {
419                IRegion r = regions[i];
420                ufraf.seek(r.getOffset());
421                byte[] bytes = new byte[r.getLength()];
422                ufraf.readUnfiltered(bytes);
423                String sText = new String(bytes);
424                OASTNode node = root.getContainingChild(r.getOffset());
425                if (node != null && sText != null)
426                {
427                   UnparseableNode un = new UnparseableNode(sText);
428                   un.setOffset(r.getOffset());
429                   un.setLength(sText.length());
430                   if (!(node instanceof UnparseableNode))
431                   {
432                      un.createMarker(file);
433                   }
434                   node.insert(un, false);
435 //                  un.attach(this, false);
436                   //don't need to displace any nodes - the unparseable regions
437                   //of the file were blanked out by the UFS, not just removed,
438                   //so things are positioned correctly around them
439 
440                }
441             }
442             ufraf.close();
443          }
444       }
445       catch (Exception e)
446       {
447          OWLCore.logWarning(
448             OWLCore.getID(),
449             "Unable to restore persisted unparseable regions in " + file.getName(),
450             e);
451       }
452    }
453    
454    /***
455     * Parses an entire document into an OWL Abstract Syntax Tree.  If this
456     * instance of OAST already contains a tree, it will be replaced with the
457     * results of the parse
458     * @param file File to be parsed
459     */
460    public void parseDocument(IFile file)
461    {
462       _file = file;
463       _root = new DocumentRoot(_file, this);
464       OASTNode dummy = doParseDocument(_file);
465       _root.setLength(dummy.getLength());
466       restorePersistedUnparseables(_file, dummy);
467       attachChildren(dummy, _root);
468 
469       _bReadOnly = file.isReadOnly();
470    }
471    
472    private boolean _bReadOnly;
473    
474    /*
475     *  (non-Javadoc)
476     * @see com.bbn.swede.core.dom.IOWLAbstractSyntaxTree#isReadOnly()
477     */
478    public boolean isReadOnly()
479    {
480       return _bReadOnly;
481    }
482 
483    /***
484     * Builds a Jena model based on the current state of the OAST.  This method
485     * should only be called when something actually requests the model, as it
486     * may take some time to build it.
487     */
488    protected void createModel()
489    {
490      
491       _model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null);
492       _mapResourceToNode = new Hashtable();
493       _mapStatementToNode = new Hashtable();
494       OASTNode[] nodes = _root.getNodesOfType(OASTNode.RDF_RDF, 1);
495       if (nodes.length == 0)
496       {
497          return;
498       }
499       OASTNode nodeRdf = nodes[0];
500       addToModel(nodeRdf);
501    }
502 
503    /***
504     * Attempts to add a node to the Jena model.  This means either creating a 
505     * Resource (for subclasses of ClassNode) or a statement (for 
506     * GenericAttribute and subclasses of PropertyNode).
507     * @param node Node to integrate into the Jena model
508     */
509    /*package*/ void addToModel(OASTNode node)/package-summary/html">class="comment">package*/ void addToModel(OASTNode node)/package-summary.html">/*package*/ void addToModel(OASTNode node)/package-summary.html">class="comment">package*/ void addToModel(OASTNode node)
510    {
511       if (node instanceof ClassNode)
512       {
513          ClassNode cn = (ClassNode) node;
514          cn.createJenaResource(_model);
515          Resource res = cn.getAssociatedResource();
516          if (res == null)
517          {
518             return;
519          }
520          
521          addResource(res, node);
522          
523          Statement stmt = cn.getImplicitTypeStatement();
524          addStatement(stmt, node);
525       }
526       OASTNode[] children = node.getChildren();
527       for (int i = 0; i < children.length; i++)
528       {
529          addToModel(children[i]);
530       }
531       if (node instanceof PropertyNode)
532       {
533          PropertyNode pn = (PropertyNode) node;
534          pn.createJenaStatement(_model);
535 
536          Statement stmt = pn.getAssociatedStatement();
537          addStatement(stmt, node);
538          
539          Resource res = pn.getReification();
540          addResource(res, node);
541       }
542       if (node instanceof GenericAttribute)
543       {
544          GenericAttribute ga = (GenericAttribute) node;
545          ga.createJenaStatement(_model);
546 
547          Statement stmt = ga.getAssociatedStatement();
548          addStatement(stmt, node);
549       }
550    }
551    
552    /***
553     * Removes a node and all its descendents from the Jena model.
554     * @param node The node to remove
555     */
556    /*package*/ void removeFromModel(OASTNode node)/package-summary/html">class="comment">package*/ void removeFromModel(OASTNode node)/package-summary.html">/*package*/ void removeFromModel(OASTNode node)/package-summary.html">class="comment">package*/ void removeFromModel(OASTNode node)
557    {
558       ModelCleanupVisitor mcv = new ModelCleanupVisitor(_model,
559          _mapResourceToNode, _mapStatementToNode);
560       if (node.getParent() instanceof PropertyNode)
561       {
562          //Cleanup is starting at the object of a statement, so move up one
563          //level to ensure that the broken statement is removed.
564          node = node.getParent();
565       }
566       node.accept(mcv);
567    }
568    
569    /***
570     * Adds a resource/node pair to the resource map.
571     * @param res The resource
572     * @param node The node.  Must not be null.
573     */
574    private void addResource(Resource res, OASTNode node)
575    {
576       if (res == null)
577       {
578          return;
579       }
580       
581       List l = (List)_mapResourceToNode.get(res);
582       if (l == null)
583       {
584          l = new ArrayList(1);
585          _mapResourceToNode.put(res, l);
586       }
587       if (!l.contains(node))
588       {
589          l.add(node);
590       }            
591    }
592    
593    /***
594     * Adds a statement/node pair to the statement map.
595     * @param stmt The statement
596     * @param node The node.  Must not be null.
597     */
598    private void addStatement(Statement stmt, OASTNode node)
599    {
600       if (stmt == null)
601       {
602          return;
603       }
604       
605       List l = (List)_mapStatementToNode.get(stmt);
606       if (l == null)
607       {
608          l = new ArrayList(1);
609          _mapStatementToNode.put(stmt, l);
610       }
611       if (!l.contains(node))
612       {
613          l.add(node);
614       }
615    }
616 
617    /***
618     * Returns a Jena model representing the OAST.  If the model does not already
619     * exist, it will be built automatically. 
620     * @return The Jena model for this OAST
621     */
622    public OntModel getJenaModel()
623    {
624       if (_model == null)
625       {
626          createModel();
627       }
628       return _model;
629    }
630 
631    /***
632     * Finds the lowest-level node in the tree that contains a given offset.
633     * @param offset The offset to find
634     * @return The node containing the offset or the document root if no
635     *         containing node is found
636     */
637    public OASTNode getNode(int offset)
638    {
639       OASTNode ret = _root.getContainingChild(offset);
640       return (ret == null ? _root : ret);
641    }
642 
643    /***
644     * Finds a node that represents a specific Jena statement.
645     * @param stmt The statement to find
646     * @return The node that corresponds to the statement, or null if no such
647     *         node is found.
648     */
649    public OASTNode getNode(Statement stmt)
650    {
651       if (_model == null)
652       {
653          createModel();
654       }
655       
656       List l = (List)_mapStatementToNode.get(stmt);
657       if (l == null || l.size() == 0)
658       {
659          return null;
660       }
661       return (OASTNode)l.get(0);
662    }
663 
664    /***
665     * Finds a node that represents a specific Jena resource.
666     * @param res The resource to find
667     * @return The node that corresponds to the resource, or null if no such
668     *         node is found
669     */
670    public OASTNode getNode(Resource res)
671    {
672       if (_model == null)
673       {
674          this.createModel();
675       }
676       
677       List l = (List)_mapResourceToNode.get(res);
678       if (l == null || l.size() == 0)
679       {
680          return null;
681       }
682       
683       return (OASTNode)l.get(0);
684    }
685 
686    /***
687     * Finds a node that represents a specific URI.
688     * @param sUri The URI to find
689     * @return The node that corresponds to the URI, or null if no such node is
690     *         found
691     */
692    public OASTNode getNode(String sUri)
693    {
694       if (_model == null)
695       {
696          this.createModel();
697       }
698       Resource res = _model.getResource(sUri);
699       return (getNode(res));
700    }
701    
702    /***
703     * <p>Performs a partial parse on a string of text and inserts the resulting
704     * nodes into the tree at a specified offset.  The new node(s) will be
705     * parented to the node currently in the OAST at the insertion point and
706     * nodes beyond that point will be displaced to accomodate the new items.</p>
707     * 
708     * <p>This method is not intended for public use.</p>
709     * 
710     * @param iOffset The insert position
711     * @param sText Text to parse
712     */
713    public void insert(int iOffset, String sText)
714    {
715       OASTNode nodeParent = getNode(iOffset);
716       if (nodeParent != null && nodeParent.getOffset() == iOffset)
717       {
718          nodeParent = nodeParent.getParent();
719       }
720       if (nodeParent == null)
721       {
722          nodeParent = _root;
723       }
724       
725       try
726       {
727          String sProlog = getProlog();
728          OASTNode dummy = partialParse(nodeParent, sProlog, sText, iOffset);
729          createEvent(new OASTNode[0], dummy, nodeParent, iOffset, 0);
730       }
731       catch (IOException e)
732       {
733          OWLCore.logError(OWLCore.getID(), "Partial reparse failed", e);
734       }
735    }
736    
737    /***
738     * A node visitor that will truncate a node and its children so that
739     * they exist between a given start and end offset.
740     * @author jlerner
741     */
742    private class NodeTruncationVisitor implements IOASTNodeVisitor
743    {
744       private int _iStart, _iEnd;
745       /***
746        * Creates a visitor to truncate nodes starting before or ending after
747        * specific offsets.
748        * @param iStartOffset The desired start offset.
749        * @param iEndOffset The desired end offset.
750        */
751       public NodeTruncationVisitor(int iStartOffset, int iEndOffset)
752       {
753          _iStart = iStartOffset;
754          _iEnd = iEndOffset;
755       }
756       
757       /* (non-Javadoc)
758        * @see com.bbn.swede.core.dom.IOASTNodeVisitor#visit(com.bbn.swede.core.dom.OASTNode)
759        */
760       public boolean visit(OASTNode node)
761       {
762          try
763          {
764             if (_iEnd <= _iStart)
765             {
766                node.setLength(0);
767                node.setOffset(_iStart);
768                if (node instanceof TextNode)
769                {
770                   TextNode tn = (TextNode)node;
771                   tn.setText("");
772                }
773                return true;
774             }
775             if (node.getOffset() < _iStart)
776             {
777                int iDiff = _iStart - node.getOffset();
778                node.setLength(iDiff);
779                node.setOffset(_iStart);
780                if (node instanceof TextNode)
781                {
782                   TextNode tn = (TextNode)node;
783                   if (tn.getLength() <= 0)
784                   {
785                      //The node has been deleted, just clear its text and return
786                      tn.setText("");
787                      //...but still visit the children so their text also gets
788                      //cleared out and they won't be reinserted later
789                      return true;
790                   }
791                   tn.setText(tn.getText().substring(iDiff));
792                }
793             }
794             int iNodeEnd = node.getOffset() + node.getLength(); 
795             if (iNodeEnd > _iEnd)
796             {
797                node.setLength(node.getLength() - (iNodeEnd - _iEnd));
798                if (node instanceof TextNode)
799                {
800                   TextNode tn = (TextNode)node;
801                   if (tn.getLength() <= 0)
802                   {
803                      //The node has been deleted, just clear its text and return
804                      tn.setText("");
805                      //...but still visit the children so their text also gets
806                      //cleared out and they won't be reinserted later
807                      return true;
808                   }
809                   tn.setText(tn.getText().substring(0, tn.getLength()));
810                }
811             }
812             return true;
813          }
814          catch (OASTException e)
815          {
816             //Do nothing.  Node truncation is always handled while the node
817             //is detached from the tree so there's no risk of read-only 
818             //exceptions
819             return true;
820          }
821       }
822    }
823    
824    /***
825     * <p>Replaces a node in the syntax tree with a new one constructed by
826     * performing a partial reparse on some new text.  The nodes created
827     * by the reparse will be positioned in the tree based on the offset
828     * and parent of the node being removed.</p>
829     * 
830     * <p>Unparseable regions within the node being replaced are blanked out to 
831     * avoid cascading XML errors up the document.  The corresponding
832     * UnparseableNodes are reattached to the tree after the new text is parsed
833     * into the OAST.  Preserving individual unparseable regions of text allows
834     * XML errors to cascade back down the document if an enclosing node becomes
835     * unparseable and is then fixed.</p>
836     * 
837     * <p>This method is not intended for public use.</p>
838     * 
839     * @param nodeReplace The node to replace
840     * @param sText Text to parse into replacement nodes
841     * @param iOffset The offset, relative to the document, of the changed text 
842     *                that caused the node replacement
843     * @param iNewLength net character change in document.  Will be negative if
844     *                   more characters were removed than inserted.
845     */
846    public void replace(OASTNode nodeReplace, String sText, int iOffset, 
847                        int iNewLength)
848    {
849       StringBuffer sbText = new StringBuffer(sText);
850       ArrayList alUnparseable = new ArrayList(
851          Arrays.asList(nodeReplace.getNodesOfType(OASTNode.UNPARSEABLE)));
852       if (nodeReplace instanceof UnparseableNode)
853       {
854          alUnparseable.remove(nodeReplace);
855       }
856       Iterator it = alUnparseable.iterator();
857       while (it.hasNext())
858       {
859          UnparseableNode node = (UnparseableNode)it.next();
860          //Only worry about top-level unparseables
861          OASTNode nodeParent = node.getParent();
862          if (nodeParent != null && nodeParent != nodeReplace 
863              && nodeParent instanceof UnparseableNode)
864          {
865             continue;
866          }
867          //detach the node from the tree so it doesn't get displaced again
868          //during event processing
869          node.detach(false);
870          int iStart = node.getOffset();
871          int iEnd = iStart + node.getLength();
872          if (iNewLength < 0 && iStart >= iOffset 
873              && (iStart + iNewLength) < iOffset)
874          {
875             //Text was deleted, including the beginning of node.
876             //Truncate it before blanking anything out in the replace text.
877             iStart = iOffset;
878             iEnd += iNewLength;
879             node.accept(new NodeDisplacementVisitor(iOffset, iNewLength));
880             node.accept(new NodeTruncationVisitor(iStart, iEnd));
881          }
882          else if (iNewLength < 0 && iStart < iOffset 
883                   && (iEnd > iOffset && iEnd <= (iOffset - iNewLength)))
884          {
885             //Text was deleted, including the end of node.
886             //Truncate it before blanking anything out in the replace text.
887             iEnd = iOffset;
888             node.accept(new NodeTruncationVisitor(iStart, iEnd));
889          }
890          else if (iStart >= iOffset)
891          {
892             //node occurs after the modification to nodeReplace's text.
893             //Displace the node accordingly.
894             iStart += iNewLength;
895             iEnd += iNewLength;
896             node.accept(new NodeDisplacementVisitor(iOffset, iNewLength));
897          }
898          //TODO: handle the very weird case where text is truncated on both
899          //      sides of the deletion, one of the truncated unparseables
900          //      becomes a valid literal, it expands to fill available
901          //      whitespace, and winds up containing the unparseable.
902          //      The "ideal" solution is to make sure at least one of them
903          //      remains unparseable and, if so, munge them together into one
904          //      node.  How to do this without another potentially costly parse,
905          //      I do not know.  Maybe this is a case best left to the syntax
906          //      builder.
907          iStart -= nodeReplace.getOffset();
908          iEnd -= nodeReplace.getOffset();
909          for(int i = iStart; i < iEnd; i++)
910          {
911             sbText.setCharAt(i, ' ');
912          }
913       }
914       
915       int iOffsetReplace = nodeReplace.getOffset();
916       OASTNode nodeParent = nodeReplace.getParent();
917       if (nodeParent == null)
918       {
919          nodeParent = _root;
920       }
921       
922       //create the nodes that are going to be inserted
923       OASTNode dummy;
924       try
925       {
926          //capture XML prolog information so it can be fed to SAX.  This ensures
927          //that partial parses are consistent about character encoding and
928          //entity substitution.
929          String sProlog = getProlog(iOffsetReplace, nodeReplace.getLength());
930          dummy = partialParse(nodeParent, sProlog.toString(), sbText.toString(), iOffsetReplace);
931          Collections.sort(alUnparseable, new NodeLevelComparator());
932          //Make sure the document root doesn't get reparsed, since that would
933          //cause improper delta calculations.
934          OASTNode[] nodes;
935          if (nodeReplace instanceof DocumentRoot)
936          {
937             nodes = nodeReplace.getChildren();
938             //Compensate for whitespace at the document root level.  This
939             //could easily be faked later, but better to ensure that the
940             //OAST state is accurate when event notifications go out
941             createEvent(nodes, dummy, _root, 0, _root.getLength(), alUnparseable);
942             return;
943          }
944          else
945          {
946             nodes = new OASTNode[]{nodeReplace};
947             createEvent(nodes, dummy, nodeParent, alUnparseable);
948          }
949 
950       }
951       catch (IOException e)
952       {
953          OWLCore.logError(OWLCore.getID(), "Partial reparse failed", e);
954       }      
955 
956    }
957    /***
958     * Replaces a node in the syntax tree with a new one constructed by
959     * performing a partial reparse on some new text.  The nodes created
960     * by the reparse will be positioned in the tree based on the offset
961     * and parent of the node being removed.
962     * 
963     * @deprecated As of SWeDE 1.0.2, use 
964     *             {@link #replace(OASTNode, String, int, int)} to automatically
965     *             filter out unparseable subregions of the node being replaced.
966     * @param nodeReplace The node to replace
967     * @param sText Text to parse into replacement nodes
968     */
969    public void replace(OASTNode nodeReplace, String sText)
970    {
971       int iOffset = nodeReplace.getOffset();
972       OASTNode nodeParent = nodeReplace.getParent();
973       if (nodeParent == null)
974       {
975          nodeParent = _root;
976       }
977       
978       //create the nodes that are going to be inserted
979       OASTNode dummy;
980       try
981       {
982          //capture XML prolog information so it can be fed to SAX.  This ensures
983          //that partial parses are consistent about character encoding and
984          //entity substitution.
985          String sProlog = getProlog(iOffset, nodeReplace.getLength());
986          dummy = partialParse(nodeParent, sProlog.toString(), sText, iOffset);
987          createEvent(new OASTNode[]{nodeReplace}, dummy, nodeParent, 0, 0);
988 
989       }
990       catch (IOException e)
991       {
992          OWLCore.logError(OWLCore.getID(), "Partial reparse failed", e);
993       }
994    }
995    
996    /***
997    * Creates an OAST delta representing a given set of node removals and 
998    * insertions.  The correct type of delta will be created depending on 
999    * whether the removed or inserted nodes lists are empty.
1000    * @param nodesRemoved the nodes removed for this event
1001    * @param dummy disposable node whose children are the nodes being inserted
1002    * @param parent the parent node for the nodes being inserted
1003    */
1004    private IOASTDelta createDelta(OASTNode[] nodesRemoved, OASTNode dummy, OASTNode parent)
1005    {
1006       IOASTDelta delta = null;
1007       OASTNode[] children = dummy.getChildren();
1008       if (nodesRemoved.length > 0)
1009       {
1010          if (children.length > 0)
1011          {
1012             delta = OASTDelta.createReparseDelta(nodesRemoved, children);
1013          }
1014          else
1015          {
1016             delta = OASTDelta.createRemoveDelta(nodesRemoved);
1017          }
1018       }
1019       else if (children.length > 0)
1020       {
1021          delta = OASTDelta.createInsertDelta(parent, children);
1022       }
1023       return delta;
1024    }
1025    /***
1026     * <p>Creates an OAST change event with corresponding OAST delta.  The correct
1027     * type of delta will be created depending on whether the removed or
1028     * inserted nodes lists are empty.  The event is automatically applied to the
1029     * tree and notifications are sent to all OAST change listeners.</p>
1030     * 
1031     * <p>This is a convenience method, fully equivalent to:
1032     * <blockquote>createEvent(nodesRemoved,dummy,parent,iOffset,iLength,null)
1033     * </blockquote></p>
1034     * @param nodesRemoved the nodes removed for this event
1035     * @param dummy disposable node whose children are the nodes being inserted
1036     * @param parent the parent node for the nodes being inserted
1037     * @param iOffset document offset of the event
1038     * @param iLength number of characters removed in the event
1039     */
1040    private void createEvent(OASTNode[] nodesRemoved, OASTNode dummy, 
1041       OASTNode parent, int iOffset, int iLength)
1042    {
1043       createEvent(nodesRemoved, dummy, parent, iOffset, iLength, null);
1044    }
1045    
1046    /***
1047     * Creates an OAST change event with corresponding OAST delta.  The correct
1048     * type of delta will be created depending on whether the removed or
1049     * inserted nodes lists are empty.  The event is automatically applied to the
1050     * tree and notifications are sent to all OAST change listeners.
1051     * @param nodesRemoved the nodes removed for this event
1052     * @param dummy disposable node whose children are the nodes being inserted
1053     * @param parent the parent node for the nodes being inserted
1054     * @param iOffset document offset of the event
1055     * @param iLength number of characters removed in the event
1056     * @param alUnparseable a list of unparseable nodes that need to be
1057     *        reattached to the tree after the event is handled
1058     */
1059    private void createEvent(OASTNode[] nodesRemoved, OASTNode dummy,
1060       OASTNode parent, int iOffset, int iLength, ArrayList alUnparseable)
1061    {
1062       IOASTDelta delta = createDelta(nodesRemoved, dummy, parent);
1063       OASTEvent event = (delta == null ? null : new OASTEvent(delta));
1064       
1065       if (iOffset >= 0)
1066       {
1067          //calculate amount of whitespace to remove
1068          int iRemove = iLength;
1069          for (int i = 0; i < nodesRemoved.length; i++)
1070          {
1071             iRemove -= nodesRemoved[i].getLength();
1072          }
1073          //calculate amount of whitespace to insert
1074          int iInsert = dummy.getLength();
1075          OASTNode[] children = dummy.getChildren();
1076          for (int i = 0; i < children.length; i++)
1077          {
1078             iInsert -= (children[i]).getLength();
1079          }
1080          
1081          applyEvent(event, iOffset, iRemove, iInsert, alUnparseable);
1082       }
1083       else
1084       {
1085          applyEvent(event, iOffset, 0, 0, alUnparseable);
1086       }
1087    }
1088 
1089    /***
1090     * Creates an OAST change event with corresponding OAST delta.  The correct
1091     * type of delta will be created depending on whether the removed or
1092     * inserted nodes lists are empty.  The event is automatically applied to the
1093     * tree and notifications are sent to all OAST change listeners.
1094     * @param nodesRemoved the nodes removed for this event
1095     * @param dummy disposable node whose children are the nodes being inserted
1096     * @param parent the parent node for the nodes being inserted
1097     * @param alUnparseable a list of unparseable nodes that need to be
1098     *        reattached to the tree after the event is handled
1099     */
1100    private void createEvent(OASTNode[] nodesRemoved, OASTNode dummy, 
1101       OASTNode parent, ArrayList alUnparseable)
1102    {
1103       if (nodesRemoved.length > 0)
1104       {
1105          int iOffset = nodesRemoved[0].getOffset();
1106          int iEnd = nodesRemoved[nodesRemoved.length - 1].getOffset()
1107                     + nodesRemoved[nodesRemoved.length - 1].getLength();
1108          int iLength = iEnd - iOffset;
1109          createEvent(nodesRemoved, dummy, parent, iOffset, iLength, alUnparseable);
1110       }
1111       else
1112       {
1113          IOASTDelta delta = createDelta(nodesRemoved, dummy, parent);
1114          OASTEvent event = (delta == null ? null : new OASTEvent(delta));
1115 
1116          applyEvent(event, alUnparseable);
1117       }
1118    }
1119    
1120    /***
1121     * <p>Replaces a range of nodes in the syntax tree with any number of new nodes
1122     * in a specified ArrayList.  The inserted nodes will be parented to the 
1123     * parent of the first node removed.</p>
1124     * 
1125     * <p>This method is not intended for public use.</p>
1126     * 
1127     * @param iOffset Begin offset of the removal
1128     * @param iLength Length of the removal
1129     * @param dummy The dummy parent node resulting from a partial reparse
1130     *              executed outside the OAST.  The node itself will be discarded,
1131     *              but its children will be integrated into the tree.
1132     */
1133    public void replace(int iOffset, int iLength, OASTNode dummy)
1134    {
1135       OASTNode[] nodes = getNodes(iOffset, iLength);
1136       //getNode(iOffset) should be safe if clients are well-behaved about 
1137       //calling this method.
1138       OASTNode nodeParent = (nodes.length > 0 ? nodes[0].getParent() 
1139                                               : getNode(iOffset));
1140       if (nodeParent == null)
1141       {
1142          nodeParent = _root;
1143       }
1144       
1145       createEvent(nodes, dummy, nodeParent, iOffset, iLength);
1146 
1147    }
1148 
1149    /***
1150     * <p>Replaces a range of nodes in the syntax tree with one or more new nodes
1151     * constructed by performing a partial reparse on some new text.  The nodes 
1152     * created by the reparse will be positioned in the tree based on the offset
1153     * of the remove range and the parent of the first node being removed.</p>
1154     * 
1155     * <p>This method is not intended for public use.</p>
1156     * 
1157     * @param iOffset Begin offset of the removal
1158     * @param iLength Length of removal
1159     * @param sText Text to parse into replacement nodes
1160     */
1161    public void replace(int iOffset, int iLength, String sText)
1162    {
1163       OASTNode[] nodes = getNodes(iOffset, iLength);
1164       //getNode(iOffset) should be safe if clients are well-behaved about 
1165       //calling this method.
1166       OASTNode nodeParent = (nodes.length > 0 ? nodes[0].getParent() 
1167                                               : getNode(iOffset));
1168       if (nodeParent == null)
1169       {
1170          nodeParent = _root;
1171       }
1172       
1173       try
1174       {
1175          //capture XML prolog information so it can be fed to SAX.  This ensures
1176          //that partial parses are consistent about character encoding and
1177          //entity substitution.
1178          String sProlog = getProlog(iOffset, iLength);
1179          OASTNode dummy = partialParse(nodeParent, sProlog, sText, iOffset);
1180          IOASTDelta delta = null;
1181          createEvent(nodes, dummy, nodeParent, iOffset, iLength);
1182 
1183       }
1184       catch (IOException e)
1185       {
1186          OWLCore.logError(OWLCore.getID(), "Partial reparse failed", e);
1187       }
1188    }
1189    
1190    /***
1191     * Captures and returns the full XML prolog for the document.  The XML
1192     * prolog consists of the XML version tag, other processing instructions,
1193     * and DOCTYPE.
1194     * @return A string containing the full XML prolog.  This may be an empty
1195     *         string.
1196     */
1197    private String getProlog()
1198    {
1199       StringBuffer sbProlog = new StringBuffer();
1200       OASTNode[] children = _root.getChildren();
1201       for (int i = 0; i < children.length; i++)
1202       {
1203          OASTNode n = children[i];
1204          if (!(n instanceof TextNode))
1205          {
1206             break;
1207          }
1208          if (n instanceof ProcessingInstruction //will also detect XMLVersion
1209              || n instanceof Doctype)
1210          {
1211             String s = ((TextNode)n).getText();
1212             sbProlog.append(s);
1213          }
1214       }
1215       return sbProlog.toString();
1216    }
1217    
1218    /***
1219     * Captures and returns the XML prolog (processing instructions & DOCTYPE) 
1220     * for the document, excluding any prolog elements that intersect with a
1221     * specified range of the text.
1222     * @param iOffset Begin offset of the ignore region
1223     * @param iLength Length of the ignore region
1224     * @return A string containing the non-intersecting portions of the XML prolog
1225     */
1226    private String getProlog(int iOffset, int iLength)
1227    {
1228       StringBuffer sbProlog = new StringBuffer();
1229       OASTNode[] children = _root.getChildren();
1230       for (int i = 0; i < children.length; i++)
1231       {
1232          OASTNode n = children[i];
1233          if (!(n instanceof TextNode))
1234          {
1235             break;
1236          }
1237          if (n instanceof ProcessingInstruction //will also detect XMLVersion
1238              || n instanceof Doctype)
1239          {
1240             if (n.getOffset() >= iOffset && n.getOffset() < iOffset + iLength)
1241             {
1242                continue;
1243             }
1244             String s = ((TextNode)n).getText();
1245             sbProlog.append(s);
1246          }
1247       }
1248       return sbProlog.toString();
1249       
1250    }
1251 
1252    /***
1253     * Removes a node from the OAST.  The tree 
1254     * will be displaced by an appropriate amount to compensate for the removal.
1255     * 
1256     * @deprecated As of SWeDE 2.0.0, use 
1257     *             {@link OASTNode#remove(OASTNode, boolean)} instead.  The OAST
1258     *             class's edit operations are now intended exclusively for
1259     *             internal use by the editor and related classes.
1260     * @param node The node to remove
1261     * @return The parent of the removed node
1262     */
1263    public OASTNode remove(OASTNode node)
1264    {
1265       OASTNode parent = node.getParent();
1266       try
1267       {
1268          parent.remove(node, true);
1269       }
1270       catch (OASTException e)
1271       {
1272          //Do nothing.  This method should no longer be used anyway so the
1273          //remove can just quietly fail.
1274       }
1275 
1276       return parent;
1277    }
1278 
1279    /***
1280     * <p>Remove a range of nodes from the abstract syntax tree.  The tree will be
1281     * displaced to compensate for the removed node(s) as well as surrounding
1282     * whitespace.</p>
1283     * 
1284     * <p>This is a convenience method, fully equivalent to:
1285     * <blockquote>replace(iOffset, iLength, "")</blockquote></p>
1286     * 
1287     * <p>This method is not intended for public use.</p>
1288     * 
1289     * @param iOffset Start offset of the removal.  Nodes that start before this
1290     *                offset will not be removed.
1291     * @param iLength Length of the removal.  Nodes that end after 
1292     *                iOffset + iLength will not be removed.
1293     */
1294    public void remove(int iOffset, int iLength)
1295    {
1296       replace(iOffset, iLength, "");
1297    }
1298    
1299    /***
1300     * Produces an array of nodes contained in a specific text range.  The first
1301     * complete node found within the range determines the tree level of the
1302     * search.  Only nodes that are both completely contained in the specified 
1303     * range and at the same level in the OAST will be returned.
1304     * @param iOffset Start offset of the range.  Nodes that start before this
1305     *                offset will not be included.
1306     * @param iLength Length of the range.  Nodes that end after 
1307     *                iOffset + iLength will not be included.
1308     * @return An array of nodes contained in the specified range, all at the
1309     *         same level in the OAST.
1310     */
1311    protected OASTNode[] getNodes(int iOffset, int iLength)
1312    {
1313       OASTNode parent = getNode(iOffset);
1314       if (parent.getOffset() == iOffset && parent.getLength() < iLength)
1315       {
1316          parent = parent.getParent();
1317       }
1318       OASTNode[] children = parent.getChildren();
1319       ArrayList alReturn = new ArrayList();
1320       if (parent.startsAfter(iOffset) && parent.endsBefore(iOffset + iLength))
1321       {
1322          alReturn.add(parent);
1323       }
1324       else
1325       {
1326          for (int i = 0; i < children.length; i++)
1327          {
1328             OASTNode node = children[i];
1329             if (node.startsAfter(iOffset)
1330                && node.endsBefore(iOffset + iLength))
1331             {
1332                alReturn.add(node);
1333             }
1334          }
1335       }
1336       OASTNode[] nodes = new OASTNode[alReturn.size()];
1337       alReturn.toArray(nodes);
1338       return nodes;
1339    }
1340 
1341 
1342    /***
1343     * Helper method for <code>applyEvent()</code>.  Handles node removals for
1344     * an event, including displacement of the remaining nodes in the tree.
1345     * @param event The event to apply.  Must be a non-composite event.
1346     */
1347    private void removeNodes(OASTEvent event)
1348    {
1349       OASTNodeRef[] removed = event.getRemoved(0);
1350       for (int i = 0; i < removed.length; i++)
1351       {
1352          OASTNodeRef nodeRef = removed[i];
1353          nodeRef.getNode().detach(true);
1354       }
1355    }
1356    
1357    /***
1358     * Helper method for <code>applyEvent()</code>.  Handles node insertions for
1359     * an event, including displacement of the existing nodes in the tree.
1360     * @param event The event to apply.  Must be a non-composite event.
1361     */
1362    private void insertNodes(OASTEvent event)
1363    {
1364       OASTNodeRef[] inserted = event.getInserted(0);
1365       for (int i = 0; i < inserted.length; i++)
1366       {
1367          OASTNodeRef nodeRef = inserted[i];
1368          nodeRef.getNode().attach(this, true);
1369       }
1370    }
1371    
1372    /***
1373     * <p>Helper method for <code>applyEvent()</code>.  Performs additional
1374     * displacement on the tree to compensate for whitespace that was removed
1375     * and/or inserted that is not included in the length of the removed and
1376     * inserted nodes.</p>
1377     * 
1378     * <p>Additionally, a list of unparseable nodes that will later be reattached
1379     * to the tree may be specified.  If a non-empty list is provided, the
1380     * nodes it contains will also be adjusted to compensate for the extra removed
1381     * and added whitespace.</p>
1382     * @param iOffset The offset of the document event that caused the OAST event
1383     * @param iRemoveWhitespace Number of extra whitespace characters removed
1384     * @param iInsertWhitespace Number of extra whitespace characters inserted
1385     */
1386    private void adjustForWhitespace(int iOffset, int iRemoveWhitespace, 
1387       int iInsertWhitespace)
1388    {
1389       if (iRemoveWhitespace > 0)
1390       {
1391          _root.accept(new NodeDisplacementVisitor(iOffset, -iRemoveWhitespace));
1392       }
1393       if (iInsertWhitespace > 0)
1394       {
1395          _root.accept(new NodeDisplacementVisitor(iOffset, iInsertWhitespace));
1396       }
1397    }
1398    
1399    /***
1400     * Helper method for <code>applyEvent()</code>.  Reattaches a list of 
1401     * filtered-out unparseable nodes to the tree after a reparse event has been 
1402     * applied.
1403     * @param lUnparseable the list of filtered-out unparseables
1404     */
1405    private void reattachUnparseables(List lUnparseable)
1406    {
1407       if (lUnparseable == null)
1408       {
1409          return;
1410       }
1411       Iterator it = lUnparseable.iterator();
1412       while (it.hasNext())
1413       {
1414          UnparseableNode node = (UnparseableNode)it.next();
1415          //disregard unparseables that contain only whitespace
1416          //and unparseables whose length became nonpositive as a result of
1417          //the text modification
1418          if (node.getText().trim().length() == 0)
1419          {
1420             continue;
1421          }
1422          node.attach(this, false);
1423       }      
1424    }
1425    
1426 
1427    /***
1428     * Applies an event caused by text modification to the OAST.  This includes
1429     * detaching removed nodes, displacing the tree to compensate for the event,
1430     * and attaching inserted nodes, as well as sending event notifications to
1431     * OAST change listeners.  The event will be flagged as a text event before
1432     * OAST change notifications are sent out.
1433     * @param event The OAST event resulting from the text modification.  Must
1434     *              be a non-composite event.
1435     * @param alUnparseable list of unparseable nodes to manually attach to the
1436     *                      tree after applying the event 
1437     */
1438    private void applyEvent(OASTEvent event, ArrayList alUnparseable)
1439    {
1440       if (event != null)
1441       {
1442          event.setTextEvent(true);
1443          removeNodes(event);
1444          insertNodes(event);
1445       }
1446       reattachUnparseables(alUnparseable);
1447       if (event != null)
1448       {
1449          try
1450          {
1451             changed(event);
1452          }
1453          catch (OASTException e)
1454          {
1455             OWLCore.logError(OWLCore.getID(), "OAST locked during manual text edit.", e);
1456          }
1457       }
1458    }
1459    
1460    /***
1461     * Applies an event caused by text modification to the OAST.  This includes
1462     * detaching removed nodes, displacing the tree to compensate for the event,
1463     * and attaching inserted nodes, as well as sending event notifications to
1464     * OAST change listeners.  The event will be flagged as a text event before
1465     * OAST change notifications are sent out.
1466     * @param event The OAST event resulting from the text modification.  Must
1467     *              be a non-composite event.
1468     * @param iOffset The offset of the document event that caused the OAST event 
1469     * @param iRemoveWhitespace Amount of extra whitespace that must be removed
1470     *                          at the event offset
1471     * @param iInsertWhitespace Amount of whitespace that must be added at the
1472     *                       event offset  
1473     * @param alUnparseable list of unparseable nodes to manually attach to the
1474     *                      tree after applying the event 
1475     */
1476    private void applyEvent(OASTEvent event, int iOffset, int iRemoveWhitespace,
1477       int iInsertWhitespace, ArrayList alUnparseable)
1478    {
1479       if (event != null)
1480       {
1481          event.setTextEvent(true);
1482          removeNodes(event);
1483       }
1484       adjustForWhitespace(iOffset, iRemoveWhitespace, iInsertWhitespace);
1485       if (event != null)
1486       {
1487          insertNodes(event);
1488          reattachUnparseables(alUnparseable);
1489          try
1490          {
1491             changed(event);
1492          }
1493          catch (OASTException e)
1494          {
1495             OWLCore.logError(OWLCore.getID(), "OAST locked during manual text edit.", e);
1496          }
1497       }
1498    }
1499 
1500    /***
1501     * <p>Attempts to parse an XML fragment into a node structure.  The resulting
1502     * node(s) are parented to a dummy node which is returned, but are structured
1503     * based on the type of the supplied parent node.  It is up to the caller
1504     * to position the dummy node's children correctly for insertion in the tree
1505     * and to create the actual parent/child relationships by calling 
1506     * attachChildren</p>
1507     * 
1508     * <p>This method is not intended for public use.</p>
1509     * 
1510     * @deprecated As of SWeDE 2.0.0, use 
1511     * {@link #partialParse(OASTNode, String, String, int)}.  This version
1512     * will continue to function as a convenience method for 
1513     * <code>partialParse(parent,sProlog,sText,0)</code>.
1514     * @see #attachChildren(OASTNode, OASTNode)
1515     * @param parent The intended parent node
1516     * @param sProlog XML prolog information to prepend to the text when SAX
1517     *                parses it
1518     * @param sText The new text to parse
1519     * @return A dummy node whose children represent one or more node structures
1520     *         resulting from the parse.
1521     * @throws IOException
1522     */
1523    private OASTNode partialParse(OASTNode parent, String sProlog, String sText)
1524       throws IOException
1525    {
1526       return partialParse(parent, sProlog, sText, 0);
1527    }
1528 
1529    /***
1530     * <p>Attempts to parse an XML fragment into a node structure.  Any XML prolog
1531     * information alread stored in the OAST is used to ensure proper XML 
1532     * encoding and entity substitutions.  The resulting node(s) are parented to 
1533     * a dummy node which is returned, but are structured based on the type of 
1534     * the supplied parent node.  The dummy node will be displaced to start at a 
1535     * supplied document offset.  It is up to the caller to create the actual 
1536     * parent/child relationships by calling <code>attachChildren</code>.</p>
1537     * 
1538     * <p>This is a convenience method, fully equivalent to:
1539     * <blockquote>partialParse(parent, getProlog(iOffset, sText.length()), 
1540     * sText, iOffset)</blockquote></p>
1541     * 
1542     * <p>This method is not intended for public use.</p>
1543     * 
1544     * @see #attachChildren(OASTNode, OASTNode)
1545     * @see #attachChildren(OASTNode[], OASTNode)
1546     * @param parent Intended parent node for newly parsed nodes
1547     * @param sText Text to parse
1548     * @param iOffset Insertion offset
1549     * @return A dummy node whose children represent one or more node structures
1550     *         resulting from the parse.
1551     * @throws IOException if an I/O error occurs during the reparse.
1552     */
1553    public OASTNode partialParse(OASTNode parent, String sText, int iOffset)
1554       throws IOException
1555    {
1556       return partialParse(parent, getProlog(iOffset, sText.length()), sText, iOffset);
1557    }
1558 
1559    /***
1560     * A specialized OAST root, usable as a placeholder root node for partial
1561     * parses.  DummyRoot differs from DocumentRoot only in its displace 
1562     * behavior: DocumentRoot nodes always want to maintain a starting offset
1563     * of 0, so any displacement is handled by lengthening the node.  DummyRoots
1564     * may start at any offset and so they displace just like other nodes.
1565     * @author jlerner
1566     */
1567    private class DummyRoot extends DocumentRoot
1568    {
1569       /***
1570        * Creates a dummy root representing a portion of a file.
1571        * @param file The file
1572        */
1573       public DummyRoot(IFile file)
1574       {
1575          super(file, null);
1576       }
1577       
1578       public boolean displace(int offset, int length)
1579       {
1580          return OASTNode.displace(this, offset, length);
1581       }
1582    }
1583    /***
1584     * <p>Attempts to parse an XML fragment into a node structure.  The resulting
1585     * node(s) are parented to a dummy node which is returned, but are structured
1586     * based on the type of the supplied parent node.  The dummy node will be
1587     * displaced to start at a supplied document offset.  It is up to the 
1588     * caller to create the actual parent/child relationships by calling 
1589     * <code>attachChildren</code>.</p>
1590     * 
1591     * <p>This method is not intended for public use.</p>
1592     * 
1593     * @see #attachChildren(OASTNode, OASTNode)
1594     * @see #attachChildren(OASTNode[], OASTNode)
1595     * @param parent The intended parent node
1596     * @param sProlog XML prolog information to prepend to the text when SAX
1597     *                parses it
1598     * @param sText The new text to parse
1599     * @param iOffset The document offset the dummy node should be displaced to
1600     * @return A dummy node whose children represent one or more node structures
1601     *         resulting from the parse.
1602     * @throws IOException
1603     */
1604    private OASTNode partialParse(OASTNode parent, String sProlog, String sText, int iOffset)
1605       throws IOException
1606    {
1607       if (sProlog == null)
1608       {
1609          sProlog = "";
1610       }
1611       if (parent == null)
1612       {
1613          parent = _root;
1614       }
1615 
1616       OASTNode root;
1617       if (parent instanceof ClassNode)
1618       {
1619          root = new GenericPredicate("");
1620       }
1621       else if (parent instanceof PropertyNode || parent instanceof Rdf)
1622       {
1623          root = new GenericThing("");
1624       }
1625       else
1626       {
1627          IOWLDocument doc = parent.getOWLDocument();
1628          IFile file = (IFile) doc.getCorrespondingResource();
1629          //use DummyRoot rather than DocumentRoot because the result of the
1630          //partial parse needs to be displacable to a nonzero starting offset
1631          root = new DummyRoot(file);
1632       }
1633       StringBuffer sbText = new StringBuffer(sText);
1634       ArrayList alComments = filterComments(sbText);
1635       OASTStringHandler handler =
1636          new OASTStringHandler(
1637             root,
1638             sbText.toString(),
1639             (parent == _root ? false : true));
1640       if (sbText.toString().trim().length() == 0)
1641       {
1642          if (sText.trim().length() > 0) //Only XML comments were inserted
1643          {
1644             root.setLength(sText.length());
1645             attachComments(alComments, root);
1646             //there's no root dummy node here, the comments are directly
1647             //parented to the root
1648          }
1649          else //Only whitespace was inserted
1650          {
1651             root.setLength(sText.length());
1652          }
1653          root.accept(new NodeDisplacementVisitor(0, iOffset));
1654          return root;
1655       }
1656       SAXParser parser;
1657       try
1658       {
1659          parser = SAXParserFactory.newInstance().newSAXParser();
1660          //Wrapping the new text in a dummy tag ensures that SAX
1661          //won't freak out about a missing root tag.  In turn , this 
1662          //ensures proper differentiation between literals and 
1663          //unparseable nodes, and should allow pasting of multiple 
1664          //complete node substructures.
1665          StringReader sr;
1666          if (parent == _root)
1667          {
1668             sr = new StringReader(sProlog + sbText.toString());
1669          }
1670          else
1671          {
1672             sr = new StringReader(sProlog + "<a>" + sbText.toString() + "</a>");
1673          }
1674          InputSource is = new InputSource(sr);
1675          try
1676          {
1677             parser.parse(is, handler);
1678          }
1679          catch (Exception e)
1680          {
1681             //My code is written against the specification for the SAX parser,
1682             //which indicates that it will throw a SAX exception if basically
1683             //anything goes wrong at runtime.  Java 1.5 helpfully violates this
1684             //by throwing an array index out of bounds exception in response
1685             //to certain types of malformed XML prologs.  This stupid workaround
1686             //allows me to handle that case while leaving the rest of my code 
1687             //intact.
1688             throw new SAXException(e);
1689          }
1690          OASTNode node;
1691          if (parent != _root)
1692          {
1693             node = (OASTNode) root.getChildren()[0]; //the root dummy node
1694          }
1695          else
1696          {
1697             node = root;
1698             //detect and attach XML prolog nodes
1699             OASTNode n = (OASTNode)node.getChildren()[0];
1700             XMLPrologParser xpp = new XMLPrologParser();
1701             OASTNode[] nodes = xpp.parse(sbText.toString().substring(0, n.getOffset()));
1702             attachChildren(nodes, node);
1703          }
1704          attachComments(alComments, root);
1705          node.accept(new NodeDisplacementVisitor(0, iOffset));
1706          return node;
1707       }
1708       catch (SAXException se)
1709       {
1710 //       se.printStackTrace();
1711 //       String sTrimmed = sText.trim();
1712 //       iOffset += sText.indexOf(sTrimmed);
1713          if (parent == _root)
1714          {
1715             try
1716             {
1717                StringReader sr = new StringReader(sbText.toString() + "<a/>");
1718                SAXParser p = SAXParserFactory.newInstance().newSAXParser();
1719                p.parse(new InputSource(sr), new DefaultHandler());
1720                //If we got here, the string must have been a valid XML prolog
1721                //with no root tag, so we can build a tree for it after all
1722 
1723                //alComments already contains nodes for comments found in sText
1724                //and sbText already has those comments blanked out
1725                XMLPrologParser xpp = new XMLPrologParser();
1726                OASTNode[] nodes = xpp.parse(sbText.toString());
1727                OASTNode dummy = new GenericThing("");
1728                dummy.setOffset(0);
1729                dummy.setLength(sbText.toString().length());
1730                attachChildren(nodes, dummy);
1731                attachComments(alComments, dummy);
1732                dummy.accept(new NodeDisplacementVisitor(0, iOffset));
1733                return dummy;
1734             }
1735             catch (SAXException e1)
1736             {
1737             }
1738             catch (ParserConfigurationException e1)
1739             {
1740             }
1741             catch (FactoryConfigurationError e1)
1742             {
1743             }
1744             catch (IOException e1)
1745             {
1746             }
1747          }
1748          OASTNode dummy = new GenericThing("");
1749          UnparseableNode un = new UnparseableNode(sText);
1750          dummy.appendChild(un);
1751          un.setLength(sText.length());
1752          dummy.setOffset(un.getOffset());
1753          dummy.setLength(un.getLength());
1754          dummy.accept(new NodeDisplacementVisitor(0, iOffset));
1755          return dummy;
1756       }
1757       catch (ParserConfigurationException e)
1758       {
1759          OWLCore.logError(
1760             OWLCore.getID(),
1761             "SAX creation failed for partial reparse",
1762             e);
1763          return null;
1764       }
1765       catch (FactoryConfigurationError e)
1766       {
1767          OWLCore.logError(
1768             OWLCore.getID(),
1769             "SAX creation failed for partial reparse",
1770             new Exception(e));
1771          return null;
1772       }
1773    }
1774 
1775 
1776    /***
1777     * Attaches an array of nodes as children of a specified parent.  This
1778     * method doesn't do any node displacement.  Offsets and lengths should be
1779     * set before calling attachChildren
1780     * @param nodes Array of child nodes to attach
1781     * @param parent Parent to receive the new child nodes
1782     */
1783    private void attachChildren(OASTNode[] nodes, OASTNode parent)
1784    {
1785       for (int i = 0; i < nodes.length; i++)
1786       {
1787          OASTNode n = nodes[i];
1788          if (parent.getOWLAbstractSyntaxTree() == this)
1789          {
1790             //This is only used for parsing so use attach rather than insert
1791             //so no event is created
1792             n.attach(this, false);
1793          }
1794          else
1795          {
1796             try
1797             {
1798                parent.insert(n, false);
1799             }
1800             catch (OASTException e)
1801             {
1802                //If we're parsing text the file should be writable, but log
1803                //the error in case the impossible happens
1804                OWLCore.logWarning(OWLCore.getID(), "Unable to attach children during partial parse", e);
1805             }
1806          }
1807       }
1808 
1809    }
1810    
1811    /***
1812     * Attaches the children of a given node to a specified parent.  This method
1813     * doesn't do any node displacement.  Offsets and lengths should be set
1814     * before calling attachChildren.
1815     * @param node The node whose children will be attached
1816     * @param parent Node to act as the parent for the attached children
1817     */
1818    private void attachChildren(OASTNode node, OASTNode parent)
1819    {
1820       OASTNode[] children = node.getChildren();
1821       attachChildren(children, parent);
1822    }
1823 
1824    /***
1825     * Finds the IOWLDocument corresponding to a namespace URI.
1826     * @param uri URI of the namespace to locate
1827     * @return The IOWLDocument for the specified URI, or null if the project
1828     *         does not contain a document with the specified base URI
1829     */
1830    public IOWLDocument getNamespace(String uri)
1831    {
1832       if (uri == null || uri.length() == 0)
1833       {
1834          //assume current document namespace
1835          return (IOWLDocument) SWResourceManager.getModel().getCorrespondingElement(
1836             _file);
1837       }
1838       IOWLProject proj =
1839          (IOWLProject) SWResourceManager.getModel().getCorrespondingElement(
1840             _file.getProject());
1841       if (uri.endsWith("#"))
1842       {
1843          uri = uri.substring(0, uri.length() - 1);
1844       }
1845       return proj.getDocument(uri);
1846    }
1847    
1848    /***
1849     * Returns the base URI for the document represented by this OAST.
1850     * @return The document's base URI
1851     */
1852    public String getBaseURI()
1853    {
1854       IOWLDocument doc = getNamespace(null);
1855       if (doc != null)
1856       {
1857          return doc.getURI();
1858       }
1859       return "";
1860    }
1861 
1862    /***
1863     * Lists all namespace URIs in the current project.
1864     * @return A java.util.List of all known document URIs
1865     */
1866    public List listNamespaces()
1867    {
1868       IOWLProject proj =
1869          (IOWLProject) SWResourceManager.getModel().getCorrespondingElement(
1870             _file.getProject());
1871       final List l = new ArrayList();
1872       IOWLElementVisitor ev = new IOWLElementVisitor()
1873       {
1874          public boolean visit(IOWLElement element)
1875          {
1876             if (element instanceof IOWLDocument)
1877             {
1878                IOWLDocument doc = (IOWLDocument) element;
1879                if (!l.contains(doc.getURI()))
1880                {
1881                   l.add(doc.getURI());
1882                }
1883             }
1884             return true;
1885          }
1886       };
1887       proj.accept(ev);
1888       return l;
1889    }
1890 
1891    /***
1892     * Saves a snapshot of which the document's unparseable regions to the
1893     * project metadata.
1894     */
1895    public synchronized void persistUnparseability()
1896    {
1897       OASTNode[] nodes = _root.getNodesOfType(OASTNode.UNPARSEABLE);
1898       if (nodes.length == 0)
1899       {
1900          try
1901          {
1902             _file.setPersistentProperty(QNAME_UNPARSEABLE, null);
1903          }
1904          catch (CoreException e)
1905          {
1906             OWLCore.logWarning(
1907                OWLCore.getID(),
1908                "Unable to set persistent property",
1909                e);
1910          }
1911          return;
1912       }
1913       StringBuffer sbUnparseable = new StringBuffer();
1914       for (int i = 0; i < nodes.length; i++)
1915       {
1916          OASTNode node = nodes[i];
1917          sbUnparseable.append(node.getOffset() + " " + node.getLength() + " ");
1918       }
1919       try
1920       {
1921          _file.setPersistentProperty(
1922             QNAME_UNPARSEABLE,
1923             sbUnparseable.toString());
1924       }
1925       catch (CoreException e)
1926       {
1927          OWLCore.logWarning(
1928             OWLCore.getID(),
1929             "Unable to set persistent property",
1930             e);
1931       }
1932    }
1933 
1934    /***
1935     * <p>Reparses the entire document from the file and the unparseability state
1936     * state stored in the metadata.  Use this method to revert changes that
1937     * have been made to the OAST as a result of unsaved changes in the editor.</p>
1938     * 
1939     * <p>This method is not intended for public use.</p>
1940     * 
1941     */
1942    public void reload()
1943    {
1944       try
1945       {
1946          _file.deleteMarkers(S_UNPARSEABLE, false, IResource.DEPTH_INFINITE);
1947       }
1948       catch (CoreException e)
1949       {
1950          OWLCore.logWarning(
1951             OWLCore.getID(),
1952             "Unable to delete markers for full reparse",
1953             e);
1954       }
1955       OASTNode[] oldNodes = _root.getChildren();
1956       OASTNode dummy = doParseDocument(_file);
1957       restorePersistedUnparseables(_file, dummy);
1958       createEvent(oldNodes, dummy, _root, 0, _root.getLength());
1959    }
1960    
1961    /***
1962     * Registers a new OASTChangeListener.  Listeners receive notifications
1963     * after nodes are added or removed from the OAST.
1964     * @param ocl The listener to register
1965     */
1966    public void addOASTChangeListener(IOASTChangeListener ocl)
1967    {
1968       _alListeners.add(ocl);
1969    }
1970 
1971    /***
1972     * Unregisters an OASTChangeListener.  It will no longer receive
1973     * OASTChange notifications.
1974     * @param ocl The listener to unregister.  If ocl is not registered as an
1975     *            OASTChangeListener, this method does nothing.
1976     */
1977    public void removeOASTChangeListener(IOASTChangeListener ocl)
1978    {
1979       _alListeners.remove(ocl);
1980    }
1981    
1982    /***
1983     * <p>Registers an OASTChangeListener that gets notified of modifications before
1984     * those registered with addOASTChangeListener.</p>
1985     * 
1986     * <p>This method is not intended for public use.</p>
1987     * @param ocl The listener to register
1988     */
1989    public void addPrenotifiedOASTChangeListener(IOASTChangeListener ocl)
1990    {
1991       _alPrenotifiedListeners.add(ocl);
1992    }
1993    
1994    /***
1995     * <p>Unregisters a prenotified OASTChangeListener.  It will no longer receive
1996     * OASTChange notifications.</p>
1997     * 
1998     * <p>This method is not intended for public use.</p>
1999     * @param ocl The listener to unregister.  If ocl is not registered as a
2000     *            prenotified OASTChangeListener, this method does nothing.
2001     */
2002    public void removePrenotifiedOASTChangelistener(IOASTChangeListener ocl)
2003    {
2004       _alPrenotifiedListeners.remove(ocl);
2005    }
2006    
2007    private void preDispatchEvent(OASTEvent event)
2008    {
2009       _bPreLock = true;
2010       _preEvent = new OASTEvent(event);
2011       Iterator it = _alPrenotifiedListeners.iterator();
2012       try
2013       {
2014          while (it.hasNext())
2015          {
2016             IOASTChangeListener ocl = (IOASTChangeListener)it.next();
2017             ocl.oastChanged(event);
2018          }
2019       }
2020       finally
2021       {
2022          _bPreLock = false;
2023       }
2024    }
2025    private void dispatchEvent(OASTEvent event)
2026    {
2027       _bLock = true;
2028       try
2029       {
2030          for (int i = 0; i < _alListeners.size(); i++)
2031          {
2032             IOASTChangeListener ocl = (IOASTChangeListener)_alListeners.get(i);
2033             ocl.oastChanged(event);
2034          }
2035       }
2036       finally
2037       {
2038          _bLock = false;
2039       }
2040    }
2041    
2042    private boolean _bLock;
2043    private boolean _bPreLock;
2044    private OASTEvent _preEvent;
2045    /***
2046     * Notifies all OASTChangeListeners that the OAST has just changed.
2047     * @param event Describes the change that was just made
2048     * @throws OASTException if the OAST is locked for modifications.
2049     */
2050    protected synchronized void changed(OASTEvent event) throws OASTException
2051    {
2052       if (_bLock)
2053       {
2054          throw new OASTException("The OAST is locked for modifications.", this);
2055       }
2056       if (_iCollectEvents > 0)
2057       {
2058          if (_compositeEvent == null)
2059          {
2060             _compositeEvent = event;
2061          }
2062          else
2063          {
2064             _compositeEvent.combine(event);
2065          }
2066          return;
2067       }
2068       //prenotified listeners are allowed to cause additional events, which
2069       //get quietly combined into a copy of the original event.  The resulting
2070       //composite event is what will ultimately be sent out to ordinary OAST
2071       //change listeners.  This is required for content formatting to do some 
2072       //of its adjustemnts to attributes and literals, but it's kind of sketchy
2073       //and nobody should really use this "feature" except the editor.
2074       if (_bPreLock)
2075       {
2076          _preEvent.combine(event);
2077          return;
2078       }
2079       //Make sure the file is open in an OWL editor to ensure proper
2080       //updating of the document's text.
2081       if (!event.isTextEvent())
2082       {
2083          IOWLDocument doc = getRoot().getOWLDocument();
2084          try
2085          {
2086             OWLCore.openEditor(doc);
2087          }
2088          catch (PartInitException e)
2089          {
2090             OWLCore.logError(OWLCore.getID(), "Error opening editor for " + doc.getElementName(), e);
2091          }
2092       }
2093 
2094       preDispatchEvent(event);
2095       dispatchEvent(_preEvent);
2096       _preEvent = null;
2097    }
2098 
2099    private int _iCollectEvents;
2100    private OASTEvent _compositeEvent;
2101    /* (non-Javadoc)
2102     * @see com.bbn.swede.core.dom.IOWLAbstractSyntaxTree#run(com.bbn.swede.core.dom.IOASTRunnable)
2103     */
2104    public synchronized void run(IOASTRunnable action) throws OASTException
2105    {
2106       _iCollectEvents++;
2107       try
2108       {
2109          action.run();
2110       }
2111       finally
2112       {
2113          _iCollectEvents--;
2114          if (_iCollectEvents <= 0 && _compositeEvent != null)
2115          {
2116             OASTNode nodePrimary = action.getPrimaryNode();
2117             _compositeEvent.setPrimaryNode(nodePrimary);
2118             _iCollectEvents = 0;
2119             changed(_compositeEvent);
2120             _compositeEvent = null;
2121          }
2122       }
2123    }
2124 
2125 
2126    /* (non-Javadoc)
2127     * @see com.bbn.swede.core.dom.IOWLAbstractSyntaxTree#hasModel()
2128     */
2129    public boolean hasModel()
2130    {
2131       return (_model != null);
2132    }
2133 }