This is an automated email from the ASF dual-hosted git repository.
hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new c3b8a4512d issue #7292 : Search all known file types in project search
(#7491)
c3b8a4512d is described below
commit c3b8a4512dfabe1d42b81ba586c01ecea05b13aa
Author: Matt Casters <[email protected]>
AuthorDate: Mon Jul 13 15:59:21 2026 +0200
issue #7292 : Search all known file types in project search (#7491)
Allow IHopFileType to opt into search via CAPABILITY_SEARCH and
createSearchable(), so ProjectSearchablesIterator discovers text and
other registered file types instead of only pipelines and workflows.
---
.../search/ProjectSearchablesIterator.java | 82 +++++++++++------
.../hop/ui/hopgui/file/HopFileTypeRegistry.java | 43 +++++++++
.../apache/hop/ui/hopgui/file/IHopFileType.java | 29 ++++++
.../hop/ui/hopgui/file/IHopFileTypeHandler.java | 23 +++++
.../hopgui/file/pipeline/HopPipelineFileType.java | 15 ++++
.../hopgui/file/workflow/HopWorkflowFileType.java | 15 ++++
.../perspective/explorer/ExplorerPerspective.java | 27 ++++--
.../file/types/text/BaseTextExplorerFileType.java | 41 +++++++++
.../text/BaseTextExplorerFileTypeHandler.java | 25 ++++++
.../hop/ui/hopgui/search/HopGuiSearchHelper.java | 11 ++-
.../ui/hopgui/search/HopGuiTextFileSearchable.java | 100 +++++++++++++++++++++
.../hop/ui/hopgui/search/TextFileContent.java | 41 +++++++++
.../search/TextFileContentSearchableAnalyser.java | 61 +++++++++++++
.../ui/hopgui/search/HopGuiSearchHelperTest.java | 31 +++++++
.../TextFileContentSearchableAnalyserTest.java | 86 ++++++++++++++++++
15 files changed, 593 insertions(+), 37 deletions(-)
diff --git
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/search/ProjectSearchablesIterator.java
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/search/ProjectSearchablesIterator.java
index 475a952668..938f98cdee 100644
---
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/search/ProjectSearchablesIterator.java
+++
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/search/ProjectSearchablesIterator.java
@@ -19,8 +19,11 @@ package org.apache.hop.projects.search;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
+import java.util.Locale;
+import java.util.Set;
import org.apache.commons.vfs2.FileObject;
import org.apache.hop.core.config.DescribedVariablesConfigFile;
import org.apache.hop.core.config.HopConfig;
@@ -33,16 +36,14 @@ import org.apache.hop.core.vfs.HopVfs;
import org.apache.hop.metadata.api.IHopMetadata;
import org.apache.hop.metadata.api.IHopMetadataProvider;
import org.apache.hop.metadata.api.IHopMetadataSerializer;
-import org.apache.hop.pipeline.PipelineMeta;
import org.apache.hop.projects.config.ProjectsConfig;
import org.apache.hop.projects.config.ProjectsConfigSingleton;
import org.apache.hop.projects.environment.LifecycleEnvironment;
import org.apache.hop.projects.project.ProjectConfig;
+import org.apache.hop.ui.hopgui.file.HopFileTypeRegistry;
+import org.apache.hop.ui.hopgui.file.IHopFileType;
import org.apache.hop.ui.hopgui.search.HopGuiDescribedVariableSearchable;
import org.apache.hop.ui.hopgui.search.HopGuiMetadataSearchable;
-import org.apache.hop.ui.hopgui.search.HopGuiPipelineSearchable;
-import org.apache.hop.ui.hopgui.search.HopGuiWorkflowSearchable;
-import org.apache.hop.workflow.WorkflowMeta;
// TODO: implement lazy loading of the searchables.
//
@@ -68,32 +69,61 @@ public class ProjectSearchablesIterator implements
Iterator<ISearchable> {
configurationFiles.addAll(environments.get(0).getConfigurationFiles());
}
- // Find all the pipelines and workflows in the project homefolder...
+ // Discover files via registered hop file types that opt into search.
//
- FileObject homeFolderFile =
HopVfs.getFileObject(projectConfig.getProjectHome());
- Collection<FileObject> pipelineFiles = HopVfs.findFiles(homeFolderFile,
"hpl", true);
- for (FileObject pipelineFile : pipelineFiles) {
- String pipelineFilePath = pipelineFile.getName().getURI();
- try {
- PipelineMeta pipelineMeta =
- new PipelineMeta(pipelineFilePath, metadataProvider, variables);
- searchables.add(new HopGuiPipelineSearchable("Project pipeline
file", pipelineMeta));
- } catch (Exception e) {
- // There was an error loading the XML file...
- LogChannel.GENERAL.logError("Error loading pipeline metadata: " +
pipelineFilePath, e);
+ HopFileTypeRegistry fileTypeRegistry = HopFileTypeRegistry.getInstance();
+ fileTypeRegistry.ensureLoaded();
+
+ Set<String> searchableExtensions = new HashSet<>();
+ boolean hasSearchableTypes = false;
+ for (IHopFileType fileType : fileTypeRegistry.getFileTypes()) {
+ if (!fileType.hasCapability(IHopFileType.CAPABILITY_SEARCH)) {
+ continue;
+ }
+ hasSearchableTypes = true;
+ for (String filterExtension : fileType.getFilterExtensions()) {
+ if (filterExtension == null || filterExtension.isEmpty()) {
+ continue;
+ }
+ // filter extensions look like "*.hpl" or "*.sql;*.SQL"
+ for (String part : filterExtension.split(";")) {
+ String ext = part.trim();
+ if (ext.startsWith("*.")) {
+ ext = ext.substring(2);
+ } else if (ext.startsWith(".")) {
+ ext = ext.substring(1);
+ }
+ if (!ext.isEmpty() && !"*".equals(ext)) {
+ searchableExtensions.add(ext.toLowerCase(Locale.ROOT));
+ }
+ }
}
}
- Collection<FileObject> workflowFiles = HopVfs.findFiles(homeFolderFile,
"hwf", true);
- for (FileObject workflowFile : workflowFiles) {
- String workflowFilePath = workflowFile.getName().getURI();
- try {
- WorkflowMeta workflowMeta =
- new WorkflowMeta(variables, workflowFilePath, metadataProvider);
- searchables.add(new HopGuiWorkflowSearchable("Project workflow
file", workflowMeta));
- } catch (Exception e) {
- // There was an error loading the XML file...
- LogChannel.GENERAL.logError("Error loading workflow metadata: " +
workflowFilePath, e);
+ FileObject homeFolderFile =
HopVfs.getFileObject(projectConfig.getProjectHome());
+ if (hasSearchableTypes) {
+ Collection<FileObject> projectFiles = HopVfs.findFiles(homeFolderFile,
null, true);
+ for (FileObject projectFile : projectFiles) {
+ String extension = projectFile.getName().getExtension();
+ if (extension == null
+ || extension.isEmpty()
+ ||
!searchableExtensions.contains(extension.toLowerCase(Locale.ROOT))) {
+ continue;
+ }
+ String filePath = projectFile.getName().getURI();
+ try {
+ IHopFileType fileType = fileTypeRegistry.findHopFileType(filePath);
+ if (fileType == null ||
!fileType.hasCapability(IHopFileType.CAPABILITY_SEARCH)) {
+ continue;
+ }
+ ISearchable searchable =
+ fileType.createSearchable(filePath, "Project file", variables,
metadataProvider);
+ if (searchable != null) {
+ searchables.add(searchable);
+ }
+ } catch (Exception e) {
+ LogChannel.GENERAL.logError("Error loading searchable file: " +
filePath, e);
+ }
}
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/HopFileTypeRegistry.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/HopFileTypeRegistry.java
index 96c2125b3b..7e28d9df98 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/file/HopFileTypeRegistry.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/file/HopFileTypeRegistry.java
@@ -22,6 +22,10 @@ import java.util.Arrays;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.exception.HopPluginException;
+import org.apache.hop.core.logging.LogChannel;
+import org.apache.hop.core.plugins.IPlugin;
+import org.apache.hop.core.plugins.PluginRegistry;
import org.apache.hop.i18n.BaseMessages;
/** This class contains all the available Hop File types */
@@ -32,9 +36,11 @@ public class HopFileTypeRegistry {
private static HopFileTypeRegistry hopFileTypeRegistry;
private List<IHopFileType> hopFileTypes;
+ private boolean pluginsLoaded;
private HopFileTypeRegistry() {
hopFileTypes = new ArrayList<>();
+ pluginsLoaded = false;
}
public static final HopFileTypeRegistry getInstance() {
@@ -54,6 +60,43 @@ public class HopFileTypeRegistry {
}
}
+ /**
+ * Ensure {@link HopFileTypePluginType} plugins are registered and loaded
into this registry. Safe
+ * to call multiple times (e.g. from Hop GUI and from project search /
hop-search). When the GUI
+ * already populated the registry, this is a no-op.
+ */
+ public synchronized void ensureLoaded() {
+ if (pluginsLoaded || !hopFileTypes.isEmpty()) {
+ pluginsLoaded = true;
+ return;
+ }
+ try {
+ PluginRegistry registry = PluginRegistry.getInstance();
+ List<IPlugin> plugins = registry.getPlugins(HopFileTypePluginType.class);
+ // hop-search may not have called HopGuiEnvironment; register and scan
if needed.
+ if (plugins.isEmpty()) {
+ PluginRegistry.addPluginType(HopFileTypePluginType.getInstance());
+ HopFileTypePluginType.getInstance().searchPlugins();
+ plugins = registry.getPlugins(HopFileTypePluginType.class);
+ }
+ for (IPlugin plugin : plugins) {
+ try {
+ IHopFileType hopFileType = registry.loadClass(plugin,
IHopFileType.class);
+ registerHopFile(hopFileType);
+ } catch (HopPluginException e) {
+ LogChannel.GENERAL.logError(
+ "Unable to load hop file type plugin with ID '"
+ + (plugin.getIds().length > 0 ? plugin.getIds()[0] : "?")
+ + "'",
+ e);
+ }
+ }
+ pluginsLoaded = true;
+ } catch (Exception e) {
+ LogChannel.GENERAL.logError("Error loading hop file type plugins for
search", e);
+ }
+ }
+
/**
* This method first tries to find a HopFile by looking at the extension. If
none can be found the
* content is looked at by each IHopFileType
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/file/IHopFileType.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/IHopFileType.java
index c1c4c5002d..151ec1c60b 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/file/IHopFileType.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/file/IHopFileType.java
@@ -21,7 +21,9 @@ import java.util.List;
import java.util.Properties;
import org.apache.hop.core.exception.HopException;
import org.apache.hop.core.file.IHasFilename;
+import org.apache.hop.core.search.ISearchable;
import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
import org.apache.hop.ui.hopgui.HopGui;
import org.apache.hop.ui.hopgui.context.IGuiContextHandler;
@@ -56,6 +58,12 @@ public interface IHopFileType {
String CAPABILITY_HANDLE_METADATA = "HandleMetadata";
+ /**
+ * Opt-in capability: include files of this type when enumerating project
(and open-tab)
+ * searchables. Types without this capability are not discovered by
project-wide search.
+ */
+ String CAPABILITY_SEARCH = "Search";
+
/**
* @return The name of this file type
*/
@@ -146,4 +154,25 @@ public interface IHopFileType {
default boolean supportsOpening() {
return true;
}
+
+ /**
+ * Build a searchable representation of the given file without opening it in
the UI. Called by
+ * project search when this type {@linkplain #hasCapability(String) has}
{@link
+ * #CAPABILITY_SEARCH}. The default returns {@code null} (not searchable).
+ *
+ * @param filename the path of the file on disk
+ * @param locationDescription a short label for where the file was found
(e.g. "Project file")
+ * @param variables the variables to use when loading the file
+ * @param metadataProvider the metadata provider (pipelines/workflows may
need it)
+ * @return an {@link ISearchable}, or {@code null} if this type cannot
search the file
+ * @throws HopException if loading the file fails
+ */
+ default ISearchable createSearchable(
+ String filename,
+ String locationDescription,
+ IVariables variables,
+ IHopMetadataProvider metadataProvider)
+ throws HopException {
+ return null;
+ }
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/IHopFileTypeHandler.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/IHopFileTypeHandler.java
index eb4acb1a80..2e39abd317 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/file/IHopFileTypeHandler.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/file/IHopFileTypeHandler.java
@@ -19,7 +19,9 @@ package org.apache.hop.ui.hopgui.file;
import java.util.Map;
import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.search.ISearchable;
import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
import org.apache.hop.ui.hopgui.context.IActionContextHandlersProvider;
/** This describes the main file operations for a supported Hop file */
@@ -174,4 +176,25 @@ public interface IHopFileTypeHandler extends
IActionContextHandlersProvider {
default boolean hasCapability(String capability) {
return getFileType() != null && getFileType().hasCapability(capability);
}
+
+ /**
+ * Build a searchable for the file this handler represents (typically an
open tab). Defaults to
+ * delegating to {@link IHopFileType#createSearchable} when the file type
has {@link
+ * IHopFileType#CAPABILITY_SEARCH}. Handlers with in-memory content may
override to search the
+ * editor buffer instead of reloading from disk.
+ *
+ * @param locationDescription a short label for where the file was found
(e.g. "Open file")
+ * @param metadataProvider the metadata provider
+ * @return an {@link ISearchable}, or {@code null} if this handler is not
searchable
+ * @throws HopException if building the searchable fails
+ */
+ default ISearchable createSearchable(
+ String locationDescription, IHopMetadataProvider metadataProvider)
throws HopException {
+ IHopFileType type = getFileType();
+ if (type == null || !type.hasCapability(IHopFileType.CAPABILITY_SEARCH)) {
+ return null;
+ }
+ return type.createSearchable(
+ getFilename(), locationDescription, getVariables(), metadataProvider);
+ }
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopPipelineFileType.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopPipelineFileType.java
index c05e7cb470..30acd970c2 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopPipelineFileType.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopPipelineFileType.java
@@ -26,11 +26,13 @@ import org.apache.hop.core.extension.HopExtensionPoint;
import org.apache.hop.core.file.IHasFilename;
import org.apache.hop.core.gui.plugin.action.GuiAction;
import org.apache.hop.core.gui.plugin.action.GuiActionType;
+import org.apache.hop.core.search.ISearchable;
import org.apache.hop.core.variables.IVariables;
import org.apache.hop.core.vfs.HopVfs;
import org.apache.hop.core.xml.XmlHandler;
import org.apache.hop.history.AuditManager;
import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
import org.apache.hop.pipeline.PipelineMeta;
import org.apache.hop.ui.core.dialog.ErrorDialog;
import org.apache.hop.ui.core.gui.HopNamespace;
@@ -41,6 +43,7 @@ import org.apache.hop.ui.hopgui.file.HopFileTypeBase;
import org.apache.hop.ui.hopgui.file.HopFileTypePlugin;
import org.apache.hop.ui.hopgui.file.IHopFileType;
import org.apache.hop.ui.hopgui.file.IHopFileTypeHandler;
+import org.apache.hop.ui.hopgui.search.HopGuiPipelineSearchable;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
@@ -105,6 +108,7 @@ public class HopPipelineFileType<T extends PipelineMeta>
extends HopFileTypeBase
capabilities.setProperty(IHopFileType.CAPABILITY_DISTRIBUTE_VERTICAL,
"true");
capabilities.setProperty(IHopFileType.CAPABILITY_FILE_HISTORY, "true");
+ capabilities.setProperty(IHopFileType.CAPABILITY_SEARCH, "true");
return capabilities;
}
@@ -249,4 +253,15 @@ public class HopPipelineFileType<T extends PipelineMeta>
extends HopFileTypeBase
public String getFileTypeImage() {
return "ui/images/pipeline.svg";
}
+
+ @Override
+ public ISearchable createSearchable(
+ String filename,
+ String locationDescription,
+ IVariables variables,
+ IHopMetadataProvider metadataProvider)
+ throws HopException {
+ PipelineMeta pipelineMeta = new PipelineMeta(filename, metadataProvider,
variables);
+ return new HopGuiPipelineSearchable(locationDescription, pipelineMeta);
+ }
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopWorkflowFileType.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopWorkflowFileType.java
index b8cae0adcc..50b4a6fad8 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopWorkflowFileType.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopWorkflowFileType.java
@@ -26,11 +26,13 @@ import org.apache.hop.core.extension.HopExtensionPoint;
import org.apache.hop.core.file.IHasFilename;
import org.apache.hop.core.gui.plugin.action.GuiAction;
import org.apache.hop.core.gui.plugin.action.GuiActionType;
+import org.apache.hop.core.search.ISearchable;
import org.apache.hop.core.variables.IVariables;
import org.apache.hop.core.vfs.HopVfs;
import org.apache.hop.core.xml.XmlHandler;
import org.apache.hop.history.AuditManager;
import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
import org.apache.hop.ui.core.dialog.ErrorDialog;
import org.apache.hop.ui.core.gui.HopNamespace;
import org.apache.hop.ui.hopgui.HopGui;
@@ -40,6 +42,7 @@ import org.apache.hop.ui.hopgui.file.HopFileTypeBase;
import org.apache.hop.ui.hopgui.file.HopFileTypePlugin;
import org.apache.hop.ui.hopgui.file.IHopFileType;
import org.apache.hop.ui.hopgui.file.IHopFileTypeHandler;
+import org.apache.hop.ui.hopgui.search.HopGuiWorkflowSearchable;
import org.apache.hop.workflow.WorkflowMeta;
import org.apache.hop.workflow.action.ActionMeta;
import org.apache.hop.workflow.actions.start.ActionStart;
@@ -110,6 +113,7 @@ public class HopWorkflowFileType<T extends WorkflowMeta>
extends HopFileTypeBase
capabilities.setProperty(IHopFileType.CAPABILITY_DISTRIBUTE_VERTICAL,
"true");
capabilities.setProperty(IHopFileType.CAPABILITY_FILE_HISTORY, "true");
+ capabilities.setProperty(IHopFileType.CAPABILITY_SEARCH, "true");
return capabilities;
}
@@ -259,4 +263,15 @@ public class HopWorkflowFileType<T extends WorkflowMeta>
extends HopFileTypeBase
public String getFileTypeImage() {
return "ui/images/workflow.svg";
}
+
+ @Override
+ public ISearchable createSearchable(
+ String filename,
+ String locationDescription,
+ IVariables variables,
+ IHopMetadataProvider metadataProvider)
+ throws HopException {
+ WorkflowMeta workflowMeta = new WorkflowMeta(variables, filename,
metadataProvider);
+ return new HopGuiWorkflowSearchable(locationDescription, workflowMeta);
+ }
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
index 83f5e80c97..a6adbaed89 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
@@ -51,6 +51,7 @@ import
org.apache.hop.core.gui.plugin.key.GuiOsxKeyboardShortcut;
import org.apache.hop.core.gui.plugin.menu.GuiMenuElement;
import org.apache.hop.core.gui.plugin.toolbar.GuiToolbarElement;
import org.apache.hop.core.listeners.IContentChangedListener;
+import org.apache.hop.core.logging.LogChannel;
import org.apache.hop.core.plugins.IPlugin;
import org.apache.hop.core.plugins.PluginRegistry;
import org.apache.hop.core.search.ISearchResult;
@@ -3901,19 +3902,31 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
@Override
public List<ISearchable> getSearchables() {
- // Surface the pipelines and workflows that are currently open in tabs so
they show up - and can
- // be prioritized - in the global search.
+ // Surface open tabs so they show up - and can be prioritized - in the
global search.
+ // Pipelines/workflows use the in-memory subject (includes unsaved edits).
Other file types
+ // that opt in via CAPABILITY_SEARCH are loaded through
IHopFileTypeHandler.createSearchable.
List<ISearchable> searchables = new ArrayList<>();
for (TabItemHandler item : items) {
IHopFileTypeHandler typeHandler = item.getTypeHandler();
if (typeHandler == null) {
continue;
}
- Object subject = typeHandler.getSubject();
- if (subject instanceof PipelineMeta pipelineMeta) {
- searchables.add(new HopGuiPipelineSearchable("Open file",
pipelineMeta));
- } else if (subject instanceof WorkflowMeta workflowMeta) {
- searchables.add(new HopGuiWorkflowSearchable("Open file",
workflowMeta));
+ try {
+ Object subject = typeHandler.getSubject();
+ if (subject instanceof PipelineMeta pipelineMeta) {
+ searchables.add(new HopGuiPipelineSearchable("Open file",
pipelineMeta));
+ } else if (subject instanceof WorkflowMeta workflowMeta) {
+ searchables.add(new HopGuiWorkflowSearchable("Open file",
workflowMeta));
+ } else {
+ ISearchable searchable =
+ typeHandler.createSearchable("Open file",
hopGui.getMetadataProvider());
+ if (searchable != null) {
+ searchables.add(searchable);
+ }
+ }
+ } catch (Exception e) {
+ LogChannel.UI.logError(
+ "Error creating searchable for open file: " +
typeHandler.getFilename(), e);
}
}
return searchables;
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/text/BaseTextExplorerFileType.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/text/BaseTextExplorerFileType.java
index 94edfff9c1..02faf65e5a 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/text/BaseTextExplorerFileType.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/text/BaseTextExplorerFileType.java
@@ -17,14 +17,25 @@
package org.apache.hop.ui.hopgui.perspective.explorer.file.types.text;
+import java.io.InputStream;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
import java.util.Properties;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.vfs2.FileObject;
import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.search.ISearchable;
import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.core.vfs.HopVfs;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.file.IHopFileType;
import org.apache.hop.ui.hopgui.file.IHopFileTypeHandler;
import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerFile;
import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerPerspective;
import
org.apache.hop.ui.hopgui.perspective.explorer.file.types.base.BaseExplorerFileType;
+import org.apache.hop.ui.hopgui.search.HopGuiTextFileSearchable;
+import org.apache.hop.ui.hopgui.search.TextFileContent;
public abstract class BaseTextExplorerFileType<T extends
BaseTextExplorerFileTypeHandler>
extends BaseExplorerFileType<T> {
@@ -38,6 +49,9 @@ public abstract class BaseTextExplorerFileType<T extends
BaseTextExplorerFileTyp
String[] filterNames,
Properties capabilities) {
super(name, defaultFileExtension, filterExtensions, filterNames,
capabilities);
+ if (capabilities != null) {
+ capabilities.setProperty(IHopFileType.CAPABILITY_SEARCH, "true");
+ }
}
public abstract T createFileTypeHandler(
@@ -46,4 +60,31 @@ public abstract class BaseTextExplorerFileType<T extends
BaseTextExplorerFileTyp
@Override
public abstract IHopFileTypeHandler newFile(HopGui hopGui, IVariables
parentVariableSpace)
throws HopException;
+
+ @Override
+ public ISearchable createSearchable(
+ String filename,
+ String locationDescription,
+ IVariables variables,
+ IHopMetadataProvider metadataProvider)
+ throws HopException {
+ try {
+ FileObject fileObject = HopVfs.getFileObject(filename, variables);
+ if (!fileObject.exists()) {
+ throw new HopException("File '" + filename + "' doesn't exist");
+ }
+ String text;
+ try (InputStream inputStream = HopVfs.getInputStream(fileObject)) {
+ StringWriter writer = new StringWriter();
+ IOUtils.copy(inputStream, writer, StandardCharsets.UTF_8);
+ text = writer.toString();
+ }
+ TextFileContent content = new TextFileContent(filename, text);
+ return new HopGuiTextFileSearchable(locationDescription, getName(),
content);
+ } catch (HopException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new HopException("Error loading text file for search: " +
filename, e);
+ }
+ }
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/text/BaseTextExplorerFileTypeHandler.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/text/BaseTextExplorerFileTypeHandler.java
index 36859628fb..a6f72ace63 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/text/BaseTextExplorerFileTypeHandler.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/text/BaseTextExplorerFileTypeHandler.java
@@ -23,14 +23,19 @@ import org.apache.commons.vfs2.FileObject;
import org.apache.hop.core.Const;
import org.apache.hop.core.exception.HopException;
import org.apache.hop.core.logging.LogChannel;
+import org.apache.hop.core.search.ISearchable;
import org.apache.hop.core.vfs.HopVfs;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
import org.apache.hop.ui.core.dialog.MessageBox;
import org.apache.hop.ui.core.widget.editor.IContentEditorWidget;
import org.apache.hop.ui.hopgui.ContentEditorFacade;
import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.file.IHopFileType;
import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerFile;
import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerPerspective;
import
org.apache.hop.ui.hopgui.perspective.explorer.file.types.base.BaseExplorerFileTypeHandler;
+import org.apache.hop.ui.hopgui.search.HopGuiTextFileSearchable;
+import org.apache.hop.ui.hopgui.search.TextFileContent;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Composite;
@@ -161,4 +166,24 @@ public class BaseTextExplorerFileTypeHandler extends
BaseExplorerFileTypeHandler
public void copySelectedToClipboard() {
editorWidget.copy();
}
+
+ @Override
+ public ISearchable createSearchable(
+ String locationDescription, IHopMetadataProvider metadataProvider)
throws HopException {
+ IHopFileType type = getFileType();
+ if (type == null || !type.hasCapability(IHopFileType.CAPABILITY_SEARCH)) {
+ return null;
+ }
+ // Prefer the open editor buffer so unsaved edits are searchable.
+ String text;
+ if (editorWidget != null
+ && editorWidget.getControl() != null
+ && !editorWidget.getControl().isDisposed()) {
+ text = Const.NVL(editorWidget.getText(), "");
+ } else {
+ text = Const.NVL(readTextFileContent(StandardCharsets.UTF_8), "");
+ }
+ TextFileContent content = new TextFileContent(getFilename(), text);
+ return new HopGuiTextFileSearchable(locationDescription, type.getName(),
content);
+ }
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/search/HopGuiSearchHelper.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/HopGuiSearchHelper.java
index fce4d42a30..d2d46baf52 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/search/HopGuiSearchHelper.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/search/HopGuiSearchHelper.java
@@ -268,16 +268,19 @@ public final class HopGuiSearchHelper {
}
/**
- * Whether a searchable is an object that is currently <em>open</em> in a
Hop GUI tab. Only files
- * (pipelines and workflows) can be open; metadata items and variables are
always project-wide.
+ * Whether a searchable is an object that is currently <em>open</em> in a
Hop GUI tab. Openable
+ * file objects (pipelines, workflows, text files) can be open; metadata and
variables are always
+ * project-wide even when the GUI location also reports them.
*
* @param searchable the searchable to classify
* @param sourceByKey the source-location map from {@link #enumerateAll}
- * @return true if this is an open pipeline/workflow tab
+ * @return true if this is an open file tab
*/
public static boolean isOpenObject(ISearchable searchable, Map<String,
Integer> sourceByKey) {
Object object = searchable.getSearchableObject();
- if (object instanceof PipelineMeta || object instanceof WorkflowMeta) {
+ if (object instanceof PipelineMeta
+ || object instanceof WorkflowMeta
+ || object instanceof TextFileContent) {
Integer source = sourceByKey.get(searchableKey(searchable));
return source != null && source == 0;
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/search/HopGuiTextFileSearchable.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/HopGuiTextFileSearchable.java
new file mode 100644
index 0000000000..c0a3a031b7
--- /dev/null
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/HopGuiTextFileSearchable.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.hop.ui.hopgui.search;
+
+import org.apache.commons.vfs2.FileName;
+import org.apache.commons.vfs2.FileObject;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.search.ISearchable;
+import org.apache.hop.core.search.ISearchableCallback;
+import org.apache.hop.core.vfs.HopVfs;
+import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.file.IHopFileTypeHandler;
+import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerPerspective;
+
+/** An {@link ISearchable} for a text-based explorer file (SQL, JSON, plain
text, …). */
+public class HopGuiTextFileSearchable implements ISearchable<TextFileContent> {
+
+ private final String location;
+ private final String type;
+ private final TextFileContent content;
+
+ public HopGuiTextFileSearchable(String location, String type,
TextFileContent content) {
+ this.location = location;
+ this.type = type;
+ this.content = content;
+ }
+
+ @Override
+ public String getLocation() {
+ return location;
+ }
+
+ @Override
+ public String getName() {
+ String filename = content.getFilename();
+ if (filename == null || filename.isEmpty()) {
+ return "";
+ }
+ try {
+ FileObject fileObject = HopVfs.getFileObject(filename);
+ FileName fileName = fileObject.getName();
+ return fileName.getBaseName();
+ } catch (Exception e) {
+ int slash = Math.max(filename.lastIndexOf('/'),
filename.lastIndexOf('\\'));
+ return slash >= 0 ? filename.substring(slash + 1) : filename;
+ }
+ }
+
+ @Override
+ public String getType() {
+ return type;
+ }
+
+ @Override
+ public String getFilename() {
+ return content.getFilename();
+ }
+
+ @Override
+ public TextFileContent getSearchableObject() {
+ return content;
+ }
+
+ @Override
+ public ISearchableCallback getSearchCallback() {
+ return (searchable, searchResult) -> {
+ String filename = content.getFilename();
+ if (filename == null || filename.isEmpty()) {
+ return;
+ }
+ try {
+ ExplorerPerspective perspective = HopGui.getExplorerPerspective();
+ IHopFileTypeHandler fileTypeHandler =
perspective.findFileTypeHandlerByFilename(filename);
+ if (fileTypeHandler != null) {
+ perspective.setActiveFileTypeHandler(fileTypeHandler);
+ } else {
+ HopGui.getInstance().fileDelegate.fileOpen(filename);
+ }
+ perspective.activate();
+ } catch (Exception e) {
+ throw new HopException("Error opening searchable text file: " +
filename, e);
+ }
+ };
+ }
+}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/search/TextFileContent.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/TextFileContent.java
new file mode 100644
index 0000000000..9531f11902
--- /dev/null
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/search/TextFileContent.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.hop.ui.hopgui.search;
+
+/**
+ * Content of a text-based explorer file, used as the searchable object for
text/SQL/JSON/XML and
+ * similar file types.
+ */
+public class TextFileContent {
+
+ private final String filename;
+ private final String content;
+
+ public TextFileContent(String filename, String content) {
+ this.filename = filename;
+ this.content = content == null ? "" : content;
+ }
+
+ public String getFilename() {
+ return filename;
+ }
+
+ public String getContent() {
+ return content;
+ }
+}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/search/TextFileContentSearchableAnalyser.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/TextFileContentSearchableAnalyser.java
new file mode 100644
index 0000000000..d75f8efb52
--- /dev/null
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/TextFileContentSearchableAnalyser.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.hop.ui.hopgui.search;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.search.BaseSearchableAnalyser;
+import org.apache.hop.core.search.ISearchQuery;
+import org.apache.hop.core.search.ISearchResult;
+import org.apache.hop.core.search.ISearchable;
+import org.apache.hop.core.search.ISearchableAnalyser;
+import org.apache.hop.core.search.SearchableAnalyserPlugin;
+
+@SearchableAnalyserPlugin(
+ id = "TextFileContentSearchableAnalyser",
+ name = "Search in a text file",
+ description = "Search content of text-based explorer files")
+public class TextFileContentSearchableAnalyser extends
BaseSearchableAnalyser<TextFileContent>
+ implements ISearchableAnalyser<TextFileContent> {
+
+ @Override
+ public Class<TextFileContent> getSearchableClass() {
+ return TextFileContent.class;
+ }
+
+ @Override
+ public List<ISearchResult> search(
+ ISearchable<TextFileContent> searchable, ISearchQuery searchQuery) {
+ TextFileContent textFileContent = searchable.getSearchableObject();
+ List<ISearchResult> results = new ArrayList<>();
+
+ String content = textFileContent.getContent();
+ if (content == null || content.isEmpty()) {
+ return results;
+ }
+
+ String[] lines = content.split("\\R", -1);
+ for (int i = 0; i < lines.length; i++) {
+ String line = lines[i];
+ String lineNumber = String.valueOf(i + 1);
+ matchProperty(searchable, results, searchQuery, "line " + lineNumber,
line, lineNumber);
+ }
+
+ return results;
+ }
+}
diff --git
a/ui/src/test/java/org/apache/hop/ui/hopgui/search/HopGuiSearchHelperTest.java
b/ui/src/test/java/org/apache/hop/ui/hopgui/search/HopGuiSearchHelperTest.java
index 0e8bab1136..c6328f9b95 100644
---
a/ui/src/test/java/org/apache/hop/ui/hopgui/search/HopGuiSearchHelperTest.java
+++
b/ui/src/test/java/org/apache/hop/ui/hopgui/search/HopGuiSearchHelperTest.java
@@ -60,6 +60,16 @@ class HopGuiSearchHelperTest {
return searchable;
}
+ /** A text-file searchable (searchable object is TextFileContent so it can
be "open"). */
+ private static ISearchable textFile(String name, String filename) {
+ ISearchable searchable = mock(ISearchable.class);
+ when(searchable.getName()).thenReturn(name);
+ when(searchable.getType()).thenReturn("SQL File");
+ when(searchable.getFilename()).thenReturn(filename);
+ when(searchable.getSearchableObject()).thenReturn(new
TextFileContent(filename, "select 1"));
+ return searchable;
+ }
+
private static ISearchResult nameMatch(ISearchable searchable) {
return new SearchResult(searchable, searchable.getName(), "name", null,
searchable.getName());
}
@@ -107,6 +117,27 @@ class HopGuiSearchHelperTest {
assertFalse(HopGuiSearchHelper.isOpenObject(meta, source(meta, 0)));
}
+ @Test
+ void openTextFileGoesToOpenSection() {
+ ISearchable open = textFile("query.sql", "/p/query.sql");
+ List<SearchSection> sections =
+ HopGuiSearchHelper.groupResults(List.of(nameMatch(open)), source(open,
0));
+
+ assertEquals(1, sections.size());
+ assertEquals(HopGuiSearchHelper.SECTION_OPEN, sections.get(0).getKey());
+ assertTrue(HopGuiSearchHelper.isOpenObject(open, source(open, 0)));
+ }
+
+ @Test
+ void projectTextFileGoesToProjectSection() {
+ ISearchable onDisk = textFile("query.sql", "/p/query.sql");
+ List<SearchSection> sections =
+ HopGuiSearchHelper.groupResults(List.of(nameMatch(onDisk)),
source(onDisk, 1));
+
+ assertEquals(HopGuiSearchHelper.SECTION_PROJECT, sections.get(0).getKey());
+ assertFalse(HopGuiSearchHelper.isOpenObject(onDisk, source(onDisk, 1)));
+ }
+
@Test
void nameMatchMarksObjectButAddsNoChildRow() {
ISearchable open = pipeline("loader", "/p/loader.hpl");
diff --git
a/ui/src/test/java/org/apache/hop/ui/hopgui/search/TextFileContentSearchableAnalyserTest.java
b/ui/src/test/java/org/apache/hop/ui/hopgui/search/TextFileContentSearchableAnalyserTest.java
new file mode 100644
index 0000000000..484bdb8b9d
--- /dev/null
+++
b/ui/src/test/java/org/apache/hop/ui/hopgui/search/TextFileContentSearchableAnalyserTest.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.hop.ui.hopgui.search;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import org.apache.hop.core.search.ISearchResult;
+import org.apache.hop.core.search.ISearchable;
+import org.apache.hop.core.search.ISearchableCallback;
+import org.apache.hop.core.search.SearchQuery;
+import org.junit.jupiter.api.Test;
+
+class TextFileContentSearchableAnalyserTest {
+
+ @Test
+ void matchesLinesAndReportsLineNumbers() {
+ TextFileContent content =
+ new TextFileContent("/project/query.sql", "select 1;\nselect foo from
bar;\nselect 2;");
+ ISearchable<TextFileContent> searchable =
+ new HopGuiTextFileSearchable("test", "SQL File", content);
+
+ TextFileContentSearchableAnalyser analyser = new
TextFileContentSearchableAnalyser();
+ List<ISearchResult> results = analyser.search(searchable, new
SearchQuery("foo", true, false));
+
+ assertEquals(1, results.size());
+ assertEquals("2", results.get(0).getComponent());
+ assertTrue(results.get(0).getMatchingString().contains("foo"));
+ }
+
+ @Test
+ void emptyContentReturnsNoResults() {
+ TextFileContent content = new TextFileContent("/project/empty.txt", "");
+ ISearchable<TextFileContent> searchable =
+ new ISearchable<>() {
+ @Override
+ public String getLocation() {
+ return "test";
+ }
+
+ @Override
+ public String getName() {
+ return "empty.txt";
+ }
+
+ @Override
+ public String getType() {
+ return "TXT File";
+ }
+
+ @Override
+ public String getFilename() {
+ return content.getFilename();
+ }
+
+ @Override
+ public TextFileContent getSearchableObject() {
+ return content;
+ }
+
+ @Override
+ public ISearchableCallback getSearchCallback() {
+ return null;
+ }
+ };
+
+ TextFileContentSearchableAnalyser analyser = new
TextFileContentSearchableAnalyser();
+ assertTrue(analyser.search(searchable, new SearchQuery("anything", true,
false)).isEmpty());
+ }
+}