Esempio n. 1
0
/** @author: Roman Chernyatchik */
public class SMTestRunnerResultsForm extends TestResultsPanel
    implements TestFrameworkRunningModel, TestResultsViewer, SMTRunnerEventsListener {
  @NonNls public static final String HISTORY_DATE_FORMAT = "yyyy.MM.dd 'at' HH'h' mm'm' ss's'";

  @NonNls
  private static final String DEFAULT_SM_RUNNER_SPLITTER_PROPERTY =
      "SMTestRunner.Splitter.Proportion";

  public static final Color DARK_YELLOW = JBColor.YELLOW.darker();
  private static final Logger LOG =
      Logger.getInstance("#" + SMTestRunnerResultsForm.class.getName());

  private SMTRunnerTestTreeView myTreeView;

  private TestsProgressAnimator myAnimator;

  /** Fake parent suite for all tests and suites */
  private final SMTestProxy.SMRootTestProxy myTestsRootNode;

  private SMTRunnerTreeBuilder myTreeBuilder;

  private final List<EventsListener> myEventListeners =
      ContainerUtil.createLockFreeCopyOnWriteList();

  private PropagateSelectionHandler myShowStatisticForProxyHandler;

  private final Project myProject;

  private int myTotalTestCount = 0;
  private int myStartedTestCount = 0;
  private int myFinishedTestCount = 0;
  private int myFailedTestCount = 0;
  private int myIgnoredTestCount = 0;
  private long myStartTime;
  private long myEndTime;
  private StatisticsPanel myStatisticsPane;

  // custom progress
  private String myCurrentCustomProgressCategory;
  private final Set<String> myMentionedCategories = new LinkedHashSet<String>();
  private boolean myTestsRunning = true;
  private AbstractTestProxy myLastSelected;
  private Alarm myUpdateQueue;
  private Set<Update> myRequests = Collections.synchronizedSet(new HashSet<Update>());

  public SMTestRunnerResultsForm(
      @NotNull final JComponent console, final TestConsoleProperties consoleProperties) {
    this(console, AnAction.EMPTY_ARRAY, consoleProperties, null);
  }

  public SMTestRunnerResultsForm(
      @NotNull final JComponent console,
      AnAction[] consoleActions,
      final TestConsoleProperties consoleProperties,
      @Nullable String splitterPropertyName) {
    super(
        console,
        consoleActions,
        consoleProperties,
        StringUtil.notNullize(splitterPropertyName, DEFAULT_SM_RUNNER_SPLITTER_PROPERTY),
        0.2f);
    myProject = consoleProperties.getProject();

    // Create tests common suite root
    //noinspection HardCodedStringLiteral
    myTestsRootNode = new SMTestProxy.SMRootTestProxy();
    // todo myTestsRootNode.setOutputFilePath(runConfiguration.getOutputFilePath());

    // Fire selection changed and move focus on SHIFT+ENTER
    // TODO[romeo] improve
    /*
    final ArrayList<Component> components = new ArrayList<Component>();
    components.add(myTreeView);
    components.add(myTabs.getComponent());
    myContentPane.setFocusTraversalPolicy(new MyFocusTraversalPolicy(components));
    myContentPane.setFocusCycleRoot(true);
    */
  }

  @Override
  public void initUI() {
    super.initUI();

    if (Registry.is("tests.view.old.statistics.panel")) {
      final KeyStroke shiftEnterKey =
          KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.SHIFT_MASK);
      SMRunnerUtil.registerAsAction(
          shiftEnterKey,
          "show-statistics-for-test-proxy",
          new Runnable() {
            public void run() {
              showStatisticsForSelectedProxy();
            }
          },
          myTreeView);
    }
  }

  protected ToolbarPanel createToolbarPanel() {
    return new SMTRunnerToolbarPanel(myProperties, this, this);
  }

  protected JComponent createTestTreeView() {
    myTreeView = new SMTRunnerTestTreeView();

    myTreeView.setLargeModel(true);
    myTreeView.attachToModel(this);
    myTreeView.setTestResultsViewer(this);
    if (Registry.is("tests.view.old.statistics.panel")) {
      addTestsTreeSelectionListener(
          new TreeSelectionListener() {
            @Override
            public void valueChanged(TreeSelectionEvent e) {
              AbstractTestProxy selectedTest = getTreeView().getSelectedTest();
              if (selectedTest instanceof SMTestProxy) {
                myStatisticsPane.selectProxy(((SMTestProxy) selectedTest), this, false);
              }
            }
          });
    }

    final SMTRunnerTreeStructure structure = new SMTRunnerTreeStructure(myProject, myTestsRootNode);
    myTreeBuilder = new SMTRunnerTreeBuilder(myTreeView, structure);
    myTreeBuilder.setTestsComparator(TestConsoleProperties.SORT_ALPHABETICALLY.value(myProperties));
    Disposer.register(this, myTreeBuilder);

    myAnimator = new TestsProgressAnimator(myTreeBuilder);

    TrackRunningTestUtil.installStopListeners(
        myTreeView,
        myProperties,
        new Pass<AbstractTestProxy>() {
          @Override
          public void pass(AbstractTestProxy testProxy) {
            if (testProxy == null) return;
            // drill to the first leaf
            while (!testProxy.isLeaf()) {
              final List<? extends AbstractTestProxy> children = testProxy.getChildren();
              if (!children.isEmpty()) {
                final AbstractTestProxy firstChild = children.get(0);
                if (firstChild != null) {
                  testProxy = firstChild;
                  continue;
                }
              }
              break;
            }

            // pretend the selection on the first leaf
            // so if test would be run, tracking would be restarted
            myLastSelected = testProxy;
          }
        });

    // TODO always hide root node
    // myTreeView.setRootVisible(false);
    myUpdateQueue = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, this);
    return myTreeView;
  }

  protected JComponent createStatisticsPanel() {
    // Statistics tab
    final StatisticsPanel statisticsPane = new StatisticsPanel(myProject, this);
    // handler to select in results viewer by statistics pane events
    statisticsPane.addPropagateSelectionListener(createSelectMeListener());
    // handler to select test statistics pane by result viewer events
    setShowStatisticForProxyHandler(statisticsPane.createSelectMeListener());

    myStatisticsPane = statisticsPane;
    return myStatisticsPane.getContentPane();
  }

  public StatisticsPanel getStatisticsPane() {
    return myStatisticsPane;
  }

  public void addTestsTreeSelectionListener(final TreeSelectionListener listener) {
    myTreeView.getSelectionModel().addTreeSelectionListener(listener);
  }

  /**
   * Is used for navigation from tree view to other UI components
   *
   * @param handler
   */
  public void setShowStatisticForProxyHandler(final PropagateSelectionHandler handler) {
    myShowStatisticForProxyHandler = handler;
  }

  /**
   * Returns root node, fake parent suite for all tests and suites
   *
   * @param testsRoot
   * @return
   */
  public void onTestingStarted(@NotNull SMTestProxy.SMRootTestProxy testsRoot) {
    myAnimator.setCurrentTestCase(myTestsRootNode);
    myTreeBuilder.updateFromRoot();

    // Status line
    myStatusLine.setStatusColor(ColorProgressBar.GREEN);

    // Tests tree
    selectAndNotify(myTestsRootNode);

    myStartTime = System.currentTimeMillis();
    boolean printTestingStartedTime = true;
    if (myProperties instanceof SMTRunnerConsoleProperties) {
      printTestingStartedTime =
          ((SMTRunnerConsoleProperties) myProperties).isPrintTestingStartedTime();
    }
    if (printTestingStartedTime) {
      myTestsRootNode.addSystemOutput(
          "Testing started at " + DateFormatUtil.formatTime(myStartTime) + " ...\n");
    }

    updateStatusLabel(false);

    // TODO : show info - "Loading..." msg

    fireOnTestingStarted();
  }

  public void onTestingFinished(@NotNull SMTestProxy.SMRootTestProxy testsRoot) {
    myEndTime = System.currentTimeMillis();

    if (myTotalTestCount == 0) {
      myTotalTestCount = myStartedTestCount;
      myStatusLine.setFraction(1);
    }

    updateStatusLabel(true);
    updateIconProgress(true);

    myAnimator.stopMovie();
    myTreeBuilder.updateFromRoot();

    LvcsHelper.addLabel(this);

    final Runnable onDone =
        new Runnable() {
          @Override
          public void run() {
            myTestsRunning = false;
            final boolean sortByDuration =
                TestConsoleProperties.SORT_BY_DURATION.value(myProperties);
            if (sortByDuration) {
              myTreeBuilder.setStatisticsComparator(myProperties, sortByDuration);
            }
          }
        };
    if (myLastSelected == null) {
      selectAndNotify(myTestsRootNode, onDone);
    } else {
      onDone.run();
    }

    fireOnTestingFinished();

    if (testsRoot.wasTerminated() && myStatusLine.getStatusColor() == ColorProgressBar.GREEN) {
      myStatusLine.setStatusColor(JBColor.LIGHT_GRAY);
    }

    if (testsRoot.isEmptySuite()
        && testsRoot.isTestsReporterAttached()
        && myProperties instanceof SMTRunnerConsoleProperties
        && ((SMTRunnerConsoleProperties) myProperties).fixEmptySuite()) {
      return;
    }
    final TestsUIUtil.TestResultPresentation presentation =
        new TestsUIUtil.TestResultPresentation(testsRoot, myStartTime > 0, null)
            .getPresentation(
                myFailedTestCount,
                myFinishedTestCount - myFailedTestCount - myIgnoredTestCount,
                myTotalTestCount - myFinishedTestCount,
                myIgnoredTestCount);
    TestsUIUtil.notifyByBalloon(myProperties.getProject(), testsRoot, myProperties, presentation);
    addToHistory(testsRoot, myProperties, this);
  }

  private static void addToHistory(
      final SMTestProxy.SMRootTestProxy root,
      TestConsoleProperties consoleProperties,
      Disposable parentDisposable) {
    final RunProfile configuration = consoleProperties.getConfiguration();
    if (configuration instanceof RunConfiguration
        && !(consoleProperties instanceof ImportedTestConsoleProperties)) {
      final MySaveHistoryTask backgroundable =
          new MySaveHistoryTask(consoleProperties, root, (RunConfiguration) configuration);
      final BackgroundableProcessIndicator processIndicator =
          new BackgroundableProcessIndicator(backgroundable);
      Disposer.register(
          parentDisposable,
          new Disposable() {
            @Override
            public void dispose() {
              processIndicator.cancel();
              backgroundable.dispose();
            }
          });
      Disposer.register(parentDisposable, processIndicator);
      ProgressManager.getInstance()
          .runProcessWithProgressAsynchronously(backgroundable, processIndicator);
    }
  }

  public void onTestsCountInSuite(final int count) {
    updateCountersAndProgressOnTestCount(count, false);
  }

  /**
   * Adds test to tree and updates status line. Test proxy should be initialized, proxy parent must
   * be some suite (already added to tree)
   *
   * @param testProxy Proxy
   */
  public void onTestStarted(@NotNull final SMTestProxy testProxy) {
    if (!testProxy.isConfig()) {
      updateOnTestStarted(false);
    }
    _addTestOrSuite(testProxy);
    fireOnTestNodeAdded(testProxy);
  }

  @Override
  public void onSuiteTreeNodeAdded(SMTestProxy testProxy) {
    myTotalTestCount++;
  }

  @Override
  public void onSuiteTreeStarted(SMTestProxy suite) {}

  public void onTestFailed(@NotNull final SMTestProxy test) {
    updateOnTestFailed(false);
    if (test.isConfig()) {
      myStartedTestCount++;
      myFinishedTestCount++;
    }
    updateIconProgress(false);

    // still expand failure when user selected another test
    if (myLastSelected != null
        && TestConsoleProperties.TRACK_RUNNING_TEST.value(myProperties)
        && TestConsoleProperties.HIDE_PASSED_TESTS.value(myProperties)) {
      myTreeBuilder.expand(test, null);
    }
  }

  public void onTestIgnored(@NotNull final SMTestProxy test) {
    updateOnTestIgnored();
  }

  /**
   * Adds suite to tree Suite proxy should be initialized, proxy parent must be some suite (already
   * added to tree) If parent is null, then suite will be added to tests root.
   *
   * @param newSuite Tests suite
   */
  public void onSuiteStarted(@NotNull final SMTestProxy newSuite) {
    _addTestOrSuite(newSuite);
  }

  public void onCustomProgressTestsCategory(@Nullable String categoryName, int testCount) {
    myCurrentCustomProgressCategory = categoryName;
    updateCountersAndProgressOnTestCount(testCount, true);
  }

  public void onCustomProgressTestStarted() {
    updateOnTestStarted(true);
  }

  public void onCustomProgressTestFailed() {
    updateOnTestFailed(true);
  }

  @Override
  public void onCustomProgressTestFinished() {
    updateOnTestFinished(true);
  }

  public void onTestFinished(@NotNull final SMTestProxy test) {
    if (!test.isConfig()) {
      updateOnTestFinished(false);
    }
    updateIconProgress(false);
  }

  public void onSuiteFinished(@NotNull final SMTestProxy suite) {
    // Do nothing
  }

  public SMTestProxy.SMRootTestProxy getTestsRootNode() {
    return myTestsRootNode;
  }

  public TestConsoleProperties getProperties() {
    return myProperties;
  }

  public void setFilter(final Filter filter) {
    // is used by Test Runner actions, e.g. hide passed, etc
    final SMTRunnerTreeStructure treeStructure = myTreeBuilder.getSMRunnerTreeStructure();
    treeStructure.setFilter(filter);

    // TODO - show correct info if no children are available
    // (e.g no tests found or all tests passed, etc.)
    // treeStructure.getChildElements(treeStructure.getRootElement()).length == 0

    myTreeBuilder.queueUpdate();
  }

  public boolean isRunning() {
    return myTestsRunning;
  }

  public TestTreeView getTreeView() {
    return myTreeView;
  }

  @Override
  public SMTRunnerTreeBuilder getTreeBuilder() {
    return myTreeBuilder;
  }

  public boolean hasTestSuites() {
    return getRoot().getChildren().size() > 0;
  }

  @NotNull
  public AbstractTestProxy getRoot() {
    return myTestsRootNode;
  }

  /**
   * Manual test proxy selection in tests tree. E.g. do select root node on testing started or do
   * select current node if TRACK_RUNNING_TEST is enabled
   *
   * <p>
   *
   * <p>Will select proxy in Event Dispatch Thread. Invocation of this method may be not in event
   * dispatch thread
   *
   * @param testProxy Test or suite
   */
  @Override
  public void selectAndNotify(AbstractTestProxy testProxy) {
    selectAndNotify(testProxy, null);
  }

  private void selectAndNotify(
      @Nullable final AbstractTestProxy testProxy, @Nullable Runnable onDone) {
    selectWithoutNotify(testProxy, onDone);

    // Is used by Statistic tab to differ use selection in tree
    // from manual selection from API (e.g. test runner events)
    if (Registry.is("tests.view.old.statistics.panel")) {
      showStatisticsForSelectedProxy(testProxy, false);
    }
  }

  public void addEventsListener(final EventsListener listener) {
    myEventListeners.add(listener);
    addTestsTreeSelectionListener(
        new TreeSelectionListener() {
          public void valueChanged(final TreeSelectionEvent e) {
            // We should fire event only if it was generated by this component,
            // e.g. it is focused. Otherwise it is side effect of selecting proxy in
            // try by other component
            // if (myTreeView.isFocusOwner()) {
            @Nullable
            final SMTestProxy selectedProxy = (SMTestProxy) getTreeView().getSelectedTest();
            listener.onSelected(
                selectedProxy, SMTestRunnerResultsForm.this, SMTestRunnerResultsForm.this);
            // }
          }
        });
  }

  public void dispose() {
    super.dispose();
    myShowStatisticForProxyHandler = null;
    myEventListeners.clear();
    myStatisticsPane.doDispose();
  }

  public void showStatisticsForSelectedProxy() {
    TestConsoleProperties.SHOW_STATISTICS.set(myProperties, true);
    final AbstractTestProxy selectedProxy = myTreeView.getSelectedTest();
    showStatisticsForSelectedProxy(selectedProxy, true);
  }

  private void showStatisticsForSelectedProxy(
      final AbstractTestProxy selectedProxy, final boolean requestFocus) {
    if (selectedProxy instanceof SMTestProxy && myShowStatisticForProxyHandler != null) {
      myShowStatisticForProxyHandler.handlePropagateSelectionRequest(
          (SMTestProxy) selectedProxy, this, requestFocus);
    }
  }

  protected int getTotalTestCount() {
    return myTotalTestCount;
  }

  protected int getStartedTestCount() {
    return myStartedTestCount;
  }

  protected int getFinishedTestCount() {
    return myFinishedTestCount;
  }

  protected int getFailedTestCount() {
    return myFailedTestCount;
  }

  protected int getIgnoredTestCount() {
    return myIgnoredTestCount;
  }

  protected Color getTestsStatusColor() {
    return myStatusLine.getStatusColor();
  }

  public Set<String> getMentionedCategories() {
    return myMentionedCategories;
  }

  protected long getStartTime() {
    return myStartTime;
  }

  protected long getEndTime() {
    return myEndTime;
  }

  private void _addTestOrSuite(@NotNull final SMTestProxy newTestOrSuite) {

    final SMTestProxy parentSuite = newTestOrSuite.getParent();
    assert parentSuite != null;

    // Tree
    final Update update =
        new Update(parentSuite) {
          @Override
          public void run() {
            myRequests.remove(this);
            myTreeBuilder.updateTestsSubtree(parentSuite);
          }
        };
    if (ApplicationManager.getApplication().isUnitTestMode()) {
      update.run();
    } else if (myRequests.add(update) && !myUpdateQueue.isDisposed()) {
      myUpdateQueue.addRequest(update, 100);
    }
    myTreeBuilder.repaintWithParents(newTestOrSuite);

    myAnimator.setCurrentTestCase(newTestOrSuite);

    if (TestConsoleProperties.TRACK_RUNNING_TEST.value(myProperties)) {
      if (myLastSelected == null || myLastSelected == newTestOrSuite) {
        myLastSelected = null;
        selectAndNotify(newTestOrSuite);
      }
    }
  }

  private void fireOnTestNodeAdded(final SMTestProxy test) {
    for (EventsListener eventListener : myEventListeners) {
      eventListener.onTestNodeAdded(this, test);
    }
  }

  private void fireOnTestingFinished() {
    for (EventsListener eventListener : myEventListeners) {
      eventListener.onTestingFinished(this);
    }
  }

  private void fireOnTestingStarted() {
    for (EventsListener eventListener : myEventListeners) {
      eventListener.onTestingStarted(this);
    }
  }

  private void selectWithoutNotify(
      final AbstractTestProxy testProxy, @Nullable final Runnable onDone) {
    if (testProxy == null) {
      return;
    }

    SMRunnerUtil.runInEventDispatchThread(
        new Runnable() {
          public void run() {
            if (myTreeBuilder.isDisposed()) {
              return;
            }
            myTreeBuilder.select(testProxy, onDone);
          }
        },
        ModalityState.NON_MODAL);
  }

  private void updateStatusLabel(final boolean testingFinished) {
    if (myFailedTestCount > 0) {
      myStatusLine.setStatusColor(ColorProgressBar.RED);
    }

    if (testingFinished) {
      if (myTotalTestCount == 0) {
        myStatusLine.setStatusColor(
            myTestsRootNode.wasLaunched() || !myTestsRootNode.isTestsReporterAttached()
                ? JBColor.LIGHT_GRAY
                : ColorProgressBar.RED);
      }
      // else color will be according failed/passed tests
    }

    // launchedAndFinished - is launched and not in progress. If we remove "launched' that
    // onTestingStarted() before
    // initializing will be "launchedAndFinished"
    final boolean launchedAndFinished =
        myTestsRootNode.wasLaunched() && !myTestsRootNode.isInProgress();
    if (!TestsPresentationUtil.hasNonDefaultCategories(myMentionedCategories)) {
      myStatusLine.formatTestMessage(
          myTotalTestCount,
          myFinishedTestCount,
          myFailedTestCount,
          myIgnoredTestCount,
          myTestsRootNode.getDuration(),
          myEndTime);
    } else {
      myStatusLine.setText(
          TestsPresentationUtil.getProgressStatus_Text(
              myStartTime,
              myEndTime,
              myTotalTestCount,
              myFinishedTestCount,
              myFailedTestCount,
              myMentionedCategories,
              launchedAndFinished));
    }
  }

  /** for java unit tests */
  public void performUpdate() {
    myTreeBuilder.performUpdate();
  }

  private void updateIconProgress(boolean updateWithAttention) {
    final int totalTestCount, doneTestCount;
    if (myTotalTestCount == 0) {
      totalTestCount = 2;
      doneTestCount = 1;
    } else {
      totalTestCount = myTotalTestCount;
      doneTestCount = myFinishedTestCount;
    }
    TestsUIUtil.showIconProgress(
        myProject, doneTestCount, totalTestCount, myFailedTestCount, updateWithAttention);
  }

  /**
   * On event change selection and probably requests focus. Is used when we want navigate from other
   * component to this
   *
   * @return Listener
   */
  public PropagateSelectionHandler createSelectMeListener() {
    return new PropagateSelectionHandler() {
      public void handlePropagateSelectionRequest(
          @Nullable final SMTestProxy selectedTestProxy,
          @NotNull final Object sender,
          final boolean requestFocus) {
        SMRunnerUtil.addToInvokeLater(
            new Runnable() {
              public void run() {
                selectWithoutNotify(selectedTestProxy, null);

                // Request focus if necessary
                if (requestFocus) {
                  // myTreeView.requestFocusInWindow();
                  IdeFocusManager.getInstance(myProject).requestFocus(myTreeView, true);
                }
              }
            });
      }
    };
  }

  private void updateCountersAndProgressOnTestCount(
      final int count, final boolean isCustomMessage) {
    if (!isModeConsistent(isCustomMessage)) return;

    // This is for better support groups of TestSuites
    // Each group notifies about it's size
    myTotalTestCount += count;
    updateStatusLabel(false);
  }

  private void updateOnTestStarted(final boolean isCustomMessage) {
    if (!isModeConsistent(isCustomMessage)) return;

    // for mixed tests results : mention category only if it contained tests
    myMentionedCategories.add(
        myCurrentCustomProgressCategory != null
            ? myCurrentCustomProgressCategory
            : TestsPresentationUtil.DEFAULT_TESTS_CATEGORY);

    myStartedTestCount++;

    // fix total count if it is corrupted
    // but if test count wasn't set at all let's process such case separately
    if (myStartedTestCount > myTotalTestCount && myTotalTestCount != 0) {
      myTotalTestCount = myStartedTestCount;
    }

    updateStatusLabel(false);
  }

  private void updateProgressOnTestDone() {
    int doneTestCount = myFinishedTestCount;
    // update progress
    if (myTotalTestCount != 0) {
      // if total is set
      myStatusLine.setFraction((double) doneTestCount / myTotalTestCount);
    } else {
      // if at least one test was launcher than just set progress in the middle to show user that
      // tests are running
      myStatusLine.setFraction(doneTestCount > 0 ? 0.5 : 0);
    }
  }

  private void updateOnTestFailed(final boolean isCustomMessage) {
    if (!isModeConsistent(isCustomMessage)) return;
    myFailedTestCount++;
    updateProgressOnTestDone();
    updateStatusLabel(false);
  }

  private void updateOnTestFinished(final boolean isCustomMessage) {
    if (!isModeConsistent(isCustomMessage)) return;
    myFinishedTestCount++;
    updateProgressOnTestDone();
  }

  private void updateOnTestIgnored() {
    myIgnoredTestCount++;
    updateProgressOnTestDone();
    updateStatusLabel(false);
  }

  private boolean isModeConsistent(boolean isCustomMessage) {
    // check that we are in consistent mode
    return isCustomMessage != (myCurrentCustomProgressCategory == null);
  }

  private static class MySaveHistoryTask extends Task.Backgroundable {

    private final TestConsoleProperties myConsoleProperties;
    private SMTestProxy.SMRootTestProxy myRoot;
    private RunConfiguration myConfiguration;
    private File myOutputFile;

    public MySaveHistoryTask(
        TestConsoleProperties consoleProperties,
        SMTestProxy.SMRootTestProxy root,
        RunConfiguration configuration) {
      super(consoleProperties.getProject(), "Save Test Results", true);
      myConsoleProperties = consoleProperties;
      myRoot = root;
      myConfiguration = configuration;
    }

    @Override
    public void run(@NotNull ProgressIndicator indicator) {
      try {
        SAXTransformerFactory transformerFactory =
            (SAXTransformerFactory) TransformerFactory.newInstance();
        TransformerHandler handler = transformerFactory.newTransformerHandler();
        handler.getTransformer().setOutputProperty(OutputKeys.INDENT, "yes");
        handler
            .getTransformer()
            .setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");

        final String configurationNameIncludedDate =
            PathUtil.suggestFileName(myConfiguration.getName())
                + " - "
                + new SimpleDateFormat(HISTORY_DATE_FORMAT).format(new Date());

        myOutputFile =
            new File(
                AbstractImportTestsAction.getTestHistoryRoot(myProject),
                configurationNameIncludedDate + ".xml");
        FileUtilRt.createParentDirs(myOutputFile);
        handler.setResult(new StreamResult(new FileWriter(myOutputFile)));
        final SMTestProxy.SMRootTestProxy root = myRoot;
        final RunConfiguration configuration = myConfiguration;
        if (root != null && configuration != null) {
          TestResultsXmlFormatter.execute(root, configuration, myConsoleProperties, handler);
        }
      } catch (ProcessCanceledException e) {
        throw e;
      } catch (Exception e) {
        LOG.info("Export to history failed", e);
      }
    }

    @Override
    public void onSuccess() {
      if (myOutputFile != null && myOutputFile.exists()) {
        AbstractImportTestsAction.adjustHistory(myProject);
        TestHistoryConfiguration.getInstance(myProject)
            .registerHistoryItem(
                myOutputFile.getName(),
                myConfiguration.getName(),
                myConfiguration.getType().getId());
      }
    }

    public void dispose() {
      myConfiguration = null;
      myRoot = null;
      myOutputFile = null;
    }
  }
}
public abstract class PsiDocumentManagerBase extends PsiDocumentManager
    implements ProjectComponent, DocumentListener {
  protected static final Logger LOG =
      Logger.getInstance("#com.intellij.psi.impl.PsiDocumentManagerImpl");
  protected static final Key<PsiFile> HARD_REF_TO_PSI = new Key<PsiFile>("HARD_REFERENCE_TO_PSI");
  protected static final Key<List<Runnable>> ACTION_AFTER_COMMIT =
      Key.create("ACTION_AFTER_COMMIT");

  protected final Project myProject;
  protected final PsiManager myPsiManager;
  protected final DocumentCommitProcessor myDocumentCommitProcessor;
  protected final Set<Document> myUncommittedDocuments =
      Collections.synchronizedSet(new HashSet<Document>());

  protected volatile boolean myIsCommitInProgress;
  protected final PsiToDocumentSynchronizer mySynchronizer;

  protected final List<Listener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList();
  protected final SmartPointerManagerImpl mySmartPointerManager;

  public PsiDocumentManagerBase(
      @NotNull final Project project,
      @NotNull PsiManager psiManager,
      @NotNull SmartPointerManager smartPointerManager,
      @NotNull MessageBus bus,
      @NonNls @NotNull final DocumentCommitProcessor documentCommitProcessor) {
    myProject = project;
    myPsiManager = psiManager;
    myDocumentCommitProcessor = documentCommitProcessor;
    mySmartPointerManager = (SmartPointerManagerImpl) smartPointerManager;
    mySynchronizer = new PsiToDocumentSynchronizer(this, bus);
    myPsiManager.addPsiTreeChangeListener(mySynchronizer);
  }

  @Override
  public void projectOpened() {}

  @Override
  public void projectClosed() {}

  @Override
  @NotNull
  public String getComponentName() {
    return "PsiDocumentManager";
  }

  @Override
  public void initComponent() {}

  @Override
  public void disposeComponent() {}

  @Override
  @Nullable
  public PsiFile getPsiFile(@NotNull Document document) {
    final PsiFile userData = document.getUserData(HARD_REF_TO_PSI);
    if (userData != null) return userData;

    PsiFile psiFile = getCachedPsiFile(document);
    if (psiFile != null) return psiFile;

    final VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document);
    if (virtualFile == null || !virtualFile.isValid()) return null;

    psiFile = getPsiFile(virtualFile);
    if (psiFile == null) return null;

    fireFileCreated(document, psiFile);

    return psiFile;
  }

  public static void cachePsi(@NotNull Document document, @NotNull PsiFile file) {
    document.putUserData(HARD_REF_TO_PSI, file);
  }

  @Override
  public PsiFile getCachedPsiFile(@NotNull Document document) {
    final PsiFile userData = document.getUserData(HARD_REF_TO_PSI);
    if (userData != null) return userData;

    final VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document);
    if (virtualFile == null || !virtualFile.isValid()) return null;
    return getCachedPsiFile(virtualFile);
  }

  @Nullable
  public FileViewProvider getCachedViewProvider(@NotNull Document document) {
    final VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document);
    if (virtualFile == null || !virtualFile.isValid()) return null;
    return ((PsiManagerEx) myPsiManager).getFileManager().findCachedViewProvider(virtualFile);
  }

  @Nullable
  protected PsiFile getCachedPsiFile(VirtualFile virtualFile) {
    return ((PsiManagerEx) myPsiManager).getFileManager().getCachedPsiFile(virtualFile);
  }

  @Nullable
  protected PsiFile getPsiFile(VirtualFile virtualFile) {
    return ((PsiManagerEx) myPsiManager).getFileManager().findFile(virtualFile);
  }

  @Nullable
  @Override
  public Document getDocument(@NotNull PsiFile file) {
    if (file instanceof PsiBinaryFile) return null;

    Document document = getCachedDocument(file);
    if (document != null) {
      if (!file.getViewProvider().isPhysical() && document.getUserData(HARD_REF_TO_PSI) == null) {
        cachePsi(document, file);
      }
      return document;
    }

    if (!file.getViewProvider().isEventSystemEnabled()) return null;
    document =
        FileDocumentManager.getInstance().getDocument(file.getViewProvider().getVirtualFile());

    if (document != null) {
      if (document.getTextLength() != file.getTextLength()) {
        throw new AssertionError(
            "Modified PSI with no document: "
                + file
                + "; physical="
                + file.getViewProvider().isPhysical());
      }

      if (!file.getViewProvider().isPhysical()) {
        cachePsi(document, file);
      }
    }

    return document;
  }

  @Override
  public Document getCachedDocument(@NotNull PsiFile file) {
    if (!file.isPhysical()) return null;
    VirtualFile vFile = file.getViewProvider().getVirtualFile();
    return FileDocumentManager.getInstance().getCachedDocument(vFile);
  }

  @Override
  public void commitAllDocuments() {
    ApplicationManager.getApplication().assertIsDispatchThread();
    if (myUncommittedDocuments.isEmpty()) return;

    final Document[] documents = getUncommittedDocuments();
    for (Document document : documents) {
      commitDocument(document);
    }

    LOG.assertTrue(!hasUncommitedDocuments(), myUncommittedDocuments);
  }

  @Override
  public void performForCommittedDocument(
      @NotNull final Document doc, @NotNull final Runnable action) {
    final Document document =
        doc instanceof DocumentWindow ? ((DocumentWindow) doc).getDelegate() : doc;
    if (isCommitted(document)) {
      action.run();
    } else {
      addRunOnCommit(document, action);
    }
  }

  private final Map<Object, Runnable> actionsWhenAllDocumentsAreCommitted =
      new LinkedHashMap<Object, Runnable>(); // accessed from EDT only
  private static final Object PERFORM_ALWAYS_KEY =
      new Object() {
        @Override
        @NonNls
        public String toString() {
          return "PERFORM_ALWAYS";
        }
      };

  /**
   * Cancel previously registered action and schedules (new) action to be executed when all
   * documents are committed.
   *
   * @param key the (unique) id of the action.
   * @param action The action to be executed after automatic commit. This action will overwrite any
   *     action which was registered under this key earlier. The action will be executed in EDT.
   * @return true if action has been run immediately, or false if action was scheduled for execution
   *     later.
   */
  public boolean cancelAndRunWhenAllCommitted(
      @NonNls @NotNull Object key, @NotNull final Runnable action) {
    ApplicationManager.getApplication().assertIsDispatchThread();
    if (myProject.isDisposed()) {
      action.run();
      return true;
    }
    if (myUncommittedDocuments.isEmpty()) {
      action.run();
      assert actionsWhenAllDocumentsAreCommitted.isEmpty() : actionsWhenAllDocumentsAreCommitted;
      return true;
    }
    actionsWhenAllDocumentsAreCommitted.put(key, action);
    return false;
  }

  public static void addRunOnCommit(@NotNull Document document, @NotNull Runnable action) {
    synchronized (ACTION_AFTER_COMMIT) {
      List<Runnable> list = document.getUserData(ACTION_AFTER_COMMIT);
      if (list == null) {
        document.putUserData(ACTION_AFTER_COMMIT, list = new SmartList<Runnable>());
      }
      list.add(action);
    }
  }

  @Override
  public void commitDocument(@NotNull final Document doc) {
    final Document document =
        doc instanceof DocumentWindow ? ((DocumentWindow) doc).getDelegate() : doc;
    if (!isCommitted(document)) {
      doCommit(document, null);
    }
  }

  public boolean finishCommit(
      @NotNull final Document document,
      @NotNull final List<Processor<Document>> finishProcessors,
      final boolean synchronously,
      @NotNull final Object reason) {
    assert !myProject.isDisposed() : "Already disposed";
    final boolean[] ok = {true};
    ApplicationManager.getApplication()
        .runWriteAction(
            new CommitToPsiFileAction(document, myProject) {
              @Override
              public void run() {
                ok[0] = finishCommitInWriteAction(document, finishProcessors, synchronously);
              }
            });

    if (ok[0]) {
      // otherwise changes maybe not synced to the document yet, and injectors will crash
      if (!mySynchronizer.isDocumentAffectedByTransactions(document)) {
        final InjectedLanguageManager injectedLanguageManager =
            InjectedLanguageManager.getInstance(myProject);
        if (injectedLanguageManager != null)
          injectedLanguageManager.startRunInjectors(document, synchronously);
      }
      // run after commit actions outside write action
      runAfterCommitActions(document);
      if (DebugUtil.DO_EXPENSIVE_CHECKS) {
        checkAllElementsValid(document, reason);
      }
    }
    return ok[0];
  }

  protected boolean finishCommitInWriteAction(
      @NotNull final Document document,
      @NotNull final List<Processor<Document>> finishProcessors,
      final boolean synchronously) {
    if (myProject.isDisposed()) return false;

    assert !(document instanceof DocumentWindow);
    myIsCommitInProgress = true;
    boolean success = true;
    try {
      final FileViewProvider viewProvider = getCachedViewProvider(document);
      if (viewProvider != null) {
        for (Processor<Document> finishRunnable : finishProcessors) {
          success = finishRunnable.process(document);
          if (synchronously) {
            assert success;
          }
          if (!success) {
            break;
          }
        }
        viewProvider.contentsSynchronized();
      }
    } finally {
      myDocumentCommitProcessor.log(
          "in PDI.finishDoc: ", null, synchronously, success, myUncommittedDocuments);
      if (success) {
        myUncommittedDocuments.remove(document);
        myDocumentCommitProcessor.log(
            "in PDI.finishDoc: removed doc", null, synchronously, success, myUncommittedDocuments);
      }
      myIsCommitInProgress = false;
      myDocumentCommitProcessor.log(
          "in PDI.finishDoc: exit", null, synchronously, success, myUncommittedDocuments);
    }

    return success;
  }

  private void checkAllElementsValid(@NotNull Document document, @NotNull final Object reason) {
    final PsiFile psiFile = getCachedPsiFile(document);
    if (psiFile != null) {
      psiFile.accept(
          new PsiRecursiveElementWalkingVisitor() {
            @Override
            public void visitElement(PsiElement element) {
              if (!element.isValid()) {
                throw new AssertionError(
                    "Commit to '"
                        + psiFile.getVirtualFile()
                        + "' has led to invalid element: "
                        + element
                        + "; Reason: '"
                        + reason
                        + "'");
              }
            }
          });
    }
  }

  protected void doCommit(@NotNull final Document document, final PsiFile excludeFile) {
    assert !myIsCommitInProgress : "Do not call commitDocument() from inside PSI change listener";
    ApplicationManager.getApplication()
        .runWriteAction(
            new Runnable() {
              @Override
              public void run() {
                // otherwise there are many clients calling commitAllDocs() on PSI childrenChanged()
                if (getSynchronizer().isDocumentAffectedByTransactions(document)
                    && excludeFile == null) return;

                myIsCommitInProgress = true;
                try {
                  myDocumentCommitProcessor.commitSynchronously(document, myProject, excludeFile);
                } finally {
                  myIsCommitInProgress = false;
                }
                assert !myUncommittedDocuments.contains(document) : "Document :" + document;
              }
            });
  }

  public void commitOtherFilesAssociatedWithDocument(
      final Document document, final PsiFile psiFile) {}

  @Override
  public <T> T commitAndRunReadAction(@NotNull final Computable<T> computation) {
    final Ref<T> ref = Ref.create(null);
    commitAndRunReadAction(
        new Runnable() {
          @Override
          public void run() {
            ref.set(computation.compute());
          }
        });
    return ref.get();
  }

  @Override
  public void commitAndRunReadAction(@NotNull final Runnable runnable) {
    final Application application = ApplicationManager.getApplication();
    if (SwingUtilities.isEventDispatchThread()) {
      commitAllDocuments();
      runnable.run();
    } else {
      LOG.assertTrue(
          !ApplicationManager.getApplication().isReadAccessAllowed(),
          "Don't call commitAndRunReadAction inside ReadAction, it will cause a deadlock otherwise.");

      final Semaphore s1 = new Semaphore();
      final Semaphore s2 = new Semaphore();
      final boolean[] committed = {false};

      application.runReadAction(
          new Runnable() {
            @Override
            public void run() {
              if (myUncommittedDocuments.isEmpty()) {
                runnable.run();
                committed[0] = true;
              } else {
                s1.down();
                s2.down();
                final Runnable commitRunnable =
                    new Runnable() {
                      @Override
                      public void run() {
                        commitAllDocuments();
                        s1.up();
                        s2.waitFor();
                      }
                    };
                final ProgressIndicator progressIndicator =
                    ProgressManager.getInstance().getProgressIndicator();
                if (progressIndicator == null) {
                  ApplicationManager.getApplication().invokeLater(commitRunnable);
                } else {
                  ApplicationManager.getApplication()
                      .invokeLater(commitRunnable, progressIndicator.getModalityState());
                }
              }
            }
          });

      if (!committed[0]) {
        s1.waitFor();
        application.runReadAction(
            new Runnable() {
              @Override
              public void run() {
                s2.up();
                runnable.run();
              }
            });
      }
    }
  }

  /**
   * Schedules action to be executed when all documents are committed.
   *
   * @return true if action has been run immediately, or false if action was scheduled for execution
   *     later.
   */
  @Override
  public boolean performWhenAllCommitted(@NotNull final Runnable action) {
    ApplicationManager.getApplication().assertIsDispatchThread();
    assert !myProject.isDisposed() : "Already disposed: " + myProject;
    if (myUncommittedDocuments.isEmpty()) {
      action.run();
      return true;
    }
    CompositeRunnable actions =
        (CompositeRunnable) actionsWhenAllDocumentsAreCommitted.get(PERFORM_ALWAYS_KEY);
    if (actions == null) {
      actions = new CompositeRunnable();
      actionsWhenAllDocumentsAreCommitted.put(PERFORM_ALWAYS_KEY, actions);
    }
    actions.add(action);
    myDocumentCommitProcessor.log(
        "PDI: added performWhenAllCommitted", null, false, action, myUncommittedDocuments);
    return false;
  }

  private static class CompositeRunnable extends ArrayList<Runnable> implements Runnable {
    @Override
    public void run() {
      for (Runnable runnable : this) {
        runnable.run();
      }
    }
  }

  private void runAfterCommitActions(@NotNull Document document) {
    ApplicationManager.getApplication().assertIsDispatchThread();
    List<Runnable> list;
    synchronized (ACTION_AFTER_COMMIT) {
      list = document.getUserData(ACTION_AFTER_COMMIT);
      if (list != null) {
        list = new ArrayList<Runnable>(list);
        document.putUserData(ACTION_AFTER_COMMIT, null);
      }
    }
    if (list != null) {
      for (final Runnable runnable : list) {
        runnable.run();
      }
    }

    if (!hasUncommitedDocuments() && !actionsWhenAllDocumentsAreCommitted.isEmpty()) {
      List<Object> keys = new ArrayList<Object>(actionsWhenAllDocumentsAreCommitted.keySet());
      for (Object key : keys) {
        Runnable action = actionsWhenAllDocumentsAreCommitted.remove(key);
        myDocumentCommitProcessor.log("Running after commit runnable: ", null, false, key, action);
        action.run();
      }
    }
  }

  @Override
  public void addListener(@NotNull Listener listener) {
    myListeners.add(listener);
  }

  @Override
  public void removeListener(@NotNull Listener listener) {
    myListeners.remove(listener);
  }

  @Override
  public boolean isDocumentBlockedByPsi(@NotNull Document doc) {
    return false;
  }

  @Override
  public void doPostponedOperationsAndUnblockDocument(@NotNull Document doc) {}

  protected void fireDocumentCreated(@NotNull Document document, PsiFile file) {
    for (Listener listener : myListeners) {
      listener.documentCreated(document, file);
    }
  }

  private void fireFileCreated(Document document, PsiFile file) {
    for (Listener listener : myListeners) {
      listener.fileCreated(file, document);
    }
  }

  @Override
  @NotNull
  public Document[] getUncommittedDocuments() {
    ApplicationManager.getApplication().assertIsDispatchThread();
    return myUncommittedDocuments.toArray(new Document[myUncommittedDocuments.size()]);
  }

  public Collection<Document> getUncommittedDocumentsUnsafe() {
    return myUncommittedDocuments;
  }

  @Override
  public boolean isUncommited(@NotNull Document document) {
    return !isCommitted(document);
  }

  @Override
  public boolean isCommitted(@NotNull Document document) {
    if (getSynchronizer().isInSynchronization(document)) return true;
    return !((DocumentEx) document).isInEventsHandling()
        && !myUncommittedDocuments.contains(document);
  }

  @Override
  public boolean hasUncommitedDocuments() {
    return !myIsCommitInProgress && !myUncommittedDocuments.isEmpty();
  }

  @Override
  public void beforeDocumentChange(DocumentEvent event) {
    final Document document = event.getDocument();

    final FileViewProvider viewProvider = getCachedViewProvider(document);
    if (viewProvider == null) return;
    if (!isRelevant(viewProvider)) return;

    VirtualFile virtualFile = viewProvider.getVirtualFile();
    if (virtualFile.getFileType().isBinary()) return;

    final List<PsiFile> files = viewProvider.getAllFiles();
    PsiFile psiCause = null;
    for (PsiFile file : files) {
      mySmartPointerManager.fastenBelts(file, event.getOffset(), null);

      if (TextBlock.get(file).isLocked()) {
        psiCause = file;
      }
    }

    if (psiCause == null) {
      beforeDocumentChangeOnUnlockedDocument(viewProvider);
    }

    ((SingleRootFileViewProvider) viewProvider).beforeDocumentChanged(psiCause);
  }

  protected void beforeDocumentChangeOnUnlockedDocument(
      @NotNull final FileViewProvider viewProvider) {}

  @Override
  public void documentChanged(DocumentEvent event) {
    final Document document = event.getDocument();
    final FileViewProvider viewProvider = getCachedViewProvider(document);
    if (viewProvider == null) return;
    if (!isRelevant(viewProvider)) return;

    ApplicationManager.getApplication().assertWriteAccessAllowed();
    final List<PsiFile> files = viewProvider.getAllFiles();
    boolean commitNecessary = true;
    for (PsiFile file : files) {
      mySmartPointerManager.unfastenBelts(file, event.getOffset());

      final TextBlock textBlock = TextBlock.get(file);
      if (textBlock.isLocked()) {
        commitNecessary = false;
        continue;
      }

      textBlock.documentChanged(event);
      assert file instanceof PsiFileImpl
              || "mock.file".equals(file.getName())
                  && ApplicationManager.getApplication().isUnitTestMode()
          : event + "; file=" + file + "; allFiles=" + files + "; viewProvider=" + viewProvider;
    }

    boolean forceCommit =
        ApplicationManager.getApplication().hasWriteAction(ExternalChangeAction.class)
            && (SystemProperties.getBooleanProperty("idea.force.commit.on.external.change", false)
                || ApplicationManager.getApplication().isHeadlessEnvironment());

    // Consider that it's worth to perform complete re-parse instead of merge if the whole document
    // text is replaced and
    // current document lines number is roughly above 5000. This makes sense in situations when
    // external change is performed
    // for the huge file (that causes the whole document to be reloaded and 'merge' way takes a
    // while to complete).
    if (event.isWholeTextReplaced() && document.getTextLength() > 100000) {
      document.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, Boolean.TRUE);
    }

    if (commitNecessary) {
      myUncommittedDocuments.add(document);
      if (forceCommit) {
        commitDocument(document);
      } else if (!((DocumentEx) document).isInBulkUpdate()) {
        myDocumentCommitProcessor.commitAsynchronously(myProject, document, event);
      }
    }
  }

  private boolean isRelevant(@NotNull FileViewProvider viewProvider) {
    VirtualFile virtualFile = viewProvider.getVirtualFile();
    return !virtualFile.getFileType().isBinary()
        && viewProvider.getManager() == myPsiManager
        && !myPsiManager.getProject().isDisposed();
  }

  public static boolean checkConsistency(PsiFile psiFile, Document document) {
    // todo hack
    if (psiFile.getVirtualFile() == null) return true;

    CharSequence editorText = document.getCharsSequence();
    int documentLength = document.getTextLength();
    if (psiFile.textMatches(editorText)) {
      LOG.assertTrue(psiFile.getTextLength() == documentLength);
      return true;
    }

    char[] fileText = psiFile.textToCharArray();
    @SuppressWarnings({"NonConstantStringShouldBeStringBuffer"})
    @NonNls
    String error =
        "File '"
            + psiFile.getName()
            + "' text mismatch after reparse. "
            + "File length="
            + fileText.length
            + "; Doc length="
            + documentLength
            + "\n";
    int i = 0;
    for (; i < documentLength; i++) {
      if (i >= fileText.length) {
        error += "editorText.length > psiText.length i=" + i + "\n";
        break;
      }
      if (i >= editorText.length()) {
        error += "editorText.length > psiText.length i=" + i + "\n";
        break;
      }
      if (editorText.charAt(i) != fileText[i]) {
        error += "first unequal char i=" + i + "\n";
        break;
      }
    }
    // error += "*********************************************" + "\n";
    // if (i <= 500){
    //  error += "Equal part:" + editorText.subSequence(0, i) + "\n";
    // }
    // else{
    //  error += "Equal part start:\n" + editorText.subSequence(0, 200) + "\n";
    //  error += "................................................" + "\n";
    //  error += "................................................" + "\n";
    //  error += "................................................" + "\n";
    //  error += "Equal part end:\n" + editorText.subSequence(i - 200, i) + "\n";
    // }
    error += "*********************************************" + "\n";
    error +=
        "Editor Text tail:("
            + (documentLength - i)
            + ")\n"; // + editorText.subSequence(i, Math.min(i + 300, documentLength)) + "\n";
    error += "*********************************************" + "\n";
    error += "Psi Text tail:(" + (fileText.length - i) + ")\n";
    error += "*********************************************" + "\n";

    if (document instanceof DocumentWindow) {
      error += "doc: '" + document.getText() + "'\n";
      error += "psi: '" + psiFile.getText() + "'\n";
      error += "ast: '" + psiFile.getNode().getText() + "'\n";
      error += psiFile.getLanguage() + "\n";
      PsiElement context =
          InjectedLanguageManager.getInstance(psiFile.getProject()).getInjectionHost(psiFile);
      if (context != null) {
        error += "context: " + context + "; text: '" + context.getText() + "'\n";
        error += "context file: " + context.getContainingFile() + "\n";
      }
      error +=
          "document window ranges: "
              + Arrays.asList(((DocumentWindow) document).getHostRanges())
              + "\n";
    }
    LOG.error(error);
    // document.replaceString(0, documentLength, psiFile.getText());
    return false;
  }

  @TestOnly
  public void clearUncommittedDocuments() {
    myUncommittedDocuments.clear();
    mySynchronizer.cleanupForNextTest();
  }

  public PsiToDocumentSynchronizer getSynchronizer() {
    return mySynchronizer;
  }
}
Esempio n. 3
0
/**
 * Discards up or down messages based on a percentage; e.g., setting property 'up' to 0.1 causes 10%
 * of all up messages to be discarded. Setting 'down' or 'up' to 0 causes no loss, whereas 1
 * discards all messages (not very useful).
 */
