001: /*
002: * uDig - User Friendly Desktop Internet GIS client http://udig.refractions.net (C) 2004,
003: * Refractions Research Inc. This library is free software; you can redistribute it and/or modify it
004: * under the terms of the GNU Lesser General Public License as published by the Free Software
005: * Foundation; version 2.1 of the License. This library is distributed in the hope that it will be
006: * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
007: * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
008: */
009: package net.refractions.udig.project.command;
010:
011: import java.util.Iterator;
012: import java.util.LinkedList;
013: import java.util.Queue;
014: import java.util.Set;
015: import java.util.concurrent.ConcurrentLinkedQueue;
016: import java.util.concurrent.CopyOnWriteArraySet;
017:
018: import net.refractions.udig.project.internal.Messages;
019: import net.refractions.udig.project.internal.ProjectPlugin;
020: import net.refractions.udig.project.internal.commands.edit.RollbackCommand;
021: import net.refractions.udig.project.internal.commands.selection.CommitCommand;
022: import net.refractions.udig.project.internal.impl.MapImpl.MapCommandListener;
023: import net.refractions.udig.project.preferences.PreferenceConstants;
024: import net.refractions.udig.ui.PlatformGIS;
025: import net.refractions.udig.ui.ProgressMonitorTaskNamer;
026:
027: import org.eclipse.core.runtime.IProgressMonitor;
028: import org.eclipse.core.runtime.IStatus;
029: import org.eclipse.core.runtime.Status;
030: import org.eclipse.core.runtime.SubProgressMonitor;
031: import org.eclipse.core.runtime.jobs.Job;
032: import org.eclipse.jface.dialogs.IDialogConstants;
033: import org.eclipse.jface.dialogs.MessageDialogWithToggle;
034: import org.eclipse.jface.preference.IPreferenceStore;
035: import org.eclipse.swt.widgets.Display;
036: import org.eclipse.ui.PlatformUI;
037:
038: /**
039: * A commands Manager executes commands in a seperate thread, either synchronously or a
040: * synchronously.
041: *
042: * @author Jesse
043: * @since 1.0.0
044: */
045: public class CommandManager implements CommandStack, NavCommandStack {
046:
047: private static final String TRACE_ID = "net.refractions.udig.project/debug/commands/manager/trace"; //$NON-NLS-1$
048: /**
049: * If -1 then Synchronous commands will wait indefinately. Otherwise they will try for this many
050: * milliseconds.
051: */
052: private long timeout = -1;
053:
054: Set<ErrorHandler> handlers = new CopyOnWriteArraySet<ErrorHandler>();
055: Set<CommandListener> completionHandlers = new CopyOnWriteArraySet<CommandListener>();
056:
057: Executor commandExecutor;
058: private final String managerName;
059:
060: /**
061: * Creates a new instance of CommandManager
062: *
063: * @param handler an error handler to use to handle thrown exceptions.
064: */
065: public CommandManager(String managerName, ErrorHandler handler,
066: CommandListener commandCompletionListener) {
067: this .managerName = managerName;
068: handlers.add(handler);
069: if (commandCompletionListener != null)
070: completionHandlers.add(commandCompletionListener);
071: }
072:
073: /**
074: * Creates a new instance of CommandManager
075: *
076: * @param handler an error handler to use to handle thrown exceptions.
077: */
078: public CommandManager(String managerName, ErrorHandler handler) {
079: this (managerName, handler, null);
080: }
081:
082: /**
083: * Creates a new instance of CommandManager
084: *
085: * @param handler an error handler to use to handle thrown exceptions.
086: */
087: public CommandManager(String managerName, ErrorHandler handler,
088: CommandListener commandCompletionListener, long timeout2) {
089: this (managerName, handler, commandCompletionListener);
090: this .timeout = timeout2;
091:
092: }
093:
094: /**
095: * Executes a command. Calls the Errorhandler if an exception is thrown.
096: *
097: * @param command The command to execute
098: * @param async flag indicating wether command should be executed sync vs async.
099: * @return true if no problems were encountered while queueing command. Problems will typically
100: * occur when the command is synchronous and it times out or is interrupted.
101: */
102: public boolean execute(final Command command, boolean async) {
103: int type = Request.RUN;
104: return doMakeRequest(command, async, type);
105:
106: }
107:
108: /**
109: * @param command command to perform
110: * @param async whether to do request synchronously
111: * @param type type of request (REDO UNOD RUN )
112: * @return
113: */
114: private boolean doMakeRequest(final Command command, boolean async,
115: int type) {
116: Request request = new Request(type, command, async, Display
117: .getCurrent() != null);
118: synchronized (this ) {
119: if (commandExecutor == null) {
120: commandExecutor = new Executor(managerName);
121: }
122: }
123: commandExecutor.addRequest(request);
124: if (request.isSync()) {
125:
126: // synchronous execution, current thread needs to wait
127: // it is unlikely that the command will be complete by the time
128: // we get here but better to be safe than sorry
129: try {
130: Display current = Display.getCurrent();
131: if (current != null)
132: waitInDisplay(current, request);
133: else
134: waitOnRequest(request);
135: if (!request.completed) {
136: ProjectPlugin.trace(TRACE_ID, getClass(),
137: "Request didn't complete", null); //$NON-NLS-1$
138: commandExecutor.removeCommand(request);
139: return false;
140: }
141: ProjectPlugin.trace(TRACE_ID, getClass(),
142: "Request completed", null); //$NON-NLS-1$
143: } catch (InterruptedException e) {
144: ProjectPlugin.log(
145: "Error running commands synchronously", e); //$NON-NLS-1$
146: return false;
147: }
148: }
149: return true;
150: }
151:
152: private long waitOnRequest(Request request)
153: throws InterruptedException {
154: ProjectPlugin
155: .trace(
156: TRACE_ID,
157: getClass(),
158: "synchronous command NOT in display thread\nTimout=" + timeout, null); //$NON-NLS-1$
159: long tries = 0;
160:
161: while (mustWait(request, tries)) {
162: tries += 500;
163: synchronized (request) {
164: request.wait(500);
165: }
166: }
167: return tries;
168: }
169:
170: /**
171: * This method is special wait command that ensures that the display does not block. It executes
172: * the jobs waiting for display.
173: *
174: * @param current the current display
175: * @param request
176: * @throws InterruptedException
177: * @see Display#readAndDispatch()
178: */
179: private long waitInDisplay(Display current, Request request)
180: throws InterruptedException {
181: ProjectPlugin
182: .trace(
183: TRACE_ID,
184: getClass(),
185: "synchronous command IN display thread\nTimout=" + timeout, null); //$NON-NLS-1$
186: long start = System.currentTimeMillis();
187:
188: while (mustWait(request, System.currentTimeMillis() - start)) {
189: // run a display event continue if there is more work todo.
190: if (current.readAndDispatch()) {
191: continue;
192: }
193:
194: // no more work to do in display thread, wait on request if request has not
195: // finished
196: if (!mustWait(request, System.currentTimeMillis() - start))
197: return System.currentTimeMillis() - start;
198:
199: synchronized (request) {
200: request.wait(300);
201: }
202: }
203: return System.currentTimeMillis() - start;
204: }
205:
206: private boolean mustWait(Request request, long tries) {
207: ProjectPlugin
208: .trace(
209: TRACE_ID,
210: getClass(),
211: "timeout :" + timeout + ", tries: " + tries + ", completed:" + request.completed, null); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
212: return !request.completed && (tries < timeout || timeout == -1);
213: }
214:
215: /**
216: * Executes the last undone command, if there are any commands to undo.
217: *
218: * @param runAsync true to run undo asynchronously
219: */
220: public void redo(boolean runAsync) {
221: doMakeRequest(null, runAsync, Request.REDO);
222: }
223:
224: /**
225: * Undoes the last command if possible.
226: *
227: * @param runAsync true to run undo asynchronously
228: */
229: public void undo(boolean runAsync) {
230: doMakeRequest(null, runAsync, Request.UNDO);
231: }
232:
233: /**
234: * Adds an Errorhandler to the list of error handlers
235: *
236: * @param handler the error handler to add.
237: * @see ErrorHandler
238: */
239: public void addErrorHandler(ErrorHandler handler) {
240: handlers.add(handler);
241: }
242:
243: /**
244: * Removes an Errorhandler from the list of error handlers
245: *
246: * @param handler the error handler to remove.
247: * @see ErrorHandler
248: */
249: public void removeErrorHandler(ErrorHandler handler) {
250: handlers.remove(handler);
251: }
252:
253: /**
254: * @see net.refractions.udig.project.command.CommandStack#canUndo()
255: */
256: public boolean canUndo() {
257: if (commandExecutor == null)
258: return false;
259:
260: Command c;
261: if (!commandExecutor.history.isEmpty()) {
262: c = (Command) commandExecutor.history.peek();
263: if (c instanceof UndoableCommand)
264: return true;
265: }
266: return false;
267: }
268:
269: /**
270: * @see net.refractions.udig.project.command.CommandStack#canRedo()
271: */
272: public boolean canRedo() {
273: if (commandExecutor != null
274: && !commandExecutor.undone.isEmpty()) {
275: return true;
276: }
277: return false;
278: }
279:
280: /**
281: * @see net.refractions.udig.project.command.NavCommandStack#hasBackHistory()
282: */
283: public boolean hasBackHistory() {
284: return canUndo();
285: }
286:
287: /**
288: * @see net.refractions.udig.project.command.NavCommandStack#hasForwardHistory()
289: */
290: public boolean hasForwardHistory() {
291: return canRedo();
292: }
293:
294: /**
295: * Executes commands in a seperate thread from the requesting thread. JONES: Should support
296: * force kill of a command.
297: *
298: * @author Jesse
299: * @since 1.0.0
300: */
301: public class Executor extends Job {
302: LinkedList<Command> history = new LinkedList<Command>();
303:
304: LinkedList<Command> undone = new LinkedList<Command>();
305:
306: Queue<Request> commands = new ConcurrentLinkedQueue<Request>();
307:
308: /**
309: * Construct <code>Executor</code>.
310: *
311: * @param name the name of the job
312: * @param type the type of the executor. (RUN, UNDO, REDO)
313: */
314: public Executor(String name) {
315: super (name);
316: setSystem(false);
317: }
318:
319: IProgressMonitor progressMonitor;
320: Request currentRequest;
321:
322: @Override
323: protected IStatus run(IProgressMonitor monitor) {
324: monitor.beginTask(Messages.CommandManager_ProgressMonitor,
325: IProgressMonitor.UNKNOWN);
326: while (!getThread().isInterrupted()) {
327:
328: synchronized (this ) {
329: currentRequest = commands.poll();
330: if (currentRequest == null)
331: return Status.OK_STATUS;
332: }
333: progressMonitor = new ProgressMonitorTaskNamer(monitor,
334: 10);
335: run(progressMonitor, currentRequest);
336:
337: if (currentRequest.isSync()) {
338: // notify those wating for command to finish
339: synchronized (currentRequest) {
340: currentRequest.notifyAll();
341: }
342: }
343: }
344: return Status.OK_STATUS;
345: }
346:
347: private void run(IProgressMonitor monitor, Request request) {
348: switch (request.type) {
349: case Request.RUN:
350: execute(request.command, monitor);
351: break;
352: case Request.UNDO:
353: undo(monitor);
354: break;
355: case Request.REDO:
356: redo(monitor);
357: break;
358: case Request.RERUN:
359: rerunCommands(monitor);
360: break;
361: }
362:
363: request.completed = true;
364: }
365:
366: /**
367: * Adds a command to the stack of commands that needs to be executed.
368: *
369: * @param request
370: */
371: public void addRequest(Request request) {
372: if (getThread() == Thread.currentThread()
373: || isDisplayThreadDeadlockDetected(request)) {
374: run(progressMonitor, request);
375: } else {
376: synchronized (this ) {
377: commands.offer(request);
378: schedule();
379: }
380: Thread.yield();
381: }
382: }
383:
384: private boolean isDisplayThreadDeadlockDetected(Request request) {
385: return (Display.getCurrent() != null
386: && currentRequest != null
387: && currentRequest.requestByDisplay && currentRequest
388: .isSync());
389: }
390:
391: /**
392: * This method is only called by {@linkplain CommandManager#execute(Command, boolean)}
393: */
394: synchronized void removeCommand(Request request) {
395: if (commands.remove(request))
396: return;
397:
398: if (currentRequest == request) {
399: // JONES interrupt running command if command is running
400: }
401:
402: // If it has already been executed then don't worry.
403: }
404:
405: /**
406: * Executes a command. Calls the Errorhandler if an exception is thrown.
407: *
408: * @param command The command to execute
409: */
410: private void execute(final Command command,
411: IProgressMonitor monitor) {
412:
413: long time = System.currentTimeMillis();
414: if (command.getName() != null)
415: monitor.beginTask(command.getName(),
416: IProgressMonitor.UNKNOWN);
417: try {
418: final boolean runCommand = openWarning(command);
419: if (!runCommand)
420: return;
421:
422: if (command instanceof PostDeterminedEffectCommand) {
423:
424: PostDeterminedEffectCommand c = (PostDeterminedEffectCommand) command;
425: if (c
426: .execute(new SubProgressMonitor(monitor,
427: 1000))) {
428: undone.clear();
429: addToHistory(command);
430: }
431:
432: } else {
433: command.run(new SubProgressMonitor(monitor, 1000));
434:
435: undone.clear();
436: addToHistory(command);
437:
438: }
439:
440: if (ProjectPlugin.isDebugging(TRACE_ID)) {
441: long l = (System.currentTimeMillis() - time);
442: if (l > 100) {
443: System.out.println(command.toString()
444: + "--" + l); //$NON-NLS-1$
445: }
446: }
447: if (history.size() > getMaxHistorySize())
448: history.removeFirst();
449: notifyOwner(command);
450:
451: } catch (Throwable e) {
452: undone.clear();
453: handleError(command, e);
454: }
455:
456: }
457:
458: private void addToHistory(final Command command) {
459: if (history.size() > ProjectPlugin.getPlugin()
460: .getPreferenceStore().getInt(
461: PreferenceConstants.P_MAX_UNDO))
462: history.removeFirst();
463: history.addLast(command);
464: }
465:
466: private boolean openWarning(final Command command) {
467: final boolean[] runCommand = new boolean[1];
468: if (!(command instanceof UndoableCommand)
469: && ProjectPlugin.getPlugin()
470: .getUndoableCommandWarning()) {
471: final IPreferenceStore preferenceStore = ProjectPlugin
472: .getPlugin().getPreferenceStore();
473: if (!preferenceStore
474: .getBoolean(PreferenceConstants.P_WARN_IRREVERSIBLE_COMMAND)) {
475: return preferenceStore
476: .getBoolean(PreferenceConstants.P_IRREVERSIBLE_COMMAND_VALUE);
477: }
478: PlatformGIS.syncInDisplayThread(new Runnable() {
479: public void run() {
480: String string = Messages.CommandManager_warning
481: + command.getName();
482: if (command instanceof RollbackCommand
483: || command instanceof CommitCommand)
484: string += "?"; //$NON-NLS-1$
485: else
486: string += Messages.CommandManager_warning2;
487: MessageDialogWithToggle dialog = MessageDialogWithToggle
488: .openOkCancelConfirm(
489: PlatformUI
490: .getWorkbench()
491: .getActiveWorkbenchWindow()
492: .getShell(),
493: Messages.CommandManager_warningTitle,
494: string,
495: Messages.CommandManager_toggleMessage,
496: false,
497: preferenceStore,
498: PreferenceConstants.P_WARN_IRREVERSIBLE_COMMAND);
499: runCommand[0] = dialog.getReturnCode() == IDialogConstants.OK_ID;
500: if (dialog.getToggleState()) {
501: preferenceStore
502: .setValue(
503: PreferenceConstants.P_IRREVERSIBLE_COMMAND_VALUE,
504: runCommand[0]);
505: }
506: }
507: });
508: } else {
509: return true;
510: }
511: return runCommand[0];
512: }
513:
514: /**
515: * Notifies the owner that the command has been executed.
516: */
517: private void notifyOwner(Command command) {
518:
519: for (CommandListener listener : completionHandlers) {
520: if (command instanceof NavCommand) {
521: listener
522: .commandExecuted(MapCommandListener.NAV_COMMAND);
523: } else {
524: listener
525: .commandExecuted(MapCommandListener.COMMAND);
526: }
527:
528: }
529:
530: }
531:
532: /**
533: * Executes the last undone command, if there are any commands to undo.
534: */
535: private void redo(IProgressMonitor monitor) {
536: if (undone.isEmpty())
537: return;
538: Command command = undone.getLast();
539: monitor.beginTask(Messages.CommandManager_redo
540: + command.getName(), 1000);
541: try {
542: if (command instanceof PostDeterminedEffectCommand) {
543: PostDeterminedEffectCommand post = (PostDeterminedEffectCommand) command;
544: post.execute(new SubProgressMonitor(monitor, 1000));
545: } else {
546: command.run(new SubProgressMonitor(monitor, 1000));
547: }
548: addToHistory(command);
549: notifyOwner(command);
550: } catch (Exception e) {
551: handleError(command, e);
552: }
553: }
554:
555: /**
556: * Undoes the last command if possible.
557: */
558: private void undo(IProgressMonitor monitor) {
559: Command c;
560: // First check if there's a command on the commands stack that hasn't
561: // been executed and should just be removed
562: if (!commands.isEmpty()) {
563: Request r;
564: synchronized (this ) {
565: r = commands.peek();
566: if (r.type != Request.UNDO) {
567: commands.remove(0);
568: c = r.command;
569: if (c instanceof UndoableCommand) {
570: // We've already popped it off...so we're done
571: } else {
572: throw new RuntimeException(
573: "Undoing Commands (No Undoable Command) is not handled " //$NON-NLS-1$
574: + "yet because it involves rolling back the current transaction and redoing all " //$NON-NLS-1$
575: + "the commands in the stack"); //$NON-NLS-1$
576: }
577: return;
578: }
579: }
580: }
581: // Nothing on the Command stack, we'll actually undo something
582: if (!history.isEmpty()) {
583: c = history.removeLast();
584: if (c instanceof UndoableCommand) {
585: UndoableCommand command = (UndoableCommand) c;
586: monitor.beginTask(Messages.CommandManager_undo
587: + command.getName(), 1000);
588: try {
589: command.rollback(new SubProgressMonitor(
590: monitor, 1000));
591: addToUndone(command);
592: } catch (Throwable e) {
593: handleRollbackError(command, e);
594: }
595: } else {
596: throw new RuntimeException(
597: "Undoing Commands (No Undoable Command) is not handled " //$NON-NLS-1$
598: + "yet because it involves rolling back the current transaction and redoing all " //$NON-NLS-1$
599: + "the commands in the stack"); //$NON-NLS-1$
600: }
601: notifyOwner(c);
602: }
603: }
604:
605: private void addToUndone(UndoableCommand command) {
606: if (undone.size() > ProjectPlugin.getPlugin()
607: .getPreferenceStore().getInt(
608: PreferenceConstants.P_MAX_UNDO))
609: undone.removeFirst();
610: undone.add(command);
611: }
612:
613: private void handleError(Command command, Throwable e) {
614: for (ErrorHandler handler : handlers) {
615: handler.handleError(command, e);
616: }
617: }
618:
619: private void handleRollbackError(UndoableCommand command,
620: Throwable e) {
621: for (ErrorHandler handler : handlers) {
622: handler.handleRollbackError(command, e);
623: }
624: }
625:
626: /**
627: * Executes all the commands in the history again.
628: */
629: public void rerunCommands(IProgressMonitor monitor) {
630: Queue<Command> q = history;
631: history = new LinkedList<Command>();
632: for (Iterator<Command> iter = q.iterator(); iter.hasNext();) {
633: Command command = (Command) iter.next();
634: execute(command, monitor);
635: }
636: }
637: }
638:
639: /**
640: * TODO Purpose of net.refractions.udig.project.command
641: * <p>
642: * </p>
643: *
644: * @author Jesse
645: * @since 1.0.0
646: */
647: public static class Request {
648: /** <code>RUN</code> field */
649: public static final int RUN = 0;
650: /** <code>UNDO</code> field */
651: public static final int UNDO = 1;
652: /** <code>REDO</code> field */
653: public static final int REDO = 2;
654: /** <code>RERUN</code> field */
655: public static final int RERUN = 4;
656:
657: /** the type of request */
658: public final int type;
659: /** sync/async * */
660: public final boolean async;
661: /** the command to be done/undone/redone */
662: public final Command command;
663: /** flag to signal wether command is complete * */
664: public volatile boolean completed;
665: public final boolean requestByDisplay;
666:
667: /**
668: * Construct <code>Request</code>.
669: *
670: * @param type the type of request
671: * @param command the command to be done/undone/redone
672: */
673: public Request(int type, Command command, boolean async,
674: boolean requestByDisplay2) {
675: this .requestByDisplay = requestByDisplay2;
676: this .command = command;
677: this .type = type;
678: this .async = async;
679: completed = false;
680: }
681:
682: /**
683: * Determines if the request is synchronous.
684: *
685: * @return
686: */
687: public boolean isSync() {
688: return !async;
689: }
690: }
691:
692: /**
693: * Execute Command syncrounously. IE wait until command is complete before returning.
694: *
695: * @return true if no problems were encountered while queueing command. Problems will typically
696: * occur when the command is synchronous and it times out or is interrupted.
697: */
698: public boolean syncExecute(Command command) {
699: return execute(command, false);
700: }
701:
702: public int getMaxHistorySize() {
703: return 20;
704: }
705:
706: /**
707: * Execute Command asyncrounously. IE return immediately, do not wait until command is complete
708: * before returning.
709: *
710: * @return true if no problems were encountered while queueing command. Problems will typically
711: * occur when the command is synchronous and it times out or is interrupted.
712: */
713: public boolean aSyncExecute(Command command) {
714: return execute(command, true);
715: }
716:
717: }
|