001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041: package org.netbeans.modules.visualweb.insync;
042:
043: import java.beans.PropertyChangeEvent;
044: import java.beans.PropertyChangeListener;
045: import java.io.BufferedWriter;
046: import java.io.OutputStream;
047: import java.io.OutputStreamWriter;
048: import java.io.PrintWriter;
049: import java.io.StringWriter;
050: import java.io.Writer;
051: import java.io.IOException;
052: import java.util.Date;
053: import java.util.HashSet;
054: import java.util.Iterator;
055: import java.util.concurrent.atomic.AtomicBoolean;
056:
057: import javax.swing.event.DocumentEvent;
058: import javax.swing.event.DocumentListener;
059: import javax.swing.event.UndoableEditEvent;
060: import javax.swing.event.UndoableEditListener;
061: import javax.swing.text.StyledDocument;
062:
063: import org.openide.ErrorManager;
064: import org.openide.cookies.EditorCookie;
065: import org.openide.filesystems.FileAttributeEvent;
066: import org.openide.filesystems.FileChangeListener;
067: import org.openide.filesystems.FileEvent;
068: import org.openide.filesystems.FileObject;
069: import org.openide.filesystems.FileRenameEvent;
070: import org.openide.filesystems.FileUtil;
071: import org.openide.loaders.DataObject;
072: import org.openide.text.CloneableEditorSupport;
073: import org.openide.text.NbDocument;
074: import org.openide.cookies.SaveCookie;
075:
076: import org.netbeans.modules.visualweb.extension.openide.util.Trace;
077:
078: /**
079: * A partial Unit implementation that provides common functionality for all source-based Units.
080: * @author Carl Quinn
081: */
082: public abstract class SourceUnit implements Unit, DocumentListener,
083: UndoableEditListener, PropertyChangeListener,
084: FileChangeListener {
085:
086: // Information about this unit
087: protected FileObject fobj; // may be null, e.g. for testsuite & stand-alone use
088: protected StyledDocument styledDocument;
089: protected UndoManager undoManager;
090: private DataObject dataObject = null;
091: private EditorCookie ec = null;
092:
093: protected Date lastModified;
094: private FileChangeListener lastModifiedTracker;
095:
096: protected Date lastModelDirty;
097:
098: private AtomicBoolean documentListenerAdded = new AtomicBoolean(
099: false);
100: private AtomicBoolean undoableEditListenerAdded = new AtomicBoolean(
101: false);
102:
103: //--------------------------------------------------------------------------------- Construction
104:
105: /**
106: * Construct a SourceUnit from an existing source Document
107: * @param dobj
108: */
109: protected SourceUnit(FileObject fobj, UndoManager undoManager) {
110: this .fobj = fobj;
111: // Cache the data object. If the data object is not cached
112: // then diffrent CloneableEditorSupport may be created for
113: // the FileObject on request and our added listener may be lost.
114:
115: try {
116: dataObject = DataObject.find(fobj);
117: } catch (Exception exc) {
118: ErrorManager.getDefault().notify(exc);
119: }
120: this .undoManager = undoManager;
121: //Trace.enableTraceCategory("insync");
122:
123: state = State.SOURCEDIRTY; // need to perform initial read of source into model
124:
125: // Listen for the editor closing the document so that we can release it
126: ec = (EditorCookie) Util.getCookie(fobj, EditorCookie.class);
127: if (ec instanceof CloneableEditorSupport)
128: ((CloneableEditorSupport) ec)
129: .addPropertyChangeListener(this );
130:
131: styledDocument = ec.getDocument(); // get if open. Do not block
132:
133: if (styledDocument != null) {
134: addDocumentListener();
135: addUndoableEditListener();
136: } else {
137:
138: // Add listener to the File Object so that we can listen to the FileObject modification
139: // When the StyledDocument is obtained, the listener will be removed and then on we listen
140: // only to the StyledDocument events.
141: fobj.addFileChangeListener(this );
142: }
143: lastModified = fobj.lastModified();
144: lastModifiedTracker = new FileChangeListener() {
145: public void fileAttributeChanged(FileAttributeEvent fe) {
146: }
147:
148: public void fileChanged(FileEvent fe) {
149: lastModified = SourceUnit.this .fobj.lastModified();
150: }
151:
152: public void fileDataCreated(FileEvent fe) {
153: }
154:
155: public void fileDeleted(FileEvent fe) {
156: }
157:
158: public void fileFolderCreated(FileEvent fe) {
159: }
160:
161: public void fileRenamed(FileRenameEvent fe) {
162: lastModified = SourceUnit.this .fobj.lastModified();
163: }
164: };
165: fobj.addFileChangeListener(lastModifiedTracker);
166: }
167:
168: /*
169: * @see org.netbeans.modules.visualweb.insync.Unit#destroy()
170: */
171: public void destroy() {
172: releaseDocument();
173: // Make sure FileObject listener is also removed that was added by the
174: // above releaseDocument() call
175: if (fobj != null) {
176: fobj.removeFileChangeListener(this );
177: fobj.removeFileChangeListener(lastModifiedTracker);
178: }
179: listeners = null;
180:
181: if (ec instanceof CloneableEditorSupport) {
182: ((CloneableEditorSupport) ec)
183: .removePropertyChangeListener(this );
184: ec = null;
185: }
186: undoManager = null;
187: }
188:
189: //----------------------------------- Implementation of file change listener ------------------
190:
191: public void fileFolderCreated(FileEvent fe) {
192: }
193:
194: public void fileDataCreated(FileEvent fe) {
195: }
196:
197: public void fileChanged(FileEvent fe) {
198: // If both the model and the disk copy have been changed,
199: // take the model.
200: if (state != State.MODELDIRTY) {
201: setSourceDirty();
202: }
203: }
204:
205: public void fileDeleted(FileEvent fe) {
206: }
207:
208: public void fileRenamed(FileRenameEvent fe) {
209: }
210:
211: public void fileAttributeChanged(FileAttributeEvent fe) {
212: }
213:
214: //---------------------------------------------------------------------------- Document handling
215:
216: /**
217: * Release the current document and all listener hooks. The document will have to be grabbed
218: * again before use.
219: */
220: protected void releaseDocument() {
221: if (styledDocument != null) {
222: removeDocumentListener();
223: removeUndoableEditListener();
224: styledDocument = null;
225:
226: //need to tell the undo manager to clear events on the Undo/Redo stack
227: if (undoManager != null) { // XXX Prevent NPE from leaked unit?
228: undoManager.notifyBufferEdited(this );
229: }
230:
231: // Add back the FileObject change listener, now that we no longer
232: // listen to the StyledDocument changes
233: if (fobj != null) { // XXX Prevent NPE from leaked unit?
234: fobj.addFileChangeListener(this );
235: }
236: }
237: }
238:
239: /*
240: * Looks like the editor is getting a new document--release our grip on it for now.
241: * @see java.beans.PropertyChangeListener#propertyChange(java.beans.PropertyChangeEvent)
242: */
243: public void propertyChange(PropertyChangeEvent event) {
244: if (EditorCookie.Observable.PROP_DOCUMENT.equals(event
245: .getPropertyName())) {
246: if (event.getNewValue() == null) {
247: // Check if the model was dirtied after opening or last saving
248: try {
249: if (lastModelDirty != null
250: && lastModified.before(lastModelDirty)) {
251: setSourceDirty();
252: }
253: } finally {
254: lastModified = fobj.lastModified();
255: lastModelDirty = null;
256: releaseDocument();
257: }
258: }
259: if ((event.getNewValue() != null)
260: && (event.getOldValue() == null)) {
261: // Remove the FileObject change listener, now that we start
262: // listening to the StyledDocument changes
263: fobj.removeFileChangeListener(this );
264: styledDocument = (StyledDocument) event.getNewValue();
265: addDocumentListener();
266: addUndoableEditListener();
267: }
268:
269: } else if (EditorCookie.Observable.PROP_MODIFIED.equals(event
270: .getPropertyName())) {
271: // TODO !EAT: Take a look at cleaning up the need for listeners ?
272: Boolean newValue = (Boolean) event.getNewValue();
273: if (newValue != null && newValue.equals(Boolean.FALSE)) {
274: fireSaved();
275: }
276: }
277: }
278:
279: //------------------------------------------------------------------------------- State tracking
280:
281: State state = State.CLEAN;
282:
283: /*
284: * @see org.netbeans.modules.visualweb.insync.Unit#getState()
285: */
286: public State getState() {
287: return state;
288: }
289:
290: /*
291: * @see org.netbeans.modules.visualweb.insync.Unit#getErrors()
292: */
293: public ParserAnnotation[] getErrors() {
294: return ParserAnnotation.EMPTY_ARRAY;
295: }
296:
297: /**
298: * Called by various subclasses when they actually mutate their model
299: */
300: public void setModelDirty() {
301: // Record model dirty timestamp
302: lastModelDirty = new Date();
303: //This would be good, but many sync() handlers would need a lock and cant place it with source dirty...
304: //if (writerCount == 0)
305: // throw new IllegalStateException("Illegal model modification without a lock " + name);
306: if (state == State.SOURCEDIRTY)
307: throw new IllegalStateException(
308: "Illegal model modification with dirty source "
309: + getName());
310: if (state == State.MODELDIRTY)
311: return; // Already dirty
312: // We know that this is just a temporary state and that a flush will be coming soon anyway
313: //markDocumentModified();
314: if (state == State.CLEAN) {
315: assert Trace.trace("insync",
316: "SU.setModelDirty UpToDate => ModelDirty");
317: state = State.MODELDIRTY;
318: fireModelDirtied();
319: }
320: }
321:
322: /**
323: * Called by document listeners to let us know that our buffer is dirty and needs re-syncing
324: */
325: public void setSourceDirty() {
326: if (state == State.MODELDIRTY) {
327: // When a file is moved, it is copied to new destination, and old file is deleted.
328: // We could be notified of the copy being created prior to the delete, in that case
329: // I will still be listening on document events that do not pertain to me anymore, so
330: // ignore that case.
331: if (fobj != null && !fobj.isValid())
332: return;
333: throw new IllegalStateException(
334: "Illegal source modification with dirty model "
335: + getName());
336: }
337: if (state != State.SOURCEDIRTY) {
338: // See above, breaking it out so test is not always done
339: if (fobj != null && !fobj.isValid())
340: return;
341: if (state == State.CLEAN) {
342: assert Trace.trace("insync",
343: "SU.setModelDirty Clean => SourceDirty");
344: state = State.SOURCEDIRTY;
345: } else if (state == State.BUSTED) {
346: assert Trace.trace("insync",
347: "SU.setModelDirty Busted => SourceDirty");
348: state = State.SOURCEDIRTY;
349: }
350: fireSourceDirtied();
351: }
352: }
353:
354: /**
355: * Return true if my state is busted.
356: * @return
357: */
358: public boolean isBusted() {
359: return state.isBusted();
360: }
361:
362: /**
363: * Mark the source as being busted - e.g. cannot be parsed.
364: * @todo Provide notification of invalid state changes?
365: */
366: public void setBusted() {
367: if (state == State.BUSTED)
368: return; // Already busted
369: if (state == State.SOURCEDIRTY) {
370: assert Trace.trace("insync",
371: "SU.setInvalid SourceDirty => Busted");
372: state = State.BUSTED;
373: } else { // should only be called during a sync - from source dirty state
374: throw new IllegalStateException(
375: "Illegal source busting from " + state + " "
376: + getName());
377: }
378: }
379:
380: /**
381: *
382: */
383: public void setClean() {
384: //Note: applying a model brings you up to date but not saved - don't
385: // call notifyUnmodified
386: if (state == State.SOURCEDIRTY)
387: assert Trace.trace("insync",
388: "SU.setSourceUpToDate SourceDirty => Clean");
389: else if (state == State.BUSTED)
390: assert Trace.trace("insync",
391: "SU.setSourceUpToDate Busted => Clean");
392: else if (state == State.MODELDIRTY)
393: assert Trace.trace("insync",
394: "SU.setSourceUpToDate ModelDirty => Clean");
395: state = State.CLEAN;
396: }
397:
398: //----------------------------------------------------------------------------- DocumentListener
399:
400: /*
401: * @see javax.swing.event.DocumentListener#changedUpdate(javax.swing.event.DocumentEvent)
402: */
403: public void changedUpdate(DocumentEvent e) {
404: assert Trace.trace("insync-listener", "SU.changedUpdate");
405: //setSourceDirty(); // these are usually non-substantial changes but things
406: // like editor annotations uses attributes so just adding a breakpoint to
407: // a line will cause a changedUpdate which we definitely don't want to treat
408: // as an insert/delete/source dirty operation
409: }
410:
411: /*
412: * @see javax.swing.event.DocumentListener#insertUpdate(javax.swing.event.DocumentEvent)
413: */
414: public void insertUpdate(DocumentEvent e) {
415: assert Trace.trace("insync-listener", "SU.insertUpdate");
416: undoManager.notifyBufferEdited(this );
417: setSourceDirty();
418: }
419:
420: /*
421: * @see javax.swing.event.DocumentListener#removeUpdate(javax.swing.event.DocumentEvent)
422: */
423: public void removeUpdate(DocumentEvent e) {
424: assert Trace.trace("insync-listener", "SU.removeUpdate");
425: undoManager.notifyBufferEdited(this );
426: setSourceDirty();
427: }
428:
429: /*
430: * @see javax.swing.event.UndoableEditListener#undoableEditHappened(javax.swing.event.UndoableEditEvent)
431: */
432: public void undoableEditHappened(UndoableEditEvent e) {
433: if (undoManager != null) {
434: undoManager.notifyUndoableEditEvent(this );
435: }
436: }
437:
438: //-------------------------------------------------------------------------------------- Locking
439:
440: int readerCount;
441: int writerCount;
442: Thread writingThread;
443:
444: /**
445: * Fetches the current writing thread if there is one. This can be used to distinguish whether a
446: * method is being called as part of an existing modification or if a lock needs to be acquired
447: * and a new transaction started.
448: *
449: * @return the thread actively modifying the document or <code>null</code> if there are no
450: * modifications in progress
451: */
452: protected synchronized final Thread getWritingThread() {
453: return writingThread;
454: }
455:
456: /*
457: * @see org.netbeans.modules.visualweb.insync.Unit#writeLock(org.netbeans.modules.visualweb.insync.UndoEvent)
458: */
459: public synchronized final void writeLock(UndoEvent event) {
460: //if (state == State.SOURCEDIRTY)
461: // throw new IllegalStateException("SU.writeLock attempt to writeLock a model with dirty source");
462: //if (state == State.BUSTED) {
463: // throw new IllegalStateException("SU.writeLock attempt to writeLock a model with busted source");
464: //}
465: try {
466: while (readerCount > 0 || writingThread != null) {
467: if (Thread.currentThread() == writingThread) {
468: /*
469: if (notifyingListeners) {
470: // Assuming one doesn't do something wrong in a subclass this should only
471: // happen if a UnitListener tries to mutate the unit.
472: throw new IllegalStateException("Attempt to mutate in notification");
473: }*/
474: writerCount++;
475: return;
476: }
477: wait();
478: }
479: writingThread = Thread.currentThread();
480: writerCount = 1;
481: firstWriteLock();
482: //if (doc instanceof BaseDocument)
483: // ((BaseDocument)doc).atomicLock();
484: // or even:
485: // doc.writeLock();
486: } catch (InterruptedException e) {
487: throw new UnsupportedOperationException(
488: "Interrupted attempt to aquire write lock");
489: }
490: return;
491: }
492:
493: /*
494: * @see org.netbeans.modules.visualweb.insync.Unit#writeUnlock(org.netbeans.modules.visualweb.insync.UndoEvent)
495: */
496: public synchronized final boolean writeUnlock(UndoEvent event) {
497: if (--writerCount <= 0) {
498: //if (doc instanceof BaseDocument)
499: // ((BaseDocument)doc).atomicUnlock();
500: // or even:
501: // doc.writeUnlock();
502: writerCount = 0;
503: flush();
504: writingThread = null;
505: lastWriteUnlock();
506: notifyAll();
507: return true;
508: }
509: return false;
510: }
511:
512: /*
513: * @see org.netbeans.modules.visualweb.insync.Unit#isWriteLocked()
514: */
515: public boolean isWriteLocked() {
516: return writerCount > 0;
517: }
518:
519: /*
520: * @see org.netbeans.modules.visualweb.insync.Unit#readLock()
521: */
522: public synchronized final void readLock() {
523: try {
524: while (writingThread != null) {
525: if (writingThread == Thread.currentThread()) {
526: // writer has full read access.... may try to acquire
527: // lock in notification
528: return;
529: }
530: wait();
531: }
532: readerCount += 1;
533: } catch (InterruptedException e) {
534: throw new UnsupportedOperationException(
535: "Interrupted attempt to aquire read lock");
536: }
537: }
538:
539: /*
540: * @see org.netbeans.modules.visualweb.insync.Unit#readUnlock()
541: */
542: public synchronized final void readUnlock() {
543: if (writingThread == Thread.currentThread()) {
544: // writer has full read access.... may try to acquire
545: // lock in notification
546: return;
547: }
548: if (readerCount <= 0)
549: throw new IllegalStateException("BAD_LOCK_STATE");
550: readerCount -= 1;
551: notify();
552: }
553:
554: //---------------------------------------------------------------------------------------- Input
555:
556: /**
557: * Read the actual characters from the source document's content. Concrete subclasses must
558: * override this method to process the buffer characters into the model.
559: */
560: protected abstract void read(char[] cbuf, int len);
561:
562: /**
563: * Load the document into buf
564: */
565: private final Util.BufferResult loadBuf() {
566: Util.BufferResult bufferResult;
567: if (styledDocument == null) {
568: bufferResult = Util.loadFileObjectBuffer(fobj);
569: } else {
570: bufferResult = Util.loadDocumentBuffer(styledDocument);
571: }
572: return bufferResult;
573: }
574:
575: /**
576: * Implicit read. Read the document supplied during construction into this model.
577: *
578: * @return whether or not the read affected the model.
579: */
580: public boolean sync() {
581: // make sure it is necessary & ok to read.
582: //assert Trace.trace("insync", "SU.sync of " + getName() + " state:" + state + " len:" + styledDocument.getLength());
583: if (state == State.CLEAN)
584: return false;
585: if (state == State.MODELDIRTY) {
586: assert Trace
587: .trace("insync",
588: "SU.sync attempt to read source into a dirty model");
589: //Trace.printStackTrace();
590: return false;
591: }
592:
593: Util.BufferResult bufferResult = loadBuf();
594: read(bufferResult.getBuffer(), bufferResult.getSize());
595:
596: if (state == State.SOURCEDIRTY) // read() may have set an error, if so don't do to Clean
597: setClean(); //state = State.CLEAN;
598:
599: return true;
600: }
601:
602: //--------------------------------------------------------------------------------------- Output
603:
604: /**
605: * @param w
606: * @throws java.io.IOException
607: */
608: public abstract void writeTo(Writer w) throws java.io.IOException;
609:
610: /**
611: * @param out
612: * @throws java.io.IOException
613: */
614: public void writeTo(OutputStream out) throws java.io.IOException {
615: Writer w = new BufferedWriter(new OutputStreamWriter(out));
616: writeTo(w);
617: w.flush();
618: }
619:
620: /**
621: * @param out
622: */
623: public final void dumpTo(OutputStream out) {
624: PrintWriter w = new PrintWriter(new BufferedWriter(
625: new OutputStreamWriter(out)));
626: dumpTo(w);
627: w.flush();
628: }
629:
630: /**
631: * Internal routine for performing the smallest single update operation to the document given an
632: * old document and a new text string.
633: *
634: * @param newText The new document text contents.
635: */
636: protected boolean minimalReplace(String newText) {
637: try {
638: Util.BufferResult bufferResult = loadBuf();
639: char[] buf = bufferResult.getBuffer();
640: int end = styledDocument.getLength();
641: int newEnd = newText.length();
642: int start = 0;
643: while (start < end && start < newEnd
644: && buf[start] == newText.charAt(start))
645: start++;
646: while (end > start && newEnd > start
647: && buf[end - 1] == newText.charAt(newEnd - 1)) {
648: end--;
649: newEnd--;
650: }
651: if (end > start || newEnd > start) {
652: String newSeg = (start > 0 || newEnd < newText.length()) ? newText
653: .substring(start, newEnd)
654: : newText;
655: styledDocument.remove(start, end - start);
656: styledDocument.insertString(start, newSeg, null);
657: return true;
658: }
659: } catch (javax.swing.text.BadLocationException e) {
660: // we know the location we passed is good...
661: assert Trace.trace("insync",
662: "Unexpected error in SU.flush: " + e);
663: }
664: return false;
665: }
666:
667: /**
668: * Flush this unit to its document, writing changes as needed and updating flags.
669: *
670: * @see #writeLock
671: * @return true iff the document was written to by the operation public abstract boolean
672: * flush();
673: */
674: public boolean flush() {
675: // make sure it is necessary & ok to write.
676: Trace.trace("insync", "SU.flush of " + getName() + " state:"
677: + state);
678: if (state == State.CLEAN)
679: return false;
680:
681: // these two should not happen since we should have caught this at writeLock time
682: if (state == State.SOURCEDIRTY) {
683: assert Trace
684: .trace("insync",
685: "SU.flush attempt to flush a model with dirty source");
686: assert Trace.printStackTrace();
687: return false;
688: }
689: if (state == State.BUSTED) {
690: assert Trace
691: .trace("insync",
692: "SU.flush attempt to flush a model with busted source");
693: return false;
694: }
695:
696: //grabDocument(State.MODELDIRTY, true); // if the disk copy was dirty, too bad: nothing we can do
697: if (styledDocument == null) {
698: styledDocument = Util.retrieveDocument(fobj, true);
699: }
700:
701: final boolean[] didReplace = new boolean[1];
702:
703: // Flush to the document if the document exists
704: if (styledDocument != null) {
705: // System.out.println("SU.flush of " + getName() + " state:" + state + "Mode: Document");
706: // Seems no op
707: startFlush();
708: try {
709: // Stop listening to doc while it is us doing the writing
710: removeDocumentListener();
711:
712: // Lock the document atomically
713: // !TN TODO: BaseDocument.replace() should do this automatically;
714: // investigating that now
715: NbDocument.runAtomic(styledDocument, new Runnable() {
716: public void run() {
717: try {
718: Writer w = new StringWriter();
719: writeTo(w);
720: didReplace[0] = minimalReplace(w.toString());
721: } catch (java.io.IOException e) {
722: //!CQ TODO: log this better
723: assert Trace.trace("insync",
724: "Unexpected error in SU.flush: "
725: + e);
726: ErrorManager.getDefault().notify(e);
727: }
728: }
729: });
730: } finally {
731: // Start listening again
732: addDocumentListener();
733: // Seems no op
734: endFlush(didReplace[0]);
735: }
736: }
737:
738: // Flush file documents -- used for testing
739: if (styledDocument instanceof FileDocument) {
740: FileDocument fdoc = (FileDocument) styledDocument;
741: try {
742: fdoc.write(); // flush to file
743: } catch (java.io.IOException e) {
744: assert Trace.trace("insync", "Can't write file: "
745: + getName());
746: }
747: }
748:
749: //state = State.CLEAN;
750: setClean();
751: return didReplace[0];
752: }
753:
754: /**
755: * Saves the owning file using project apis
756: */
757: public void save() {
758: DataObject dataObject = getDataObject();
759: if (dataObject != null && state == State.CLEAN) {
760: SaveCookie cookie = (SaveCookie) dataObject
761: .getCookie(SaveCookie.class);
762: if (cookie != null) {
763: try {
764: cookie.save();
765: } catch (IOException e) {
766: throw new RuntimeException(e);
767: }
768: }
769: }
770: }
771:
772: /**
773: * Writer implementation to assist with brute-force output position rtacking.
774: * @author cquinn
775: */
776: public class CountingWriter extends Writer {
777: public int pos;
778:
779: public void close() {
780: }
781:
782: public void flush() {
783: //System.err.flush();
784: }
785:
786: public void write(char[] buf) {
787: //System.err.print("("+pos+")");
788: //System.err.print(buf);
789: pos += buf.length;
790: }
791:
792: public void write(char[] buf, int off, int len) {
793: //System.err.print("("+pos+")");
794: //System.err.print(new String(buf).substring(off, len));
795: pos += len;
796: }
797:
798: public void write(int c) {
799: //System.err.print("("+pos+")");
800: //System.err.print(c);
801: pos += 1;
802: }
803:
804: public void write(String str) {
805: //System.err.print("("+pos+")");
806: //System.err.print(str);
807: pos += str.length();
808: }
809:
810: public void write(String str, int off, int len) {
811: //System.err.print("("+pos+")");
812: //System.err.print(str.substring(off, len));
813: pos += len;
814: }
815: }
816:
817: //------------------------------------------------------------------------------------ Accessors
818:
819: /**
820: * @return
821: */
822: public String getName() {
823: return FileUtil.toFile(fobj).getAbsolutePath();
824: }
825:
826: /**
827: * @return
828: */
829: public StyledDocument getSourceDocument() {
830: if (styledDocument == null) {
831: styledDocument = Util.retrieveDocument(fobj, true);
832: }
833: if (state != State.MODELDIRTY) {
834: state = State.CLEAN;
835: }
836: return styledDocument;
837: }
838:
839: /**
840: * @return
841: */
842: public FileObject getFileObject() {
843: return fobj;
844: }
845:
846: /**
847: * @return
848: */
849: public DataObject getDataObject() {
850: if (fobj != null && !fobj.isValid())
851: return null;
852: return Util.findDataObject(fobj);
853: }
854:
855: protected HashSet listeners = null;
856:
857: public void addListener(SourceUnitListener listener) {
858: if (listeners == null)
859: listeners = new HashSet();
860: listeners.add(listener);
861: }
862:
863: public void removeListener(SourceUnitListener listener) {
864: if (listeners == null)
865: return;
866: listeners.remove(listener);
867: }
868:
869: /**
870: * Notify my listeners of the fact that my model has been made dirty.
871: *
872: */
873: protected void fireModelDirtied() {
874: if (listeners == null)
875: return;
876: for (Iterator iterator = listeners.iterator(); iterator
877: .hasNext();) {
878: SourceUnitListener listener = (SourceUnitListener) iterator
879: .next();
880: listener.sourceUnitModelDirtied(this );
881: }
882: }
883:
884: /**
885: * Notify my listeners of the fact that my source has been made dirty.
886: *
887: */
888: protected void fireSourceDirtied() {
889: if (listeners == null)
890: return;
891: for (Iterator iterator = listeners.iterator(); iterator
892: .hasNext();) {
893: SourceUnitListener listener = (SourceUnitListener) iterator
894: .next();
895: listener.sourceUnitSourceDirtied(this );
896: }
897: }
898:
899: protected void fireSaved() {
900: if (listeners == null)
901: return;
902: for (Iterator iterator = listeners.iterator(); iterator
903: .hasNext();) {
904: SourceUnitListener listener = (SourceUnitListener) iterator
905: .next();
906: listener.sourceUnitSaved(this );
907: }
908: }
909:
910: /**
911: * Improve the readability of String display in debugger's variable inspector.
912: */
913: public String toString() {
914: StringBuffer sb = new StringBuffer(30);
915: sb.append("["); // NOI18N
916: toString(sb);
917: sb.append("]");
918: return sb.toString();
919: }
920:
921: /**
922: * Subclasses should override to Improve the readability of String display in debugger's variable inspector.
923: */
924: protected void toString(StringBuffer sb) {
925: sb.append(getClass().getName().substring(
926: getClass().getPackage().getName().length() + 1));
927: sb.append(" fobj: ");
928: if (fobj == null) {
929: sb.append("null");
930: } else {
931: sb.append(fobj.getNameExt());
932: }
933: }
934:
935: protected void startFlush() {
936: }
937:
938: protected void endFlush(boolean madeDirty) {
939: }
940:
941: protected synchronized void firstWriteLock() {
942: //nop
943: }
944:
945: protected synchronized void lastWriteUnlock() {
946: //nop
947: }
948:
949: // Methods to prevent repeated registration of listeners
950: private void addDocumentListener() {
951: if (styledDocument != null
952: && documentListenerAdded.compareAndSet(false, true)) {
953: styledDocument.addDocumentListener(this );
954: }
955: }
956:
957: private void removeDocumentListener() {
958: if (styledDocument != null
959: && documentListenerAdded.compareAndSet(true, false)) {
960: styledDocument.removeDocumentListener(this );
961: }
962: }
963:
964: private void addUndoableEditListener() {
965: if (styledDocument != null
966: && undoableEditListenerAdded.compareAndSet(false, true)) {
967: styledDocument.addUndoableEditListener(this );
968: }
969: }
970:
971: private void removeUndoableEditListener() {
972: if (styledDocument != null
973: && undoableEditListenerAdded.compareAndSet(true, false)) {
974: styledDocument.removeUndoableEditListener(this);
975: }
976: }
977:
978: }
|