This commit is contained in:
coco
2026-07-03 15:56:07 +08:00
commit caef23209c
5767 changed files with 1004268 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
# binjr-adapter-csv
[![Maven Central](https://img.shields.io/maven-central/v/eu.binjr/binjr-adapter-csv.svg?label=Maven%20Central&style=flat-square)](https://search.maven.org/search?q=g:%22eu.binjr%22%20AND%20a:%22binjr-adapter-csv%22)
This module implements a DataAdapter capable of consuming data from Comma Separated Values (i.e. CSV) files.
+31
View File
@@ -0,0 +1,31 @@
/*
* Copyright 2018_2021 Frederic Thevenet
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
dependencies {
compileOnly project(':binjr-core')
}
jar {
manifest {
attributes(
'Specification-Title': project.name,
'Specification-Version': project.version,
'Implementation-Title': project.name,
'Implementation-Version': project.version,
'Build-Number': BINJR_BUILD_NUMBER
)
}
}
@@ -0,0 +1,89 @@
/*
* Copyright 2022 Frederic Thevenet
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.binjr.sources.csv.adapters;
import com.google.gson.Gson;
import eu.binjr.common.auth.JfxKrb5LoginModule;
import eu.binjr.common.preferences.ObservablePreference;
import eu.binjr.core.data.adapters.DataAdapter;
import eu.binjr.core.data.adapters.DataAdapterPreferences;
import eu.binjr.core.data.indexes.parser.profile.CustomParsingProfile;
import eu.binjr.core.data.indexes.parser.profile.ParsingProfile;
import eu.binjr.sources.csv.data.parsers.BuiltInCsvParsingProfile;
import eu.binjr.sources.csv.data.parsers.CsvParsingProfile;
import eu.binjr.sources.csv.data.parsers.CustomCsvParsingProfile;
/**
* Defines the preferences associated with the Log files adapter.
*
* @author Frederic Thevenet
*/
public class CsvAdapterPreferences extends DataAdapterPreferences {
private static final Gson gson = new Gson();
/**
* The default text panel font size preference.
*/
public ObservablePreference<Number> defaultTextViewFontSize = integerPreference("defaultTextViewFontSize", 10);
/**
* The filters used when scanning folders in the source filesystem.
*/
public ObservablePreference<String[]> folderFilters = objectPreference(String[].class,
"folderFilters",
new String[]{"*"},
gson::toJson,
s -> gson.fromJson(s, String[].class));
/**
* The filters used to prune file extensions to scan in the source filesystem.
*/
public ObservablePreference<String[]> fileExtensionFilters = objectPreference(String[].class,
"extensionFilters",
new String[]{"*.log", "*.txt"},
gson::toJson,
s -> gson.fromJson(s, String[].class));
/**
* The most recently used {@link ParsingProfile}
*/
public ObservablePreference<String> mostRecentlyUsedParsingProfile =
stringPreference("mruCsvParsingProfile", BuiltInCsvParsingProfile.ISO.getProfileId());
public ObservablePreference<CsvParsingProfile[]> csvTimestampParsingProfiles =
objectPreference(CsvParsingProfile[].class,
"csvTimestampParsingProfiles",
new CsvParsingProfile[0],
s -> gson.toJson(s),
s -> gson.fromJson(s, CustomCsvParsingProfile[].class)
);
public ObservablePreference<String> mruEncoding = stringPreference("mruEncoding", "utf-8");
public ObservablePreference<String> mruCsvSeparator = stringPreference("mruCsvSeparator", ";");
public ObservablePreference<Number> mruDateColumnPosition = integerPreference("mruDateColumnPosition", 0);
/**
* Initialize a new instance of the {@link CsvAdapterPreferences} class associated to
* a {@link DataAdapter} instance.
*
* @param dataAdapterClass the associated {@link DataAdapter}
*/
public CsvAdapterPreferences(Class<? extends DataAdapter<?>> dataAdapterClass) {
super(dataAdapterClass);
}
}
@@ -0,0 +1,317 @@
/*
* Copyright 2017-2022 Frederic Thevenet
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.binjr.sources.csv.adapters;
import com.google.gson.Gson;
import eu.binjr.common.function.CheckedLambdas;
import eu.binjr.common.io.FileSystemBrowser;
import eu.binjr.common.io.IOUtils;
import eu.binjr.common.javafx.controls.TimeRange;
import eu.binjr.common.logging.Logger;
import eu.binjr.core.data.adapters.*;
import eu.binjr.core.data.exceptions.CannotInitializeDataAdapterException;
import eu.binjr.core.data.exceptions.DataAdapterException;
import eu.binjr.core.data.exceptions.FetchingDataFromAdapterException;
import eu.binjr.core.data.exceptions.InvalidAdapterParameterException;
import eu.binjr.core.data.indexes.Index;
import eu.binjr.core.data.indexes.Indexes;
import eu.binjr.core.data.indexes.IndexingStatus;
import eu.binjr.core.data.timeseries.DoubleTimeSeriesProcessor;
import eu.binjr.core.data.timeseries.TimeSeriesProcessor;
import eu.binjr.core.data.workspace.TimeSeriesInfo;
import eu.binjr.core.data.workspace.XYChartsWorksheet;
import eu.binjr.sources.csv.data.parsers.BuiltInCsvParsingProfile;
import eu.binjr.sources.csv.data.parsers.CsvEventFormat;
import eu.binjr.sources.csv.data.parsers.CsvParsingProfile;
import eu.binjr.sources.csv.data.parsers.CustomCsvParsingProfile;
import javafx.beans.property.LongProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleLongProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.TreeItem;
import org.apache.lucene.document.StoredField;
import org.eclipse.fx.ui.controls.tree.FilterableTreeItem;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.NumberFormat;
import java.time.Instant;
import java.time.ZoneId;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* A {@link DataAdapter} implementation used to feed {@link XYChartsWorksheet} instances
* with data from a local CSV formatted file.
*
* @author Frederic Thevenet
*/
public class CsvFileAdapter extends BaseDataAdapter<Double> {
private static final Logger logger = Logger.create(CsvFileAdapter.class);
private static final Gson gson = new Gson();
private static final Property<IndexingStatus> INDEXING_OK = new SimpleObjectProperty<>(IndexingStatus.OK);
private static final String ZONE_ID = "zoneId";
private static final String ENCODING = "encoding";
private static final String DELIMITER = "delimiter";
private static final String PARSING_PROFILE = "parsingProfile";
private static final String PATH = "csvPath";
private static final String TIMESTAMP_POSITION = "timestampPosition";
private CsvEventFormat parser;
private CsvParsingProfile csvParsingProfile;
private Path csvPath;
private ZoneId zoneId;
private String encoding;
private final Map<String, IndexingStatus> indexedFiles = new HashMap<>();
private Index index;
private FileSystemBrowser fileBrowser;
private String[] folderFilters;
private String[] fileExtensionsFilters;
private List<String> headers;
private long sequence = 0;
/**
* Initializes a new instance of the {@link CsvFileAdapter} class with a set of default values.
*
* @throws DataAdapterException if the {@link DataAdapter} could not be initializes.
*/
public CsvFileAdapter() throws DataAdapterException {
this("", ZoneId.systemDefault(), "utf-8", BuiltInCsvParsingProfile.ISO);
}
/**
* Initializes a new instance of the {@link CsvFileAdapter} class for the provided file and time zone.
*
* @param csvPath the path to the csv file.
* @param zoneId the time zone to used.
* @throws DataAdapterException if the {@link DataAdapter} could not be initialized.
*/
public CsvFileAdapter(String csvPath, ZoneId zoneId) throws DataAdapterException {
this(csvPath, zoneId, "utf-8", BuiltInCsvParsingProfile.ISO);
}
/**
* Initializes a new instance of the {@link CsvFileAdapter} class with the provided parameters.
*
* @param csvPath the path to the csv file.
* @param zoneId the time zone to used.
* @param encoding the encoding for the csv file.
* @param csvParsingProfile a pattern to decode time stamps.
* @throws DataAdapterException if the {@link DataAdapter} could not be initialized.
*/
public CsvFileAdapter(String csvPath,
ZoneId zoneId,
String encoding,
CsvParsingProfile csvParsingProfile)
throws DataAdapterException {
super();
initParams(zoneId, csvPath, encoding, csvParsingProfile);
}
@Override
public FilterableTreeItem<SourceBinding> getBindingTree() throws DataAdapterException {
FilterableTreeItem<SourceBinding> tree = new FilterableTreeItem<>(
new TimeSeriesBinding.Builder()
.withLabel(getSourceName())
.withPath("/")
.withAdapter(this)
.build());
try (InputStream in = Files.newInputStream(csvPath)) {
this.headers = parser.getDataColumnHeaders(in);
for (int i = 0; i < headers.size(); i++) {
if (i != csvParsingProfile.getTimestampColumn()) {
String header = headers.get(i).isBlank() ? "Column #" + i : headers.get(i);
var b = new TimeSeriesBinding.Builder()
.withLabel(Integer.toString(i))
.withPath(getId() + "/" + csvPath.toString())
.withLegend(header)
.withParent(tree.getValue())
.withAdapter(this)
.build();
tree.getInternalChildren().add(new TreeItem<>(b));
}
}
} catch (IOException e) {
throw new FetchingDataFromAdapterException(e);
}
return tree;
}
@Override
public TimeRange getInitialTimeRange(String path, List<TimeSeriesInfo<Double>> seriesInfo) throws DataAdapterException {
try {
ensureIndexed(seriesInfo.stream().map(TimeSeriesInfo::getBinding).collect(Collectors.toSet()), ReloadPolicy.UNLOADED);
return index.getTimeRangeBoundaries(seriesInfo.stream().map(ts -> ts.getBinding().getPath()).toList(), getTimeZoneId());
} catch (IOException e) {
throw new DataAdapterException("Error retrieving initial time range", e);
}
}
@Override
public Map<TimeSeriesInfo<Double>, TimeSeriesProcessor<Double>> fetchData(String path,
Instant begin,
Instant end,
List<TimeSeriesInfo<Double>> seriesInfo,
boolean bypassCache) throws DataAdapterException {
try {
ensureIndexed(seriesInfo.stream().map(TimeSeriesInfo::getBinding).collect(Collectors.toSet()), ReloadPolicy.UNLOADED);
Map<TimeSeriesInfo<Double>, TimeSeriesProcessor<Double>> series = new HashMap<>();
for (TimeSeriesInfo<Double> info : seriesInfo) {
series.put(info, new DoubleTimeSeriesProcessor());
}
var nbHits = index.search(
begin.toEpochMilli(),
end.toEpochMilli(),
series,
zoneId,
bypassCache);
logger.debug(() -> "Retrieved " + nbHits + " hits");
return series;
} catch (Exception e) {
throw new DataAdapterException("Error fetching data from " + path, e);
}
}
@Override
public String getEncoding() {
return encoding;
}
@Override
public ZoneId getTimeZoneId() {
return zoneId;
}
@Override
public String getSourceName() {
return new StringBuilder("[CSV] ")
.append(csvPath != null ? csvPath.getFileName() : "???")
.append(" (")
.append(zoneId != null ? zoneId : "???")
.append(")")
.toString();
}
@Override
public Map<String, String> getParams() {
Map<String, String> params = new HashMap<>();
params.put(ZONE_ID, zoneId.toString());
params.put(ENCODING, encoding);
params.put(PARSING_PROFILE, gson.toJson(CustomCsvParsingProfile.of(csvParsingProfile)));
params.put(PATH, csvPath.toString());
return params;
}
@Override
public void loadParams(Map<String, String> params) throws DataAdapterException {
if (params == null) {
throw new InvalidAdapterParameterException("Could not find parameter list for adapter " + getSourceName());
}
initParams(validateParameter(params, ZONE_ID,
s -> {
if (s == null) {
throw new InvalidAdapterParameterException("Parameter '" + ZONE_ID + "' is missing in adapter " + getSourceName());
}
return ZoneId.of(s);
}),
validateParameterNullity(params, PATH),
validateParameterNullity(params, ENCODING),
gson.fromJson(validateParameterNullity(params, PARSING_PROFILE), CustomCsvParsingProfile.class));
}
private void initParams(ZoneId zoneId,
String csvPath,
String encoding,
CsvParsingProfile parsingProfile) {
this.zoneId = zoneId;
this.csvPath = Path.of(csvPath);
this.encoding = encoding;
this.csvParsingProfile = parsingProfile;
this.parser = new CsvEventFormat(parsingProfile, zoneId, Charset.forName(encoding));
}
@Override
public void onStart() throws DataAdapterException {
super.onStart();
try {
this.fileBrowser = FileSystemBrowser.of(csvPath.getParent());
this.index = Indexes.NUM_SERIES.acquire();
} catch (IOException e) {
throw new CannotInitializeDataAdapterException("An error occurred during the data adapter initialization", e);
}
}
@Override
public void close() {
try {
Indexes.NUM_SERIES.release();
} catch (Exception e) {
logger.error("An error occurred while releasing index " + Indexes.NUM_SERIES.name() + ": " + e.getMessage());
logger.debug("Stack Trace:", e);
}
IOUtils.close(fileBrowser);
super.close();
}
private double formatToDouble(String value, NumberFormat numberFormat) {
if (value != null) {
try {
return numberFormat.parse(value).doubleValue();
} catch (Exception e) {
logger.trace(() -> "Failed to convert '" + value + "' to double");
}
}
return Double.NaN;
}
private synchronized void ensureIndexed(Set<SourceBinding<Double>> bindings, ReloadPolicy reloadPolicy) throws IOException {
if (reloadPolicy == ReloadPolicy.ALL) {
bindings.stream().map(SourceBinding::getPath).forEach(indexedFiles::remove);
}
final LongProperty charRead = new SimpleLongProperty(0);
for (var binding : bindings) {
String path = binding.getPath();
indexedFiles.computeIfAbsent(path, CheckedLambdas.wrap(p -> {
ThreadLocal<NumberFormat> formatters =
ThreadLocal.withInitial(() -> NumberFormat.getNumberInstance(csvParsingProfile.getNumberFormattingLocale()));
try {
index.add(p,
fileBrowser.getData(path.replace(getId() + "/", "")),
true,
parser,
(doc, event) -> {
event.getTextFields().forEach((key, value) -> doc.add(new StoredField(key, formatToDouble(value, formatters.get()))));
return doc;
},
charRead,
INDEXING_OK);
return IndexingStatus.OK;
} finally {
formatters.remove();
}
}));
}
}
}
@@ -0,0 +1,171 @@
/*
* Copyright 2017-2023 Frederic Thevenet
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.binjr.sources.csv.adapters;
import eu.binjr.common.javafx.controls.LabelWithInlineHelp;
import eu.binjr.common.javafx.controls.NodeUtils;
import eu.binjr.core.data.adapters.DataAdapter;
import eu.binjr.core.data.adapters.DataAdapterFactory;
import eu.binjr.core.data.exceptions.CannotInitializeDataAdapterException;
import eu.binjr.core.data.exceptions.DataAdapterException;
import eu.binjr.core.data.exceptions.NoAdapterFoundException;
import eu.binjr.core.dialogs.DataAdapterDialog;
import eu.binjr.core.dialogs.Dialogs;
import eu.binjr.sources.csv.data.parsers.BuiltInCsvParsingProfile;
import eu.binjr.sources.csv.data.parsers.CsvParsingProfile;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.stage.FileChooser;
import org.controlsfx.control.textfield.TextFields;
import java.io.File;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.ZoneId;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
/**
* An implementation of the {@link DataAdapterDialog} class that presents a dialog box to retrieve the parameters specific {@link CsvFileAdapter}
*
* @author Frederic Thevenet
*/
public class CsvFileAdapterDialog extends DataAdapterDialog<Path> {
private final TextField encodingField;
private int pos = 2;
private final CsvAdapterPreferences prefs;
private final ChoiceBox<CsvParsingProfile> parsingChoiceBox = new ChoiceBox<>();
/**
* Initializes a new instance of the {@link CsvFileAdapterDialog} class.
*
* @param owner the owner window for the dialog
*/
public CsvFileAdapterDialog(Node owner) throws NoAdapterFoundException {
super(owner, Mode.PATH, "mostRecentCsvFiles", true);
this.prefs = (CsvAdapterPreferences) DataAdapterFactory.getInstance().getAdapterPreferences(CsvFileAdapter.class.getName());
this.setUriLabelInlineHelp("""
The path of the CSV file.
""");
this.setTimezoneLabelInlineHelp("""
The default timezone for timestamps in the CSV file.
""");
this.setDialogHeaderText("Add a csv file");
this.encodingField = new TextField(prefs.mruEncoding.get());
TextFields.bindAutoCompletion(encodingField, Charset.availableCharsets().keySet());
addParamField(this.encodingField, "Encoding", """
The text encoding of the CSV file.
""");
addParsingField(owner);
}
private void addParsingField(Node owner) {
var parsingLabel = new LabelWithInlineHelp("Parsing profile", """
The parsing profile used to process timestamps in the CSV file.
""");
parsingLabel.setAlignment(Pos.CENTER_RIGHT);
var parsingHBox = new HBox();
parsingHBox.setSpacing(5);
updateProfileList(prefs.csvTimestampParsingProfiles.get());
parsingChoiceBox.setMaxWidth(Double.MAX_VALUE);
var editParsingButton = new Button("Edit");
editParsingButton.setOnAction(event -> {
try {
new CsvParsingProfileDialog(this.getOwner(), parsingChoiceBox.getValue()).showAndWait().ifPresent(selection -> {
prefs.mostRecentlyUsedParsingProfile.set(selection.getProfileId());
updateProfileList(prefs.csvTimestampParsingProfiles.get());
});
} catch (Exception e) {
Dialogs.notifyException("Failed to show parsing profile windows", e, owner);
}
});
parsingHBox.getChildren().addAll(parsingChoiceBox, editParsingButton);
HBox.setHgrow(parsingChoiceBox, Priority.ALWAYS);
GridPane.setConstraints(parsingLabel, 0, pos, 1, 1, HPos.LEFT, VPos.CENTER, Priority.ALWAYS, Priority.ALWAYS, new Insets(4, 0, 4, 0));
GridPane.setConstraints(parsingHBox, 1, pos, 1, 1, HPos.LEFT, VPos.CENTER, Priority.ALWAYS, Priority.ALWAYS, new Insets(4, 0, 4, 0));
getParamsGridPane().getChildren().addAll(parsingLabel, parsingHBox);
pos++;
}
private void addParamField(TextField field, String label, String inlineHelp) {
GridPane.setConstraints(field, 1, pos, 1, 1, HPos.LEFT, VPos.CENTER, Priority.ALWAYS, Priority.ALWAYS, new Insets(4, 0, 4, 0));
var tabsLabel = new LabelWithInlineHelp(label, inlineHelp);
tabsLabel.setAlignment(Pos.CENTER_RIGHT);
GridPane.setConstraints(tabsLabel, 0, pos, 1, 1, HPos.LEFT, VPos.CENTER, Priority.ALWAYS, Priority.ALWAYS, new Insets(4, 0, 4, 0));
getParamsGridPane().getChildren().addAll(field, tabsLabel);
pos++;
}
@Override
protected File displayFileChooser(Node owner) {
try {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open CSV file");
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Comma-separated values files", "*.csv"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("All files", "*.*", "*"));
Dialogs.getInitialDir(getMostRecentList()).ifPresent(fileChooser::setInitialDirectory);
return fileChooser.showOpenDialog(NodeUtils.getStage(owner));
} catch (Exception e) {
Dialogs.notifyException("Error while displaying file chooser: " + e.getMessage(), e, owner);
}
return null;
}
@Override
protected Collection<DataAdapter> getDataAdapters() throws DataAdapterException {
Path csvPath = Paths.get(getSourceUri());
if (!Files.exists(csvPath)) {
throw new CannotInitializeDataAdapterException("Cannot find " + getSourceUri());
}
if (!csvPath.isAbsolute()) {
throw new CannotInitializeDataAdapterException("The provided path is not valid.");
}
getMostRecentList().push(csvPath);
prefs.mostRecentlyUsedParsingProfile.set(parsingChoiceBox.getValue().getProfileId());
String charsetName = encodingField.getText();
if (!Charset.isSupported(charsetName)) {
throw new CannotInitializeDataAdapterException("Invalid or unsupported encoding: " + charsetName);
}
prefs.mruEncoding.set(charsetName);
return List.of(new CsvFileAdapter(
getSourceUri(),
ZoneId.of(getSourceTimezone()),
charsetName,
parsingChoiceBox.getValue()));
}
private void updateProfileList(CsvParsingProfile[] newValue) {
parsingChoiceBox.getItems().clear();
parsingChoiceBox.getItems().setAll(BuiltInCsvParsingProfile.values());
parsingChoiceBox.getItems().addAll(newValue);
parsingChoiceBox.getSelectionModel().select(parsingChoiceBox.getItems().stream()
.filter(p -> Objects.equals(p.getProfileId(), prefs.mostRecentlyUsedParsingProfile.get()))
.findAny().orElse(BuiltInCsvParsingProfile.ISO));
}
}
@@ -0,0 +1,53 @@
/*
* Copyright 2017-2023 Frederic Thevenet
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.binjr.sources.csv.adapters;
import eu.binjr.core.data.adapters.AdapterMetadata;
import eu.binjr.core.data.adapters.BaseDataAdapterInfo;
import eu.binjr.core.data.adapters.SourceLocality;
import eu.binjr.core.data.adapters.VisualizationType;
import eu.binjr.core.data.exceptions.CannotInitializeDataAdapterException;
import eu.binjr.core.preferences.AppEnvironment;
/**
* Defines the metadata associated with the CsvFileDataAdapter.
*
* @author Frederic Thevenet
*/
@AdapterMetadata(
name = "CSV",
description = "CSV File Data Adapter",
copyright = AppEnvironment.COPYRIGHT_NOTICE,
license = AppEnvironment.LICENSE,
siteUrl = AppEnvironment.HTTP_WWW_BINJR_EU,
adapterClass = CsvFileAdapter.class,
dialogClass = CsvFileAdapterDialog.class,
preferencesClass = CsvAdapterPreferences.class,
sourceLocality = SourceLocality.LOCAL,
apiLevel = AppEnvironment.PLUGIN_API_LEVEL,
visualizationType = VisualizationType.CHARTS
)
public class CsvFileDataAdapterInfo extends BaseDataAdapterInfo {
/**
* Initialises a new instance of the {@link CsvFileDataAdapterInfo} class.
* @throws CannotInitializeDataAdapterException if the adapter's initialization failed
*/
public CsvFileDataAdapterInfo() throws CannotInitializeDataAdapterException {
super(CsvFileDataAdapterInfo.class);
}
}
@@ -0,0 +1,107 @@
/*
* Copyright 2022 Frederic Thevenet
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.binjr.sources.csv.adapters;
import eu.binjr.common.javafx.bindings.BindingManager;
import eu.binjr.common.javafx.controls.NodeUtils;
import eu.binjr.core.appearance.StageAppearanceManager;
import eu.binjr.core.preferences.UserPreferences;
import eu.binjr.sources.csv.data.parsers.CsvParsingProfile;
import eu.binjr.sources.csv.data.parsers.CsvParsingProfilesController;
import eu.binjr.core.data.adapters.DataAdapterFactory;
import eu.binjr.core.data.exceptions.NoAdapterFoundException;
import eu.binjr.sources.csv.data.parsers.BuiltInCsvParsingProfile;
import javafx.event.ActionEvent;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.stage.Modality;
import javafx.stage.StageStyle;
import javafx.stage.Window;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.ZoneId;
public class CsvParsingProfileDialog extends Dialog<CsvParsingProfile> {
private final DialogPane root;
public CsvParsingProfileDialog(Window owner, CsvParsingProfile selectedProfile) throws NoAdapterFoundException {
FXMLLoader fXMLLoader = new FXMLLoader(getClass().getResource("/eu/binjr/views/CsvParsingProfileDialogView.fxml"));
final CsvAdapterPreferences prefs;
prefs = (CsvAdapterPreferences) DataAdapterFactory.getInstance().getAdapterPreferences(CsvFileAdapter.class.getName());
var controller = new CsvParsingProfilesController(
BuiltInCsvParsingProfile.values(),
prefs.csvTimestampParsingProfiles.get(),
BuiltInCsvParsingProfile.ISO,
selectedProfile,
true,
StandardCharsets.UTF_8,
ZoneId.systemDefault());
fXMLLoader.setController(controller);
try {
root = fXMLLoader.load();
} catch (IOException e) {
throw new IllegalArgumentException("Failed to load " + fXMLLoader.getLocation());
}
this.setTitle("Edit CSV Parsing Profile");
this.setDialogPane(root);
this.setResizable(true);
this.initOwner(owner);
this.initStyle(StageStyle.UTILITY);
this.initModality(Modality.APPLICATION_MODAL);
BindingManager manager = new BindingManager();
var stage = NodeUtils.getStage(root);
stage.setUserData(manager);
stage.addEventFilter(KeyEvent.KEY_PRESSED, manager.registerHandler(e -> {
if (e.getCode() == KeyCode.F1) {
UserPreferences.getInstance().showInlineHelpButtons.set(!UserPreferences.getInstance().showInlineHelpButtons.get());
e.consume();
}
}));
this.setOnCloseRequest(event -> manager.registerHandler(e -> manager.close()));
StageAppearanceManager.getInstance().register(NodeUtils.getStage(root),
StageAppearanceManager.AppearanceOptions.SET_ICON,
StageAppearanceManager.AppearanceOptions.SET_THEME);
Button okButton = (Button) getDialogPane().lookupButton(ButtonType.OK);
okButton.addEventFilter(ActionEvent.ACTION, ae -> {
if (!controller.applyChanges()) {
ae.consume();
}
});
this.setResultConverter(dialogButton -> {
ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData();
if (data == ButtonBar.ButtonData.OK_DONE) {
prefs.csvTimestampParsingProfiles.set(controller.getCustomProfiles().toArray(CsvParsingProfile[]::new));
return controller.getSelectedProfile();
}
return null;
}
);
}
}
@@ -0,0 +1,179 @@
/*
* Copyright 2022 Frederic Thevenet
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.binjr.sources.csv.data.parsers;
import eu.binjr.core.data.indexes.parser.capture.CaptureGroup;
import eu.binjr.core.data.indexes.parser.capture.NamedCaptureGroup;
import eu.binjr.core.data.indexes.parser.capture.TemporalCaptureGroup;
import eu.binjr.core.data.indexes.parser.profile.ParsingProfile;
import java.text.NumberFormat;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;
public enum BuiltInCsvParsingProfile implements CsvParsingProfile {
ISO("ISO timestamps",
"BUILTIN_ISO",
Map.of(TemporalCaptureGroup.YEAR, "\\d{4}",
TemporalCaptureGroup.MONTH, "\\d{2}",
TemporalCaptureGroup.DAY, "\\d{2}",
TemporalCaptureGroup.HOUR, "\\d{2}",
TemporalCaptureGroup.MINUTE, "\\d{2}",
TemporalCaptureGroup.SECOND, "\\d{2}",
TemporalCaptureGroup.MILLI, "\\d{3}",
CaptureGroup.of("TIMEZONE"), "(Z|[+-]\\d{2}:?(\\d{2})?)"),
"$YEAR[\\s\\/-]$MONTH[\\s\\/-]$DAY([-\\sT]$HOUR:$MINUTE:$SECOND)?([\\.,]$MILLI)?$TIMEZONE?",
",",
'"',
0,
new int[0],
true,
Locale.US,
false),
EPOCH("Seconds since 01/01/1970",
"EPOCH",
Map.of(TemporalCaptureGroup.EPOCH, "\\d+"),
"$EPOCH",
",",
'"',
0,
new int[0],
true,
Locale.US,
false),
EPOCH_MS("Milliseconds since 01/01/1970",
"EPOCH_MS",
Map.of(TemporalCaptureGroup.EPOCH, "\\d+",
TemporalCaptureGroup.MILLI, "\\d{3}"),
"$EPOCH$MILLI",
",",
'"',
0,
new int[0],
true,
Locale.US,
false);
private final String profileName;
private final String lineTemplateExpression;
private final Map<NamedCaptureGroup, String> captureGroups;
private final String profileId;
private final Pattern regex;
private final String delimiter;
private final int timestampColumn;
private final int[] excludedColumns;
private final Locale numberFormattingLocale;
private final boolean readColumnNames;
private final char quoteCharacter;
private final boolean trimCellValues;
BuiltInCsvParsingProfile(String profileName,
String id,
Map<NamedCaptureGroup, String> groups,
String lineTemplateExpression,
String delimiter,
char quoteChar,
int timestampColumn,
int[] excludedColumns,
boolean readColumnNames,
Locale numberFormattingLocale,
boolean trimCellValues) {
this.profileId = id;
this.profileName = profileName;
this.captureGroups = groups;
this.lineTemplateExpression = lineTemplateExpression;
this.regex = Pattern.compile(buildParsingRegexString());
this.delimiter = delimiter;
this.quoteCharacter = quoteChar;
this.timestampColumn = timestampColumn;
this.excludedColumns = excludedColumns;
this.readColumnNames = readColumnNames;
this.numberFormattingLocale = numberFormattingLocale;
this.trimCellValues = trimCellValues;
}
@Override
public String getLineTemplateExpression() {
return lineTemplateExpression;
}
@Override
public Pattern getParsingRegex() {
return regex;
}
@Override
public boolean isBuiltIn() {
return true;
}
@Override
public String getProfileId() {
return this.profileId;
}
@Override
public String getProfileName() {
return profileName;
}
@Override
public Map<NamedCaptureGroup, String> getCaptureGroups() {
return captureGroups;
}
@Override
public String toString() {
return profileName;
}
@Override
public String getDelimiter() {
return delimiter;
}
@Override
public int getTimestampColumn() {
return timestampColumn;
}
@Override
public int[] getExcludedColumns() {
return excludedColumns;
}
@Override
public boolean isReadColumnNames() {
return readColumnNames;
}
@Override
public Locale getNumberFormattingLocale() {
return numberFormattingLocale;
}
@Override
public char getQuoteCharacter() {
return quoteCharacter;
}
@Override
public boolean isTrimCellValues() {
return trimCellValues;
}
}
@@ -0,0 +1,92 @@
/*
* Copyright 2020-2022 Frederic Thevenet
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.binjr.sources.csv.data.parsers;
import eu.binjr.common.logging.Logger;
import eu.binjr.common.text.StringUtils;
import eu.binjr.core.data.exceptions.DecodingDataFromAdapterException;
import eu.binjr.core.data.indexes.parser.EventFormat;
import eu.binjr.core.data.indexes.parser.EventParser;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVRecord;
import java.io.*;
import java.nio.charset.Charset;
import java.time.ZoneId;
import java.util.*;
public class CsvEventFormat implements EventFormat<InputStream> {
private static final Logger logger = Logger.create(CsvEventFormat.class);
private final CsvParsingProfile profile;
private final ZoneId zoneId;
private final Charset encoding;
public CsvEventFormat(CsvParsingProfile profile, ZoneId zoneId, Charset encoding) {
this.profile = profile;
this.zoneId = zoneId;
this.encoding = encoding;
}
@Override
public CsvParsingProfile getProfile() {
return profile;
}
@Override
public EventParser parse(InputStream ias) {
return new CsvEventParser(this, ias);
}
@Override
public Charset getEncoding() {
return encoding;
}
@Override
public ZoneId getZoneId() {
return zoneId;
}
/**
* Returns the columns headers of the CSV file.
*
* @param in an input stream for the CSV file.
* @return the columns headers of the CSV file.
* @throws IOException in the event of an I/O error.
* @throws DecodingDataFromAdapterException if an error occurred while decoding the CSV file.
*/
public List<String> getDataColumnHeaders(InputStream in) throws IOException, DecodingDataFromAdapterException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, encoding))) {
CSVFormat csvFormat = CSVFormat.Builder.create()
.setAllowMissingColumnNames(false)
.setDelimiter(StringUtils.stringToEscapeSequence(getProfile().getDelimiter()))
.build();
Iterable<CSVRecord> records = csvFormat.parse(reader);
CSVRecord record = records.iterator().next();
if (record == null) {
throw new DecodingDataFromAdapterException("CSV stream does not contains column header");
}
List<String> headerNames = new ArrayList<>();
for (int i = 0; i < record.size(); i++) {
String name = getProfile().isReadColumnNames() ? record.get(i) : "Column " + (i + 1);
headerNames.add(name);
}
return headerNames;
}
}
}
@@ -0,0 +1,147 @@
/*
* Copyright 2022-2023 Frederic Thevenet
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.binjr.sources.csv.data.parsers;
import eu.binjr.common.logging.Logger;
import eu.binjr.common.text.StringUtils;
import eu.binjr.core.data.indexes.parser.EventParser;
import eu.binjr.core.data.indexes.parser.ParsedEvent;
import eu.binjr.core.data.indexes.parser.capture.NamedCaptureGroup;
import eu.binjr.core.data.indexes.parser.capture.TemporalCaptureGroup;
import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleLongProperty;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
public class CsvEventParser implements EventParser {
private static final Logger logger = Logger.create(CsvEventParser.class);
private final BufferedReader reader;
private final AtomicLong sequence;
private final CsvEventFormat format;
private final CsvEventIterator eventIterator;
private final CSVParser csvParser;
private final LongProperty progress = new SimpleLongProperty(0);
CsvEventParser(CsvEventFormat format, InputStream ias) {
this.reader = new BufferedReader(new InputStreamReader(ias, format.getEncoding()));
this.sequence = new AtomicLong(0);
this.format = format;
try {
var builder = CSVFormat.Builder.create()
.setAllowMissingColumnNames(true)
.setSkipHeaderRecord(true)
.setTrim(format.getProfile().isTrimCellValues())
.setQuote(format.getProfile().getQuoteCharacter())
.setDelimiter(StringUtils.stringToEscapeSequence(format.getProfile().getDelimiter()));
if (format.getProfile().isReadColumnNames()) {
builder.setHeader();
}
this.csvParser = builder.build().parse(reader);
this.eventIterator = new CsvEventIterator();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void close() throws IOException {
reader.close();
}
@Override
public LongProperty progressIndicator() {
return progress;
}
@Override
public Iterator<ParsedEvent> iterator() {
return eventIterator;
}
public class CsvEventIterator implements Iterator<ParsedEvent> {
@Override
public ParsedEvent next() {
var csvRecord = csvParser.iterator().next();
if (csvRecord == null) {
return null;
}
ZonedDateTime timestamp;
if (format.getProfile().getTimestampColumn() == -1) {
timestamp = ZonedDateTime.of(format.getProfile().getTemporalAnchor().resolve().plus(sequence.get(), ChronoUnit.SECONDS), format.getZoneId());
} else {
if (format.getProfile().getTimestampColumn() > csvRecord.size() - 1) {
throw new UnsupportedOperationException("Cannot extract time stamp in column #" +
(format.getProfile().getTimestampColumn() + 1) +
": CSV record only has " + csvRecord.size() + " fields.");
}
String dateString = csvRecord.get(format.getProfile().getTimestampColumn());
timestamp = parseDateTime(dateString);
}
if (timestamp == null) {
throw new UnsupportedOperationException("Failed to parse time stamp in column #" +
(format.getProfile().getTimestampColumn() + 1));
}
Map<String, String> values = new LinkedHashMap<>(csvRecord.size());
for (int i = 0; i < csvRecord.size(); i++) {
if (i != format.getProfile().getTimestampColumn()) {
// don't add the timestamp column as an attribute
values.put(Integer.toString(i), csvRecord.get(i));
}
}
return ParsedEvent.withTextFields(sequence.incrementAndGet(), timestamp, " ", values);
}
@Override
public boolean hasNext() {
return csvParser.iterator().hasNext();
}
private ZonedDateTime parseDateTime(String text) {
var m = format.getProfile().getParsingRegex().matcher(text);
ZonedDateTime timestamp = ZonedDateTime.of(format.getProfile().getTemporalAnchor().resolve(), format.getZoneId());
if (m.find()) {
for (Map.Entry<NamedCaptureGroup, String> entry : format.getProfile().getCaptureGroups().entrySet()) {
var captureGroup = entry.getKey();
var parsed = m.group(captureGroup.name());
if (parsed != null && !parsed.isBlank()) {
if (captureGroup instanceof TemporalCaptureGroup temporalGroup) {
timestamp = timestamp.with(temporalGroup.getMapping(), temporalGroup.parseLong(parsed));
}
}
}
return timestamp;
}
return null;
}
}
}
@@ -0,0 +1,40 @@
/*
* Copyright 2022 Frederic Thevenet
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.binjr.sources.csv.data.parsers;
import com.google.gson.annotations.JsonAdapter;
import eu.binjr.common.json.adapters.PatternJsonAdapter;
import eu.binjr.core.data.indexes.parser.profile.ParsingProfile;
import java.text.NumberFormat;
import java.util.Locale;
public interface CsvParsingProfile extends ParsingProfile {
String getDelimiter();
int getTimestampColumn();
int[] getExcludedColumns();
boolean isReadColumnNames();
Locale getNumberFormattingLocale();
char getQuoteCharacter();
boolean isTrimCellValues();
}
@@ -0,0 +1,341 @@
/*
* Copyright 2022 Frederic Thevenet
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.binjr.sources.csv.data.parsers;
import com.google.gson.reflect.TypeToken;
import eu.binjr.common.javafx.charts.StableTicksAxis;
import eu.binjr.common.javafx.controls.AlignedTableCellFactory;
import eu.binjr.common.javafx.controls.TextFieldValidator;
import eu.binjr.common.javafx.controls.ToolButtonBuilder;
import eu.binjr.common.text.StringUtils;
import eu.binjr.core.controllers.ParsingProfilesController;
import eu.binjr.core.data.adapters.DataAdapterFactory;
import eu.binjr.core.data.exceptions.NoAdapterFoundException;
import eu.binjr.core.data.indexes.parser.EventParser;
import eu.binjr.core.data.indexes.parser.ParsedEvent;
import eu.binjr.core.data.indexes.parser.capture.NamedCaptureGroup;
import eu.binjr.sources.csv.adapters.CsvAdapterPreferences;
import eu.binjr.sources.csv.adapters.CsvFileAdapter;
import javafx.beans.property.SimpleStringProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.text.TextAlignment;
import javafx.stage.FileChooser;
import org.controlsfx.control.textfield.TextFields;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.net.URL;
import java.nio.charset.Charset;
import java.text.NumberFormat;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.UnaryOperator;
public class CsvParsingProfilesController extends ParsingProfilesController<CsvParsingProfile> {
@FXML
private Spinner<ColumnPosition> timeColumnTextField;
@FXML
private TextField delimiterTextField;
@FXML
private TextField quoteCharacterTextField;
@FXML
private TableView<ParsedEvent> testResultTable;
@FXML
private TabPane testTabPane;
@FXML
private Tab inputTab;
@FXML
private Tab resultTab;
@FXML
private CheckBox readColumnNameCheckBox;
@FXML
private TextField parsingLocaleTextField;
@FXML
private CheckBox trimCellsCheckbox;
private final UnaryOperator<TextFormatter.Change> clampToSingleChar = c -> {
if (c.isContentChange()) {
String newText = c.getControlNewText();
int newLength = newText.length();
if (newLength > 1 && (newText.charAt(0) != '\\' || !List.of('t', 'r', 'n').contains(newText.charAt(1)) || newLength > 2)) {
String tail = newText.substring(newLength - 1, newLength);
c.setText(tail);
int oldLength = c.getControlText().length();
c.setRange(0, oldLength);
}
}
return c;
};
private NumberFormat numberFormat = NumberFormat.getNumberInstance();
@Override
public void initialize(URL location, ResourceBundle resources) {
super.initialize(location, resources);
delimiterTextField.textProperty().addListener((observable) -> resetTest());
timeColumnTextField.valueProperty().addListener(observable -> resetTest());
readColumnNameCheckBox.selectedProperty().addListener(observable -> resetTest());
trimCellsCheckbox.selectedProperty().addListener(observable -> resetTest());
TextFields.bindAutoCompletion(parsingLocaleTextField,
Arrays.stream(Locale.getAvailableLocales()).map(Locale::toLanguageTag).toList());
delimiterTextField.setTextFormatter(new TextFormatter<>(clampToSingleChar));
quoteCharacterTextField.setTextFormatter(new TextFormatter<>(clampToSingleChar));
}
@Override
protected void loadParserParameters(CsvParsingProfile profile) {
super.loadParserParameters(profile);
this.delimiterTextField.setText(profile.getDelimiter());
this.quoteCharacterTextField.setText(String.valueOf(profile.getQuoteCharacter()));
this.timeColumnTextField.setValueFactory(new ColumnPositionFactory(-1, 999999, profile.getTimestampColumn()));
this.readColumnNameCheckBox.setSelected(profile.isReadColumnNames());
this.parsingLocaleTextField.setText(profile.getNumberFormattingLocale().toLanguageTag());
this.trimCellsCheckbox.setSelected(profile.isTrimCellValues());
}
public record ColumnPosition(int index) {
@Override
public String toString() {
return (index < 0) ? "line numbers" : StringUtils.integerToOrdinal(index + 1) + " column";
}
}
public static class ColumnPositionFactory extends SpinnerValueFactory<ColumnPosition> {
private final int minValue;
private final int maxValue;
private int index = 0;
public ColumnPositionFactory(int minValue, int maxValue, int initialValue) {
if (initialValue > maxValue) {
throw new IllegalArgumentException("Initial value is above maximum value");
}
if (initialValue < minValue) {
throw new IllegalArgumentException("Initial value is below minimum value");
}
this.minValue = minValue;
this.maxValue = maxValue;
this.index = initialValue;
setValue(new ColumnPosition(index));
}
@Override
public void decrement(int steps) {
int newPos = index - steps;
if (newPos >= minValue) {
this.index = newPos;
setValue(new ColumnPosition(index));
}
}
@Override
public void increment(int steps) {
int newPos = index + steps;
if (newPos <= maxValue) {
this.index = newPos;
setValue(new ColumnPosition(index));
}
}
}
public CsvParsingProfilesController(CsvParsingProfile[] builtinParsingProfiles,
CsvParsingProfile[] userParsingProfiles,
CsvParsingProfile defaultProfile,
CsvParsingProfile selectedProfile,
Charset defaultCharset,
ZoneId defaultZoneId) throws NoAdapterFoundException {
this(builtinParsingProfiles,
userParsingProfiles,
defaultProfile,
selectedProfile,
true,
defaultCharset,
defaultZoneId);
}
public CsvParsingProfilesController(CsvParsingProfile[] builtinParsingProfiles,
CsvParsingProfile[] userParsingProfiles,
CsvParsingProfile defaultProfile,
CsvParsingProfile selectedProfile,
boolean allowTemporalCaptureGroupsOnly,
Charset defaultCharset,
ZoneId defaultZoneId) throws NoAdapterFoundException {
super(builtinParsingProfiles,
userParsingProfiles,
defaultProfile,
selectedProfile,
allowTemporalCaptureGroupsOnly,
defaultCharset,
defaultZoneId);
CsvAdapterPreferences prefs = (CsvAdapterPreferences) DataAdapterFactory.getInstance().getAdapterPreferences(CsvFileAdapter.class.getName());
}
@Override
protected void handleOnRunTest(ActionEvent event) {
super.handleOnRunTest(event);
testTabPane.getSelectionModel().select(resultTab);
}
@Override
protected Optional<List<FileChooser.ExtensionFilter>> additionalExtensions() {
return Optional.of(List.of(new FileChooser.ExtensionFilter("Comma-separated values files", "*.csv")));
}
@Override
protected List<CsvParsingProfile> deSerializeProfiles(String profileString) {
Type profileListType = new TypeToken<ArrayList<CustomCsvParsingProfile>>() {
}.getType();
return gson.fromJson(profileString, profileListType);
}
@Override
protected void doTest() throws Exception {
var format = new CsvEventFormat(profileComboBox.getValue(), getDefaultZoneId(), getDefaultCharset());
try (InputStream in = new ByteArrayInputStream(testArea.getText().getBytes(getDefaultCharset()))) {
var headers = format.getDataColumnHeaders(in);
if (headers.size() == 0) {
notifyWarn("No record found.");
} else {
Map<TableColumn, String> colMap = new HashMap<>();
var tsColNum = format.getProfile().getTimestampColumn();
// Add a column for line numbers
var lineNbColumn = makeColumn(colMap, TextAlignment.CENTER, -1, tsColNum, "#");
lineNbColumn.setCellValueFactory(param ->
new SimpleStringProperty(Long.toString(param.getValue().getSequence())));
lineNbColumn.getStyleClass().add("line-number-column");
testResultTable.getColumns().add(lineNbColumn);
for (int i = 0; i < headers.size(); i++) {
String name = headers.get(i);
TableColumn<ParsedEvent, String> col = makeColumn(colMap, TextAlignment.RIGHT, i, tsColNum, name);
if (i == tsColNum) {
col.setCellValueFactory(param ->
new SimpleStringProperty(param.getValue().getTimestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss[.SSS]"))));
} else {
col.setCellValueFactory(param ->
new SimpleStringProperty(formatToDouble(param.getValue().getTextFields().get(colMap.get(param.getTableColumn())))));
}
testResultTable.getColumns().add(col);
}
}
}
try (InputStream in = new ByteArrayInputStream(testArea.getText().getBytes(getDefaultCharset()))) {
EventParser eventParser = format.parse(in);
for (ParsedEvent parsed : eventParser) {
testResultTable.getItems().add(parsed);
}
notifyInfo(String.format("Found %d record(s).", testResultTable.getItems().size()));
}
}
private TableColumn<ParsedEvent, String> makeColumn(Map<TableColumn, String> colMap,
TextAlignment alignment,
int i,
int tsColumn,
String name) {
var col = new TableColumn<ParsedEvent, String>(name);
col.setStyle("-fx-font-weight: normal;");
var isTimeColCtrl = new ToolButtonBuilder<ToggleButton>()
.setText("")
.setTooltip("Extract timestamp from this column")
.setStyleClass("dialog-button")
.setAction(event -> {
var btn = (ToggleButton) event.getSource();
if (btn.getUserData() instanceof ColumnPosition pos) {
this.timeColumnTextField.setValueFactory(new ColumnPositionFactory(-1, 999999, pos.index()));
handleOnRunTest(null);
}
})
.setIconStyleClass("time-icon", "small-icon")
.build(ToggleButton::new);
isTimeColCtrl.setUserData(new ColumnPosition(i));
isTimeColCtrl.setSelected(i == tsColumn);
col.setGraphic(isTimeColCtrl);
col.setSortable(false);
col.setReorderable(false);
var cellFactory = new AlignedTableCellFactory<ParsedEvent, String>();
cellFactory.setAlignment(alignment);
col.setCellFactory(cellFactory);
colMap.put(col, Integer.toString(i));
return col;
}
@Override
protected void resetTest() {
super.resetTest();
this.testResultTable.getItems().clear();
this.testResultTable.getColumns().clear();
testTabPane.getSelectionModel().select(inputTab);
}
@Override
protected Optional<CsvParsingProfile> updateProfile(String profileName, String profileId, Map<NamedCaptureGroup, String> groups, String lineExpression) {
List<String> errors = new ArrayList<>();
if (this.lineTemplateExpression.getText().isBlank()) {
TextFieldValidator.fail(delimiterTextField, true);
errors.add("Timestamp pattern cannot be empty");
}
if (this.delimiterTextField.getText().isEmpty()) {
TextFieldValidator.fail(delimiterTextField, true);
errors.add("Delimiting character for CSV parsing cannot be empty");
}
if (this.quoteCharacterTextField.getText().isBlank()) {
TextFieldValidator.fail(quoteCharacterTextField, true);
errors.add("Quote character for CSV parsing cannot be empty");
}
try {
var bld = new Locale.Builder();
bld.setLanguageTag(parsingLocaleTextField.getText());
var parsingLocale = bld.build();
this.numberFormat = NumberFormat.getNumberInstance(parsingLocale);
} catch (IllformedLocaleException e) {
errors.add("The locale for number parsing is invalid: " + e.getMessage());
}
if (errors.size() > 0) {
notifyError(String.join("\n", errors));
return Optional.empty();
}
return Optional.of(new CustomCsvParsingProfile(profileName,
profileId,
groups,
lineExpression,
this.delimiterTextField.getText(),
StringUtils.stringToEscapeSequence(this.quoteCharacterTextField.getText()).charAt(0),
this.timeColumnTextField.getValue().index(),
new int[0],
this.readColumnNameCheckBox.isSelected(),
Locale.forLanguageTag(parsingLocaleTextField.getText()),
this.trimCellsCheckbox.isSelected()));
}
private String formatToDouble(String value) {
if (value != null) {
try {
return numberFormat.format(numberFormat.parse(value));
} catch (Exception e) {
// Do nothing
}
}
return "NaN";
}
}
@@ -0,0 +1,111 @@
/*
* Copyright 2022 Frederic Thevenet
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.binjr.sources.csv.data.parsers;
import com.google.gson.annotations.JsonAdapter;
import eu.binjr.common.json.adapters.LocaleJsonAdapter;
import eu.binjr.core.data.indexes.parser.capture.NamedCaptureGroup;
import eu.binjr.core.data.indexes.parser.profile.CustomParsingProfile;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
public class CustomCsvParsingProfile extends CustomParsingProfile implements CsvParsingProfile {
private final String delimiter;
private final char quoteCharacter;
private final int timestampColumn;
private final int[] excludedColumns;
private final boolean readColumnNames;
@JsonAdapter(LocaleJsonAdapter.class)
private final Locale formattingLocale;
private final boolean trimCellValues;
public CustomCsvParsingProfile() {
this("", UUID.randomUUID().toString(), new HashMap<>(), "", ",", '"', 0, new int[0], true, Locale.getDefault(), false);
}
public static CsvParsingProfile of(CsvParsingProfile parsingProfile) {
return new CustomCsvParsingProfile(parsingProfile.getProfileName(),
parsingProfile.getProfileId(),
parsingProfile.getCaptureGroups(),
parsingProfile.getLineTemplateExpression(),
parsingProfile.getDelimiter(),
parsingProfile.getQuoteCharacter(),
parsingProfile.getTimestampColumn(),
parsingProfile.getExcludedColumns(),
parsingProfile.isReadColumnNames(),
parsingProfile.getNumberFormattingLocale(),
parsingProfile.isTrimCellValues());
}
public CustomCsvParsingProfile(String profileName,
String profileId,
Map<NamedCaptureGroup, String> captureGroups,
String lineTemplateExpression,
String delimiter,
char quoteCharacter, int timestampColumn,
int[] excludedColumns, boolean readColumnNames,
Locale formattingLocale, boolean trimCellValues) {
super(profileName, profileId, captureGroups, lineTemplateExpression);
this.delimiter = delimiter;
this.quoteCharacter = quoteCharacter;
this.timestampColumn = timestampColumn;
this.excludedColumns = excludedColumns;
this.readColumnNames = readColumnNames;
this.formattingLocale = formattingLocale;
this.trimCellValues = trimCellValues;
}
@Override
public String getDelimiter() {
return this.delimiter;
}
@Override
public int getTimestampColumn() {
return this.timestampColumn;
}
@Override
public int[] getExcludedColumns() {
return this.excludedColumns;
}
@Override
public boolean isReadColumnNames() {
return readColumnNames;
}
@Override
public Locale getNumberFormattingLocale() {
return formattingLocale;
}
@Override
public char getQuoteCharacter() {
return quoteCharacter;
}
@Override
public boolean isTrimCellValues() {
return trimCellValues;
}
}
@@ -0,0 +1,18 @@
#
# Copyright 2017-2018 Frederic Thevenet
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Csv file Data adapter service implementation
eu.binjr.sources.csv.adapters.CsvFileDataAdapterInfo
@@ -0,0 +1,464 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2022 Frederic Thevenet
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<?import java.lang.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.control.cell.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import org.fxmisc.flowless.*?>
<?import org.fxmisc.richtext.*?>
<?import eu.binjr.common.javafx.controls.LabelWithInlineHelp?>
<DialogPane fx:id="dialogPane" styleClass="skinnable-pane-border, tool-dialog-window"
xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1">
<content>
<AnchorPane fx:id="root" prefHeight="777.0" prefWidth="853.0">
<children>
<VBox fx:id="expressions" spacing="10.0"
AnchorPane.bottomAnchor="4.0"
AnchorPane.leftAnchor="4.0"
AnchorPane.rightAnchor="4.0"
AnchorPane.topAnchor="40.0">
<TitledPane fx:id="setupTitledPane" animated="false" maxWidth="Infinity"
maxHeight="Infinity"
contentDisplay="GRAPHIC_ONLY"
VBox.vgrow="SOMETIMES">
<graphic>
<LabelWithInlineHelp text="Setup"
inlineHelp="Setup parsing rules&#xD;&#xD;Note: The settings below are greyed out when a built-in profile is selected. If you need to customize a built-in profile, click on &quot;Duplicate profile&quot; to create a copy that you can modify."
alignment="CENTER_RIGHT"/>
</graphic>
<VBox fx:id="setupPane" spacing="10.0">
<padding>
<Insets top="0" right="0" bottom="0" left="0"/>
</padding>
<HBox alignment="CENTER_LEFT">
<Region HBox.hgrow="ALWAYS"/>
</HBox>
<TableView fx:id="captureGroupTable" prefHeight="220.0" prefWidth="762.0"
styleClass="search-field-outer">
<columns>
<TableColumn fx:id="nameColumn" prefWidth="220.0" sortable="false"
text="Group Name">
<cellValueFactory>
<PropertyValueFactory property="name"/>
</cellValueFactory>
</TableColumn>
<TableColumn fx:id="expressionColumn" minWidth="0.0" prefWidth="550.0"
sortable="false" text="Capture Expression">
<cellValueFactory>
<PropertyValueFactory property="expression"/>
</cellValueFactory>
</TableColumn>
<TableColumn editable="false" maxWidth="56" minWidth="56" prefWidth="56.0"
resizable="false" sortable="false">
<graphic>
<HBox>
<Button fx:id="addGroupButton" cache="true"
contentDisplay="GRAPHIC_ONLY" minHeight="-Infinity"
minWidth="-Infinity" mnemonicParsing="false"
onAction="#handleOnAddGroup" prefHeight="25.0"
prefWidth="25.0"
styleClass="dialog-button" text="Add">
<graphic>
<Region>
<styleClass>
<String fx:value="icon-container"/>
<String fx:value="plus-icon"/>
<String fx:value="medium-icon"/>
</styleClass>
</Region>
</graphic>
<tooltip>
<Tooltip showDelay="500ms" text="Add new capture group"/>
</tooltip>
</Button>
<Button fx:id="deleteGroupButton" contentDisplay="GRAPHIC_ONLY"
maxHeight="-Infinity" maxWidth="-Infinity"
minHeight="-Infinity"
minWidth="-Infinity" mnemonicParsing="false"
onAction="#handleOnDeleteGroup" prefHeight="25.0"
prefWidth="25.0" styleClass="dialog-button" text="Delete">
<graphic>
<Region>
<styleClass>
<String fx:value="icon-container"/>
<String fx:value="minus-icon"/>
<String fx:value="medium-icon"/>
</styleClass>
</Region>
</graphic>
<tooltip>
<Tooltip showDelay="500ms" text="Delete capture group"/>
</tooltip>
</Button>
</HBox>
</graphic>
</TableColumn>
</columns>
</TableView>
<HBox alignment="CENTER_LEFT" spacing="10.0">
<children>
<LabelWithInlineHelp minWidth="115.0" text="Timestamp pattern" inlineHelp="The regular expression pattern be used to parse timestamps.
&#xD;Use the capture groups defined in the table above to identify the individual components of a timestamp event (e.g. day, hour, second, etc...).
&#xD;Use Java regular expression to model how these components can be recognized in the particular syntax of a CSV file."
alignment="CENTER_RIGHT"/>
<CodeArea fx:id="lineTemplateExpression" maxHeight="-Infinity"
maxWidth="1.7976931348623157E308" minHeight="26.0"
prefHeight="26.0"
styleClass="search-field-outer" HBox.hgrow="ALWAYS">
<padding>
<Insets top="2.0" right="2.0" bottom="2.0" left="2.0"/>
</padding>
</CodeArea>
</children>
</HBox>
<HBox alignment="CENTER_LEFT" spacing="10.0">
<children>
<LabelWithInlineHelp minWidth="115.0" text="Get timestamps from"
inlineHelp="Indicates in which column of the CSV file to look for timestamps data.&#xD;If your CSV file does not contain any timestamps info, you may select &quot;line number&quot;, in which case &#xD;binjr will infer a timestamp for each line, starting at the temporal reference point and &#xD;increasing by 1 second for every new line."
alignment="CENTER_RIGHT"/>
<Spinner fx:id="timeColumnTextField" editable="false"
minHeight="-Infinity" prefHeight="24.0" prefWidth="120.0"/>
<Separator orientation="VERTICAL" prefHeight="20.0"/>
<CheckBox fx:id="readColumnNameCheckBox"
text="Use values from first line as column names"/>
<CheckBox fx:id="trimCellsCheckbox"
text="Trim cell values "/>
</children>
</HBox>
<HBox alignment="CENTER_LEFT" spacing="10.0">
<children>
<LabelWithInlineHelp minWidth="115.0" text="Delimiting character"
inlineHelp="The character used to delimit the columns of the CSV file."
alignment="CENTER_RIGHT"/>
<TextField fx:id="delimiterTextField" maxWidth="35" minHeight="-Infinity"
prefHeight="24.0" prefWidth="35.0"/>
<Separator orientation="VERTICAL" prefHeight="20.0"/>
<LabelWithInlineHelp text="Quote character"
inlineHelp="The character used to encapsulate the content of a column so that it is not split even if it contains instances of the delimiting character."
alignment="CENTER_RIGHT"/>
<TextField fx:id="quoteCharacterTextField" maxWidth="35" minHeight="-Infinity"
prefHeight="24.0" prefWidth="35.0"/>
<Separator orientation="VERTICAL" prefHeight="20.0"/>
<LabelWithInlineHelp text="Locale for parsing numbers"
inlineHelp="Indicates which regional settings should be applied when parsing column content as numbers."
alignment="CENTER_RIGHT"/>
<TextField fx:id="parsingLocaleTextField" maxWidth="120.0" prefHeight="24.0"/>
</children>
</HBox>
</VBox>
</TitledPane>
<TitledPane fx:id="testTitledPane" animated="false" maxWidth="Infinity"
maxHeight="Infinity"
contentDisplay="GRAPHIC_ONLY"
VBox.vgrow="ALWAYS">
<graphic>
<LabelWithInlineHelp text="Test"
inlineHelp="Test data extraction&#xD;&#xD;
You can enter text (or paste from clipboard/load it from a file) in the &quot;input&quot; tab below to&#xD;
and see the results of your parsing rules in the &quot;result&quot; tab.&#xD;&#xD;
TIP: Clicking on the clock shaped icon next to a column's header acts as a shortcut to indicate&#xD;
that this column should used to extract timestamps from."
alignment="CENTER_RIGHT"/>
</graphic>
<VBox fx:id="testPane" spacing="10.0">
<padding>
<Insets top="0" right="0" bottom="0" left="0"/>
</padding>
<HBox alignment="CENTER">
<Button fx:id="runTestButton" contentDisplay="LEFT" maxHeight="-Infinity"
maxWidth="-Infinity"
minHeight="-Infinity" minWidth="-Infinity" mnemonicParsing="false"
onAction="#handleOnRunTest" text="Test data extraction">
<HBox.margin>
<Insets bottom="2.0" top="6.0"/>
</HBox.margin>
<graphic>
<Region>
<styleClass>
<String fx:value="icon-container"/>
<String fx:value="test-icon"/>
<String fx:value="medium-icon"/>
</styleClass>
<padding>
<Insets right="25.0"/>
</padding>
</Region>
</graphic>
<padding>
<Insets bottom="5.0" left="40.0" right="45.0" top="5.0"/>
</padding>
<tooltip>
<Tooltip showDelay="500ms" text="Test data extraction"/>
</tooltip>
</Button>
</HBox>
<Label fx:id="notificationLabel" managed="false" maxHeight="1.7976931348623157E308"
maxWidth="1.7976931348623157E308" minHeight="-Infinity"
styleClass="notification-info"
text="" visible="false" wrapText="true" VBox.vgrow="NEVER">
<padding>
<Insets bottom="4.0" left="4.0" right="4.0" top="4.0"/>
</padding>
</Label>
<HBox styleClass="search-field-outer" VBox.vgrow="ALWAYS">
<children>
<TabPane fx:id="testTabPane" maxHeight="Infinity" maxWidth="Infinity" side="TOP"
HBox.hgrow="ALWAYS">
<Tab fx:id="inputTab" closable="false" text="Input">
<graphic>
<Region prefWidth="20">
<styleClass>
<String fx:value="icon-container"/>
<String fx:value="edit-icon"/>
<String fx:value="small-icon"/>
</styleClass>
</Region>
</graphic>
<VirtualizedScrollPane>
<content>
<CodeArea fx:id="testArea" maxHeight="1.7976931348623157E308"
maxWidth="1.7976931348623157E308" prefHeight="201.0"
prefWidth="795.0" HBox.hgrow="ALWAYS"/>
</content>
</VirtualizedScrollPane>
</Tab>
<Tab fx:id="resultTab" closable="false" text="Results">
<graphic>
<Region prefWidth="20">
<styleClass>
<String fx:value="icon-container"/>
<String fx:value="eye-icon"/>
<String fx:value="small-icon"/>
</styleClass>
</Region>
</graphic>
<TableView fx:id="testResultTable" prefHeight="200.0" prefWidth="200.0"
HBox.hgrow="ALWAYS"/>
</Tab>
</TabPane>
<VBox>
<children>
<Button fx:id="clearTestAreaButton" cache="true"
contentDisplay="GRAPHIC_ONLY"
minHeight="-Infinity" minWidth="-Infinity" mnemonicParsing="false"
onAction="#handleOnClearTestArea" prefHeight="30.0" prefWidth="30.0"
styleClass="dialog-button" text="Clear">
<graphic>
<Region>
<styleClass>
<String fx:value="icon-container"/>
<String fx:value="trash-icon"/>
<String fx:value="medium-icon"/>
</styleClass>
</Region>
</graphic>
<tooltip>
<Tooltip showDelay="500ms" text="Clear test area"/>
</tooltip>
</Button>
<Button fx:id="copyTestAreaButton" cache="true"
contentDisplay="GRAPHIC_ONLY"
minHeight="-Infinity" minWidth="-Infinity" mnemonicParsing="false"
onAction="#handleOnCopyTestArea" prefHeight="30.0" prefWidth="30.0"
styleClass="dialog-button" text="Copy">
<graphic>
<Region>
<styleClass>
<String fx:value="icon-container"/>
<String fx:value="copy-icon"/>
<String fx:value="medium-icon"/>
</styleClass>
</Region>
</graphic>
<tooltip>
<Tooltip showDelay="500ms" text="Copy test area"/>
</tooltip>
</Button>
<Button fx:id="pasteTestAreaButton" contentDisplay="GRAPHIC_ONLY"
maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity"
minWidth="-Infinity" mnemonicParsing="false"
onAction="#handleOnPasteToTestArea" prefHeight="30.0"
prefWidth="30.0"
styleClass="dialog-button" text="Paste">
<graphic>
<Region>
<styleClass>
<String fx:value="icon-container"/>
<String fx:value="clipboard-icon"/>
<String fx:value="medium-icon"/>
</styleClass>
</Region>
</graphic>
<tooltip>
<Tooltip showDelay="500ms" text="Paste to test area"/>
</tooltip>
</Button>
<Button fx:id="openFileButton" contentDisplay="GRAPHIC_ONLY"
maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity"
minWidth="-Infinity" mnemonicParsing="false"
onAction="#handleOnOpenFileToTestArea" prefHeight="30.0"
prefWidth="30.0"
styleClass="dialog-button" text="Open File">
<graphic>
<Region>
<styleClass>
<String fx:value="icon-container"/>
<String fx:value="fileOpen-icon"/>
<String fx:value="medium-icon"/>
</styleClass>
</Region>
</graphic>
<tooltip>
<Tooltip showDelay="500ms"
text="Preview the first few lines of a file into test area"/>
</tooltip>
</Button>
<Region maxHeight="1.7976931348623157E308" VBox.vgrow="ALWAYS"/>
</children>
</VBox>
</children>
</HBox>
</VBox>
</TitledPane>
</VBox>
<HBox alignment="CENTER_LEFT" spacing="2.0"
AnchorPane.leftAnchor="4.0"
AnchorPane.rightAnchor="4.0"
AnchorPane.topAnchor="4.0">
<children>
<HBox maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308"
styleClass="search-field-outer" HBox.hgrow="ALWAYS">
<Region fx:id="builtinIcon" prefHeight="12" prefWidth="12" minWidth="12">
<styleClass>
<String fx:value="icon-container"/>
<String fx:value="padlock-icon"/>
<String fx:value="medium-icon"/>
</styleClass>
<HBox.margin>
<Insets left="6.0" right="0.0"/>
</HBox.margin>
</Region>
<ComboBox fx:id="profileComboBox" editable="true" maxHeight="1.7976931348623157E308"
maxWidth="1.7976931348623157E308" styleClass="search-field-inner"
HBox.hgrow="ALWAYS">
</ComboBox>
<padding>
<Insets left="1.0" right="1.0"/>
</padding>
</HBox>
<Button fx:id="addProfileButton" cache="true" contentDisplay="GRAPHIC_ONLY"
minHeight="-Infinity" minWidth="-Infinity" mnemonicParsing="false"
onAction="#handleOnAddProfile" prefHeight="30.0" prefWidth="30.0"
styleClass="dialog-button" text="Add">
<graphic>
<Region>
<styleClass>
<String fx:value="icon-container"/>
<String fx:value="plus-icon"/>
</styleClass>
</Region>
</graphic>
<tooltip>
<Tooltip showDelay="500ms" text="Create a new profile"/>
</tooltip>
</Button>
<Button fx:id="deleteProfileButton" contentDisplay="GRAPHIC_ONLY" maxHeight="-Infinity"
maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" mnemonicParsing="false"
onAction="#handleOnDeleteProfile" prefHeight="30.0" prefWidth="30.0"
styleClass="dialog-button" text="delete" HBox.hgrow="NEVER">
<graphic>
<Region>
<styleClass>
<String fx:value="icon-container"/>
<String fx:value="minus-icon"/>
</styleClass>
</Region>
</graphic>
<tooltip>
<Tooltip showDelay="500ms" text="Delete profile"/>
</tooltip>
<font>
<Font size="16.0"/>
</font>
</Button>
<Button fx:id="cloneProfileButton" cache="true" contentDisplay="GRAPHIC_ONLY"
minHeight="-Infinity" minWidth="-Infinity" mnemonicParsing="false"
onAction="#handleOnCloneProfile" prefHeight="30.0" prefWidth="30.0"
styleClass="dialog-button" text="Duplicate">
<graphic>
<Region>
<styleClass>
<String fx:value="icon-container"/>
<String fx:value="copy-icon"/>
</styleClass>
</Region>
</graphic>
<tooltip>
<Tooltip showDelay="500ms" text="Duplicate profile"/>
</tooltip>
</Button>
<Button fx:id="importProfileButton" cache="true" contentDisplay="GRAPHIC_ONLY"
minHeight="-Infinity" minWidth="-Infinity" mnemonicParsing="false"
onAction="#handleOnImportProfile" prefHeight="30.0" prefWidth="30.0"
styleClass="dialog-button" text="Import" HBox.hgrow="NEVER">
<graphic>
<Region>
<styleClass>
<String fx:value="icon-container"/>
<String fx:value="upload-icon"/>
</styleClass>
</Region>
</graphic>
<tooltip>
<Tooltip showDelay="500ms" text="Import profiles"/>
</tooltip>
</Button>
<Button fx:id="exportProfileButton" contentDisplay="GRAPHIC_ONLY" maxHeight="-Infinity"
maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" mnemonicParsing="false"
onAction="#handleOnExportProfile" prefHeight="30.0" prefWidth="30.0"
styleClass="dialog-button" text="Export" HBox.hgrow="NEVER">
<graphic>
<Region>
<styleClass>
<String fx:value="icon-container"/>
<String fx:value="download-icon"/>
</styleClass>
</Region>
</graphic>
<tooltip>
<Tooltip showDelay="500ms" text="Export profiles"/>
</tooltip>
</Button>
</children>
</HBox>
</children>
</AnchorPane>
</content>
<buttonTypes>
<ButtonType fx:constant="CANCEL"/>
<ButtonType fx:constant="OK"/>
</buttonTypes>
</DialogPane>