@Unsupported
@MBean(description = "Discards messages")
public class DISCARD extends Protocol {
  @Property protected double up = 0.0; // probability of dropping up   msgs

  @Property protected double down = 0.0; // probability of dropping down msgs

  @Property
  protected boolean excludeItself =
      true; // if true don't discard messages sent/received in this stack

  protected Address localAddress;

  @ManagedAttribute(description = "Number of dropped down messages", name = "dropped_down_messages")
  protected int num_down = 0;

  @ManagedAttribute(description = "Number of dropped up messages", name = "dropped_up_messages")
  protected int num_up = 0;

  protected final Set<Address> ignoredMembers = Collections.synchronizedSet(new HashSet<>());

  protected final Collection<Address> members = Collections.synchronizedList(new ArrayList<>());

  @Property(description = "drop all messages (up or down)", writable = true)
  protected boolean discard_all = false;

  @Property(
      description = "Number of subsequent unicasts to drop in the down direction",
      writable = true)
  protected int drop_down_unicasts = 0;

  @Property(
      description = "Number of subsequent multicasts to drop in the down direction",
      writable = true)
  protected int drop_down_multicasts = 0;

  protected DiscardDialog discard_dialog = null;

  @Property(name = "gui", description = "use a GUI or not")
  protected boolean use_gui = false;

