Docjar: A Java Source and Docuemnt Enginecom.*    java.*    javax.*    org.*    all    new    plug-in

Quick Search    Search Deep

Source code: org/outerj/pollo/xmleditor/model/XmlModel.java


1   package org.outerj.pollo.xmleditor.model;
2   
3   import org.apache.xml.serialize.OutputFormat;
4   import org.apache.xml.serialize.XMLSerializer;
5   import org.jaxen.SimpleNamespaceContext;
6   import org.jaxen.XPath;
7   import org.jaxen.dom.DOMXPath;
8   import org.outerj.pollo.texteditor.XMLTokenMarker;
9   import org.outerj.pollo.texteditor.XmlTextDocument;
10  import org.w3c.dom.*;
11  import org.xml.sax.InputSource;
12  
13  import javax.swing.*;
14  import javax.swing.event.DocumentEvent;
15  import javax.swing.event.DocumentListener;
16  import javax.swing.text.BadLocationException;
17  import javax.swing.text.Segment;
18  import java.awt.*;
19  import java.io.*;
20  import java.util.ArrayList;
21  import java.util.HashMap;
22  import java.util.Iterator;
23  
24  
25  /**
26   * In-memory representation of an XML file. The XML file can be in two
27   * formats: as a DOM-tree ('parsed mode'), or as XmlTextDocument ('text mode').
28   * All views on the XmlModel should reflect this, they should be all in
29   * parsed mode or text mode.
30   *
31   * This class also has methods for loading and storing the file.
32   *
33   * There are some utility functions for searching namespace declarations and
34   * getting nodes using xpath expressions.
35   *
36   * @author Bruno Dumon
37   */
38  public class XmlModel
39  {
40      public static final int PARSED_MODE = 1;
41      public static final int TEXT_MODE   = 2;
42  
43      protected Document domDocument;
44      protected XmlTextDocument textDocument;
45      protected int mode;
46  
47      protected File file;
48      protected Undo undo;
49      protected ArrayList registeredViewsList = new ArrayList();
50      protected ArrayList xmlModelListeners = new ArrayList();
51  
52      protected boolean modified;
53      protected boolean modifiedWhileInTextMode;
54      protected TextDocumentModifiedListener textModifiedListener;
55  
56      protected static int untitledCount = 0;
57      protected int untitledNumber = -1;
58  
59      public static final int FILENAME_CHANGED = 1;
60      public static final int LAST_VIEW_CLOSED = 2;
61      public static final int FILE_CHANGED     = 3;
62      public static final int FILE_SAVED       = 4;
63      public static final int SWITCH_TO_TEXT_MODE   = 5;
64      public static final int SWITCH_TO_PARSED_MODE = 6;
65  
66      /**
67       * Constructor. By default, this will create an empty file in text mode.
68       */
69      public XmlModel(int undoLevels)
70      {
71          textDocument = new XmlTextDocument();
72          textDocument.setTokenMarker(new XMLTokenMarker());
73          textModifiedListener = new TextDocumentModifiedListener();
74          textDocument.addDocumentListener(textModifiedListener);
75          mode = TEXT_MODE;
76          undo = new Undo(this, undoLevels);
77      }
78  
79      /**
80       * Reades the xml document given by the inputSource. File is an
81       * optional parameter that is used for saving the document and
82       * displaying the file name to the user. It may be null, in wich case
83       * the document will be shown as 'Untitled'.
84       *
85       * By default, the document will be parsed and hence the XmlModel
86       * will be in parsed mode. If parsing fails, the XmlModel will be
87       * in text mode.
88       *
89       * The inputstream provided by the InputSource must be closed by
90       * the caller.
91       *
92       */
93      public void readFromResource(InputSource inputSource, File file)
94          throws Exception
95      {
96          this.file = file;
97          try
98          {
99              PolloDOMParser parser = new PolloDOMParser();
100             setFeatures(parser);
101             parser.parse(inputSource);
102             domDocument = parser.getDocument();
103             undo.reconnectToDom();
104             mode = PARSED_MODE;
105         }
106         catch (Exception e)
107         {
108             // parsing failed, read the document as text
109             try
110             {
111                 // fallback only supported if it is a file because we need to open
112                 // a new inputstream
113                 if (file != null)
114                 {
115                     // FIXME encoding!!
116                     InputStream is = new FileInputStream(file);
117                     try
118                     {
119                         InputStreamReader reader = new InputStreamReader(is);
120                         StringBuffer text = new StringBuffer();
121                         final int BUFFER_SIZE = 5000;
122                         char[] buffer = new char[BUFFER_SIZE];
123 
124                         int l;
125                         do
126                         {
127                             l = reader.read(buffer, 0, BUFFER_SIZE);
128                             if (l != -1)
129                                 text.append(buffer, 0, l);
130                         }
131                         while (l != -1);
132                         setTextDocumentText(text.toString());
133                         mode = TEXT_MODE;
134                     }
135                     finally
136                     {
137                         try { is.close(); } catch (Exception e3) {}
138                     }
139                 }
140             }
141             catch (Exception e2)
142             {
143                 throw new Exception("Could not read from the file: " + e2.toString());
144             }
145         }
146         modified = false;
147     }
148 
149     public void readFromResource(File file)
150         throws Exception
151     {
152         FileInputStream fis = new FileInputStream(file);
153         try
154         {
155             readFromResource(new InputSource(fis), file);
156         }
157         finally
158         {
159             try { fis.close(); } catch (Exception e) {}
160         }
161     }
162 
163     public void setFeatures(PolloDOMParser parser)
164         throws Exception
165     {
166         parser.setFeature("http://apache.org/xml/features/dom/defer-node-expansion",false);
167         parser.setFeature("http://xml.org/sax/features/namespaces", true);
168     }
169 
170     public Document getDocument()
171     {
172         return domDocument;
173     }
174 
175     public XmlTextDocument getTextDocument()
176     {
177         return textDocument;
178     }
179 
180     /**
181      * Sets the text of the textdocument.
182      */
183     public void setTextDocumentText(String text)
184     {
185         try
186         {
187             textDocument.stopUndo();
188             textModifiedListener.stop();
189             textDocument.beginCompoundEdit();
190             textDocument.remove(0, textDocument.getLength());
191             textDocument.insertString(0, text, null);
192         }
193         catch(BadLocationException bl)
194         {
195             bl.printStackTrace();
196         }
197         finally
198         {
199             textDocument.endCompoundEdit();
200             textDocument.startUndo();
201             textModifiedListener.start();
202         }
203     }
204 
205     /**
206      * When set to true, the document will be reparsed when
207      * switching to parsed mode, otherwise not.
208      */
209     public void setModifiedWhileInTextMode(boolean modified)
210     {
211         modifiedWhileInTextMode = modified;
212     }
213 
214     /**
215      * Returns the contents of the text document as a String.
216      */
217     public String getTextDocumentText()
218     {
219         try
220         {
221             return textDocument.getText(0, textDocument.getLength());
222         }
223         catch(BadLocationException bl)
224         {
225             bl.printStackTrace();
226             return null;
227         }
228     }
229 
230     public void store(String filename)
231         throws Exception
232     {
233         FileOutputStream output = null;
234         try
235         {
236             output = new FileOutputStream(filename);
237             if (mode == PARSED_MODE)
238             {
239                 XMLSerializer serializer = new XMLSerializer(output, createOutputFormat());
240                 serializer.serialize(domDocument);
241             }
242             else if (mode == TEXT_MODE)
243             {
244                 String encoding = textDocument.getEncoding();
245                 if (encoding == null)
246                     encoding = "UTF-8";
247                 Writer writer = null;
248                 if (encoding != null)
249                     writer = new OutputStreamWriter(output, encoding);
250                 else
251                     writer = new OutputStreamWriter(output);
252                 Segment seg = new Segment();
253                 textDocument.getText(0, textDocument.getLength(), seg);
254                 try
255                 {
256                     writer.write(seg.array, seg.offset, seg.count);
257                 }
258                 finally
259                 {
260                     writer.close();
261                 }
262             }
263             else
264             {
265                 throw new RuntimeException("XmlModel is in an invalid mode.");
266             }
267         }
268         finally
269         {
270             try { output.close(); } catch (Exception e) {}
271         }
272         
273         modified = false;
274         notify(FILE_SAVED);
275     }
276 
277     public void store()
278         throws Exception
279     {
280         store(file.getAbsolutePath());
281     }
282 
283     public OutputFormat createOutputFormat()
284     {
285         String encoding = domDocument.getEncoding();
286         OutputFormat outputFormat = new OutputFormat(domDocument, encoding != null ? encoding : "ISO-8859-1", true);
287         outputFormat.setIndent(2);
288         outputFormat.setLineWidth(0);
289         outputFormat.setLineSeparator(System.getProperty("line.separator"));
290 
291         return outputFormat;
292     }
293 
294     public String toXMLString()
295         throws Exception
296     {
297         StringWriter writer = new StringWriter();
298 
299         XMLSerializer serializer = new XMLSerializer(writer, createOutputFormat());
300         serializer.serialize(domDocument);
301 
302         return writer.toString();
303     }
304 
305     public Element getNextElementSibling(Element element)
306     {
307         if (mode != PARSED_MODE)
308             throw new RuntimeException("getNextElementSibling may not be called when the document is not in parsed mode.");
309         // search the next sibling of type element (null is also allowed)
310         Element nextElement = null;
311         Node nextNode = element;
312         while ((nextNode = nextNode.getNextSibling()) != null)
313         {
314             if (nextNode.getNodeType() == Node.ELEMENT_NODE)
315             {
316                 nextElement = (Element)nextNode;
317                 break;
318             }
319         }
320         return nextElement;
321     }
322 
323 
324     /**
325      * Finds the namespace with which the prefix is associated, or null
326      * if not found.
327      *
328      * @param element Element from which to start searching
329      */
330     public String findNamespaceForPrefix(Element element, String prefix)
331     {
332         if (mode != PARSED_MODE)
333             throw new RuntimeException("findNamespaceForPrefix may not be called when the document is not in parsed mode.");
334         if (element == null || prefix == null)
335             return null;
336 
337         if (prefix.equals("xml"))
338             return "http://www.w3.org/XML/1998/namespace";
339 
340         if (prefix.equals("xmlns"))
341             return "http://www.w3.org/2000/xmlns/";
342 
343         Element currentEl = element;
344         String searchForAttr = "xmlns:" + prefix;
345 
346         do
347         {
348             String attrValue = currentEl.getAttribute(searchForAttr);
349             if (attrValue != null && attrValue.length() > 0)
350             {
351                 return attrValue;
352             }
353 
354             if (currentEl.getParentNode().getNodeType() == currentEl.ELEMENT_NODE)
355                 currentEl = (Element)currentEl.getParentNode();
356             else
357                 currentEl = null;
358         }
359         while (currentEl != null);
360 
361         return null;
362     }
363 
364 
365     /**
366      * Finds a prefix declaration for the given namespace, or null if
367      * not found.
368      *
369      * @param element Element from which to start searching
370      *
371      * @return null if no prefix is found, an empty string if it is the
372      * default namespace, and otherwise the found prefix
373      */
374     public String findPrefixForNamespace(Element element, String ns)
375     {
376         if (mode != PARSED_MODE)
377             throw new RuntimeException("findPrefixForNamespace may not be called when the document is not in parsed mode.");
378         if (element == null || ns == null)
379             return null;
380 
381         if (ns.equals("http://www.w3.org/XML/1998/namespace"))
382             return "xml";
383 
384         Element currentEl = element;
385 
386         do
387         {
388             NamedNodeMap attrs = currentEl.getAttributes();
389 
390             for (int i = 0; i < attrs.getLength(); i++)
391             {
392                 Attr attr = (Attr)attrs.item(i);
393                 if (attr.getValue().equals(ns))
394                 {
395                     if (attr.getPrefix() != null && attr.getPrefix().equals("xmlns"))
396                     {
397                         return attr.getLocalName();
398                     }
399                     else if (attr.getLocalName().equals("xmlns"))
400                     {
401                         return "";
402                     }
403                 }
404             }
405             if (currentEl.getParentNode().getNodeType() == currentEl.ELEMENT_NODE)
406                 currentEl = (Element)currentEl.getParentNode();
407             else
408                 currentEl = null;
409         }
410         while (currentEl != null);
411 
412         return null;
413     }
414 
415     /**
416       Returns a list of all the namespace prefixes that are known in the given context.
417 
418       @param element Element from which to start searching
419      */
420     public HashMap findNamespaceDeclarations(Element element)
421     {
422         if (mode != PARSED_MODE)
423             throw new RuntimeException("findNamespaceDeclarations may not be called when the document is not in parsed mode.");
424 
425         HashMap namespaces = new HashMap();
426         Element currentEl = element;
427 
428         do
429         {
430             NamedNodeMap attrs = currentEl.getAttributes();
431 
432             for (int i = 0; i < attrs.getLength(); i++)
433             {
434                 Attr attr = (Attr)attrs.item(i);
435                 if (attr.getPrefix() != null && attr.getPrefix().equals("xmlns") )
436                 {
437                     // only the first declartion found counts.
438                     if (!namespaces.containsKey(attr.getLocalName()))
439                         namespaces.put(attr.getLocalName(), attr.getValue());
440                 }
441             }
442             if (currentEl.getParentNode().getNodeType() == currentEl.ELEMENT_NODE)
443                 currentEl = (Element)currentEl.getParentNode();
444             else
445                 currentEl = null;
446         }
447         while (currentEl != null);
448 
449         return namespaces;
450     }
451 
452 
453     /**
454       Finds a default namespace declaration.
455      */
456     public String findDefaultNamespace(Element element)
457     {
458         if (mode != PARSED_MODE)
459             throw new RuntimeException("findDefaultNamespace may not be called when the document is not in parsed mode.");
460 
461         if (element == null)
462             return null;
463 
464         Element currentEl = element;
465         do
466         {
467             String xmlns = currentEl.getAttribute("xmlns");
468             if (xmlns != null)
469                 return xmlns;
470 
471             if (currentEl.getParentNode().getNodeType() == currentEl.ELEMENT_NODE)
472                 currentEl = (Element)currentEl.getParentNode();
473             else
474                 currentEl = null;
475         }
476         while (currentEl != null);
477 
478         return null;
479     }
480 
481     public Element getNode(String xpathExpr)
482     {
483         if (mode != PARSED_MODE)
484             throw new RuntimeException("getNode may not be called when the document is not in parsed mode.");
485 
486         try
487         {
488             XPath xpath = new DOMXPath(xpathExpr);
489             SimpleNamespaceContext namespaceContext = new SimpleNamespaceContext();
490             namespaceContext.addElementNamespaces(xpath.getNavigator(), domDocument.getDocumentElement());
491             xpath.setNamespaceContext(namespaceContext);
492             Element el =  (Element)xpath.selectSingleNode(domDocument.getDocumentElement());
493             if (el == null)
494                 System.out.println("xpath returned null: " + xpathExpr);
495             return el;
496         }
497         catch (Exception e)
498         {
499             System.out.println("error executing xpath: " + xpathExpr);
500             return null;
501         }
502     }
503 
504     public Undo getUndo()
505     {
506         return undo;
507     }
508 
509     public File getFile()
510     {
511         return file;
512     }
513 
514     public String getShortTitle()
515     {
516         if (file == null)
517         {
518             if (untitledNumber == -1)
519             {
520                 untitledCount++;
521                 untitledNumber = untitledCount;
522             }
523 
524             return "Untitled" + untitledNumber;
525         }
526         else
527         {
528             return file.getName();
529         }
530     }
531 
532     public String getLongTitle()
533     {
534         if (file == null)
535         {
536             return getShortTitle();
537         }
538         else
539         {
540             return file.getAbsolutePath();
541         }
542     }
543 
544     public void switchToParsedMode()
545         throws Exception
546     {
547         if (mode == PARSED_MODE)
548             return;
549 
550         if (modifiedWhileInTextMode || domDocument == null)
551         {
552             PolloDOMParser parser = new PolloDOMParser();
553             setFeatures(parser);
554             Segment seg = new Segment();
555             textDocument.getText(0, textDocument.getLength(), seg);
556             parser.parse(new InputSource(new CharArrayReader(seg.array, seg.offset, seg.count)));
557             domDocument = parser.getDocument();
558             undo.reconnectToDom();
559         }
560         mode = PARSED_MODE;
561         notify(SWITCH_TO_PARSED_MODE);
562     }
563 
564     public void switchToTextMode()
565         throws Exception
566     {
567         if (mode == TEXT_MODE)
568             return;
569         String xml = toXMLString();
570         setTextDocumentText(xml);
571         mode = TEXT_MODE;
572         modifiedWhileInTextMode = false;
573         notify(SWITCH_TO_TEXT_MODE);
574     }
575 
576     public int getCurrentMode()
577     {
578         return mode;
579     }
580 
581     public boolean isInParsedMode()
582     {
583         return (mode == PARSED_MODE);
584     }
585 
586     public boolean isInTextMode()
587     {
588         return (mode == TEXT_MODE);
589     }
590 
591     public void markModified()
592     {
593         if (modified == false)
594         {
595             modified = true;
596             notify(FILE_CHANGED);
597         }
598     }
599 
600     public void registerView(View view)
601     {
602         registeredViewsList.add(view);
603     }
604 
605     public void addListener(XmlModelListener listener)
606     {
607         xmlModelListeners.add(listener);
608     }
609 
610     public void removeListener(XmlModelListener listener)
611     {
612         xmlModelListeners.remove(listener);
613     }
614 
615     /**
616       @return false if the user cancelled the operation
617      */
618     public boolean closeView(View view)
619         throws Exception
620     {
621         if (!registeredViewsList.contains(view))
622             throw new RuntimeException("Tried to call XmlModel.closeView for a view that was not registered.");
623 
624         if (registeredViewsList.size() == 1)
625         {
626             // this was the last view on the model
627             if (!askToSave(view.getParentForDialogs())) return false;
628 
629             // last view was closed, notified XmlModelListeners of this fact
630             notify(LAST_VIEW_CLOSED);
631         }
632         registeredViewsList.remove(view);
633         return true;
634     }
635 
636     public void notify(int eventtype)
637     {
638         Iterator xmlModelListenersIt = xmlModelListeners.iterator();
639         while (xmlModelListenersIt.hasNext())
640         {
641             XmlModelListener listener = (XmlModelListener)xmlModelListenersIt.next();
642             switch (eventtype)
643             {
644                 case FILENAME_CHANGED:
645                     listener.fileNameChanged(this);
646                     break;
647                 case LAST_VIEW_CLOSED:
648                     listener.lastViewClosed(this);
649                     break;
650                 case FILE_CHANGED:
651                     listener.fileChanged(this);
652                     break;
653                 case FILE_SAVED:
654                     listener.fileSaved(this);
655                     break;
656                 case SWITCH_TO_TEXT_MODE:
657                     listener.switchToTextMode(this);
658                     break;
659                 case SWITCH_TO_PARSED_MODE:
660                     listener.switchToParsedMode(this);
661                     break;
662             }
663         }
664     }
665 
666     public void save(Component parent)
667         throws Exception
668     {
669         if (file == null)
670         {
671             saveAs(parent);
672         }
673         if (file != null)
674             store();
675     }
676 
677     public void saveAs(Component parent)
678         throws Exception
679     {
680         // ask for a filename
681         JFileChooser fileChooser = new JFileChooser();
682         switch (fileChooser.showSaveDialog(parent))
683         {
684             case JFileChooser.APPROVE_OPTION:
685                 file = fileChooser.getSelectedFile();
686                 break;
687             case JFileChooser.CANCEL_OPTION:
688                 break;
689             case JFileChooser.ERROR_OPTION:
690                 break;
691         }
692         if (file != null)
693         {
694             notify(FILENAME_CHANGED);
695             save(parent);
696         }
697     }
698 
699     public boolean closeAllViews(Component parent)
700         throws Exception
701     {
702         if (!askToSave(parent)) return false;
703 
704         Iterator registeredViewsIt = registeredViewsList.iterator();
705 
706         while (registeredViewsIt.hasNext())
707         {
708             View view = (View)registeredViewsIt.next();
709             view.stop();
710         }
711 
712         registeredViewsList.clear();
713         notify(LAST_VIEW_CLOSED);
714 
715         return true;
716     }
717 
718 
719     /**
720      * @return false if the user pressed cancel
721      */
722     public boolean askToSave(Component parent)
723         throws Exception
724     {
725         if (modified)
726         {
727             String message = "This file is not yet saved. Save it before closing?";
728             if (file != null)
729                 message = "The file " + file.getAbsolutePath() + " was modified. Save it?";
730             switch (JOptionPane.showConfirmDialog(parent, message, "Pollo message",
731                         JOptionPane.YES_NO_CANCEL_OPTION))
732             {
733                 case JOptionPane.YES_OPTION:
734                     save(parent);
735                     break;
736                 case JOptionPane.NO_OPTION:
737                     break;
738                 case JOptionPane.CANCEL_OPTION:
739                     return false;
740             }
741         }
742         return true;
743     }
744 
745 
746     public boolean isModified()
747     {
748         return modified;
749     }
750 
751     public class TextDocumentModifiedListener implements DocumentListener
752     {
753         protected boolean enabled = false;
754 
755         public void changedUpdate(DocumentEvent e)
756         {
757             if (enabled)
758             {
759                 markModified();
760                 modifiedWhileInTextMode = true;
761             }
762         }
763 
764         public void insertUpdate(DocumentEvent e)
765         {
766             if (enabled)
767             {
768                 markModified();
769                 modifiedWhileInTextMode = true;
770             }
771         }
772 
773         public void removeUpdate(DocumentEvent e)
774         {
775             if (enabled)
776             {
777                 markModified();
778                 modifiedWhileInTextMode = true;
779             }
780         }
781 
782         public void start()
783         {
784             enabled = true;
785         }
786 
787         public void stop()
788         {
789             enabled = false;
790         }
791     }
792 }