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 org.netbeans.modules.xml.xam;
043:
044: import java.beans.PropertyChangeEvent;
045: import java.beans.PropertyChangeSupport;
046: import java.io.IOException;
047: import java.util.ArrayList;
048: import java.util.HashMap;
049: import java.util.HashSet;
050: import java.util.List;
051: import java.util.Map;
052: import java.util.Set;
053: import java.util.concurrent.Semaphore;
054: import java.util.logging.Level;
055: import java.util.logging.Logger;
056: import javax.swing.SwingUtilities;
057: import javax.swing.event.EventListenerList;
058: import javax.swing.event.UndoableEditEvent;
059: import javax.swing.event.UndoableEditListener;
060: import javax.swing.undo.CannotRedoException;
061: import javax.swing.undo.CannotUndoException;
062: import javax.swing.undo.CompoundEdit;
063: import javax.swing.undo.UndoManager;
064: import javax.swing.undo.UndoableEdit;
065: import javax.swing.undo.UndoableEditSupport;
066: import org.netbeans.modules.xml.xam.Model.State;
067:
068: /**
069: * @author Chris Webster
070: * @author Rico
071: * @author Nam Nguyen
072: */
073: public abstract class AbstractModel<T extends Component<T>> implements
074: Model<T>, UndoableEditListener {
075:
076: private PropertyChangeSupport pcs;
077: protected ModelUndoableEditSupport ues;
078: private State status;
079: private boolean inSync;
080: private boolean inUndoRedo;
081: private EventListenerList componentListeners;
082: private Semaphore transactionSemaphore;
083: private Transaction transaction;
084: private ModelSource source;
085: private UndoableEditListener[] savedUndoableEditListeners;
086:
087: public AbstractModel(ModelSource source) {
088: this .source = source;
089: pcs = new PropertyChangeSupport(this );
090: ues = new ModelUndoableEditSupport();
091: componentListeners = new EventListenerList();
092: transactionSemaphore = new Semaphore(1, true); // binary semaphore
093: status = State.VALID;
094: }
095:
096: public abstract ModelAccess getAccess();
097:
098: public void removePropertyChangeListener(
099: java.beans.PropertyChangeListener pcl) {
100: pcs.removePropertyChangeListener(pcl);
101: }
102:
103: /**
104: * Add property change listener which will receive events for any element
105: * in the underlying schema model.
106: */
107: public void addPropertyChangeListener(
108: java.beans.PropertyChangeListener pcl) {
109: pcs.addPropertyChangeListener(pcl);
110: }
111:
112: public void firePropertyChangeEvent(PropertyChangeEvent event) {
113: assert transaction != null;
114: transaction.addPropertyChangeEvent(event);
115: }
116:
117: public void removeUndoableEditListener(
118: javax.swing.event.UndoableEditListener uel) {
119: ues.removeUndoableEditListener(uel);
120: }
121:
122: public void addUndoableEditListener(
123: javax.swing.event.UndoableEditListener uel) {
124: ues.addUndoableEditListener(uel);
125: }
126:
127: public synchronized void addUndoableRefactorListener(
128: javax.swing.event.UndoableEditListener uel) {
129: savedUndoableEditListeners = ues.getUndoableEditListeners();
130: if (savedUndoableEditListeners != null) {
131: for (UndoableEditListener saved : savedUndoableEditListeners) {
132: if (saved instanceof UndoManager) {
133: ((UndoManager) saved).discardAllEdits();
134: }
135: }
136: }
137: ues = new ModelUndoableEditSupport();
138: ues.addUndoableEditListener(uel);
139: }
140:
141: public synchronized void removeUndoableRefactorListener(
142: javax.swing.event.UndoableEditListener uel) {
143: ues.removeUndoableEditListener(uel);
144: if (savedUndoableEditListeners != null) {
145: ues = new ModelUndoableEditSupport();
146: for (UndoableEditListener saved : savedUndoableEditListeners) {
147: ues.addUndoableEditListener(saved);
148: }
149: savedUndoableEditListeners = null;
150: }
151: }
152:
153: protected CompoundEdit createModelUndoableEdit() {
154: return new ModelUndoableEdit();
155: }
156:
157: protected class ModelUndoableEditSupport extends
158: UndoableEditSupport {
159:
160: @Override
161: protected CompoundEdit createCompoundEdit() {
162: return createModelUndoableEdit();
163: }
164:
165: protected void abortUpdate() {
166: ModelUndoableEdit mue = (ModelUndoableEdit) compoundEdit;
167: mue.justUndo();
168: super .compoundEdit = createCompoundEdit();
169: super .updateLevel = 0;
170: }
171: }
172:
173: public boolean inSync() {
174: return inSync;
175: }
176:
177: protected void setInSync(boolean v) {
178: inSync = v;
179: }
180:
181: public boolean inUndoRedo() {
182: return inUndoRedo;
183: }
184:
185: protected void setInUndoRedo(boolean v) {
186: inUndoRedo = v;
187: }
188:
189: public State getState() {
190: return status;
191: }
192:
193: protected void setState(State s) {
194: if (s == status) {
195: return;
196: }
197: State old = status;
198: status = s;
199: PropertyChangeEvent event = new PropertyChangeEvent(this ,
200: STATE_PROPERTY, old, status);
201: if (isIntransaction()) {
202: firePropertyChangeEvent(event);
203: } else {
204: pcs.firePropertyChange(event);
205: }
206: }
207:
208: /**
209: * This method is overridden by subclasses to determine if sync needs to be
210: * performed. The default implementation simply returns true.
211: */
212: protected boolean needsSync() {
213: return true;
214: }
215:
216: /**
217: * This template method is invoked when a transaction is started. The
218: * default implementation does nothing.
219: */
220: protected void transactionStarted() {
221:
222: }
223:
224: /**
225: * This method is invoked when a transaction has completed. The default
226: * implementation does nothing.
227: */
228: protected void transactionCompleted() {
229:
230: }
231:
232: /**
233: * This method is invoked when sync has started. The default implementation
234: * does nothing.
235: */
236: protected void syncStarted() {
237:
238: }
239:
240: /**
241: * This method is invoked when sync has completed. The default implementation
242: * does nothing.
243: */
244: protected void syncCompleted() {
245:
246: }
247:
248: /**
249: * Prepare for sync. This allow splitting calculation intensive work from
250: * event firing tasks that are mostly running on UI threads. This should be
251: * optional step, meaning the actual call sync() should task care of the
252: * preparation if it is not done.
253: */
254: private void prepareSync() {
255: if (needsSync()) {
256: getAccess().prepareSync();
257: }
258: }
259:
260: public synchronized void sync() throws java.io.IOException {
261: if (needsSync()) {
262: syncStarted();
263: boolean syncStartedTransaction = false;
264: boolean success = false;
265: try {
266: startTransaction(true, false); //start pseudo transaction for event firing
267: syncStartedTransaction = true;
268: setState(getAccess().sync());
269: endTransaction();
270: success = true;
271: } catch (IOException e) {
272: setState(State.NOT_WELL_FORMED);
273: endTransaction(false); // do want to fire just the state transition event
274: throw e;
275: } finally {
276: if (syncStartedTransaction && isIntransaction()) { //CR: consider separate try/catch
277: try {
278: endTransaction(true); // do not fire events
279: } catch (Exception ex) {
280: Logger.getLogger(getClass().getName()).log(
281: Level.INFO, "Sync cleanup error.", ex); //NOI18N
282: }
283: }
284:
285: if (!success && getState() != State.NOT_WELL_FORMED) {
286: setState(State.NOT_SYNCED);
287: refresh();
288: }
289:
290: setInSync(false);
291: syncCompleted();
292: }
293: }
294: }
295:
296: /**
297: * Refresh the domain model component trees. The model state should be VALID as the
298: * result of this call.
299: * Note: subclasses need to override to provide the actual refresh service.
300: */
301: protected void refresh() {
302: setState(State.VALID);
303: }
304:
305: public void removeComponentListener(ComponentListener cl) {
306: componentListeners.remove(ComponentListener.class, cl);
307: }
308:
309: public void addComponentListener(ComponentListener cl) {
310: componentListeners.add(ComponentListener.class, cl);
311: }
312:
313: public void fireComponentChangedEvent(ComponentEvent evt) {
314: assert transaction != null;
315: transaction.addComponentEvent(evt);
316: }
317:
318: public boolean isIntransaction() {
319: return transaction != null;
320: }
321:
322: public synchronized void endTransaction() {
323: endTransaction(false);
324: }
325:
326: protected synchronized void endTransaction(boolean quiet) {
327: if (transaction == null)
328: return; // just no-op when not in transaction
329: validateWrite(); // ensures that the releasing thread really owns trnx
330: try {
331: if (!quiet) {
332: transaction.fireEvents();
333: }
334: // no-need to flush or undo/redo support while in sync
335: if (!inSync() && transaction.hasEvents()
336: || transaction.hasEventsAfterFiring()) {
337: getAccess().flush();
338: }
339: if (!inUndoRedo()) {
340: ues.endUpdate();
341: }
342: } finally {
343: transaction = null;
344: setInSync(false);
345: setInUndoRedo(false);
346: transactionSemaphore.release();
347: transactionCompleted();
348: }
349: }
350:
351: public boolean startTransaction() {
352: return startTransaction(false, false);
353: }
354:
355: private synchronized boolean startTransaction(boolean inSync,
356: boolean inUndoRedo) {
357: if (transaction != null
358: && transaction.currentThreadIsTransactionThread()) {
359: throw new IllegalStateException(
360: "Current thread has already started a transaction");
361: }
362:
363: if (!inSync && !getModelSource().isEditable()) {
364: throw new IllegalArgumentException(
365: "Model source is read-only.");
366: }
367:
368: transactionSemaphore.acquireUninterruptibly();
369: // other correctly behaving threads will be blocked acquiring the
370: // semaphore here. Also store the current Thread to ensure that
371: // no other writes are occurring
372: assert transaction == null;
373:
374: if (!inSync && getState() == State.NOT_WELL_FORMED) {
375: transactionSemaphore.release();
376: return false;
377: }
378:
379: transaction = new Transaction();
380: transactionStarted();
381: setInSync(inSync);
382: setInUndoRedo(inUndoRedo);
383:
384: if (!inUndoRedo) {
385: ues.beginUpdate();
386: }
387:
388: return true;
389: }
390:
391: public synchronized void rollbackTransaction() {
392: if (transaction == null)
393: return; // just no-op when not in transaction
394: validateWrite(); // ensures that the releasing thread really owns trnx
395: try {
396: if (inSync() || inUndoRedo()) {
397: throw new IllegalArgumentException(
398: "Should never call rollback during sync or undo/redo.");
399: }
400: ues.abortUpdate();
401: } finally {
402: transaction = null;
403: setInSync(false);
404: setInUndoRedo(false);
405: transactionSemaphore.release();
406: transactionCompleted();
407: }
408: }
409:
410: // vlv # 121042
411: protected synchronized void finishTransaction() {
412: if (transaction == null)
413: return; // just no-op when not in transaction
414: validateWrite(); // ensures that the releasing thread really owns trnx
415: try {
416: if (inSync() || inUndoRedo()) {
417: throw new IllegalArgumentException(
418: "Should never call rollback during sync or undo/redo.");
419: }
420: } finally {
421: transaction = null;
422: setInSync(false);
423: setInUndoRedo(false);
424: transactionSemaphore.release();
425: transactionCompleted();
426: }
427: }
428:
429: /**
430: * This method ensures that a transaction is currently in progress and
431: * that the current thread is able to write.
432: */
433: public synchronized void validateWrite() {
434: if (transaction == null
435: || !transaction.currentThreadIsTransactionThread()) {
436: throw new IllegalStateException(
437: "attempted model write without "
438: + "invoking startTransaction");
439: }
440: }
441:
442: private class Transaction {
443: private final List<PropertyChangeEvent> propertyChangeEvents;
444: private final List<ComponentEvent> componentListenerEvents;
445: private final Thread transactionThread;
446: private boolean eventAdded;
447: private Boolean eventsAddedAfterFiring;
448: private boolean hasEvents;
449:
450: public Transaction() {
451: propertyChangeEvents = new ArrayList<PropertyChangeEvent>();
452: componentListenerEvents = new ArrayList<ComponentEvent>();
453: transactionThread = Thread.currentThread();
454: eventAdded = false;
455: eventsAddedAfterFiring = null;
456: hasEvents = false;
457: }
458:
459: public void addPropertyChangeEvent(PropertyChangeEvent pce) {
460: propertyChangeEvents.add(pce);
461: // do not chain events during undo/redo
462: if (eventsAddedAfterFiring == null || !inUndoRedo) {
463: eventAdded = true;
464: }
465: if (eventsAddedAfterFiring != null) {
466: eventsAddedAfterFiring = Boolean.TRUE;
467: }
468: hasEvents = true;
469: }
470:
471: public void addComponentEvent(ComponentEvent cle) {
472: componentListenerEvents.add(cle);
473: // do not chain events during undo/redo
474: if (eventsAddedAfterFiring == null || !inUndoRedo) {
475: eventAdded = true;
476: }
477: if (eventsAddedAfterFiring != null) {
478: eventsAddedAfterFiring = Boolean.TRUE;
479: }
480: hasEvents = true;
481: }
482:
483: public boolean currentThreadIsTransactionThread() {
484: return Thread.currentThread().equals(transactionThread);
485: }
486:
487: public void fireEvents() {
488: if (eventsAddedAfterFiring == null) {
489: eventsAddedAfterFiring = Boolean.FALSE;
490: }
491: while (eventAdded) {
492: eventAdded = false;
493: fireCompleteEventSet();
494: }
495: }
496:
497: /**
498: * This method is added to allow mutations to occur inside events. The
499: * list is cloned so that additional events can be added.
500: */
501: private void fireCompleteEventSet() {
502: final List<PropertyChangeEvent> clonedEvents = new ArrayList<PropertyChangeEvent>(
503: propertyChangeEvents);
504: //should clear event list
505: propertyChangeEvents.clear();
506: for (PropertyChangeEvent pce : clonedEvents) {
507: pcs.firePropertyChange(pce);
508: }
509:
510: final List<ComponentEvent> cEvents = new ArrayList<ComponentEvent>(
511: componentListenerEvents);
512: //should clear event list
513: componentListenerEvents.clear();
514: Map<Object, Set<ComponentEvent.EventType>> fired = new HashMap<Object, Set<ComponentEvent.EventType>>();
515:
516: for (ComponentEvent cle : cEvents) {
517: // make sure we only fire one event per component per event type.
518: Object source = cle.getSource();
519: if (fired.keySet().contains(source)) {
520: Set<ComponentEvent.EventType> types = fired
521: .get(source);
522: if (types.contains(cle.getEventType())) {
523: continue;
524: } else {
525: types.add(cle.getEventType());
526: }
527: } else {
528: Set<ComponentEvent.EventType> types = new HashSet<ComponentEvent.EventType>();
529: types.add(cle.getEventType());
530: fired.put(cle.getSource(), types);
531: }
532:
533: final ComponentListener[] listeners = componentListeners
534: .getListeners(ComponentListener.class);
535: for (ComponentListener cl : listeners) {
536: cle.getEventType().fireEvent(cle, cl);
537: }
538: }
539: }
540:
541: public boolean hasEvents() {
542: return hasEvents;
543: }
544:
545: public boolean hasEventsAfterFiring() {
546: return eventsAddedAfterFiring != null
547: && eventsAddedAfterFiring.booleanValue();
548: }
549: }
550:
551: /**
552: * Whether the model has started firing events. This is the indication of
553: * beginning of endTransaction call and any subsequent mutations are from
554: * handlers of main transaction events or some of their own events.
555: */
556: public boolean startedFiringEvents() {
557: return transaction != null
558: && transaction.eventsAddedAfterFiring != null;
559: }
560:
561: protected class ModelUndoableEdit extends CompoundEdit {
562: static final long serialVersionUID = 1L;
563:
564: public boolean addEdit(UndoableEdit anEdit) {
565: if (!isInProgress())
566: return false;
567: UndoableEdit last = lastEdit();
568: if (last == null) {
569: return super .addEdit(anEdit);
570: } else {
571: if (!last.addEdit(anEdit)) {
572: return super .addEdit(anEdit);
573: } else {
574: return true;
575: }
576: }
577: }
578:
579: @Override
580: public void redo() throws CannotRedoException {
581: boolean redoStartedTransaction = false;
582: boolean needsRefresh = true;
583: try {
584: startTransaction(true, true); //start pseudo transaction for event firing
585: redoStartedTransaction = true;
586: AbstractModel.this .getAccess().prepareForUndoRedo();
587: super .redo();
588: AbstractModel.this .getAccess().finishUndoRedo();
589: endTransaction();
590: needsRefresh = false;
591: } catch (CannotRedoException ex) {
592: needsRefresh = false;
593: throw ex;
594: } finally {
595: if (isIntransaction() && redoStartedTransaction) {
596: try {
597: endTransaction(true); // do not fire events
598: } catch (Exception e) {
599: Logger.getLogger(getClass().getName()).log(
600: Level.INFO, "Redo error", e); //NOI18N
601: }
602: }
603: if (needsRefresh) {
604: setState(State.NOT_SYNCED);
605: refresh();
606: }
607: }
608: }
609:
610: @Override
611: public void undo() throws CannotUndoException {
612: boolean undoStartedTransaction = false;
613: boolean needsRefresh = true;
614: try {
615: startTransaction(true, true); //start pseudo transaction for event firing
616: undoStartedTransaction = true;
617: AbstractModel.this .getAccess().prepareForUndoRedo();
618: super .undo();
619: AbstractModel.this .getAccess().finishUndoRedo();
620: endTransaction();
621: needsRefresh = false;
622: } catch (CannotUndoException ex) {
623: needsRefresh = false;
624: throw ex;
625: } finally {
626: if (undoStartedTransaction && isIntransaction()) {
627: try {
628: endTransaction(true); // do not fire events
629: } catch (Exception e) {
630: Logger.getLogger(getClass().getName()).log(
631: Level.INFO, "Undo error", e); //NOI18N
632: }
633: }
634: if (needsRefresh) {
635: setState(State.NOT_SYNCED);
636: refresh();
637: }
638: }
639: }
640:
641: public void justUndo() {
642: super .end();
643: boolean oldValue = AbstractModel.this .inUndoRedo;
644: AbstractModel.this .inUndoRedo = true;
645: AbstractModel.this .getAccess().prepareForUndoRedo();
646: super .undo();
647: AbstractModel.this .getAccess().finishUndoRedo();
648: AbstractModel.this .inUndoRedo = oldValue;
649: }
650: }
651:
652: public void undoableEditHappened(UndoableEditEvent e) {
653: ues.postEdit(e.getEdit());
654: }
655:
656: public ModelSource getModelSource() {
657: return source;
658: }
659:
660: EventListenerList getComponentListenerList() {
661: return componentListeners;
662: }
663:
664: public boolean isAutoSyncActive() {
665: return getAccess().isAutoSync();
666: }
667:
668: public void setAutoSyncActive(boolean v) {
669: getAccess().setAutoSync(v);
670: }
671:
672: void runAutoSync() {
673: prepareSync();
674: SwingUtilities.invokeLater(new Runnable() {
675: public void run() {
676: try {
677: sync();
678: } catch (Exception ioe) {
679: // just have to be quiet during background autosync
680: // sync() should have handled all faults
681: }
682: }
683: });
684: }
685: }
|