  public DISCARD localAddress(Address addr) {
    setLocalAddress(addr);
    return this;
  }

  public Address localAddress() {
    if (localAddress == null)
      localAddress = (Address) up_prot.up(new Event(Event.GET_LOCAL_ADDRESS));
    return localAddress;
  }

  public boolean isDiscardAll() {
    return discard_all;
  }

  public DISCARD setDiscardAll(boolean discard_all) {
    this.discard_all = discard_all;
    return this;
  }

  public boolean isExcludeItself() {
    return excludeItself;
  }

  public DISCARD setLocalAddress(Address localAddress) {
    this.localAddress = localAddress;
    if (discard_dialog != null)
      discard_dialog.setTitle(localAddress != null ? localAddress.toString() : "n/a");
    return this;
  }

  public DISCARD setExcludeItself(boolean excludeItself) {
    this.excludeItself = excludeItself;
    return this;
  }

  public double getUpDiscardRate() {
    return up;
  }

  public DISCARD setUpDiscardRate(double up) {
    this.up = up;
    return this;
  }

  public double getDownDiscardRate() {
    return down;
  }

  public DISCARD setDownDiscardRate(double down) {
    this.down = down;
    return this;
  }

  public int getDropDownUnicasts() {
    return drop_down_unicasts;
  }

