Java tutorial
/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * Copyright (c) 2016 Pixida GmbH */ package de.pixida.logtest.designer.logreader; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.StringReader; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.SortedMap; import java.util.function.Consumer; import java.util.function.Supplier; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.tuple.Triple; import org.json.JSONException; import org.json.JSONObject; import de.pixida.logtest.designer.Editor; import de.pixida.logtest.designer.IMainWindow; import de.pixida.logtest.designer.commons.ExceptionDialog; import de.pixida.logtest.designer.commons.Icons; import de.pixida.logtest.designer.commons.SelectFileButton; import de.pixida.logtest.logreaders.GenericLogReader; import de.pixida.logtest.logreaders.GenericLogReader.HandlingOfNonHeadlineLines; import de.pixida.logtest.logreaders.ILogEntry; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.ChoiceBox; import javafx.scene.control.RadioButton; import javafx.scene.control.ScrollPane; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.control.TitledPane; import javafx.scene.control.ToggleGroup; import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; import javafx.util.StringConverter; public class LogReaderEditor extends Editor { public static final Charset LOG_READER_CONFIG_ENCODING = StandardCharsets.UTF_8; public static final String LOG_FILE_ICON_NAME = "page_white_text"; private GenericLogReader logReader; private TableView<LogEntryTableRow> parsedLogEntries = new TableView<>(); private final ObservableList<LogEntryTableRow> parsedLogEntryItems = FXCollections .observableList(new ArrayList<LogEntryTableRow>()); public static class LogEntryTableRow { private final ILogEntry logEntry; private final StringProperty lineNumber; private final StringProperty time; private final StringProperty channel; private final StringProperty payload; LogEntryTableRow(final ILogEntry aLogEntry) { this.logEntry = aLogEntry; this.lineNumber = new SimpleStringProperty(this, "lineNumber", String.valueOf(this.logEntry.getLineNumber())); this.time = new SimpleStringProperty(this, "time", String.valueOf(this.logEntry.getTime())); this.channel = new SimpleStringProperty(this, "channel", StringUtils.defaultIfEmpty(this.logEntry.getChannel(), "[default]")); this.payload = new SimpleStringProperty(this, "payload", this.logEntry.getPayload()); } public StringProperty lineNumberProperty() { return this.lineNumber; } public StringProperty timeProperty() { return this.time; } public StringProperty channelProperty() { return this.channel; } public StringProperty payloadProperty() { return this.payload; } } public LogReaderEditor(final IMainWindow mainWindow) { super(Editor.Type.LOG_READER_CONFIG, mainWindow); this.parsedLogEntries = new TableView<>(this.parsedLogEntryItems); this.parsedLogEntries.setPlaceholder(new Text("No log entries to display.")); final TableColumn<LogEntryTableRow, String> lineNoCol = new TableColumn<LogEntryTableRow, String>("LineNo"); lineNoCol.setCellValueFactory(new PropertyValueFactory<>("lineNumber")); lineNoCol.setSortable(false); lineNoCol.setGraphic(Icons.getIconGraphics("key")); final TableColumn<LogEntryTableRow, String> timeCol = new TableColumn<LogEntryTableRow, String>("Time"); timeCol.setCellValueFactory(new PropertyValueFactory<>("time")); timeCol.setSortable(false); timeCol.setGraphic(Icons.getIconGraphics("clock")); final TableColumn<LogEntryTableRow, String> channelCol = new TableColumn<LogEntryTableRow, String>( "Channel"); channelCol.setCellValueFactory(new PropertyValueFactory<>("channel")); channelCol.setSortable(false); channelCol.setGraphic(Icons.getIconGraphics("connect")); final TableColumn<LogEntryTableRow, String> payloadCol = new TableColumn<LogEntryTableRow, String>( "Payload"); payloadCol.setCellValueFactory(new PropertyValueFactory<>("payload")); payloadCol.setSortable(false); payloadCol.setGraphic(Icons.getIconGraphics("page_white_text_width")); this.parsedLogEntries.getColumns().add(lineNoCol); this.parsedLogEntries.getColumns().add(timeCol); this.parsedLogEntries.getColumns().add(channelCol); this.parsedLogEntries.getColumns().add(payloadCol); } @Override protected void init() { } private void createDialogItems() { Validate.notNull(this.logReader); // Will be used to initialize input field values // CHECKSTYLE:OFF Yes, we are using lots of constants here. It does not make sense to name them using final variables. final GridPane gp = new GridPane(); gp.setAlignment(Pos.BASELINE_LEFT); gp.setHgap(10d); gp.setVgap(15d); gp.setPadding(new Insets(5d)); final ColumnConstraints column1 = new ColumnConstraints(); final ColumnConstraints column2 = new ColumnConstraints(); column1.setHgrow(Priority.NEVER); column2.setHgrow(Priority.SOMETIMES); gp.getColumnConstraints().addAll(column1, column2); this.insertConfigItemsIntoGrid(gp, this.createConfigurationForm()); final TitledPane configPane = new TitledPane("Edit Configuration", gp); configPane.setGraphic(Icons.getIconGraphics("pencil")); configPane.setCollapsible(false); final VBox lines = this.createRunForm(); final TitledPane testPane = new TitledPane("Test Configuration", lines); testPane.setGraphic(Icons.getIconGraphics("script_go")); testPane.setCollapsible(false); final VBox panes = new VBox(configPane, testPane); panes.setSpacing(10d); final ScrollPane sp = new ScrollPane(panes); sp.setPadding(new Insets(10d)); sp.setFitToWidth(true); this.setCenter(sp); // CHECKSTYLE:ON } public VBox createRunForm() { // CHECKSTYLE:OFF Yes, we are using lots of constants here. It does not make sense to name them using final variables. final VBox lines = new VBox(); lines.setSpacing(10d); final HBox inputTypeLine = new HBox(); inputTypeLine.setSpacing(30d); final ToggleGroup group = new ToggleGroup(); final RadioButton inputTypeText = new RadioButton("Paste/Enter text"); inputTypeText.setToggleGroup(group); final RadioButton inputTypeFile = new RadioButton("Read log file"); inputTypeFile.setToggleGroup(group); inputTypeLine.getChildren().add(inputTypeText); inputTypeLine.getChildren().add(inputTypeFile); inputTypeText.setSelected(true); final TextField pathInput = new TextField(); HBox.setHgrow(pathInput, Priority.ALWAYS); final Button selectLogFileButton = SelectFileButton.createButtonWithFileSelection(pathInput, LOG_FILE_ICON_NAME, "Select log file", null, null); final Text pathInputLabel = new Text("Log file path: "); final HBox fileInputConfig = new HBox(); fileInputConfig.setAlignment(Pos.CENTER_LEFT); fileInputConfig.visibleProperty().bind(inputTypeFile.selectedProperty()); fileInputConfig.managedProperty().bind(fileInputConfig.visibleProperty()); fileInputConfig.getChildren().addAll(pathInputLabel, pathInput, selectLogFileButton); final TextArea logInputText = new TextArea(); HBox.setHgrow(logInputText, Priority.ALWAYS); logInputText.setPrefRowCount(10); logInputText.setStyle("-fx-font-family: monospace"); final HBox enterTextConfig = new HBox(); enterTextConfig.getChildren().add(logInputText); enterTextConfig.visibleProperty().bind(inputTypeText.selectedProperty()); enterTextConfig.managedProperty().bind(enterTextConfig.visibleProperty()); final Button startBtn = new Button("Read Log"); startBtn.setPadding(new Insets(8d)); // CHECKSTYLE:ON startBtn.setGraphic(Icons.getIconGraphics("control_play_blue")); HBox.setHgrow(startBtn, Priority.ALWAYS); startBtn.setMaxWidth(Double.MAX_VALUE); startBtn.setOnAction(event -> this.runLogFileReader(inputTypeFile, pathInput, logInputText)); final HBox startLine = new HBox(); startLine.getChildren().add(startBtn); lines.getChildren().addAll(inputTypeLine, fileInputConfig, enterTextConfig, startLine, new Text("Results:"), this.parsedLogEntries); return lines; } public void runLogFileReader(final RadioButton inputTypeFile, final TextField pathInput, final TextArea logInputText) { this.parsedLogEntryItems.clear(); try { GenericLogReader reader; if (inputTypeFile.isSelected()) { reader = new GenericLogReader(new File(pathInput.getText())); } else { reader = new GenericLogReader(new BufferedReader(new StringReader(logInputText.getText()))); } reader.overwriteCurrentSettingsWithSettingsInConfigurationFile( this.logReader.getSettingsForConfigurationFile()); this.logReader = reader; ILogEntry nextLogEntry; while ((nextLogEntry = reader.getNextEntry()) != null) { this.parsedLogEntryItems.add(new LogEntryTableRow(nextLogEntry)); } } catch (final RuntimeException re) { ExceptionDialog.showFatalException("Error reading log entries", "An error occurred while reading the log entries.", re); } } private List<Triple<String, Node, String>> createConfigurationForm() { final List<Triple<String, Node, String>> formItems = new ArrayList<>(); // Headline pattern final TextField textInput = new TextField(this.logReader.getHeadlinePattern()); textInput.textProperty().addListener((ChangeListener<String>) (observable, oldValue, newValue) -> { this.logReader.setHeadlinePattern(newValue); this.setChanged(true); }); textInput.setStyle("-fx-font-family: monospace"); formItems.add(Triple.of("Headline Pattern", textInput, "The perl style regular expression is used to spot the beginning of" + " log entries in the log file. If a log entry consists of multiple lines, this pattern must only match the first" + " line, called \"head line\". Groups can intentionally be matched to spot values like timestamp or channel name." + " All matching groups are removed from the payload before they are processed by an automaton.")); // Index of timestamp Supplier<Integer> getter = () -> this.logReader.getHeadlinePatternIndexOfTimestamp(); Consumer<Integer> setter = value -> this.logReader.setHeadlinePatternIndexOfTimestamp(value); final TextField indexOfTimestampInput = this.createIntegerInputField(textInput, getter, setter); formItems.add(Triple.of("Timestamp Group", indexOfTimestampInput, "Denotes which matching group in the headline pattern contains" + " the timestamp. Index 0 references the whole pattern match, index 1 is the first matching group etc. The timestamp must" + " always be a valid integer. Currently, this integer is always interpreted as milliseconds. If no value is set, no" + " timestamp will be extracted and timing conditions cannot be used. If the referenced matching group is optional" + " and does not match for a specific head line, the last recent timestamp will be used for the extracted log entry.")); // Index of channel getter = () -> this.logReader.getHeadlinePatternIndexOfChannel(); setter = value -> this.logReader.setHeadlinePatternIndexOfChannel(value); final TextField indexOfChannelInput = this.createIntegerInputField(textInput, getter, setter); formItems.add(Triple.of("Channel Group", indexOfChannelInput, "Denotes which matching group in the headline pattern contains" + " the channel. If the value is empty or the matching group is optional and it did not match, the default channel is used" + " for the extracted log entry.")); // Trim payload CheckBox cb = new CheckBox(); cb.setSelected(this.logReader.getTrimPayload()); cb.selectedProperty().addListener((ChangeListener<Boolean>) (observable, oldValue, newValue) -> { this.logReader.setTrimPayload(BooleanUtils.toBoolean(newValue)); this.setChanged(true); }); formItems.add(Triple.of("Trim Payload", cb, "Only has effect on multiline payloads." + " If enabled, all leading and trailing whitespaces are removed from the payload" + " after the matching groups are removed. This allows for regular expressions in the automaton that match the beginning" + " and the end of the payload, without having to take care too much for whitespaces in the source log file.")); // Handling of non headline lines this.createInputForHandlingOfNonHeadlineLines(formItems); // Trim payload cb = new CheckBox(); cb.setSelected(this.logReader.getRemoveEmptyPayloadLinesFromMultilineEntry()); cb.selectedProperty().addListener((ChangeListener<Boolean>) (observable, oldValue, newValue) -> { this.logReader.setRemoveEmptyPayloadLinesFromMultilineEntry(BooleanUtils.toBoolean(newValue)); this.setChanged(true); }); formItems.add(Triple.of("Remove Empty Lines", cb, "If enabled, empty lines will be removed from multiline payload entries.")); // Charset final SortedMap<String, Charset> charsets = Charset.availableCharsets(); final ChoiceBox<String> encodingInput = new ChoiceBox<>( FXCollections.observableArrayList(charsets.keySet())); encodingInput.getSelectionModel().select(this.logReader.getLogFileCharset().name()); encodingInput.getSelectionModel().selectedItemProperty() .addListener((ChangeListener<String>) (observable, oldValue, newValue) -> { this.logReader.setLogFileCharset(charsets.get(newValue)); this.setChanged(true); }); formItems.add(Triple.of("Log File Encoding", encodingInput, "Encoding of the log file. Note that some of the encodings are" + " platform specific such that reading the log on a different platform might fail. Usually, log files are written" + " using UTF-8, UTF-16, ISO-8859-1 or ASCII.")); return formItems; } public void createInputForHandlingOfNonHeadlineLines(final List<Triple<String, Node, String>> formItems) { final Map<HandlingOfNonHeadlineLines, String> mapValueToChoice = new HashMap<>(); mapValueToChoice.put(HandlingOfNonHeadlineLines.FAIL, "Abort - Each line in the log file is assumed to be a log entry"); mapValueToChoice.put(HandlingOfNonHeadlineLines.CREATE_MULTILINE_ENTRY, "Append to payload - This will create multiline payloads"); mapValueToChoice.put(HandlingOfNonHeadlineLines.ASSUME_LAST_TIMESTAMP, "Create new log entry and use timestamp of recent log entry"); mapValueToChoice.put(HandlingOfNonHeadlineLines.ASSUME_LAST_TIMESTAMP_AND_CHANNEL, "Create new log entry and use timestamp and channel of recent log entry"); final ChoiceBox<HandlingOfNonHeadlineLines> handlingOfNonHeadlineLinesInput = new ChoiceBox<>( FXCollections.observableArrayList(HandlingOfNonHeadlineLines.values())); handlingOfNonHeadlineLinesInput.setConverter(new StringConverter<HandlingOfNonHeadlineLines>() { @Override public String toString(final HandlingOfNonHeadlineLines object) { return mapValueToChoice.get(object); } @Override public HandlingOfNonHeadlineLines fromString(final String string) { for (final Entry<HandlingOfNonHeadlineLines, String> entry : mapValueToChoice.entrySet()) { if (entry.getValue() == string) // Intentionally comparing references to obtain a bijection { return entry.getKey(); } } return null; // Should never happen } }); handlingOfNonHeadlineLinesInput.getSelectionModel().select(this.logReader.getHandlingOfNonHeadlineLines()); handlingOfNonHeadlineLinesInput.getSelectionModel().selectedIndexProperty() .addListener((ChangeListener<Number>) (observable, oldValue, newValue) -> { this.logReader.setHandlingOfNonHeadlineLines( handlingOfNonHeadlineLinesInput.getItems().get(newValue.intValue())); this.setChanged(true); }); formItems.add(Triple.of("Dangling Lines", handlingOfNonHeadlineLinesInput, "Define what to do if dangling lines are spotted. Dangling lines are lines which do not match the headline pattern, i.e." + " which do not introduce a new log entry.")); } private TextField createIntegerInputField(final TextField textInput, final Supplier<Integer> getter, final Consumer<Integer> setter) { final TextField integerInput = new TextField(getter.get() == null ? "" : String.valueOf(getter.get())); integerInput.textProperty().addListener((ChangeListener<String>) (observable, oldValue, newValue) -> { if (!newValue.matches("\\d*")) { integerInput.setText(newValue.replaceAll("[^\\d]", "")); newValue = textInput.getText(); } if (StringUtils.isNotBlank(newValue)) { try { setter.accept(Integer.parseInt(newValue)); } catch (final NumberFormatException nfe) { // This can only happen if the value is "too long" / too high for "int" integerInput.setText(String.valueOf(Integer.MAX_VALUE)); setter.accept(Integer.MAX_VALUE); } } else { setter.accept(null); } this.setChanged(true); }); final double maxWidthOfIntegerInput = 80d; integerInput.setMaxWidth(maxWidthOfIntegerInput); return integerInput; } private void insertConfigItemsIntoGrid(final GridPane gp, final List<Triple<String, Node, String>> formItems) { for (int i = 0; i < formItems.size(); i++) { final String title = formItems.get(i).getLeft(); final Node inputElement = formItems.get(i).getMiddle(); final String description = formItems.get(i).getRight(); // Put text flow object into cell. If a Text instance is used only, it will grab the whole cell size and center the text // (horizontally and vertically). Therefore, the table cell alignment does not work. final TextFlow titleText = new TextFlow(new Text(title)); titleText.setStyle("-fx-font-weight: bold;"); final TextFlow fieldName = new TextFlow(titleText); fieldName.autosize(); fieldName.setMinWidth(fieldName.getWidth()); gp.add(fieldName, 0, i); final Text descriptionText = new Text(description); final VBox vbox = new VBox(inputElement, new TextFlow(descriptionText)); gp.add(vbox, 1, i); } } @Override protected void createNewDocument() { this.createLogReaderWithDummyInput(); this.createDialogItems(); } @Override protected void loadDocumentFromFileAndAssignToFile(final File srcFile) { this.createLogReaderWithDummyInput(); try { this.logReader.overwriteCurrentSettingsWithSettingsInConfigurationFile( new JSONObject(IOUtils.toString(srcFile.toURI(), LOG_READER_CONFIG_ENCODING))); } catch (JSONException | IOException e) { throw new RuntimeException(e); } this.assignDocumentToFile(srcFile); this.createDialogItems(); } @Override protected void saveDocument() { Validate.notNull(this.getFile()); final int indentSpaces = 4; try { FileUtils.write(this.getFile(), this.logReader.getSettingsForConfigurationFile().toString(indentSpaces), LOG_READER_CONFIG_ENCODING); } catch (final JSONException e) { throw new RuntimeException("JSON format exception while writing file", e); } catch (final IOException e) { throw new RuntimeException("Error while saving file: " + e.getMessage()); } this.setChanged(false); } private void createLogReaderWithDummyInput() { final BufferedReader br = new BufferedReader(new StringReader("")); // Dummy input this.logReader = new GenericLogReader(br); } }