blob: 71ef4182f234ee143b665fb1f791c19bdfe089e3 [file] [log] [blame]
/*
* Copyright (c) 2010-2020 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
*/
package org.eclipse.scout.sdk.s2e.util;
import static java.util.Collections.emptySet;
import static java.util.Collections.unmodifiableCollection;
import static java.util.stream.Collectors.toSet;
import static org.eclipse.scout.sdk.s2e.environment.EclipseEnvironment.callInEclipseEnvironment;
import static org.eclipse.scout.sdk.s2e.environment.EclipseEnvironment.callInEclipseEnvironmentSync;
import static org.eclipse.scout.sdk.s2e.environment.EclipseEnvironment.toScoutProgress;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiPredicate;
import java.util.stream.Stream;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.scout.sdk.core.log.SdkLog;
import org.eclipse.scout.sdk.core.s.environment.IEnvironment;
import org.eclipse.scout.sdk.core.s.environment.IFuture;
import org.eclipse.scout.sdk.core.s.environment.IProgress;
import org.eclipse.scout.sdk.core.s.util.search.FileQueryInput;
import org.eclipse.scout.sdk.core.s.util.search.IFileQuery;
import org.eclipse.scout.sdk.core.s.util.search.IFileQueryResult;
import org.eclipse.scout.sdk.core.util.Ensure;
import org.eclipse.scout.sdk.core.util.FinalValue;
import org.eclipse.scout.sdk.core.util.SdkException;
import org.eclipse.scout.sdk.core.util.Strings;
import org.eclipse.scout.sdk.s2e.environment.EclipseProgress;
/**
* <h3>{@link EclipseWorkspaceWalker}</h3>
* <p>
* Supports visiting all files within a Eclipse workspace.
* </p>
* <p>
* By default project output directories, hidden paths (leading dot) and node_modules folders are skipped. This
* behaviour may be changed using the methods {@link #withExtensionsAccepted(String...)},
* {@link #withFilter(BiPredicate)}, {@link #withSkipHiddenPaths(boolean)}, {@link #withSkipNodeModules(boolean)} and
* {@link #withSkipOutputLocation(boolean)}.
* </p>
*
* @since 7.0.100
*/
public class EclipseWorkspaceWalker {
private final String m_taskName;
private final Collection<String> m_fileExtensions;
private boolean m_skipOutputLocation;
private boolean m_skipHiddenPaths;
private boolean m_skipNodeModules;
private BiPredicate<Path, BasicFileAttributes> m_fileFilter;
/**
* @param taskName
* The task name of this walker. This name is used in the progress monitor while visiting the workspace.
*/
public EclipseWorkspaceWalker(String taskName) {
m_taskName = Ensure.notNull(taskName);
m_fileExtensions = new ArrayList<>();
m_skipOutputLocation = true;
m_skipHiddenPaths = true;
m_skipNodeModules = true;
}
/**
* Executes the query specified in the current Eclipse workspace. The execution uses default visit settings (see
* {@link EclipseWorkspaceWalker}).
*
* @param query
* The {@link IFileQuery} to execute. Must not be {@code null}.
* @param monitor
* For progress indication or cancellation. Must not be {@code null}.
* @return The {@link IFileQueryResult} after execution.
*/
public static IFileQueryResult executeQuerySync(IFileQuery query, IProgressMonitor monitor) {
return callInEclipseEnvironmentSync((e, p) -> executeQueryInWorkspace(query, e, p), monitor);
}
/**
* Asynchronously executes the query specified in the current Eclipse workspace. The execution uses default visit
* settings (see {@link EclipseWorkspaceWalker}).
*
* @param query
* The {@link IFileQuery} to execute. Must not be {@code null}.
* @return An {@link IFuture} to control the asynchronous computation (cancel, wait, retrieve result).
*/
public static IFuture<IFileQueryResult> executeQuery(IFileQuery query) {
return callInEclipseEnvironment((e, p) -> executeQueryInWorkspace(query, e, p), null, query.name());
}
protected static IFileQuery executeQueryInWorkspace(IFileQuery query, IEnvironment e, EclipseProgress p) {
try {
new EclipseWorkspaceWalker(query.name())
.walk((file, progress) -> executeQueryInFile(query, file, e, progress), p.monitor());
return query;
}
catch (CoreException ex) {
throw new SdkException(ex);
}
}
protected static void executeQueryInFile(IFileQuery query, WorkspaceFile file, IEnvironment env, IProgress progress) {
Optional<IFile> iFile = file.inWorkspace();
if (!iFile.isPresent()) {
SdkLog.warning("File '{}' could not be found in the current Eclipse Workspace.", file.path());
return;
}
Path modulePath = iFile.get().getProject().getLocation().toFile().toPath();
FileQueryInput candidate = new FileQueryInput(file.path(), modulePath, file::content);
query.searchIn(candidate, env, progress);
}
/**
* Executes the visitor in the active Eclipse workspace.
*
* @param visitor
* The visitor to call for each file in the workspace. Must not be {@code null}.
* @param monitor
* For progress monitoring or cancellation. May be {@code null}.
* @throws CoreException
*/
public void walk(BiConsumer<WorkspaceFile, IProgress> visitor, IProgressMonitor monitor) throws CoreException {
Ensure.notNull(visitor);
IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects();
SubMonitor subMonitor = SubMonitor.convert(monitor, taskName(), projects.length * 2);
for (IProject root : projects) {
Set<Path> outputLocations = emptySet();
subMonitor.subTask(root.getName());
if (isSkipOutputLocation()) {
IJavaProject jp = JavaCore.create(root);
outputLocations = getOutputLocations(jp);
}
Path projectPath = root.getLocation().toFile().toPath();
if (Files.exists(projectPath)) {
searchInFolder(visitor, projectPath, Charset.forName(root.getDefaultCharset()), outputLocations, subMonitor.newChild(1));
}
if (subMonitor.isCanceled()) {
return;
}
subMonitor.worked(1);
}
}
protected static Set<Path> getOutputLocations(IJavaProject jp) throws JavaModelException {
if (!JdtUtils.exists(jp)) {
return emptySet();
}
String projectDir = jp.getProject().getLocation().toOSString();
IClasspathEntry[] rawClasspath = jp.getRawClasspath();
return Stream.of(rawClasspath)
.map(IClasspathEntry::getOutputLocation)
.filter(Objects::nonNull)
.map(location -> location.removeFirstSegments(1).toOSString())
.map(location -> Paths.get(projectDir, location))
.collect(toSet());
}
protected void searchInFolder(BiConsumer<WorkspaceFile, IProgress> visitor, Path folder, Charset charset, Collection<Path> outputFolders, IProgressMonitor monitor) {
try {
Files.walkFileTree(folder,
new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
if (monitor.isCanceled()) {
return FileVisitResult.TERMINATE;
}
if (outputFolders.contains(dir)) {
return FileVisitResult.SKIP_SUBTREE;
}
if (!directoryFiltersAccepted(dir, attrs)) {
return FileVisitResult.SKIP_SUBTREE;
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (monitor.isCanceled()) {
return FileVisitResult.TERMINATE;
}
if (allFiltersAccepted(file, attrs)) {
visitor.accept(new WorkspaceFile(file, charset), toScoutProgress(monitor));
}
return FileVisitResult.CONTINUE;
}
});
}
catch (IOException e) {
throw new SdkException(e);
}
}
protected boolean directoryFiltersAccepted(Path file, BasicFileAttributes attrs) {
if (isSkipHiddenPaths() && isHidden(file)) {
return false;
}
Path fileName = file.getFileName();
if (isSkipNodeModules() && fileName != null && "node_modules".equals(fileName.toString())) {
return false;
}
return fileFilter()
.map(filter -> filter.test(file, attrs))
.orElse(Boolean.TRUE);
}
protected boolean allFiltersAccepted(Path file, BasicFileAttributes attrs) {
if (!acceptFileExtension(file)) {
return false;
}
return directoryFiltersAccepted(file, attrs);
}
protected boolean acceptFileExtension(Path file) {
if (extensionsAccepted().isEmpty()) {
return true; // no filter
}
Path path = file.getFileName();
if (path == null) {
return false;
}
String fileName = path.toString().toLowerCase(Locale.ENGLISH);
for (String extension : extensionsAccepted()) {
if (fileName.endsWith(extension)) {
return true;
}
}
return false;
}
protected static boolean isHidden(Path path) {
Path fileName = path.getFileName();
return fileName != null && fileName.toString().startsWith(".");
}
/**
* @return The task name of this walker. This string is used in the progress monitor.
*/
public String taskName() {
return m_taskName;
}
/**
* @return {@code true} if module output directories (like target/classes or bin) should e skipped when visiting.
*/
public boolean isSkipOutputLocation() {
return m_skipOutputLocation;
}
/**
* @param skipOutputLocation
* {@code true} if module output directories (like target/classes or bin) should e skipped when visiting.
* @return this instance
*/
public EclipseWorkspaceWalker withSkipOutputLocation(boolean skipOutputLocation) {
m_skipOutputLocation = skipOutputLocation;
return this;
}
/**
* @return {@code true} if hidden paths (file or folder names with leading dot) should be skipped when visiting.
*/
public boolean isSkipHiddenPaths() {
return m_skipHiddenPaths;
}
/**
* @param skipHiddenPaths
* {@code true} if hidden paths (file or folder names with leading dot) should be skipped when visiting.
* @return this instance
*/
public EclipseWorkspaceWalker withSkipHiddenPaths(boolean skipHiddenPaths) {
m_skipHiddenPaths = skipHiddenPaths;
return this;
}
/**
* @return {@code true} if "node_modules" folders should be skipped when visiting.
*/
public boolean isSkipNodeModules() {
return m_skipNodeModules;
}
/**
* @param skipNodeModules
* {@code true} if "node_modules" folders should be skipped when visiting.
* @return this instance
*/
public EclipseWorkspaceWalker withSkipNodeModules(boolean skipNodeModules) {
m_skipNodeModules = skipNodeModules;
return this;
}
/**
* @return A custom file filter if available.
*/
public Optional<BiPredicate<Path, BasicFileAttributes>> fileFilter() {
return Optional.ofNullable(m_fileFilter);
}
/**
* @param fileFilter
* A custom file filter if available.
* @return this instance
*/
public EclipseWorkspaceWalker withFilter(BiPredicate<Path, BasicFileAttributes> fileFilter) {
m_fileFilter = fileFilter;
return this;
}
/**
* @return An unmodifiable collection holding the file extensions (with or without extension separator dot) which are
* visited or an empty collection if all files should be visited.
*/
public Collection<String> extensionsAccepted() {
return unmodifiableCollection(m_fileExtensions);
}
/**
* @param extensions
* Sets all file extensions (with or without extension separator dot) which should be visited. May be
* {@code null}.
* @return this instance
*/
public EclipseWorkspaceWalker withExtensionsAccepted(String... extensions) {
Collection<String> l = extensions == null ? null : Arrays.asList(extensions);
return withExtensionsAccepted(l);
}
/**
* @param extensions
* Sets all file extensions (with or without extension separator dot) which should be visited. May be
* {@code null}.
* @return this instance
*/
public EclipseWorkspaceWalker withExtensionsAccepted(Collection<String> extensions) {
m_fileExtensions.clear();
if (extensions != null && !extensions.isEmpty()) {
for (String e : extensions) {
if (Strings.hasText(e)) {
m_fileExtensions.add(e);
}
}
}
return this;
}
/**
* Represents a file in an Eclipse workspace
*/
public static class WorkspaceFile {
private final Path m_file;
private final Charset m_charset;
private final FinalValue<IFile> m_workspaceFile; // loaded on request
private char[] m_content; // loaded on request
public WorkspaceFile(Path file, Charset charset) {
m_file = Ensure.notNull(file);
m_charset = Ensure.notNull(charset);
m_workspaceFile = new FinalValue<>();
}
/**
* @return The file encoding
*/
public Charset charset() {
return m_charset;
}
/**
* @return The absolute path to the file on the filesystem
*/
public Path path() {
return m_file;
}
/**
* @return Resolves the file in the Eclipse workspace. Returns an empty {@link Optional} if it could not be found
* (e.g. because the path is not part of the Eclipse workspace).
*/
public Optional<IFile> inWorkspace() {
return Optional.ofNullable(m_workspaceFile.computeIfAbsentAndGet(() -> resolveInWorkspace(path())));
}
protected static IFile resolveInWorkspace(Path file) {
IFile[] workspaceFiles = ResourcesPlugin.getWorkspace().getRoot().findFilesForLocationURI(file.toUri());
if (workspaceFiles.length < 1) {
return null;
}
IFile workspaceFile = workspaceFiles[0];
if (!workspaceFile.exists()) {
return null;
}
return workspaceFile;
}
/**
* @return The content of the file
*/
public char[] content() {
if (m_content == null) {
try {
m_content = Strings.fromFileAsChars(path(), charset());
}
catch (IOException e) {
throw new SdkException("Unable to read content of file '{}'.", path(), e);
}
}
return m_content;
}
@Override
public String toString() {
return WorkspaceFile.class.getSimpleName() + ": " + path();
}
@Override
public int hashCode() {
return m_file.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
WorkspaceFile other = (WorkspaceFile) obj;
return m_file.equals(other.m_file);
}
}
}