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-2006 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:
042: package threaddemo.util;
043:
044: import java.beans.PropertyChangeEvent;
045: import java.beans.PropertyChangeListener;
046: import java.io.IOException;
047: import java.util.ArrayList;
048: import java.util.List;
049: import java.util.logging.Logger;
050: import javax.swing.event.DocumentEvent;
051: import javax.swing.event.DocumentListener;
052: import javax.swing.text.StyledDocument;
053: import org.openide.ErrorManager;
054: import org.openide.cookies.EditorCookie;
055: import org.openide.text.NbDocument;
056: import threaddemo.locking.RWLock;
057: import threaddemo.locking.LockAction;
058:
059: // XXX helper methods to parse/rewrite an entire document atomically using Reader/Writer
060:
061: /**
062: * Supports two-way parsing of an arbitrary model from a text document and
063: * writing to the text document from the model.
064: * <p>The underlying model is a text document. The deltas to the underlying model
065: * in this implementation are document text changes or reload events, though that
066: * fact should not matter to subclasses. The derived model must be defined by the
067: * subclass.
068: * @author Jesse Glick
069: */
070: public abstract class DocumentParseSupport<DM, DMD> extends
071: TwoWaySupport<DM, DocumentParseSupportDelta, DMD> {
072:
073: private static final Logger logger = Logger
074: .getLogger(DocumentParseSupport.class.getName());
075:
076: private final EditorCookie.Observable edit;
077:
078: /** document loaded; may be null initially */
079: private StyledDocument document = null;
080: private int listenerCount = 0; // for assertions only
081: private int cookieListenerCount = 0; // for assertions only
082: private final Listener listener;
083:
084: /**
085: * Create a support based on an editor cookie and lock.
086: * @param edit the container for the document containing some parsable data
087: * @param lock a lock
088: */
089: protected DocumentParseSupport(EditorCookie.Observable edit,
090: RWLock lock) {
091: super (lock);
092: this .edit = edit;
093: listener = new Listener();
094: edit.addPropertyChangeListener(listener);
095: cookieListenerCount++;
096: }
097:
098: /**
099: * In this implementation, deltas are either {@link PropertyChangeEvent}s
100: * of {@link org.openide.cookies.EditorCookie.Observable#PROP_DOCUMENT} indicating that the whole
101: * document changed (was reloaded, for example), or lists of {@link DocumentEvent}s.
102: */
103: protected final DocumentParseSupportDelta composeUnderlyingDeltas(
104: DocumentParseSupportDelta underlyingDelta1,
105: DocumentParseSupportDelta underlyingDelta2) {
106: if (underlyingDelta1.changeEvent != null) {
107: // PROP_DOCUMENT that is. Need to recreate the whole thing generally.
108: return underlyingDelta1;
109: } else if (underlyingDelta2.changeEvent != null) {
110: // Ditto.
111: return underlyingDelta2;
112: } else {
113: // Append changes.
114: underlyingDelta1.documentEvents
115: .addAll(underlyingDelta2.documentEvents);
116: return underlyingDelta1;
117: }
118: }
119:
120: /**
121: * In this implementation, prepares the document so that it will soon be loaded,
122: * if it is not already.
123: */
124: protected final void initiating() {
125: if (requiresUnmodifiedDocument()) {
126: edit.prepareDocument();
127: logger.finer("initiating...");
128: }
129: }
130:
131: /**
132: * Make sure the correct document is open, and that the correct listeners
133: * are attached to it and not its predecessor.
134: * @param requireDocument if true, force a document to be loaded; if false,
135: * permit {@link #document} to remain null, but refresh
136: * it with a newer document if it has in fact changed
137: */
138: private void refreshDocument(boolean requireDocument)
139: throws IOException {
140: logger.finer("rD begin");
141: StyledDocument oldDocument = document;
142: edit.removePropertyChangeListener(listener);
143: assert --cookieListenerCount == 0;
144: try {
145: //new Exception("will call " + (requireDocument ? "openDocument" : "getDocument") + " on " + this).printStackTrace();
146: document = requireDocument ? edit.openDocument()
147: : (document != null ? edit.getDocument() : null);
148: //new Exception("called " + (requireDocument ? "openDocument" : "getDocument") + " on " + this).printStackTrace();
149: } finally {
150: edit.addPropertyChangeListener(listener);
151: assert ++cookieListenerCount == 1;
152: }
153: assert !requireDocument || document != null;
154: if (document != oldDocument) {
155: if (oldDocument != null) {
156: oldDocument.removeDocumentListener(listener);
157: assert --listenerCount == 0;
158: }
159: if (document != null) {
160: document.addDocumentListener(listener);
161: assert ++listenerCount == 1 : listenerCount;
162: }
163: }
164: logger.finer("rD end");
165: }
166:
167: /**
168: * Parse the document.
169: * Calls {@link #doDerive(StyledDocument, List, Object)}.
170: */
171: protected final DerivationResult<DM, DMD> doDerive(
172: final DM oldValue,
173: final DocumentParseSupportDelta underlyingDelta)
174: throws Exception {
175: if (document == null) {
176: refreshDocument(requiresUnmodifiedDocument());
177: }
178: final List<DerivationResult<DM, DMD>> val = new ArrayList<DerivationResult<DM, DMD>>(
179: 1);
180: final Exception[] exc = new Exception[1];
181: Runnable r = new Runnable() {
182: public void run() {
183: try {
184: val
185: .add(doDerive(
186: document,
187: underlyingDelta != null ? underlyingDelta.documentEvents
188: : null, oldValue));
189: } catch (Exception e) {
190: exc[0] = e;
191: }
192: }
193: };
194: if (document != null) {
195: document.render(r);
196: } else {
197: r.run();
198: }
199: if (exc[0] != null) {
200: throw exc[0];
201: }
202: return val.get(0);
203: }
204:
205: /**
206: * Declare whether the support always requires a document object, even to
207: * parse an unmodified file.
208: * <p>If true, {@link #doDerive(StyledDocument,List,Object)} is always given a
209: * document; if the editor support had never been opened at all, it is
210: * nonetheless opened (invisibly) just to provide this document to parse.
211: * <p>If false, {@link #doDerive(StyledDocument,List,Object)} may be passed null for
212: * its <code>document</code> parameter, meaning that the editor support has
213: * not yet loaded a document. In this case the support is expected to run the
214: * parse from the editor cookie's underlying storage, e.g. a file. This style
215: * is potentially much more efficient when performing model reads from a large
216: * number of (unmodified) files.
217: * <p>Recreation always uses an open document regardless of this choice.
218: * <p>The default value is true, i.e. always open the document for parsing.
219: * @return true to always parse from a real document, or false to permit faster
220: * parses from underlying storage
221: * @see EditorCookie#getDocument
222: * @see EditorCookie#openDocument
223: */
224: protected boolean requiresUnmodifiedDocument() {
225: return true;
226: }
227:
228: /**
229: * Create the derived model from a text document.
230: * Called with the read lock and with read access to the document.
231: * @param document the text document to parse, or may be null if
232: * {@link #requiresUnmodifiedDocument} if false
233: * @param documentEvents a list of {@link DocumentEvent} that happened since
234: * the last parse, or null if unknown (do a full reparse)
235: * @param oldValue the last derived model value, or null
236: * @return the new derived model value plus the change made to it
237: * @throws Exception (checked) in case of parsing problems
238: */
239: protected abstract DerivationResult<DM, DMD> doDerive(
240: StyledDocument document,
241: List<DocumentEvent> documentEvents, DM oldValue)
242: throws Exception;
243:
244: /**
245: * Regenerates the document.
246: * Calls {@link #doRecreate(StyledDocument, Object, Object)}.
247: */
248: protected final DM doRecreate(final DM oldValue,
249: final DMD derivedDelta) throws Exception {
250: if (document == null) {
251: refreshDocument(true);
252: }
253: final List<DM> val = new ArrayList<DM>(1);
254: final Exception[] exc = new Exception[1];
255: Runnable r = new Runnable() {
256: public void run() {
257: document.removeDocumentListener(listener);
258: assert --listenerCount == 0;
259: try {
260: val
261: .add(doRecreate(document, oldValue,
262: derivedDelta));
263: } catch (Exception e) {
264: exc[0] = e;
265: } finally {
266: document.addDocumentListener(listener);
267: assert ++listenerCount == 1;
268: }
269: }
270: };
271: if (runAsUser(derivedDelta)) {
272: NbDocument.runAtomicAsUser(document, r);
273: } else {
274: NbDocument.runAtomic(document, r);
275: }
276: if (exc[0] != null) {
277: throw exc[0];
278: }
279: return val.get(0);
280: }
281:
282: /**
283: * Decide whether the given change to the derived model must occur in "user"
284: * mode, that is, be prevented from modifying guard blocks.
285: * The default implementation always returns false.
286: * @return true to run using {@link NbDocument#runAtomicAsUser}, false for
287: * {@link NbDocument#runAtomic}
288: */
289: protected boolean runAsUser(Object derivedDelta) {
290: return false;
291: }
292:
293: /**
294: * Update the text document to reflect changes in the derived model.
295: * Called with the write lock and holding a document lock if possible.
296: * @param document the document to modify
297: * @param oldValue the old derived model, if any
298: * @param derivedDelta the change to the derived model
299: * @return the new derived model
300: * @see org.openide.text.NbDocument.WriteLockable
301: */
302: protected abstract DM doRecreate(StyledDocument document,
303: DM oldValue, DMD derivedDelta) throws Exception;
304:
305: /**
306: * Listens to changes in identity or content of the text document.
307: */
308: private final class Listener implements DocumentListener,
309: PropertyChangeListener {
310:
311: // XXX getting >1 i/rU for one change?
312:
313: public void insertUpdate(DocumentEvent e) {
314: logger.finer("DPS.iU");
315: documentUpdate(e);
316: }
317:
318: public void removeUpdate(DocumentEvent e) {
319: logger.finer("DPS.rU");
320: documentUpdate(e);
321: }
322:
323: private void documentUpdate(DocumentEvent e) {
324: final List<DocumentEvent> l = new ArrayList<DocumentEvent>(
325: 1);
326: l.add(e);
327: getLock().read(new LockAction<Void>() {
328: public Void run() {
329: invalidate(new DocumentParseSupportDelta(l));
330: return null;
331: }
332: });
333: }
334:
335: public void changedUpdate(DocumentEvent e) {
336: // attr change - ignore
337: }
338:
339: public void propertyChange(final PropertyChangeEvent evt) {
340: if (evt.getPropertyName().equals(
341: EditorCookie.Observable.PROP_DOCUMENT)) {
342: logger.finer("DPS.pC<PROP_DOCUMENT>");
343: //new Exception("got PROP_DOCUMENT on " + DocumentParseSupport.this).printStackTrace();
344: try {
345: refreshDocument(true);
346: } catch (IOException e) {
347: ErrorManager.getDefault().notify(
348: ErrorManager.INFORMATIONAL, e);
349: }
350: // Avoid blocking: because CES fires
351: // PROP_DOCUMENT from within a RP task, and we may already be locking
352: // the EQ with CES.open or .openDocument.
353: getLock().readLater(new Runnable() {
354: public void run() {
355: invalidate(new DocumentParseSupportDelta(evt));
356: }
357: });
358: }
359: }
360:
361: }
362:
363: }
|