Move auxclasspath in view menu, record new exceptions

This commit is contained in:
Clément Fournier
2019-01-15 10:27:43 +01:00
parent 8b30cee921
commit efec05fd76
8 changed files with 246 additions and 90 deletions

View File

@ -138,8 +138,8 @@ public class MainDesignerController extends AbstractController {
openEventLogMenuItem.setOnAction(e -> eventLogController.showPopup());
openEventLogMenuItem.textProperty().bind(
eventLogController.numLogEntriesProperty()
.map(i -> "Open event log (" + i + " entries)"));
designerRoot.getLogger().numNewLogEntriesProperty().map(i -> "Exception log (" + (i > 0 ? i : "no") + " new)")
);
}

View File

@ -4,30 +4,87 @@
package net.sourceforge.pmd.util.fxdesigner.model;
import java.util.Objects;
import static net.sourceforge.pmd.util.fxdesigner.model.LogEntry.Category.PARSE_EXCEPTION;
import static net.sourceforge.pmd.util.fxdesigner.model.LogEntry.Category.PARSE_OK;
import static net.sourceforge.pmd.util.fxdesigner.model.LogEntry.Category.XPATH_EVALUATION_EXCEPTION;
import static net.sourceforge.pmd.util.fxdesigner.model.LogEntry.Category.XPATH_OK;
import java.time.Duration;
import java.util.EnumSet;
import org.reactfx.EventSource;
import org.reactfx.EventStream;
import org.reactfx.EventStreams;
import org.reactfx.collection.LiveArrayList;
import org.reactfx.collection.LiveList;
import org.reactfx.value.Val;
import net.sourceforge.pmd.util.fxdesigner.model.LogEntry.Category;
import net.sourceforge.pmd.util.fxdesigner.util.DesignerUtil;
/**
* Logs events.
* Logs events. Stores the whole log in case no view was open.
*
* @author Clément Fournier
* @since 6.0.0
*/
public class EventLogger {
/**
* Exceptions from XPath evaluation or parsing are never emitted
* within less than that time interval to keep them from flooding the tableview.
*/
private static final Duration PARSE_EXCEPTION_DELAY = Duration.ofMillis(3000);
private final EventSource<LogEntry> latestEvent = new EventSource<>();
private final LiveList<LogEntry> fullLog = new LiveArrayList<>();
public EventLogger() {
EventStream<LogEntry> onlyParseException =
latestEvent.filter(x -> x.getCategory() == PARSE_EXCEPTION || x.getCategory() == PARSE_OK)
.successionEnds(PARSE_EXCEPTION_DELAY)
// don't output anything when the last state recorded was OK
.filter(x -> x.getCategory() != PARSE_OK);
EventStream<LogEntry> onlyXPathException =
latestEvent.filter(x -> x.getCategory() == XPATH_EVALUATION_EXCEPTION || x.getCategory() == XPATH_OK)
.successionEnds(PARSE_EXCEPTION_DELAY)
// don't output anything when the last state recorded was OK
.filter(x -> x.getCategory() != XPATH_OK);
EnumSet<Category> otherExceptionSet = EnumSet.complementOf(EnumSet.of(PARSE_EXCEPTION, XPATH_EVALUATION_EXCEPTION, PARSE_OK, XPATH_OK));
EventStream<LogEntry> otherExceptions = latestEvent.filter(x -> otherExceptionSet.contains(x.getCategory()));
EventStreams.merge(onlyParseException, otherExceptions, onlyXPathException)
.subscribe(fullLog::add);
}
/** Number of log entries that were not yet examined by the user. */
public Val<Integer> numNewLogEntriesProperty() {
return DesignerUtil.countNotMatching(fullLog.map(LogEntry::wasExaminedProperty));
}
/** Total number of log entries. */
public Val<Integer> numLogEntriesProperty() {
return fullLog.sizeProperty();
}
public void logEvent(LogEntry event) {
latestEvent.push(event);
if (event != null) {
latestEvent.push(event);
}
}
/**
* Returns a stream that emits an event each time an exception is logged by some
* part of the application.
* Returns the full log.
*/
public EventStream<LogEntry> getLog() {
return latestEvent.filter(Objects::nonNull);
public LiveList<LogEntry> getLog() {
return fullLog;
}
}

View File

@ -7,6 +7,8 @@ package net.sourceforge.pmd.util.fxdesigner.model;
import java.util.Date;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.reactfx.value.Var;
/**
* Log entry of an {@link EventLogger}.
@ -20,7 +22,7 @@ public class LogEntry {
private final Throwable throwable;
private final Category category;
private final Date timestamp;
private Var<Boolean> wasExamined = Var.newSimpleVar(false);
public LogEntry(Throwable thrown, Category cat) {
this.throwable = thrown;
@ -29,6 +31,20 @@ public class LogEntry {
}
public boolean isWasExamined() {
return wasExamined.getValue();
}
public void setExamined(boolean wasExamined) {
this.wasExamined.setValue(wasExamined);
}
public Var<Boolean> wasExaminedProperty() {
return wasExamined;
}
public Throwable getThrown() {
return throwable;
}

View File

@ -4,24 +4,16 @@
package net.sourceforge.pmd.util.fxdesigner.popups;
import static net.sourceforge.pmd.util.fxdesigner.model.LogEntry.Category.PARSE_EXCEPTION;
import static net.sourceforge.pmd.util.fxdesigner.model.LogEntry.Category.PARSE_OK;
import static net.sourceforge.pmd.util.fxdesigner.model.LogEntry.Category.XPATH_EVALUATION_EXCEPTION;
import static net.sourceforge.pmd.util.fxdesigner.model.LogEntry.Category.XPATH_OK;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import org.reactfx.EventStream;
import org.kordamp.ikonli.javafx.FontIcon;
import org.reactfx.EventStreams;
import org.reactfx.value.Val;
import org.reactfx.Subscription;
import org.reactfx.value.Var;
import net.sourceforge.pmd.lang.ast.Node;
@ -34,6 +26,9 @@ import net.sourceforge.pmd.util.fxdesigner.util.DesignerUtil;
import net.sourceforge.pmd.util.fxdesigner.util.SoftReferenceCache;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.css.PseudoClass;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
@ -41,6 +36,7 @@ import javafx.scene.Scene;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.SortType;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.TextArea;
import javafx.scene.control.cell.PropertyValueFactory;
@ -49,6 +45,8 @@ import javafx.stage.Stage;
/**
* A presenter over the {@link net.sourceforge.pmd.util.fxdesigner.model.EventLogger}.
*
* @author Clément Fournier
* @since 6.0.0
*/
@ -59,6 +57,7 @@ public class EventLogController extends AbstractController {
* within less than that time interval to keep them from flooding the tableview.
*/
private static final Duration PARSE_EXCEPTION_DELAY = Duration.ofMillis(3000);
private static final PseudoClass NEW_ENTRY = PseudoClass.getPseudoClass("new-entry");
private final DesignerRoot designerRoot;
private final MainDesignerController mediator;
@ -66,7 +65,7 @@ public class EventLogController extends AbstractController {
@FXML
private TableView<LogEntry> eventLogTableView;
@FXML
private TableColumn<LogEntry, Date> logDateColumn;
private TableColumn<LogEntry, LogEntry> logDateColumn;
@FXML
private TableColumn<LogEntry, Category> logCategoryColumn;
@FXML
@ -79,7 +78,9 @@ public class EventLogController extends AbstractController {
private SoftReferenceCache<Stage> popupStageCache = new SoftReferenceCache<>(this::createStage);
private Var<Integer> numLogEntries = Var.newSimpleVar(0);
// subscription allowing to unbind from the popup. If that's not done,
// the popup fxml is always bound to the controller we have a memory leak
private Subscription popupBinding = () -> {};
public EventLogController(DesignerRoot owner, MainDesignerController mediator) {
this.designerRoot = owner;
@ -87,66 +88,41 @@ public class EventLogController extends AbstractController {
}
// this is only called each time a popup is created
@Override
protected void beforeParentInit() {
popupBinding.unsubscribe();
logCategoryColumn.setCellValueFactory(new PropertyValueFactory<>("category"));
logMessageColumn.setCellValueFactory(new PropertyValueFactory<>("message"));
final DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
logDateColumn.setCellValueFactory(entry -> new SimpleObjectProperty<>(entry.getValue().getTimestamp()));
logDateColumn.setCellFactory(column -> new TableCell<LogEntry, Date>() {
logDateColumn.setCellValueFactory(entry -> new SimpleObjectProperty<>(entry.getValue()));
logDateColumn.setCellFactory(column -> new TableCell<LogEntry, LogEntry>() {
Subscription sub = null;
@Override
protected void updateItem(Date item, boolean empty) {
protected void updateItem(LogEntry item, boolean empty) {
super.updateItem(item, empty);
if (sub != null) {
sub.unsubscribe();
}
if (item == null || empty) {
setText(null);
setGraphic(null);
} else {
setText(dateFormat.format(item));
setText(dateFormat.format(item.getTimestamp()));
sub = item.wasExaminedProperty()
.map(wasExamined -> wasExamined ? null : new FontIcon("fas-exclamation-circle"))
.values()
.subscribe(graphicProperty()::setValue);
}
}
});
EventStream<LogEntry> onlyParseException = designerRoot.getLogger().getLog()
.filter(x -> x.getCategory() == PARSE_EXCEPTION || x.getCategory() == PARSE_OK)
.successionEnds(PARSE_EXCEPTION_DELAY)
// don't output anything when the last state recorded was OK
.filter(x -> x.getCategory() != PARSE_OK);
EventStream<LogEntry> onlyXPathException = designerRoot.getLogger().getLog()
.filter(x -> x.getCategory() == XPATH_EVALUATION_EXCEPTION || x.getCategory() == XPATH_OK)
.successionEnds(PARSE_EXCEPTION_DELAY)
// don't output anything when the last state recorded was OK
.filter(x -> x.getCategory() != XPATH_OK);
EnumSet<Category> otherExceptionSet = EnumSet.complementOf(EnumSet.of(PARSE_EXCEPTION, XPATH_EVALUATION_EXCEPTION, PARSE_OK, XPATH_OK));
EventStream<LogEntry> otherExceptions = designerRoot.getLogger().getLog()
.filter(x -> otherExceptionSet.contains(x.getCategory()));
EventStreams.merge(onlyParseException, otherExceptions, onlyXPathException)
.hook(e -> numLogEntries.setValue(numLogEntries.getValue() + 1))
.subscribe(t -> eventLogTableView.getItems().add(t));
eventLogTableView.getSelectionModel()
.selectedItemProperty()
.addListener((obs, oldVal, newVal) -> onExceptionSelectionChanges(oldVal, newVal));
EventStreams.combine(EventStreams.changesOf(eventLogTableView.focusedProperty()),
EventStreams.changesOf(selectedErrorNodes));
EventStreams.valuesOf(eventLogTableView.focusedProperty())
.successionEnds(Duration.ofMillis(100))
.subscribe(b -> {
if (b) {
mediator.handleSelectedNodeInError(selectedErrorNodes.getValue());
} else {
mediator.resetSelectedErrorNodes();
}
});
selectedErrorNodes.values().subscribe(mediator::handleSelectedNodeInError);
eventLogTableView.resizeColumn(logMessageColumn, -1);
logMessageColumn.prefWidthProperty()
@ -156,6 +132,55 @@ public class EventLogController extends AbstractController {
.subtract(2)); // makes it work
logDateColumn.setSortType(SortType.DESCENDING);
eventLogTableView.setRowFactory(tv -> {
TableRow<LogEntry> row = new TableRow<>();
ChangeListener<Boolean> examinedListener = (obs, oldVal, newVal) -> row.pseudoClassStateChanged(NEW_ENTRY, !newVal);
row.itemProperty().addListener((obs, previousEntry, currentEntry) -> {
if (previousEntry != null) {
previousEntry.wasExaminedProperty().removeListener(examinedListener);
}
if (currentEntry != null) {
currentEntry.wasExaminedProperty().addListener(examinedListener);
row.pseudoClassStateChanged(NEW_ENTRY, !currentEntry.isWasExamined());
} else {
row.pseudoClassStateChanged(NEW_ENTRY, false);
}
});
return row;
});
}
private Subscription bindPopupToController() {
Subscription binding =
EventStreams.valuesOf(eventLogTableView.getSelectionModel().selectedItemProperty())
.distinct()
.subscribe(this::onExceptionSelectionChanges);
binding = binding.and(
EventStreams.valuesOf(eventLogTableView.focusedProperty())
.successionEnds(Duration.ofMillis(100))
.subscribe(b -> {
if (b) {
mediator.handleSelectedNodeInError(selectedErrorNodes.getValue());
} else {
mediator.resetSelectedErrorNodes();
}
})
);
binding = binding.and(
selectedErrorNodes.values().subscribe(mediator::handleSelectedNodeInError)
);
eventLogTableView.itemsProperty().setValue(designerRoot.getLogger().getLog());
binding = binding.and(
() -> eventLogTableView.itemsProperty().setValue(FXCollections.emptyObservableList())
);
return binding;
}
@ -164,6 +189,9 @@ public class EventLogController extends AbstractController {
selectedErrorNodes.setValue(Collections.emptyList());
return;
}
entry.setExamined(true);
switch (entry.getCategory()) {
case OTHER:
break;
@ -182,25 +210,22 @@ public class EventLogController extends AbstractController {
public void showPopup() {
popupStageCache.getValue().show();
popupBinding = bindPopupToController();
}
public void hidePopup() {
popupStageCache.getValue().hide();
}
private void onExceptionSelectionChanges(LogEntry oldVal, LogEntry newVal) {
logDetailsTextArea.setText(newVal == null ? "" : newVal.getStackTrace());
if (!Objects.equals(newVal, oldVal)) {
handleSelectedEntry(newVal);
if (popupStageCache.hasValue()) {
popupStageCache.getValue().hide();
popupBinding.unsubscribe();
popupBinding = () -> {};
}
}
public Val<Integer> numLogEntriesProperty() {
return numLogEntries;
private void onExceptionSelectionChanges(LogEntry newVal) {
logDetailsTextArea.setText(newVal == null ? "" : newVal.getStackTrace());
handleSelectedEntry(newVal);
}

View File

@ -15,14 +15,21 @@ import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.reactfx.EventStream;
import org.reactfx.EventStreams;
import org.reactfx.collection.LiveList;
import org.reactfx.value.Val;
import org.reactfx.value.Var;
import net.sourceforge.pmd.lang.Language;
@ -34,6 +41,7 @@ import net.sourceforge.pmd.lang.rule.xpath.XPathRuleQuery;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.ToggleGroup;
@ -263,4 +271,41 @@ public final class DesignerUtil {
public static Var<Boolean> booleanVar(BooleanProperty p) {
return Var.mapBidirectional(p, Boolean::booleanValue, Function.identity());
}
// Creating a real function Val<LiveList<T>> => LiveList<T> or LiveList<Val<T>> => LiveList<T> would
// allow implementing LiveList.flatMap, which is a long-standing feature request in ReactFX
// These utilities are very inefficient, but sufficient for our use case...
public static <T> Val<LiveList<T>> flatMapChanges(ObservableList<? extends ObservableValue<T>> listOfObservables) {
// every time an element changes an invalidation stream
EventStream<?> invalidations =
LiveList.map(listOfObservables, EventStreams::valuesOf)
.reduce(EventStreams::merge)
.values()
.filter(Objects::nonNull)
.flatMap(Function.identity());
return Val.create(() -> LiveList.map(listOfObservables, ObservableValue::getValue), invalidations);
}
public static <T, U> Val<U> reduceWElts(ObservableList<? extends ObservableValue<T>> list, U zero, BiFunction<U, T, U> mapper) {
return flatMapChanges(list).map(l -> l.stream().reduce(zero, mapper, (u, v) -> v));
}
public static <T> Val<Integer> countMatching(ObservableList<? extends ObservableValue<T>> list, Predicate<? super T> predicate) {
return reduceWElts(list, 0, (cur, t) -> predicate.test(t) ? cur + 1 : cur);
}
public static Val<Integer> countMatching(ObservableList<? extends ObservableValue<Boolean>> list) {
return countMatching(list, b -> b);
}
public static Val<Integer> countNotMatching(ObservableList<? extends ObservableValue<Boolean>> list) {
return countMatching(list, b -> !b);
}
}

View File

@ -23,6 +23,10 @@ public class SoftReferenceCache<T> {
}
public boolean hasValue() {
return myRef != null && myRef.get() != null;
}
public T getValue() {
if (myRef == null || myRef.get() == null) {
T val = mySupplier.get();

View File

@ -19,6 +19,7 @@
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Pane?>
<?import org.kordamp.ikonli.javafx.FontIcon?>
<AnchorPane prefHeight="750.0" prefWidth="1200.0" stylesheets="@../css/designer.css" xmlns="http://javafx.com/javafx/8.0.172-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="net.sourceforge.pmd.util.fxdesigner.MainDesignerController">
<children>
<BorderPane prefHeight="600.0" prefWidth="900.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
@ -27,7 +28,7 @@
<children>
<MenuBar layoutX="-11.0" maxHeight="20.0" prefHeight="20.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="-11.0" AnchorPane.rightAnchor="11.0" AnchorPane.topAnchor="0.0">
<menus>
<Menu fx:id="fileMenu" mnemonicParsing="false" text="File">
<Menu fx:id="fileMenu" text="File">
<items>
<MenuItem fx:id="openFileMenuItem" mnemonicParsing="false" text="Open..." />
<Menu fx:id="openRecentMenu" mnemonicParsing="false" text="Open recent" />
@ -35,26 +36,30 @@
</Menu>
<Menu mnemonicParsing="false" text="View">
<Menu text="View">
<items>
<MenuItem fx:id="openEventLogMenuItem" mnemonicParsing="false"
text="Open event log...">
<MenuItem fx:id="openEventLogMenuItem" mnemonicParsing="false">
<graphic>
<!-- Font awesome 5.5.0 -->
<!--<FontIcon iconLiteral="fas-bolt" />-->
</graphic>
</MenuItem>
<MenuItem fx:id="setupAuxclasspathMenuItem"
mnemonicParsing="false"
text="Analysis classpath">
<graphic>
<!-- TODO Font awesome 5.0.1 (apparently not supported) -->
<!--<FontIcon iconLiteral="fab-java"/>-->
</graphic>
</MenuItem>
</items>
</Menu>
<Menu mnemonicParsing="false" text="Configuration">
<items>
<MenuItem fx:id="setupAuxclasspathMenuItem" mnemonicParsing="false" text="Set up the auxclasspath...">
</MenuItem>
</items>
</Menu>
<Menu mnemonicParsing="false" text="About">
<Menu text="About">
<items>
<!-- TODO add link to doc pages -->
<!--<MenuItem mnemonicParsing="false" text="About" />-->
<MenuItem fx:id="licenseMenuItem" mnemonicParsing="false" text="License..." />
<MenuItem fx:id="licenseMenuItem" mnemonicParsing="false" text="License" />
</items>
</Menu>

View File

@ -61,6 +61,10 @@
}
}
.table-row-cell:new-entry {
-fx-background-color: lightyellow;
}
.show-hide-columns-button {
-fx-background-color: @darker-accent;
-fx-border-color: @darker-accent-border;