  /**
   * Drop the next N unicasts down the stack
   *
   * @param drop_down_unicasts
   */
  public DISCARD setDropDownUnicasts(int drop_down_unicasts) {
    this.drop_down_unicasts = drop_down_unicasts;
    return this;
  }

  public int getDropDownMulticasts() {
    return drop_down_multicasts;
  }

  public DISCARD setDropDownMulticasts(int drop_down_multicasts) {
    this.drop_down_multicasts = drop_down_multicasts;
    return this;
  }

  /** Messages from this sender will get dropped */
  public DISCARD addIgnoreMember(Address sender) {
    ignoredMembers.add(sender);
    return this;
  }

  public DISCARD removeIgnoredMember(Address member) {
    ignoredMembers.remove(member);
    return this;
  }

  public DISCARD resetIgnoredMembers() {
    ignoredMembers.clear();
    return this;
  }

  @ManagedOperation
  public void startGui() {
    if (discard_dialog == null) {
      discard_dialog = new DiscardDialog();
      discard_dialog.init();
      discard_dialog.setTitle(localAddress() != null ? localAddress().toString() : "n/a");
      discard_dialog.handleView(members);
    }
  }

  @ManagedOperation
  public void stopGui() {
    if (discard_dialog != null) discard_dialog.dispose();
    discard_dialog = null;
  }

