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
+31
View File
@@ -0,0 +1,31 @@
/*
* Copyright 2020-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,63 @@
/*
* Copyright 2020 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.text.adapters;
import com.google.gson.Gson;
import eu.binjr.common.preferences.ObservablePreference;
import eu.binjr.core.data.adapters.DataAdapter;
import eu.binjr.core.data.adapters.DataAdapterPreferences;
/**
* Defines the preferences associated with the Text files adapter.
*/
public class TextAdapterPreferences 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,
"fileExtensionFilters",
new String[]{".xml", ".txt", ".env", ".properties", ".csv", ".log", "md", ".json", ".yml"},
gson::toJson,
s -> gson.fromJson(s, String[].class));
/**
* Initialize a new instance of the {@link TextAdapterPreferences} class associated to
* a {@link DataAdapter} instance.
*
* @param dataAdapterClass the associated {@link DataAdapter}
*/
public TextAdapterPreferences(Class<? extends DataAdapter<?>> dataAdapterClass) {
super(dataAdapterClass, false);
}
}
@@ -0,0 +1,253 @@
/*
* Copyright 2020-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.text.adapters;
import com.google.gson.Gson;
import eu.binjr.common.io.FileSystemBrowser;
import eu.binjr.common.javafx.controls.TreeViewUtils;
import eu.binjr.common.logging.Logger;
import eu.binjr.common.logging.Profiler;
import eu.binjr.common.text.BinaryPrefixFormatter;
import eu.binjr.core.data.adapters.BaseDataAdapter;
import eu.binjr.core.data.adapters.SourceBinding;
import eu.binjr.core.data.adapters.TextFilesBinding;
import eu.binjr.core.data.exceptions.DataAdapterException;
import eu.binjr.core.data.timeseries.TextProcessor;
import eu.binjr.core.data.timeseries.TimeSeriesProcessor;
import eu.binjr.core.data.workspace.TimeSeriesInfo;
import eu.binjr.core.dialogs.Dialogs;
import javafx.scene.chart.XYChart;
import org.eclipse.fx.ui.controls.tree.FilterableTreeItem;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* A {@link eu.binjr.core.data.adapters.DataAdapter} implementation to retrieve data from a text file.
*
* @author Frederic Thevenet
*/
public class TextDataAdapter extends BaseDataAdapter<String> {
private static final Logger logger = Logger.create(TextDataAdapter.class);
private static final Gson gson = new Gson();
private final BinaryPrefixFormatter binaryPrefixFormatter = new BinaryPrefixFormatter("###,###.## ");
private Path rootPath;
private FileSystemBrowser fileBrowser;
private String[] folderFilters;
private String[] fileExtensionsFilters;
/**
* Initializes a new instance of the {@link TextDataAdapter} class.
*
* @throws DataAdapterException if an error occurs while initializing the adapter.
*/
public TextDataAdapter() throws DataAdapterException {
super();
}
/**
* Initializes a new instance of the {@link TextDataAdapter} class from the provided {@link Path}
*
* @param rootPath the {@link Path} from which to load content.
* @param folderFilters a list of names of folders to inspect for content.
* @param fileExtensionsFilters a list of file extensions to inspect for content.
* @throws DataAdapterException if an error occurs initializing the adapter.
*/
public TextDataAdapter(Path rootPath, String[] folderFilters, String[] fileExtensionsFilters) throws DataAdapterException {
super();
this.rootPath = rootPath;
Map<String, String> params = new HashMap<>();
initParams(rootPath, folderFilters, fileExtensionsFilters);
}
@Override
public Map<String, String> getParams() {
Map<String, String> params = new HashMap<>();
params.put("rootPath", rootPath.toString());
params.put("folderFilters", gson.toJson(folderFilters));
params.put("fileExtensionsFilters", gson.toJson(fileExtensionsFilters));
return params;
}
@Override
public void loadParams(Map<String, String> params) throws DataAdapterException {
if (logger.isDebugEnabled()) {
logger.debug(() -> "TextDataAdapter params:");
params.forEach((s, s2) -> logger.debug(() -> "key=" + s + ", value=" + s2));
}
initParams(Paths.get(validateParameterNullity(params, "rootPath")),
gson.fromJson(validateParameterNullity(params, "folderFilters"), String[].class),
gson.fromJson(validateParameterNullity(params, "fileExtensionsFilters"), String[].class));
}
private void initParams(Path rootPath, String[] folderFilters, String[] fileExtensionsFilters) throws DataAdapterException {
this.rootPath = rootPath;
this.folderFilters = folderFilters;
this.fileExtensionsFilters = fileExtensionsFilters;
try {
this.fileBrowser = FileSystemBrowser.of(rootPath);
} catch (IOException e) {
throw new DataAdapterException("Could not create file system browser instance", e);
}
}
@Override
public FilterableTreeItem<SourceBinding> getBindingTree() throws DataAdapterException {
FilterableTreeItem<SourceBinding> rootNode = new FilterableTreeItem<>(
new TextFilesBinding.Builder()
.withLabel(getSourceName())
.withAdapter(this)
.build());
attachTextFilesTree(rootNode);
return rootNode;
}
private void attachTextFilesTree(FilterableTreeItem<SourceBinding> rootNode) throws DataAdapterException {
try (var p = Profiler.start("Building text binding tree", logger::perf)) {
Map<Path, FilterableTreeItem<SourceBinding>> nodeDict = new HashMap<>();
nodeDict.put(fileBrowser.toInternalPath("/"), rootNode);
for (var fsEntry : fileBrowser.listEntries(folderFilters, fileExtensionsFilters)) {
String fileName = fsEntry.getPath().getFileName().toString();
var attachTo = rootNode;
if (fsEntry.getPath().getParent() != null) {
attachTo = nodeDict.get(fsEntry.getPath().getParent());
if (attachTo == null) {
attachTo = makeBranchNode(nodeDict, fsEntry.getPath().getParent(), rootNode);
}
}
FilterableTreeItem<SourceBinding> filenode = new FilterableTreeItem<>(
new TextFilesBinding.Builder()
.withLabel(fileName + " (" + binaryPrefixFormatter.format(fsEntry.getSize()) + "B)")
.withPath(fsEntry.getPath().toString())
.withParent(attachTo.getValue())
.withAdapter(this)
.build());
attachTo.getInternalChildren().add(filenode);
}
TreeViewUtils.sortFromBranch(rootNode);
} catch (Exception e) {
Dialogs.notifyException("Error while enumerating files: " + e.getMessage(), e);
}
}
private FilterableTreeItem<SourceBinding> makeBranchNode(Map<Path, FilterableTreeItem<SourceBinding>> nodeDict,
Path path,
FilterableTreeItem<SourceBinding> root) {
var parent = root;
var rootPath = path.isAbsolute() ? path.getRoot() : path.getName(0);
for (int i = 0; i < path.getNameCount(); i++) {
Path current = rootPath.resolve(path.getName(i));
FilterableTreeItem<SourceBinding> filenode = nodeDict.get(current);
if (filenode == null) {
filenode = new FilterableTreeItem<>(
new TextFilesBinding.Builder()
.withLabel(current.getFileName().toString())
.withPath(path.toString())
.withParent(parent.getValue())
.withAdapter(this)
.build());
nodeDict.put(current, filenode);
parent.getInternalChildren().add(filenode);
}
parent = filenode;
rootPath = current;
}
return parent;
}
@Override
public Map<TimeSeriesInfo<String>, TimeSeriesProcessor<String>> fetchData(String path,
Instant start,
Instant end,
List<TimeSeriesInfo<String>> seriesInfos,
boolean bypassCache) throws DataAdapterException {
Map<TimeSeriesInfo<String>, TimeSeriesProcessor<String>> data = new HashMap<>();
for (var info : seriesInfos) {
try {
var proc = new TextProcessor();
proc.setData(List.of(new XYChart.Data<>(ZonedDateTime.now(), readTextFile(info.getBinding().getPath()))));
data.put(info, proc);
} catch (IOException e) {
throw new DataAdapterException("Error fetching text from " + info.getBinding().getPath(), e);
}
}
return data;
}
@Override
public String getEncoding() {
return "utf-8";
}
@Override
public ZoneId getTimeZoneId() {
return ZoneId.systemDefault();
}
@Override
public String getSourceName() {
return "[Text] " + (rootPath != null ? rootPath.getFileName() : "???");
}
@Override
public void onStart() throws DataAdapterException {
super.onStart();
}
@Override
public void close() {
if (fileBrowser != null) {
try {
fileBrowser.close();
} catch (IOException e) {
logger.error("An error occurred while closing file system browser instance: " + e.getMessage());
logger.debug(e);
}
}
super.close();
}
/**
* Read the content of the text file located at the provided path
*
* @param path the path of the text file to read
* @return The content of the text file, as a String
* @throws IOException if an error occurs while reading the file.
*/
public String readTextFile(String path) throws IOException {
try (Profiler ignored = Profiler.start("Extracting text from file " + path, logger::perf)) {
try (var reader = new BufferedReader(new InputStreamReader(fileBrowser.getData(path), StandardCharsets.UTF_8))) {
return reader.lines().collect(Collectors.joining("\n"));
}
}
}
}
@@ -0,0 +1,140 @@
/*
* Copyright 2020-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.text.adapters;
import com.google.gson.Gson;
import eu.binjr.common.javafx.controls.NodeUtils;
import eu.binjr.common.logging.Logger;
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 javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Side;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.List;
/**
* An implementation of the {@link DataAdapterDialog} class that presents a dialog box to retrieve the parameters specific {@link TextDataAdapterDialog}
*
* @author Frederic Thevenet
*/
public class TextDataAdapterDialog extends DataAdapterDialog<Path> {
private static final Logger logger = Logger.create(TextDataAdapterDialog.class);
private final TextField extensionFiltersTextField;
private final TextAdapterPreferences prefs;
private static final Gson gson = new Gson();
/**
* Initializes a new instance of the {@link TextDataAdapterDialog} class.
*
* @param owner the owner window for the dialog
* @throws NoAdapterFoundException if no adapter could be found to get preferences for.
*/
public TextDataAdapterDialog(Node owner) throws NoAdapterFoundException {
super(owner, Mode.PATH, "mostRecentTextArchives", false);
this.prefs = (TextAdapterPreferences) DataAdapterFactory.getInstance().getAdapterPreferences(TextDataAdapter.class.getName());
setDialogHeaderText("Add a TExt File, Zip Archive or Folder");
extensionFiltersTextField = new TextField(gson.toJson(prefs.fileExtensionFilters.get()));
var label = new Label("Extensions:");
GridPane.setConstraints(label, 0, 1, 1, 1, HPos.LEFT, VPos.CENTER, Priority.ALWAYS, Priority.ALWAYS, new Insets(4, 0, 4, 0));
GridPane.setConstraints(extensionFiltersTextField, 1, 1, 1, 1, HPos.LEFT, VPos.CENTER, Priority.ALWAYS, Priority.ALWAYS, new Insets(4, 0, 4, 0));
getParamsGridPane().getChildren().addAll(label, extensionFiltersTextField);
}
@Override
protected File displayFileChooser(Node owner) {
try {
ContextMenu sourceMenu = new ContextMenu();
MenuItem fileMenuItem = new MenuItem("Text file");
fileMenuItem.setOnAction(eventHandler -> {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open Text File");
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Text files", "*.txt", "*.text"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("All files", "*.*", "*"));
Dialogs.getInitialDir(getMostRecentList()).ifPresent(fileChooser::setInitialDirectory);
File selectedFile = fileChooser.showOpenDialog(NodeUtils.getStage(owner));
if (selectedFile != null) {
setSourceUri(selectedFile.getPath());
}
});
sourceMenu.getItems().add(fileMenuItem);
MenuItem menuItem = new MenuItem("Zip file");
menuItem.setOnAction(eventHandler -> {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open Zip Archive");
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Zip archive", "*.zip"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("All files", "*.*", "*"));
Dialogs.getInitialDir(getMostRecentList()).ifPresent(fileChooser::setInitialDirectory);
File selectedFile = fileChooser.showOpenDialog(NodeUtils.getStage(owner));
if (selectedFile != null) {
setSourceUri(selectedFile.getPath());
}
});
sourceMenu.getItems().add(menuItem);
MenuItem folderMenuItem = new MenuItem("Folder");
folderMenuItem.setOnAction(eventHandler -> {
DirectoryChooser dirChooser = new DirectoryChooser();
dirChooser.setTitle("Open Folder");
Dialogs.getInitialDir(getMostRecentList()).ifPresent(dirChooser::setInitialDirectory);
File selectedFile = dirChooser.showDialog(NodeUtils.getStage(owner));
if (selectedFile != null) {
setSourceUri(selectedFile.getPath());
}
});
sourceMenu.getItems().add(folderMenuItem);
sourceMenu.show(owner, Side.RIGHT, 0, 0);
} catch (Exception e) {
Dialogs.notifyException("Error while displaying file chooser: " + e.getMessage(), e, owner);
}
return null;
}
@Override
protected Collection<DataAdapter> getDataAdapters() throws DataAdapterException {
Path path = Paths.get(getSourceUri());
if (!Files.exists(path)) {
throw new CannotInitializeDataAdapterException("Cannot find " + getSourceUri());
}
if (!path.isAbsolute()) {
throw new CannotInitializeDataAdapterException("The provided path is not valid.");
}
getMostRecentList().push(path);
prefs.fileExtensionFilters.set(gson.fromJson(extensionFiltersTextField.getText(), String[].class));
return List.of(new TextDataAdapter(path, prefs.folderFilters.get(), prefs.fileExtensionFilters.get()));
}
}
@@ -0,0 +1,56 @@
/*
* Copyright 2020-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.text.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 TextDataAdapterInfo.
*
* @author Frederic Thevenet
*/
@AdapterMetadata(
name = "Text",
description = "Text Files Data Adapter",
copyright = AppEnvironment.COPYRIGHT_NOTICE,
license = AppEnvironment.LICENSE,
siteUrl = AppEnvironment.HTTP_WWW_BINJR_EU,
adapterClass = TextDataAdapter.class,
dialogClass = TextDataAdapterDialog.class,
preferencesClass = TextAdapterPreferences.class,
sourceLocality = SourceLocality.LOCAL,
apiLevel = AppEnvironment.PLUGIN_API_LEVEL,
visualizationType = VisualizationType.TEXT
)
public class TextDataAdapterInfo extends BaseDataAdapterInfo {
/**
* Initialises a new instance of the {@link TextDataAdapterInfo} class.
*
* @throws CannotInitializeDataAdapterException if the adapter's initialization failed
*/
public TextDataAdapterInfo() throws CannotInitializeDataAdapterException {
super(TextDataAdapterInfo.class);
}
}
@@ -0,0 +1,18 @@
#
# Copyright 2020 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.
#
# Text Data adapter service implementation
# eu.binjr.sources.text.adapters.TextDataAdapterInfo