blob: b4c86fa59473881b6027fc80c8e5b87e21e69bc7 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2018 Ericsson
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License 2.0 which
* accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.eclipse.tracecompass.tmf.core.io;
import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.ICoreRunnable;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.internal.tmf.core.Activator;
/**
* Utility class for handling {@link IResource} instances.
*
* @author Bernd Hufmann
* @since 4.0
*/
@NonNullByDefault
public class ResourceUtil {
// ------------------------------------------------------------------------
// Attributes
// ------------------------------------------------------------------------
private static final boolean IS_WINDOWS = System.getProperty("os.name").contains("Windows"); //$NON-NLS-1$ //$NON-NLS-2$
private static boolean fIsSymLinkSupported = !IS_WINDOWS;
// ------------------------------------------------------------------------
// Constructors
// ------------------------------------------------------------------------
private ResourceUtil() {
// Do nothing
}
// ------------------------------------------------------------------------
// Operations
// ------------------------------------------------------------------------
/**
* Creates a symbolic link to a target file or folder. If the local file system
* supports symbolic links and the user requests to use a file system link it
* will create a symbolic link on the file system. If the local file system
* doesn't support symbolic links or on Windows, it will always use Eclipse
* links.
*
* If a file system or Eclipse symbolic link already exists at the destination
* it will replace the existing link. If a file or folder exists at the
* destination the symbolic link won't be created and the method will return
* false.
*
* @param link
* the resource in the workspace representing the link
* @param targetLocation
* the target location in the local file system
* @param useFileSystemLinks
* true for using file system symbolic links, false for Eclipse links
* Note: If file system doesn't support symbolic links or on Windows
* Eclipse links will be used.
* @param monitor
* the progress monitor or null.
* @return <code>true</code> if link was created successfully, else
* <code>false</code>
* @throws CoreException
* if an error occurs
*/
public static boolean createSymbolicLink(IResource link, @Nullable IPath targetLocation, boolean useFileSystemLinks, @Nullable IProgressMonitor monitor) throws CoreException {
// Validate the input parameters
if (!(link instanceof IFile) && !(link instanceof IFolder) || targetLocation == null) {
return false;
}
SubMonitor subMon = SubMonitor.convert(monitor, 3);
try {
IPath location = getLocation(link);
// Only create link if it doesn't already exist.
if ((location != null) && link.exists() && location.equals(targetLocation)) {
return true;
}
Path linkPath = Paths.get(link.getProject().getLocation().append(link.getProjectRelativePath()).toOSString());
if (!checkResource(link, linkPath)) {
return false;
}
if (!fIsSymLinkSupported || !useFileSystemLinks) {
return createEclipseLink(link, targetLocation, subMon);
}
String targetPathString = targetLocation.toOSString();
Path targetPath = Paths.get(targetPathString);
// if the target is a file system symbolic link
if (Files.isSymbolicLink(targetPath)) {
targetPath = Files.readSymbolicLink(targetPath);
}
// Create files system symbolic link
Files.createSymbolicLink(linkPath, targetPath);
subMon.worked(1);
if (link.getParent() != null) {
link.getParent().refreshLocal(IResource.DEPTH_ONE, subMon.split(1));
}
link.refreshLocal(IResource.DEPTH_INFINITE, subMon.split(1));
return true;
} catch (IOException e) {
throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Error creating symbolic link", e)); //$NON-NLS-1$
} catch (UnsupportedOperationException e) {
// Remember that the symbolic link creation is not supported
fIsSymLinkSupported = false;
// Use file system symbolic link
return createEclipseLink(link, targetLocation, subMon.split(2));
}
}
/**
* Deletes a resource from the file system.
*
* Notes: If the resource is a project, then the project will be deleted as well
* as the content on the file system.
*
* The resource of a broken file system symbolic link will be also deleted.
*
* @param resource
* the resource in the workspace.
* @param monitor
* the progress monitor or null.
* @throws CoreException
* if an error occurs
*/
public static void deleteResource(@Nullable IResource resource, @Nullable IProgressMonitor monitor) throws CoreException {
SubMonitor subMon = SubMonitor.convert(monitor, 1);
if (resource == null) {
return;
}
if (isFileSystemSymbolicLink(resource) &&
!resource.exists() &&
((resource instanceof IFile) ||
(resource instanceof IFolder))) {
// Broken link
try {
Path linkPath = Paths.get(resource.getProject().getLocation().append(resource.getProjectRelativePath()).toOSString());
Files.delete(linkPath);
subMon.worked(1);
return;
} catch (IOException e) {
// do nothing ... try Resource.delete() below
}
}
resource.delete(true, subMon);
}
/**
* Copy a resource in the file system.
*
* The supplied destination path may be absolute or relative. Absolute paths
* fully specify the new location for the resource relative to the workspace
* root, including its project. Relative paths are considered to be relative to
* the container of the resource being copied.
*
* @param resource
* the resource in the workspace
* @param destinationPath
* the destination path
* @param flags
* update flags according to IResource.copy(). Note for file system
* symbolic links only IResource.SHALLOW is supported
* @param monitor
* the progress monitor or null.
* @return the copied resource or null
* @throws CoreException
* if an error occurs
*/
public static @Nullable IResource copyResource(@Nullable IResource resource, @Nullable IPath destinationPath, int flags, @Nullable IProgressMonitor monitor) throws CoreException {
if (resource == null || destinationPath == null) {
return null;
}
SubMonitor subMon = SubMonitor.convert(monitor, 1);
IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();
boolean isFileSystemSymbolicLink = ResourceUtil.isFileSystemSymbolicLink(resource);
// make path absolute
IPath target = destinationPath.isAbsolute() ? destinationPath : resource.getParent().getFullPath().append(destinationPath);
if (isFileSystemSymbolicLink &&
((flags & IResource.SHALLOW) != 0) &&
((resource instanceof IFile)
|| (resource instanceof IFolder))) {
return copySymlink(resource, checkNotNull(target), checkNotNull(subMon), checkNotNull(workspaceRoot));
}
// use eclipse copy
resource.copy(destinationPath, flags, subMon);
return workspaceRoot.findMember(target);
}
/**
* Checks whether the resource is a linked resource (file system or Eclipse
* symbolic link) regardless of link is broken or not.
*
* @param resource
* the resources to check
* @return <code>true</code> if it is a linked resource else <code>false</code>
*/
public static boolean isSymbolicLink(@Nullable IResource resource) {
boolean isLinked = false;
if (resource != null) {
isLinked = resource.isLinked();
if (!isLinked) {
isLinked = isFileSystemSymbolicLink(resource);
}
}
return isLinked;
}
/**
* Delete a broken symbolic link. Has no effect if the resource is not a
* symbolic link or if the target of the symbolic link exists.
*
* @param resource
* the resource in the workspace.
* @throws CoreException
* if an error occurs
*/
public static void deleteIfBrokenSymbolicLink(@Nullable IResource resource) throws CoreException {
if (resource == null) {
return;
}
if (resource.isLinked()) {
IPath location = resource.getLocation();
if (location == null || !location.toFile().exists()) {
resource.delete(true, null);
}
} else {
URI uri = resource.getLocationURI();
if (uri != null) {
Path linkPath = Paths.get(uri);
if (Files.isSymbolicLink(linkPath) && !resource.exists()) {
try {
Files.delete(linkPath);
} catch (Exception e) {
// Do nothing.
}
}
}
}
}
/**
* Returns the location path of the given resource. If the resource is file
* system symbolic link then it will return the link target location.
*
* @param resource
* the resource to check
* @return the {@link IPath} of the resource or null
*/
public static @Nullable IPath getLocation(@Nullable IResource resource) {
URI locationUri = getLocationURI(resource);
if (locationUri != null) {
return new org.eclipse.core.runtime.Path(locationUri.getPath());
}
return null;
}
/**
* Returns the location URI of the given resource. If the resource is file
* system symbolic link then it will return the link target location.
*
* @param resource
* the resource to check
* @return the URI of the resource or null
*/
public static @Nullable URI getLocationURI(@Nullable IResource resource) {
if (resource != null) {
if (isFileSystemSymbolicLink(resource)) {
try {
return Files.readSymbolicLink(Paths.get(resource.getLocationURI())).toUri();
} catch (IOException e) {
// Do nothing... return null below
}
} else {
return resource.getLocationURI();
}
}
return null;
}
// ------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------
private static boolean createEclipseLink(IResource link, IPath targetLocation, @Nullable IProgressMonitor monitor) throws CoreException {
SubMonitor subMon = SubMonitor.convert(monitor);
if (link instanceof IFile) {
((IFile) link).createLink(targetLocation, IResource.REPLACE, subMon);
} else if (link instanceof IFolder) {
((IFolder) link).createLink(targetLocation, IResource.REPLACE, subMon);
}
return true;
}
private static boolean isFileSystemSymbolicLink(IResource resource) {
URI uri = resource.getLocationURI();
return (uri == null ? false : Files.isSymbolicLink(Paths.get(uri)));
}
private static boolean checkResource(IResource link, Path linkPath) throws CoreException, IOException {
if (link.exists()) {
// Eclipse resource already exists
if (link.isLinked() ||
Files.isSymbolicLink(linkPath)) {
// Remove the link
link.delete(true, null);
} else {
// Abort because folder or file exists
return false;
}
}
if (Files.isSymbolicLink(linkPath)) {
// Delete the broken file system symbolic link
Files.delete(linkPath);
} else if (linkPath.toFile().exists()) {
// Abort because folder or file exists
return false;
}
return true;
}
private static @Nullable IResource copySymlink(IResource resource, IPath destinationPath, SubMonitor monitor, IWorkspaceRoot workspaceRoot) throws CoreException {
AtomicReference<IResource> newResourceRef = new AtomicReference<>();
Path originalLinkedFile = Paths.get(resource.getProject().getLocation().append(resource.getProjectRelativePath()).toOSString());
IPath newLocation = workspaceRoot.findMember(destinationPath.segment(0)).getLocation().append(destinationPath.removeFirstSegments(1));
Path newLinkedFile = Paths.get(newLocation.toOSString());
IResource parent = workspaceRoot.findMember(destinationPath.removeLastSegments(1));
if (parent == null) {
throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Parent resource does not exist: " + destinationPath.removeLastSegments(1))); //$NON-NLS-1$ )
}
ResourcesPlugin.getWorkspace().run((ICoreRunnable) (mon -> {
try {
SubMonitor subMon = SubMonitor.convert(mon, 2);
Files.copy(originalLinkedFile, newLinkedFile, LinkOption.NOFOLLOW_LINKS);
parent.refreshLocal(IResource.DEPTH_ONE, subMon.split(1));
IResource newResource = workspaceRoot.findMember(destinationPath);
if (newResource == null) {
return;
}
newResource.refreshLocal(IResource.DEPTH_INFINITE, subMon.split(1));
@SuppressWarnings("null")
Map<QualifiedName, String> persistentProperties = resource.getPersistentProperties();
if (persistentProperties != null) {
for (Map.Entry<QualifiedName, String> entry : persistentProperties.entrySet()) {
newResource.setPersistentProperty(entry.getKey(), entry.getValue());
}
}
newResourceRef.set(newResource);
} catch (IOException e) {
throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Error copying symbolic link", e)); //$NON-NLS-1$ )
}
}), parent.getParent(), IWorkspace.AVOID_UPDATE, monitor);
return newResourceRef.get();
}
}