  public void start() throws Exception {
    super.start();
    if (use_gui) {
      discard_dialog = new DiscardDialog();
      discard_dialog.init();
    }
  }

  public void stop() {
    super.stop();
    if (discard_dialog != null) discard_dialog.dispose();
  }

  public Object up(Event evt) {
    if (evt.getType() == Event.SET_LOCAL_ADDRESS) {
      localAddress = evt.getArg();
      if (discard_dialog != null) discard_dialog.setTitle("Discard dialog (" + localAddress + ")");
    }
    return up_prot.up(evt);
  }

  public Object up(Message msg) {
    if (shouldDropUpMessage(msg, msg.getSrc())) return null;
    return up_prot.up(msg);
  }

  public void up(MessageBatch batch) {
    for (Iterator<Message> it = batch.iterator(); it.hasNext(); ) {
      Message msg = it.next();
      if (msg != null && shouldDropUpMessage(msg, msg.getSrc())) it.remove();
    }
    if (!batch.isEmpty()) up_prot.up(batch);
  }

  public Object down(Event evt) {
    switch (evt.getType()) {
      case Event.VIEW_CHANGE:
        View view = evt.getArg();
        List<Address> mbrs = view.getMembers();
        members.clear();
        members.addAll(mbrs);
        //                ignoredMembers.retainAll(mbrs); // remove all non members
        if (discard_dialog != null) discard_dialog.handleView(mbrs);
        break;

      case Event.SET_LOCAL_ADDRESS:
        localAddress = evt.getArg();
        if (discard_dialog != null)
          discard_dialog.setTitle("Discard dialog (" + localAddress + ")");
        break;
      case Event.GET_PING_DATA:
        if (discard_all) return null;
        break;
    }
    return down_prot.down(evt);
  }

