的
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# binjr-adapter-csv
|
||||
|
||||
[](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.
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+89
@@ -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);
|
||||
}
|
||||
}
|
||||
+317
@@ -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();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+171
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
+53
@@ -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);
|
||||
}
|
||||
}
|
||||
+107
@@ -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;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
+179
@@ -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;
|
||||
}
|
||||
}
|
||||
+92
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+147
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+40
@@ -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();
|
||||
}
|
||||
+341
@@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
+111
@@ -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;
|
||||
}
|
||||
}
|
||||
+18
@@ -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
|
||||
+464
@@ -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

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 "Duplicate profile" 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.
|
||||

Use the capture groups defined in the table above to identify the individual components of a timestamp event (e.g. day, hour, second, etc...).
|
||||

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.
If your CSV file does not contain any timestamps info, you may select "line number", in which case 
binjr will infer a timestamp for each line, starting at the temporal reference point and 
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

|
||||
You can enter text (or paste from clipboard/load it from a file) in the "input" tab below to
|
||||
and see the results of your parsing rules in the "result" tab.

|
||||
TIP: Clicking on the clock shaped icon next to a column's header acts as a shortcut to indicate
|
||||
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>
|
||||
Reference in New Issue
Block a user