blob: 8fe6aa7ce3fca32239a07f08e5c79639502cdba7 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2005, 2021 Stephan Wahlbrink and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.ltk.ui.wizards;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.resources.IContainer;
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.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExecutableExtension;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.content.IContentType;
import org.eclipse.core.runtime.content.IContentTypeManager;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.wizard.Wizard;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.INewWizard;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkingSet;
import org.eclipse.ui.actions.WorkspaceModifyOperation;
import org.eclipse.ui.dialogs.ContainerGenerator;
import org.eclipse.ui.statushandlers.StatusManager;
import org.eclipse.ui.wizards.newresource.BasicNewProjectResourceWizard;
import org.eclipse.ui.wizards.newresource.BasicNewResourceWizard;
import org.eclipse.statet.ecommons.ui.util.UIAccess;
import org.eclipse.statet.internal.ltk.ui.LTKUIPlugin;
import org.eclipse.statet.ltk.ui.util.LTKWorkbenchUIUtil;
public abstract class NewElementWizard extends Wizard implements INewWizard, IExecutableExtension {
protected class NewProject {
private final IProject project;
/** Path of project, null for default location */
protected IPath locationPath;
private final IProject[] refProjects;
private final IWorkingSet[] workingSets;
public NewProject(final IProject project, final IPath location,
final IProject[] refProjects, final IWorkingSet[] workingSets) {
this.project= project;
this.locationPath= location;
this.refProjects= refProjects;
this.workingSets= workingSets;
}
/**
* Returns a project resource handle.
* <p>
* Note: Handle is cached. This method does not create the project resource;
* this is the responsibility of <code>IProject::create</code>.
* </p>
*
* @return the project resource handle
*/
public IProject getResource() {
return this.project;
}
/**
* Creates the new project resource.
* <p>
* In normal usage, this method is invoked after the user has pressed Finish
* on the wizard; the enablement of the Finish button implies that all
* controls on the pages currently contain valid values.
* </p>
* @return the created project resource, or <code>null</code> if the
* project was not created
* @throws CoreException
*/
public IProject createProject(final IProgressMonitor monitor)
throws InvocationTargetException, InterruptedException, CoreException {
final SubMonitor m= SubMonitor.convert(monitor, LTKWizardsMessages.NewElement_CreateProject_task, 20 + 2);
final IWorkspace workspace= ResourcesPlugin.getWorkspace();
final IProjectDescription description= workspace.newProjectDescription(this.project.getName());
description.setLocation(this.locationPath);
// update the referenced project if provided
if (this.refProjects != null && this.refProjects.length > 0) {
description.setReferencedProjects(this.refProjects);
}
if (monitor.isCanceled()) {
throw new OperationCanceledException();
}
doCreateProject(this.project, description, m.newChild(10));
doConfigProject(this.project, m.newChild(10));
doAddToWorkingSets(this.project, m.newChild(2));
return this.project;
}
private void doCreateProject(final IProject project, final IProjectDescription description,
final SubMonitor m)
throws CoreException {
// run the new project creation operation
m.beginTask("Install Project", 20); //$NON-NLS-1$
project.create(description, m.newChild(10));
if (m.isCanceled()) {
throw new OperationCanceledException();
}
project.open(m.newChild(10));
if (m.isCanceled()) {
throw new OperationCanceledException();
}
}
protected void doConfigProject(final IProject project, final IProgressMonitor monitor) throws CoreException {
}
private void doAddToWorkingSets(final IProject project, final IProgressMonitor monitor) {
if (this.workingSets != null && this.workingSets.length > 0) {
monitor.beginTask(LTKWizardsMessages.NewElement_AddProjectToWorkingSet_task, 1);
getWorkbench().getWorkingSetManager().addToWorkingSets(project, this.workingSets);
}
monitor.done();
}
}
protected static class NewContainer {
private final IContainer container;
public NewContainer(final IContainer container) {
this.container= container;
}
public NewContainer(final IPath path) {
this.container= (path.segmentCount() == 1) ?
ResourcesPlugin.getWorkspace().getRoot().getProject(path.segment(0)) :
ResourcesPlugin.getWorkspace().getRoot().getFolder(path);
}
/**
* Returns a container resource handle.
* <p>
*
* @return the project resource handle
*/
public IContainer getResource() {
return this.container;
}
protected void createFolder(IContainer container,
final SubMonitor m) throws CoreException {
if (container.getType() == IResource.FOLDER && !container.exists()) {
final List<IFolder> toCreate= new ArrayList<>();
do {
toCreate.add((IFolder) container);
} while ((container= container.getParent()).getType() == IResource.FOLDER
&& !container.exists());
m.setWorkRemaining(toCreate.size());
for (int i= toCreate.size() - 1; i >= 0; i--) {
toCreate.get(i).create(false, true, m.newChild(1));
}
}
}
/**
* Creates the new container resource.
* <p>
* In normal usage, this method is invoked after the user has pressed Finish
* on the wizard; the enablement of the Finish button implies that all
* controls on the pages currently contain valid values.
* </p>
* @return
*
* @return the created project resource, or <code>null</code> if the
* project was not created
* @throws CoreException
*/
public IContainer createContainer(
final IProgressMonitor monitor) throws InterruptedException, CoreException {
final SubMonitor m= SubMonitor.convert(monitor);
final IContainer handle= getResource();
createFolder(handle, m);
return handle;
}
public IFolder createSubFolder(final IPath path,
final IProgressMonitor monitor) throws InterruptedException, CoreException {
final SubMonitor m= SubMonitor.convert(monitor);
final IFolder handle= getResource().getFolder(path);
createFolder(handle, m);
return handle;
}
}
protected static class NewFile {
protected IPath containerPath;
protected String resourceName;
private IContentType contentType;
protected IRegion initialSelection;
/** No direct access, use getFileHandle() */
private IFile cachedHandle;
public NewFile(final IPath containerPath, final String resourceName) {
this.containerPath= containerPath;
this.resourceName= resourceName;
}
public NewFile(final IPath containerPath, final String resourceName,
final IContentType contentType) {
this(containerPath, resourceName);
this.contentType= (contentType != null) ? contentType :
Platform.getContentTypeManager().getContentType(IContentTypeManager.CT_TEXT);
}
/**
* Return the filehandle of the new file.
* The Filehandle is cached. File can exists or not exists.
* @return
*/
public IFile getResource() {
if (this.cachedHandle == null) {
final IPath fullPath= this.containerPath.append(this.resourceName);
this.cachedHandle= createFileHandle(fullPath);
}
return this.cachedHandle;
}
/**
* Creates a file resource handle for the file with the given workspace path.
* This method does not create the file resource; this is the responsibility
* of <code>createFile</code>.
*
* @param filePath the path of the file resource to create a handle for
* @return the new file resource handle
* @see #createFile
*/
protected IFile createFileHandle(final IPath filePath) {
return ResourcesPlugin.getWorkspace().getRoot().getFile(filePath);
}
/**
* Creates a new file resource in the selected container and with the selected
* name. Creates any missing resource containers along the path; does nothing if
* the container resources already exist.
* <p>
* In normal usage, this method is invoked after the user has pressed Finish on
* the wizard; the enablement of the Finish button implies that all controls on
* on this page currently contain valid values.
* </p>
* <p>
* Note that this page caches the new file once it has been successfully
* created; subsequent invocations of this method will answer the same
* file resource without attempting to create it again.
* </p>
* <p>
* This method should be called within a workspace modify operation since
* it creates resources.
* </p>
*
* @return the created file resource, or <code>null</code> if the file
* was not created
* @throws InterruptedException
* @throws InvocationTargetException
* @throws CoreException
*/
public void createFile(final IProgressMonitor monitor)
throws InvocationTargetException, InterruptedException, CoreException {
final IPath containerPath= this.containerPath;
final IFile newFileHandle= getResource();
assert (containerPath != null);
assert (newFileHandle != null);
try {
final SubMonitor m= SubMonitor.convert(monitor,
NLS.bind(LTKWizardsMessages.NewElement_CreateFile_task, newFileHandle.getName()),
1 + 1 + 3 );
final InputStream initialContents= getInitialFileContentStream(newFileHandle,
m.newChild(1) );
final ContainerGenerator generator= new ContainerGenerator(containerPath);
generator.generateContainer(m.newChild(1));
doCreateFile(newFileHandle, initialContents, m.newChild(3));
}
finally {
monitor.done();
}
}
/**
* Creates a file resource given the file handle and contents.
*
* @param fileHandle the file handle to create a file resource with
* @param contents the initial contents of the new file resource, or
* <code>null</code> if none (equivalent to an empty stream)
* @param m the progress monitor to show visual progress with
* @exception CoreException if the operation fails
* @exception OperationCanceledException if the operation is canceled
*/
private static void doCreateFile(final IFile fileHandle, InputStream contents,
final SubMonitor m) throws CoreException {
if (contents == null) {
contents= new ByteArrayInputStream(new byte[0]);
}
try {
m.beginTask(null, 10 + 20);
// Create a new file resource in the workspace
final IPath path= fileHandle.getFullPath();
final IWorkspaceRoot root= ResourcesPlugin.getWorkspace().getRoot();
final int numSegments= path.segmentCount();
if (numSegments > 2 && !root.getFolder(path.removeLastSegments(1)).exists()) {
final SubMonitor m1= m.newChild(10);
// If the direct parent of the path doesn't exist, try to create the
// necessary directories.
for (int i= numSegments - 2; i > 0; i--) {
m1.setWorkRemaining(i);
final IFolder folder= root.getFolder(path.removeLastSegments(i));
if (!folder.exists()) {
folder.create(false, true, m1.newChild(1));
}
m1.worked(1);
}
}
if (m.isCanceled()) {
throw new OperationCanceledException();
}
m.setWorkRemaining(20);
fileHandle.create(contents, false, m.newChild(20));
if (m.isCanceled()) {
throw new OperationCanceledException();
}
}
catch (final CoreException e) {
// If the file already existed locally, just refresh to get contents
if (e.getStatus().getCode() == IResourceStatus.PATH_OCCUPIED) {
fileHandle.refreshLocal(IResource.DEPTH_ZERO, null);
}
else {
throw e;
}
}
}
/**
* Returns a stream containing the initial contents to be given to new file resource
* instances. <b>Subclasses</b> may wish to override. This default implementation
* provides no initial contents.
* @param subMonitor
*
* @return initial contents to be given to new file resource instances
*/
protected InputStream getInitialFileContentStream(final IFile newFileHandle,
final SubMonitor m) {
final String content= getInitialFileContent(newFileHandle, m);
if (content == null) {
return null;
}
try {
// encoding of content type
final IContentType contentType= getContentType(newFileHandle);
if (contentType != null) {
final String charset= contentType.getDefaultCharset();
if (charset != null) {
return new ByteArrayInputStream(content.getBytes(charset));
}
}
// encoding of container
final String charset= newFileHandle.getCharset(true);
if (charset != null) {
return new ByteArrayInputStream(content.getBytes(charset));
}
}
catch (final UnsupportedEncodingException | CoreException e) {
}
return new ByteArrayInputStream(content.getBytes());
}
/**
* Returns the content type of the new file.
* Used e.g. to lookup the encoding.
* @return id
*/
public IContentType getContentType(final IFile newFileHandle) {
return this.contentType;
}
/**
* Returns a stream containing the initial contents to be given to new file resource
* instances. <b>Subclasses</b> may wish to override. This default implementation
* provides no initial contents.
* @param m
*
* @return initial contents to be given to new file resource instances
*/
protected String getInitialFileContent(final IFile newFileHandle,
final SubMonitor m) {
return null;
}
public IRegion getInitialSelection() {
return this.initialSelection;
}
}
private IWorkbench workbench;
private IStructuredSelection selection;
private IConfigurationElement configElement;
public NewElementWizard() {
setNeedsProgressMonitor(true);
}
/**
* Stores the configuration element for the wizard. The config element will
* be used in <code>performFinish</code> to set the result perspective.
*/
@Override
public void setInitializationData(final IConfigurationElement config, final String propertyName, final Object data) {
this.configElement= config;
}
/**
* Subclasses should override to perform the actions of the wizard.
* This method is run in the wizard container's context as a workspace runnable.
* @param monitor
* @throws InterruptedException
* @throws CoreException
* @throws InvocationTargetException
*/
protected abstract void performOperations(IProgressMonitor monitor) throws InterruptedException, CoreException, InvocationTargetException;
/**
* @return the scheduling rule for creating the element.
*/
protected ISchedulingRule getSchedulingRule() {
return ResourcesPlugin.getWorkspace().getRoot(); // lock all by default
}
/**
* @return true if the runnable should be run in a separate thread, and false to run in the same thread
*/
protected boolean canRunForked() {
return true;
}
// public abstract IJavaElement getCreatedElement();
@Override
public void init(final IWorkbench workbench, final IStructuredSelection currentSelection) {
this.workbench= workbench;
this.selection= currentSelection;
}
@Override
public boolean performFinish() {
final WorkspaceModifyOperation op= new WorkspaceModifyOperation(getSchedulingRule()) {
@Override
protected void execute(final IProgressMonitor monitor) throws CoreException, InvocationTargetException, InterruptedException {
try {
performOperations(monitor);
}
catch (final InterruptedException e) {
throw new OperationCanceledException(e.getMessage());
}
catch (final CoreException e) {
throw new InvocationTargetException(e);
}
finally {
if (monitor != null) {
monitor.done();
}
}
}
};
try {
getContainer().run(canRunForked(), true, op);
return true;
}
catch (final InvocationTargetException e) {
handleTasksException(getShell(), e);
return false;
}
catch (final InterruptedException e) {
return false;
}
}
protected void handleTasksException(final Shell shell, final InvocationTargetException e) {
StatusManager.getManager().handle(new Status(IStatus.ERROR, LTKUIPlugin.BUNDLE_ID, -1,
LTKWizardsMessages.NewElement_error_DuringOperation_message, e),
StatusManager.LOG | StatusManager.SHOW);
}
public IStructuredSelection getSelection() {
return this.selection;
}
public IWorkbench getWorkbench() {
return this.workbench;
}
/* Helper methods for subclasses **********************************************/
/**
* Returns the scheduling rule to use when creating the resource at
* the given container path. The rule should be the creation rule for
* the top-most non-existing parent.
* @param resource The resource being created
* @return The scheduling rule for creating the given resource
*/
protected ISchedulingRule createRule(IResource resource) {
IResource parent= resource.getParent();
while (parent != null) {
if (parent.exists()) {
return resource.getWorkspace().getRuleFactory().createRule(resource);
}
resource= parent;
parent= parent.getParent();
}
return resource.getWorkspace().getRoot();
}
protected void openResource(final IFile resource) {
final IWorkbenchPage activePage= UIAccess.getActiveWorkbenchPage(true);
if (activePage != null) {
LTKWorkbenchUIUtil.openEditor(activePage, resource, null);
}
}
protected void openResource(final NewFile file) {
if (file.getResource() == null) {
return;
}
final IWorkbenchPage activePage= UIAccess.getActiveWorkbenchPage(true);
if (activePage != null) {
LTKWorkbenchUIUtil.openEditor(activePage, file.getResource(), file.getInitialSelection());
}
}
protected void selectAndReveal(final IResource newResource) {
BasicNewResourceWizard.selectAndReveal(newResource, this.workbench.getActiveWorkbenchWindow());
}
protected void updatePerspective() {
BasicNewProjectResourceWizard.updatePerspective(this.configElement);
}
}