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 2004-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:
042: package org.netbeans.modules.search;
043:
044: import java.awt.EventQueue;
045: import java.lang.ref.Reference;
046: import java.lang.reflect.InvocationTargetException;
047: import java.lang.reflect.Method;
048: import java.util.List;
049: import org.openide.DialogDisplayer;
050: import org.openide.ErrorManager;
051: import org.openide.NotifyDescriptor;
052: import org.openide.awt.StatusDisplayer;
053: import org.openide.util.NbBundle;
054: import org.openide.util.RequestProcessor;
055: import org.openide.util.Task;
056: import org.openide.util.TaskListener;
057: import org.openide.windows.OutputWriter;
058: import org.openidex.search.SearchType;
059: import static org.netbeans.modules.search.ReplaceTask.ResultStatus.SUCCESS;
060: import static org.netbeans.modules.search.ReplaceTask.ResultStatus.PRE_CHECK_FAILED;
061: import static org.netbeans.modules.search.ReplaceTask.ResultStatus.PROBLEMS_ENCOUNTERED;
062:
063: /**
064: * Manager of the Search module's activities.
065: * It knows which tasks are running and manages the module's actions so that
066: * no two conflicting tasks are running at a moment.
067: *
068: * @see <a href="doc-files/manager-state-diagram.png">State diagram</a>
069: * @author Marian Petras
070: */
071: final class Manager {
072:
073: /**
074: * timeout for cleanup in the case that the module is being uninstalled
075: * (in milliseconds)
076: */
077: private static final int CLEANUP_TIMEOUT_MILLIS = 3000;
078:
079: static final int NO_TASK = 0;
080:
081: static final int SEARCHING = 0x01;
082:
083: static final int CLEANING_RESULT = 0x02;
084:
085: static final int PRINTING_DETAILS = 0x04;
086:
087: static final int REPLACING = 0x08;
088:
089: static final int EVENT_SEARCH_STARTED = 1;
090:
091: static final int EVENT_SEARCH_FINISHED = 2;
092:
093: static final int EVENT_SEARCH_INTERRUPTED = 3;
094:
095: static final int EVENT_SEARCH_CANCELLED = 4;
096:
097: private static final Manager instance = new Manager();
098:
099: private boolean moduleBeingUninstalled = false;
100:
101: private final Object lock = new Object();
102:
103: private int state = NO_TASK;
104:
105: private int pendingTasks = 0;
106:
107: private TaskListener taskListener;
108:
109: private SearchTask currentSearchTask;
110:
111: private SearchTask pendingSearchTask;
112:
113: private SearchTask lastSearchTask;
114:
115: private ReplaceTask currentReplaceTask;
116:
117: private ReplaceTask pendingReplaceTask;
118:
119: private PrintDetailsTask currentPrintDetailsTask;
120:
121: private PrintDetailsTask pendingPrintDetailsTask;
122:
123: private Task searchTask;
124:
125: private Task replaceTask;
126:
127: private Task cleanResultTask;
128:
129: private Task printDetailsTask;
130:
131: private ResultModel resultModelToClean;
132:
133: private boolean searchWindowOpen = false;
134:
135: private Reference<OutputWriter> outputWriterRef;
136:
137: /**
138: */
139: static Manager getInstance() {
140: return instance;
141: }
142:
143: /**
144: */
145: private Manager() {
146: }
147:
148: /*
149: * INVARIANTS:
150: * #1: If the Search Results window is open, its root node displays:
151: * - if the search task is in progress:
152: * - summary of results
153: * - if the Search module is inactive:
154: * - summary of current results (continuously updated)
155: * - if the search task in scheduled but another task is blocking it:
156: * - name of the current task blocking the search
157: * #2: At most one result model exists at a single moment.
158: */
159:
160: /**
161: */
162: void scheduleSearchTask(SearchTask task) {
163: assert EventQueue.isDispatchThread();
164:
165: synchronized (lock) {
166: ResultView.getInstance().setResultModel(null);
167: if (currentSearchTask != null) {
168: currentSearchTask.stop(false);
169: }
170: if (resultModelToClean != null) {
171: pendingTasks |= CLEANING_RESULT;
172: }
173: pendingTasks |= SEARCHING;
174: pendingSearchTask = task;
175: lastSearchTask = task;
176: if (state == NO_TASK) {
177: processNextPendingTask();
178: } else {
179: notifySearchPending(state); //invariant #1
180: }
181: }
182: }
183:
184: /**
185: */
186: void scheduleReplaceTask(ReplaceTask task) {
187: assert EventQueue.isDispatchThread();
188:
189: synchronized (lock) {
190: assert (state == NO_TASK) && (pendingTasks == 0);
191:
192: pendingTasks |= REPLACING;
193: pendingReplaceTask = task;
194: processNextPendingTask();
195: }
196: }
197:
198: /**
199: */
200: void scheduleSearchTaskRerun() {
201: assert EventQueue.isDispatchThread();
202:
203: synchronized (lock) {
204: SearchTask newSearchTask = lastSearchTask
205: .createNewGeneration();
206: lastSearchTask = null;
207: scheduleSearchTask(newSearchTask);
208: }
209: }
210:
211: /**
212: */
213: void schedulePrintingDetails(Object[] matchingObjects,
214: BasicSearchCriteria basicCriteria,
215: List<SearchType> searchTypes) {
216: synchronized (lock) {
217: assert state == NO_TASK;
218: pendingTasks |= PRINTING_DETAILS;
219:
220: pendingPrintDetailsTask = new PrintDetailsTask(
221: matchingObjects, basicCriteria, searchTypes);
222: processNextPendingTask();
223: }
224: }
225:
226: /**
227: * Queries whether the user should be allowed to initiate a new search.
228: * For example, the user should not be allowed to do so if the last
229: * replace action has not finished yet.
230: *
231: * @return message to the user, describing the reason why a new search
232: * cannot be started, or {@code null} if there is no such reason
233: * (i.e. if a new search may be started)
234: */
235: String mayStartSearching() {
236: boolean replacing;
237:
238: synchronized (lock) {
239: replacing = (state == REPLACING);
240: }
241:
242: String msgKey = replacing ? "MSG_Cannot_start_search__replacing"//NOI18N
243: : null;
244: return (msgKey != null) ? NbBundle.getMessage(getClass(),
245: msgKey) : null;
246: }
247:
248: /**
249: */
250: private void notifySearchStarted() {
251: notifySearchTaskStateChange(EVENT_SEARCH_STARTED);
252: }
253:
254: /**
255: */
256: private void notifySearchFinished() {
257: notifySearchTaskStateChange(EVENT_SEARCH_FINISHED);
258: }
259:
260: /**
261: */
262: private void notifySearchInterrupted() {
263: notifySearchTaskStateChange(EVENT_SEARCH_INTERRUPTED);
264: }
265:
266: /**
267: */
268: private void notifySearchCancelled() {
269: notifySearchTaskStateChange(EVENT_SEARCH_CANCELLED);
270: }
271:
272: /**
273: * Notifies the result window of a search task's state change.
274: *
275: * @param changeType constant describing what happened
276: * - one of the EVENT_xxx constants
277: */
278: private void notifySearchTaskStateChange(final int changeType) {
279: synchronized (lock) {
280: if (!searchWindowOpen) {
281: return;
282: }
283: }
284: callOnWindowFromAWT("searchTaskStateChanged", //NOI18N
285: new Integer(changeType));
286: }
287:
288: /**
289: */
290: private void notifySearchPending(final int blockingTask) {
291: if (!searchWindowOpen) {
292: return;
293: }
294: callOnWindowFromAWT("notifySearchPending", //NOI18N
295: new Integer(blockingTask));
296: }
297:
298: /**
299: */
300: private void notifyReplaceFinished() {
301: assert Thread.holdsLock(lock);
302: assert currentReplaceTask != null;
303:
304: ReplaceTask.ResultStatus resultStatus = currentReplaceTask
305: .getResultStatus();
306: if (resultStatus == SUCCESS) {
307: StatusDisplayer.getDefault().setStatusText(
308: NbBundle.getMessage(getClass(), "MSG_Success")); //NOI18N
309: if (searchWindowOpen) {
310: callOnWindowFromAWT("closeAndSendFocusToEditor", false);//NOI18N
311: }
312: } else {
313: String msgKey = (resultStatus == PRE_CHECK_FAILED) ? "MSG_Issues_found_during_precheck" //NOI18N
314: : "MSG_Issues_found_during_replace"; //NOI18N
315: String title = NbBundle.getMessage(getClass(), msgKey);
316: displayIssuesFromAWT(title, currentReplaceTask
317: .getProblems(), resultStatus != PRE_CHECK_FAILED);
318: if (resultStatus == PRE_CHECK_FAILED) {
319: offerRescanAfterIssuesFound();
320: }
321: }
322: }
323:
324: /**
325: */
326: private void offerRescanAfterIssuesFound() {
327: assert Thread.holdsLock(lock);
328: assert currentReplaceTask != null;
329:
330: String msg = NbBundle.getMessage(getClass(),
331: "MSG_IssuesFound_Rescan_"); //NOI18N
332: NotifyDescriptor nd = new NotifyDescriptor.Message(msg,
333: NotifyDescriptor.QUESTION_MESSAGE);
334: String rerunOption = NbBundle.getMessage(getClass(),
335: "LBL_Rerun"); //NOI18N
336: nd.setOptions(new Object[] { rerunOption,
337: NotifyDescriptor.CANCEL_OPTION });
338: Object dlgResult = DialogDisplayer.getDefault().notify(nd);
339: if (rerunOption.equals(dlgResult)) {
340: /*
341: * The rescan method calls 'scheduleSearchTaskRerun()' on this.
342: * But it will wait until 'taskFinished()' returns, which is
343: * exactly what we need to keep consistency of the manager's fields
344: * like 'currentReplaceTask', 'replaceTask' and 'state'.
345: * Using this mechanism also requires that, when sending a method
346: * to the EventQueue thread, we use invokeLater(...) and not
347: * invokeAndWait(...).
348: */
349: callOnWindowFromAWT("rescan", false); //NOI18N
350: }
351: }
352:
353: /**
354: */
355: private void notifyPrintingDetailsFinished() {
356: if (!searchWindowOpen) {
357: return;
358: }
359: callOnWindowFromAWT("showAllDetailsFinished"); //NOI18N
360: }
361:
362: /**
363: */
364: private void activateResultWindow() {
365: Method theMethod;
366: try {
367: theMethod = ResultView.class.getMethod("requestActive",
368: new Class[0]); //NOI18N
369: } catch (NoSuchMethodException ex) {
370: throw new IllegalArgumentException();
371: }
372: callOnWindowFromAWT(theMethod, null);
373: }
374:
375: /**
376: */
377: private void displayIssuesFromAWT(String title, String[] issues,
378: boolean att) {
379: Method theMethod;
380: try {
381: theMethod = ResultView.class.getDeclaredMethod(
382: "displayIssuesToUser", //NOI18N
383: String.class, String[].class, Boolean.TYPE);
384: } catch (NoSuchMethodException ex) {
385: throw new IllegalStateException(ex);
386: }
387: callOnWindowFromAWT(theMethod, new Object[] { title, issues,
388: Boolean.valueOf(att) }, false);
389: }
390:
391: /**
392: * Calls a given method on the Search Results window, from the AWT thread.
393: *
394: * @param methodName name of the method to be called
395: */
396: private void callOnWindowFromAWT(final String methodName) {
397: callOnWindowFromAWT(methodName, true);
398: }
399:
400: /**
401: */
402: private void callOnWindowFromAWT(final String methodName,
403: final boolean wait) {
404: Method theMethod;
405: try {
406: theMethod = ResultView.class.getDeclaredMethod(methodName,
407: new Class[0]);
408: } catch (NoSuchMethodException ex) {
409: throw new IllegalArgumentException();
410: }
411: callOnWindowFromAWT(theMethod, null, wait);
412: }
413:
414: /**
415: * Calls a given method on the Search Results window, from the AWT thread.
416: *
417: * @param methodName name of the method to be called
418: * @param param parameter to be passed to the method
419: */
420: private void callOnWindowFromAWT(final String methodName,
421: final Object param) {
422: callOnWindowFromAWT(methodName, param, true);
423: }
424:
425: /**
426: */
427: private void callOnWindowFromAWT(final String methodName,
428: final Object param, final boolean wait) {
429: Method theMethod = null;
430: Method[] methods = ResultView.class.getDeclaredMethods();
431: for (int i = 0; i < methods.length; i++) {
432: Method method = methods[i];
433: if (method.getName().equals(methodName)) {
434: Class[] parameterTypes = method.getParameterTypes();
435: if (parameterTypes.length == 1) {
436: Class paramType = parameterTypes[0];
437: if ((param == null && !paramType.isPrimitive())
438: || (paramType == Integer.TYPE)
439: && (param instanceof Integer)
440: || parameterTypes[0].isInstance(param)) {
441: theMethod = method;
442: }
443: }
444: }
445: }
446: if (theMethod == null) {
447: throw new IllegalArgumentException();
448: }
449: callOnWindowFromAWT(theMethod, new Object[] { param }, wait);
450: }
451:
452: /**
453: */
454: private void callOnWindowFromAWT(final Method method,
455: final Object[] params) {
456: callOnWindowFromAWT(method, params, true);
457: }
458:
459: /**
460: */
461: private void callOnWindowFromAWT(final Method method,
462: final Object[] params, final boolean wait) {
463: Runnable runnable = new Runnable() {
464: public void run() {
465: final ResultView resultViewInstance = ResultView
466: .getInstance();
467: try {
468: method.invoke(resultViewInstance, params);
469: } catch (Exception ex) {
470: ErrorManager.getDefault().notify(ex);
471: }
472: }
473: };
474: if (EventQueue.isDispatchThread()) {
475: runnable.run();
476: } else {
477: if (wait) {
478: try {
479: EventQueue.invokeAndWait(runnable);
480: } catch (InvocationTargetException ex1) {
481: ErrorManager.getDefault().notify(ex1);
482: } catch (Exception ex2) {
483: ErrorManager.getDefault().notify(
484: ErrorManager.ERROR, ex2);
485: }
486: } else {
487: EventQueue.invokeLater(runnable);
488: }
489: }
490: }
491:
492: /**
493: */
494: void searchWindowOpened() {
495: synchronized (lock) {
496: searchWindowOpen = true;
497: }
498: }
499:
500: /**
501: */
502: void searchWindowClosed() {
503: assert EventQueue.isDispatchThread();
504:
505: synchronized (lock) {
506: searchWindowOpen = false;
507:
508: if (moduleBeingUninstalled) {
509: return;
510: }
511:
512: if (currentSearchTask != null) {
513: currentSearchTask.stop(false);
514: }
515: if (resultModelToClean != null) {
516: pendingTasks |= CLEANING_RESULT;
517: }
518: pendingTasks &= ~SEARCHING;
519: pendingSearchTask = null;
520: lastSearchTask = null;
521: if (state == NO_TASK) {
522: processNextPendingTask();
523: }
524: }
525: }
526:
527: /**
528: */
529: private void processNextPendingTask() {
530: synchronized (lock) {
531: assert state == NO_TASK;
532: if (resultModelToClean == null) {
533: pendingTasks &= ~CLEANING_RESULT;
534: }
535: if ((pendingTasks & PRINTING_DETAILS) != 0) {
536: if ((pendingTasks & SEARCHING) != 0) {
537: notifySearchPending(PRINTING_DETAILS); //invariant #1
538: }
539: startPrintingDetails();
540: } else if ((pendingTasks & CLEANING_RESULT) != 0) {
541: if ((pendingTasks & SEARCHING) != 0) {
542: notifySearchPending(CLEANING_RESULT); //invariant #1
543: }
544: startCleaning();
545: } else if ((pendingTasks & SEARCHING) != 0) {
546: startSearching();
547: } else if ((pendingTasks & REPLACING) != 0) {
548: startReplacing();
549: } else {
550: assert pendingTasks == 0;
551: }
552: }
553: }
554:
555: /**
556: */
557: private void startSearching() {
558: synchronized (lock) {
559: assert pendingSearchTask != null;
560:
561: notifySearchStarted();
562:
563: ResultModel resultModel = pendingSearchTask
564: .getResultModel();
565: callOnWindowFromAWT("setResultModel", //NOI18N
566: resultModel);
567: resultModelToClean = resultModel;
568:
569: if (outputWriterRef != null) {
570: SearchDisplayer.clearOldOutput(outputWriterRef);
571: outputWriterRef = null;
572:
573: /*
574: * The following is necessary because clearing the output window
575: * activates the output window:
576: */
577: activateResultWindow();
578: }
579:
580: RequestProcessor.Task task;
581: task = RequestProcessor.getDefault().create(
582: pendingSearchTask);
583: task.addTaskListener(getTaskListener());
584: task.schedule(0);
585:
586: currentSearchTask = pendingSearchTask;
587: pendingSearchTask = null;
588:
589: searchTask = task;
590: pendingTasks &= ~SEARCHING;
591: state = SEARCHING;
592: }
593: }
594:
595: /**
596: */
597: private void startReplacing() {
598: synchronized (lock) {
599: assert pendingReplaceTask != null;
600:
601: RequestProcessor.Task task;
602: task = RequestProcessor.getDefault().create(
603: pendingReplaceTask);
604: task.addTaskListener(getTaskListener());
605: task.schedule(0);
606:
607: currentReplaceTask = pendingReplaceTask;
608: pendingReplaceTask = null;
609:
610: replaceTask = task;
611: pendingTasks &= ~REPLACING;
612: state = REPLACING;
613: }
614: }
615:
616: /**
617: */
618: private void startPrintingDetails() {
619: synchronized (lock) {
620: if (outputWriterRef != null) {
621: SearchDisplayer.clearOldOutput(outputWriterRef);
622: outputWriterRef = null;
623: }
624:
625: RequestProcessor.Task task;
626: task = RequestProcessor.getDefault().create(
627: pendingPrintDetailsTask);
628: task.addTaskListener(getTaskListener());
629: task.schedule(0);
630:
631: printDetailsTask = task;
632: pendingTasks &= ~PRINTING_DETAILS;
633: currentPrintDetailsTask = pendingPrintDetailsTask;
634: pendingPrintDetailsTask = null;
635:
636: state = PRINTING_DETAILS;
637: }
638: }
639:
640: /**
641: */
642: private void startCleaning() {
643: synchronized (lock) {
644: Runnable cleaner = new CleanTask(resultModelToClean);
645: resultModelToClean = null;
646:
647: RequestProcessor.Task task;
648: task = RequestProcessor.getDefault().create(cleaner);
649: task.addTaskListener(getTaskListener());
650: task.schedule(0);
651:
652: cleanResultTask = task;
653: pendingTasks &= ~CLEANING_RESULT;
654: state = CLEANING_RESULT;
655: }
656: }
657:
658: /**
659: */
660: void stopSearching() {
661: synchronized (lock) {
662: if ((pendingTasks & SEARCHING) != 0) {
663: pendingTasks &= ~SEARCHING;
664: pendingSearchTask = null;
665: notifySearchCancelled();
666: } else if (currentSearchTask != null) {
667: currentSearchTask.stop();
668: }
669: }
670: }
671:
672: /**
673: */
674: private void taskFinished(Task task) {
675: synchronized (lock) {
676: if (moduleBeingUninstalled) {
677: allTasksFinished();
678: return;
679: }
680:
681: if (task == searchTask) {
682: assert state == SEARCHING;
683: if (currentSearchTask.notifyWhenFinished()) {
684: if (currentSearchTask.wasInterrupted()) {
685: notifySearchInterrupted();
686: } else {
687: notifySearchFinished();
688: }
689: }
690: currentSearchTask = null;
691: searchTask = null;
692: state = NO_TASK;
693: } else if (task == replaceTask) {
694: assert state == REPLACING;
695: notifyReplaceFinished();
696: currentReplaceTask = null;
697: replaceTask = null;
698: state = NO_TASK;
699: } else if (task == cleanResultTask) {
700: assert state == CLEANING_RESULT;
701: cleanResultTask = null;
702: state = NO_TASK;
703: } else if (task == printDetailsTask) {
704: assert state == PRINTING_DETAILS;
705: notifyPrintingDetailsFinished();
706:
707: outputWriterRef = currentPrintDetailsTask
708: .getOutputWriterRef();
709: currentPrintDetailsTask = null;
710: printDetailsTask = null;
711: state = NO_TASK;
712: } else {
713: assert false;
714: }
715: processNextPendingTask();
716: }
717: }
718:
719: /**
720: * Called only if the module is about to be uninstalled.
721: * This method is called at the moment that there are no active tasks
722: * (searching, printing details, etc.) and the module is ready for
723: * final cleanup.
724: */
725: private void allTasksFinished() {
726: synchronized (lock) {
727: lock.notifyAll();
728: }
729: }
730:
731: /**
732: * Called from the <code>Installer</code> to notify that the module
733: * is being uninstalled.
734: * Calling this method sets a corresponding flag. When the flag is set,
735: * no new actions (cleaning results, printing details, etc.) are started
736: * and the behaviour is changed so that manipulation with the ResultView
737: * is reduced or eliminated. Also, if no tasks are currently active,
738: * immediatelly closes the results window; otherwise it postpones closing
739: * the window until the currently active task(s) finish.
740: */
741: void doCleanup() {
742: synchronized (lock) {
743: moduleBeingUninstalled = true;
744: if (state != NO_TASK) {
745: if (currentSearchTask != null) {
746: currentSearchTask.stop(false);
747: }
748: if (currentPrintDetailsTask != null) {
749: currentPrintDetailsTask.stop();
750: }
751: try {
752: lock.wait(CLEANUP_TIMEOUT_MILLIS);
753: } catch (InterruptedException ex) {
754: ErrorManager.getDefault().notify(
755: ErrorManager.EXCEPTION, ex);
756: }
757: }
758: callOnWindowFromAWT("closeResults"); //NOI18N
759: }
760: }
761:
762: /**
763: */
764: private TaskListener getTaskListener() {
765: if (taskListener == null) {
766: taskListener = new MyTaskListener();
767: }
768: return taskListener;
769: }
770:
771: /**
772: */
773: private class MyTaskListener implements TaskListener {
774:
775: /**
776: */
777: MyTaskListener() {
778: super ();
779: }
780:
781: /**
782: */
783: public void taskFinished(Task task) {
784: synchronized (lock) {
785: Manager.this.taskFinished(task);
786: }
787: }
788:
789: }
790:
791: }
|