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 }