  public Object down(Message msg) {
    Address dest = msg.getDest();
    boolean multicast = dest == null;

    if (msg.getSrc() == null) msg.setSrc(localAddress());

    if (discard_all) {
      if (dest == null || dest.equals(localAddress())) loopback(msg);
      return null;
    }

    if (!multicast && drop_down_unicasts > 0) {
      drop_down_unicasts = Math.max(0, drop_down_unicasts - 1);
      return null;
    }

    if (multicast && drop_down_multicasts > 0) {
      drop_down_multicasts = Math.max(0, drop_down_multicasts - 1);
      return null;
    }

    if (down > 0) {
      double r = Math.random();
      if (r < down) {
        if (excludeItself && dest != null && dest.equals(localAddress())) {
          if (log.isTraceEnabled()) log.trace("excluding itself");
        } else {
          log.trace("dropping message");
          num_down++;
          return null;
        }
      }
    }
    return down_prot.down(msg);
  }

  /** Checks if a message should be passed up, or not */
  protected boolean shouldDropUpMessage(
      @SuppressWarnings("UnusedParameters") Message msg, Address sender) {
    if (discard_all && !sender.equals(localAddress())) return true;

    if (ignoredMembers.contains(sender)) {
      if (log.isTraceEnabled()) log.trace(localAddress + ": dropping message from " + sender);
      num_up++;
      return true;
    }

    if (up > 0) {
      double r = Math.random();
      if (r < up) {
        if (excludeItself && sender.equals(localAddress())) {
          if (log.isTraceEnabled()) log.trace("excluding myself");
        } else {
          if (log.isTraceEnabled()) log.trace(localAddress + ": dropping message from " + sender);
          num_up++;
          return true;
        }
      }
    }

    return false;
  }

