| /*=============================================================================# |
| # 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); |
| } |
| |
| } |