blob: cad4b4c43bcfc2c7f879a88422ac4439f9e3e0d3 [file] [log] [blame]
/*******************************************************************************
* Copyright (C) 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others.
*
* 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.egit.core.internal.efs;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.text.MessageFormat;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceStatus;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.egit.core.Activator;
import org.eclipse.egit.core.internal.efs.EgitFileSystem.UriComponents;
import org.eclipse.jgit.lib.Repository;
/**
* Manages a hidden project containing {@link IFile}s linked to EFS URIs.
*/
public enum HiddenResources {
// Based on JDT's ExternalFolderManager and on
// CompareWithOtherResourceDialog from Compare UI.
/**
* The singleton instance.
*/
INSTANCE;
private static final String PROJECT_NAME = ".org.eclipse.egit.core.cmp"; //$NON-NLS-1$
private static final String SRC_FOLDER_PREFIX = "src"; //$NON-NLS-1$
private final static String PROJECT_FILE = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" //$NON-NLS-1$
+ "<projectDescription>\n" //$NON-NLS-1$
+ "\t<name>" + PROJECT_NAME + "</name>\n" //$NON-NLS-1$ //$NON-NLS-2$
+ "\t<comment></comment>\n" //$NON-NLS-1$
+ "\t<projects>\n" //$NON-NLS-1$
+ "\t</projects>\n" //$NON-NLS-1$
+ "\t<buildSpec>\n" //$NON-NLS-1$
+ "\t</buildSpec>\n" //$NON-NLS-1$
+ "\t<natures>\n" //$NON-NLS-1$
+ "\t</natures>\n" //$NON-NLS-1$
+ "</projectDescription>"; //$NON-NLS-1$
private boolean initialized;
private final Object lock = new Object();
/**
* Create new linked {@link IFile} in a hidden project with the given uri
* and encoding.
*
* @param uri
* to link to
* @param name
* for the new file
* @param encoding
* to use for the file
* @param monitor
* for progress reporting
* @return the new {@link IFile}
* @throws CoreException
* if the file could not be created
*/
public IFile createFile(URI uri, String name, Charset encoding,
IProgressMonitor monitor) throws CoreException {
SubMonitor progress = SubMonitor.convert(monitor, 3);
IProject project = getHiddenProject(progress.newChild(1));
initialize(progress.newChild(1));
IResource[] children = project.members();
progress.setWorkRemaining(children.length + 2);
for (IResource rsc : children) {
if (rsc.getType() == IResource.FOLDER) {
try {
return linkFile((IFolder) rsc, uri, name, encoding,
progress.newChild(1));
} catch (CoreException e) {
// Swallow here; try the next folder
}
} else {
progress.worked(1);
}
}
IFolder newFolder = createFolder(project, children.length,
progress.newChild(1));
return linkFile(newFolder, uri, name, encoding, progress.newChild(1));
}
/**
* Initializes the hidden project.
*
* @param monitor
* for progress reporting and cancellation
*/
public void initialize(IProgressMonitor monitor) {
SubMonitor progress = SubMonitor.convert(monitor, 2);
try {
IProject project = getHiddenProject(progress.newChild(1));
initialize(project, progress.newChild(1));
} catch (CoreException e) {
Activator.logWarning("Cannot clean up internal hidden project", e); //$NON-NLS-1$
}
}
private synchronized void initialize(IProject project,
IProgressMonitor monitor) {
if (initialized) {
return;
}
initialized = true;
// Clean out all existing linked resources. There are internal methods that would make this very simple, but they're not accessible.
IWorkspaceRunnable clean = m -> {
IResource[] resources = project.members();
SubMonitor progress = SubMonitor.convert(m, resources.length);
for (IResource rsc : project.members()) {
if (rsc.getType() == IResource.FOLDER) {
IResource[] children = ((IFolder) rsc).members();
SubMonitor sub = SubMonitor.convert(progress.newChild(1),
children.length);
for (IResource f : children) {
if (f.isLinked()) {
try {
f.delete(true, sub.newChild(1));
} catch (CoreException e) {
Activator.logWarning(MessageFormat.format(
"Cannot clean up internal hidden resource {}", //$NON-NLS-1$
f), e);
}
}
if (sub.isCanceled()) {
return;
}
}
children = ((IFolder) rsc).members();
if (children.length == 0) {
try {
rsc.delete(true, null);
} catch (CoreException e) {
Activator.logWarning(MessageFormat.format(
"Cannot clean up internal hidden folder {}", //$NON-NLS-1$
rsc), e);
}
}
} else {
progress.worked(1);
}
if (progress.isCanceled()) {
return;
}
}
};
try {
project.getWorkspace().run(clean, null, IWorkspace.AVOID_UPDATE,
monitor);
} catch (CoreException e) {
Activator.logWarning(MessageFormat.format(
"Cannot clean up internal hidden project {}", project), e); //$NON-NLS-1$
}
}
/**
* Determines whether the {@link IResource} is the hidden project.
*
* @param resource
* to test
* @return {@code true} if the resource is the hidden project, {@code false}
* otherwise
*/
public boolean isHiddenProject(IResource resource) {
if (resource.getType() != IResource.PROJECT) {
return false;
}
return PROJECT_NAME.equals(resource.getName());
}
/**
* Obtains the {@link Repository} from an EGit-internal URI.
*
* @param uri
* to get the repository from
* @return the {@link Repository}, or {@code null} if none could be
* determined
*/
public Repository getRepository(URI uri) {
if (!EgitFileSystem.SCHEME.equals(uri.getScheme())) {
return null;
}
try {
return UriComponents.parse(uri).getRepository();
} catch (URISyntaxException e) {
return null;
}
}
/**
* Obtains the git path from an EGit-internal URI.
*
* @param uri
* to get the git path from
* @return the git path, or {@code null} if none could be determined
*/
public String getGitPath(URI uri) {
if (!EgitFileSystem.SCHEME.equals(uri.getScheme())) {
return null;
}
try {
return UriComponents.parse(uri).getGitPath();
} catch (URISyntaxException e) {
return null;
}
}
/**
* Obtains the git path from an EGit-internal URI, if that URI is for the given repository.
*
* @param uri
* to get the git path from
* @param repository the URI should be for
* @return the git path, or {@code null} if none could be
* determined or the URI is not for the given repository
*/
public String getGitPath(URI uri, Repository repository) {
if (!EgitFileSystem.SCHEME.equals(uri.getScheme())) {
return null;
}
try {
UriComponents parsed = UriComponents.parse(uri);
if (parsed.getRepoDir().equals(repository.getDirectory())) {
return parsed.getGitPath();
}
} catch (URISyntaxException e) {
// Ignore
}
return null;
}
private IProject getHiddenProject(IProgressMonitor monitor) throws CoreException {
IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(PROJECT_NAME);
if (!project.isAccessible()) {
SubMonitor progress = SubMonitor.convert(monitor, 2);
if (!project.exists()) {
createProject(project, progress.newChild(1));
}
progress.setWorkRemaining(1);
openProject(project, progress.newChild(1));
}
return project;
}
private void createProject(IProject project, IProgressMonitor monitor)
throws CoreException {
IProjectDescription desc = project.getWorkspace()
.newProjectDescription(project.getName());
IPath stateLocation = Activator.getDefault().getStateLocation();
desc.setLocation(stateLocation.append(PROJECT_NAME));
project.create(desc, IResource.HIDDEN, monitor);
}
private void openProject(IProject project, IProgressMonitor monitor)
throws CoreException {
SubMonitor progress = SubMonitor.convert(monitor, 1);
try {
project.open(progress.newChild(1));
} catch (CoreException e) {
progress.setWorkRemaining(3);
if (e.getStatus()
.getCode() == IResourceStatus.FAILED_READ_METADATA) {
// Workspace moved? Re-create.
project.delete(false, true, progress.newChild(1));
createProject(project, progress.newChild(1));
} else {
// .project or folder on disk have been deleted, recreate them
IPath stateLocation = Activator.getDefault().getStateLocation();
IPath projectPath = stateLocation.append(PROJECT_NAME);
File directory = projectPath.toFile();
try {
if (!directory.mkdirs() && !directory.isDirectory()) {
throw new FileNotFoundException();
}
Files.write(
projectPath.append(
IProjectDescription.DESCRIPTION_FILE_NAME)
.toFile().toPath(),
PROJECT_FILE.getBytes(StandardCharsets.UTF_8));
progress.worked(2);
} catch (IOException ioe) {
// Re-create from scratch
project.delete(true, true, progress.newChild(1));
createProject(project, progress.newChild(1));
}
}
project.open(progress.newChild(1));
}
}
private IFolder createFolder(IProject project, int n,
IProgressMonitor monitor) throws CoreException {
IFolder folder = project.getFolder(SRC_FOLDER_PREFIX + n);
folder.create(IResource.NONE, true, monitor);
return folder;
}
private IFile linkFile(IFolder folder, URI uri, String name,
Charset encoding, IProgressMonitor monitor) throws CoreException {
SubMonitor progress = SubMonitor.convert(monitor, 2);
IFile file = folder.getFile(name);
linkFile(file, uri, progress.newChild(1));
if (encoding != null) {
file.setCharset(encoding.name(), progress.newChild(1));
}
return file;
}
private void linkFile(IFile file, URI uri, IProgressMonitor monitor)
throws CoreException {
synchronized (lock) {
boolean linkingDisabled = Platform.getPreferencesService()
.getBoolean(ResourcesPlugin.PI_RESOURCES,
ResourcesPlugin.PREF_DISABLE_LINKING, false, null);
IEclipsePreferences prefs = null;
IPreferenceChangeListener listener = null;
AtomicBoolean prefChanged = new AtomicBoolean();
if (linkingDisabled) {
// The user has disabled creating linked resources. Force-
// enable the preference, then reset it afterwards.
//
// Note that the preference only guards *creating* linked
// resources. Existing linked resources are handled perfectly
// well by Eclipse even when the preference is true.
prefs = InstanceScope.INSTANCE
.getNode(ResourcesPlugin.PI_RESOURCES);
prefs.putBoolean(ResourcesPlugin.PREF_DISABLE_LINKING, false);
listener = event -> {
if (ResourcesPlugin.PREF_DISABLE_LINKING
.equals(event.getKey())) {
prefChanged.set(true);
}
};
prefs.addPreferenceChangeListener(listener);
}
try {
file.createLink(uri, IResource.NONE, monitor);
} finally {
if (prefs != null) {
prefs.removePreferenceChangeListener(listener);
// Don't reset if somebody else changed the preference in
// the meantime.
if (!prefChanged.get()) {
prefs.putBoolean(ResourcesPlugin.PREF_DISABLE_LINKING,
linkingDisabled);
}
}
}
}
}
}