  private void loopback(Message msg) {
    final Message rsp = msg.copy(true);
    if (rsp.getSrc() == null) rsp.setSrc(localAddress());

    // pretty inefficient: creates one thread per message, okay for testing only
    Thread thread =
        new Thread(
            () -> {
              up_prot.up(rsp);
            });
    thread.start();
  }

  public void resetStats() {
    super.resetStats();
    num_down = num_up = 0;
  }

  protected class DiscardDialog extends JFrame implements ActionListener {
    private final JButton start_discarding_button = new JButton("start discarding");
    private final JButton stop_discarding_button = new JButton("stop discarding");
    private final JPanel checkboxes = new JPanel();

    protected DiscardDialog() {}

    void init() {
      getContentPane().setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS));
      checkboxes.setLayout(new BoxLayout(checkboxes, BoxLayout.Y_AXIS));
      getContentPane().add(start_discarding_button);
      getContentPane().add(stop_discarding_button);
      start_discarding_button.addActionListener(this);
      stop_discarding_button.addActionListener(this);
      getContentPane().add(checkboxes);
      pack();
      setVisible(true);
      setTitle(localAddress() != null ? localAddress().toString() : "n/a");
    }

    public void actionPerformed(ActionEvent e) {
      String command = e.getActionCommand();
      if (command.startsWith("start")) {
        discard_all = true;
      } else if (command.startsWith("stop")) {
        discard_all = false;
        Component[] comps = checkboxes.getComponents();
        for (Component c : comps) {
          if (c instanceof JCheckBox) {
            ((JCheckBox) c).setSelected(false);
          }
        }
        ignoredMembers.clear();
      }
    }

    void handleView(Collection<Address> mbrs) {
      checkboxes.removeAll();
      for (final Address addr : mbrs) {
        final MyCheckBox box = new MyCheckBox("discard traffic from " + addr, addr);
        box.addActionListener(
            e -> {
              if (box.isSelected()) ignoredMembers.add(addr);
              else ignoredMembers.remove(addr);
            });
        checkboxes.add(box);
      }

      for (Component comp : checkboxes.getComponents()) {
        MyCheckBox box = (MyCheckBox) comp;
        if (ignoredMembers.contains(box.mbr)) box.setSelected(true);
      }
      pack();
    }
  }

  private static class MyCheckBox extends JCheckBox {
    final Address mbr;

    public MyCheckBox(String name, Address member) {
      super(name);
      this.mbr = member;
    }

    public String toString() {
      return super.toString() + " [mbr=" + mbr + "]";
    }
  }
}