/* * $Id$ * * SARL is an general-purpose agent programming language. * More details on http://www.sarl.io * * Copyright (C) 2014-2017 the original authors or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.sarl.eclipse.wizards.newproject; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.InvocationTargetException; import java.net.URI; import java.net.URISyntaxException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import org.eclipse.core.filesystem.EFS; import org.eclipse.core.filesystem.IFileInfo; import org.eclipse.core.filesystem.IFileStore; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceStatus; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Status; 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.internal.ui.dialogs.StatusInfo; import org.eclipse.jdt.internal.ui.util.CoreUtility; import org.eclipse.jdt.internal.ui.util.ExceptionHandler; import org.eclipse.jdt.internal.ui.wizards.ClassPathDetector; import org.eclipse.jdt.internal.ui.wizards.NewWizardMessages; import org.eclipse.jdt.ui.JavaUI; import org.eclipse.jdt.ui.wizards.JavaCapabilityConfigurationPage; import org.eclipse.jdt.ui.wizards.NewJavaProjectWizardPageTwo; import org.eclipse.jface.dialogs.ErrorDialog; import org.eclipse.jface.operation.IRunnableWithProgress; import org.eclipse.ui.actions.WorkspaceModifyDelegatingOperation; import io.sarl.eclipse.SARLEclipseConfig; import io.sarl.eclipse.SARLEclipsePlugin; import io.sarl.eclipse.natures.SARLProjectConfigurator; import io.sarl.lang.util.OutParameter; /** * The second page of the SARL new project wizard. * Most part of the code of this class is copy/paste from {@link NewJavaProjectWizardPageTwo}. * * @author $Author: ngaud$ * @author $Author: sgalland$ * @version $FullVersion$ * @mavengroupid $GroupId$ * @mavenartifactid $ArtifactId$ */ @SuppressWarnings("checkstyle:classdataabstractioncoupling") public class BuildSettingWizardPage extends JavaCapabilityConfigurationPage { private static final String FILENAME_PROJECT = ".project"; //$NON-NLS-1$ private static final String FILENAME_CLASSPATH = ".classpath"; //$NON-NLS-1$ private static final int FILE_COPY_BLOCK_SIZE = 8192; private static final int UPDATE_PROJECT_MONITORED_STEPS = 7; private final MainProjectWizardPage firstPage; /** Location of the current project. * It is <code>null</code> if the location is a platform location. */ private URI currProjectLocation; private IProject currProject; private boolean keepContent; private File dotProjectBackup; private File dotClasspathBackup; private Boolean isAutobuild; private Set<IFileStore> orginalFolders; /** * Constructor for the {@link NewJavaProjectWizardPageTwo}. * * @param mainPage the first page of the wizard */ public BuildSettingWizardPage(MainProjectWizardPage mainPage) { setPageComplete(false); this.firstPage = mainPage; this.currProjectLocation = null; this.currProject = null; this.keepContent = false; this.dotProjectBackup = null; this.dotClasspathBackup = null; this.isAutobuild = null; setTitle(Messages.SARLProjectNewWizard_3); setDescription(Messages.SARLProjectNewWizard_2); setImageDescriptor(SARLEclipsePlugin.getDefault().getImageDescriptor( SARLEclipseConfig.NEW_PROJECT_WIZARD_DIALOG_IMAGE)); } @Override protected final boolean useNewSourcePage() { return true; } @Override public void setVisible(boolean visible) { final boolean isShownFirstTime = visible && this.currProject == null; if (visible) { // Entering from the first page if (isShownFirstTime) { createProvisonalProject(); } } else { // Leaving back to the first page if (getContainer().getCurrentPage() == this.firstPage) { removeProvisonalProject(); } } super.setVisible(visible); if (isShownFirstTime) { setFocus(); } } private static boolean hasExistingContent(URI realLocation) throws CoreException { final IFileStore file = EFS.getStore(realLocation); return file.fetchInfo().exists(); } private IStatus changeToNewProject() { final UpdateRunnable op = new UpdateRunnable(); try { getContainer().run(true, false, new WorkspaceModifyDelegatingOperation(op)); return op.getInfoStatus(); } catch (InvocationTargetException e) { final String title = NewWizardMessages.NewJavaProjectWizardPageTwo_error_title; final String message = NewWizardMessages.NewJavaProjectWizardPageTwo_error_message; updateStatus(e); ExceptionHandler.handle(e, getShell(), title, message); } catch (InterruptedException e) { // cancel pressed } return null; } /** Update the status of this page according to the given exception. * * @param event - the exception. */ private void updateStatus(Throwable event) { Throwable cause = event; while (cause != null && (!(cause instanceof CoreException)) && cause.getCause() != null && cause.getCause() != cause) { cause = cause.getCause(); } if (cause instanceof CoreException) { updateStatus(((CoreException) cause).getStatus()); } else { final String message; if (cause != null) { message = cause.getLocalizedMessage(); } else { message = event.getLocalizedMessage(); } final IStatus status = new StatusInfo(IStatus.ERROR, message); updateStatus(status); } } private static URI getRealLocation(String projectName, URI location) { URI theLocation = location; // Test if the project is inside workspace if (theLocation == null) { try { final URI rootLocation = ResourcesPlugin.getWorkspace().getRoot().getLocationURI(); theLocation = new URI(rootLocation.getScheme(), null, Path.fromPortableString( rootLocation.getPath()).append(projectName).toString(), null); } catch (URISyntaxException e) { Assert.isTrue(false, "Can't happen"); //$NON-NLS-1$ } } return theLocation; } @SuppressWarnings("checkstyle:npathcomplexity") private IStatus updateProject(IProgressMonitor monitor) throws CoreException, InterruptedException { final SubMonitor subMonitor = SubMonitor.convert(monitor, 7); IStatus result = StatusInfo.OK_STATUS; try { subMonitor.beginTask( NewWizardMessages.NewJavaProjectWizardPageTwo_operation_initialize, UPDATE_PROJECT_MONITORED_STEPS); if (subMonitor.isCanceled()) { throw new OperationCanceledException(); } final String projectName = this.firstPage.getProjectName(); this.currProject = ResourcesPlugin.getWorkspace().getRoot().getProject(projectName); this.currProjectLocation = this.firstPage.getProjectLocationURI(); final URI realLocation = getRealLocation(projectName, this.currProjectLocation); this.keepContent = hasExistingContent(realLocation); if (subMonitor.isCanceled()) { throw new OperationCanceledException(); } if (this.keepContent) { rememberExistingFiles(realLocation); rememberExisitingFolders(realLocation); } if (subMonitor.isCanceled()) { throw new OperationCanceledException(); } try { createProject(this.currProject, this.currProjectLocation, subMonitor.newChild(2)); } catch (CoreException e) { if (e.getStatus().getCode() == IResourceStatus.FAILED_READ_METADATA) { result = new StatusInfo( IStatus.INFO, MessageFormat.format( NewWizardMessages.NewJavaProjectWizardPageTwo_DeleteCorruptProjectFile_message, e.getLocalizedMessage())); deleteProjectFile(realLocation); if (this.currProject.exists()) { this.currProject.delete(true, null); } createProject(this.currProject, this.currProjectLocation, null); } else { throw e; } } if (subMonitor.isCanceled()) { throw new OperationCanceledException(); } initializeBuildPath(JavaCore.create(this.currProject), subMonitor.newChild(2)); // Create the Java project to allow the use of the new source folder page configureJavaProject(subMonitor.newChild(3)); } finally { subMonitor.done(); } return result; } private void keepExistingBuildPath(IProject project, OutParameter<IClasspathEntry[]> classpath, OutParameter<IPath> outputLocation, IProgressMonitor monitor) throws CoreException { final SubMonitor subMonitor = SubMonitor.convert(monitor, 3); IClasspathEntry[] entries = null; IPath outLocation = null; if (!project.getFile(FILENAME_CLASSPATH).exists()) { // Determine the default values if (classpath != null) { final List<IClasspathEntry> cpEntries = new ArrayList<>(); final Collection<IClasspathEntry> originalEntries = SARLProjectConfigurator.getDefaultSourceClassPathEntries( new Path(this.firstPage.getProjectName()).makeAbsolute()); cpEntries.addAll(originalEntries); this.firstPage.putDefaultClasspathEntriesIn(cpEntries); if (!cpEntries.isEmpty()) { classpath.set(cpEntries.toArray(new IClasspathEntry[cpEntries.size()])); } } if (outputLocation != null) { outputLocation.set(this.firstPage.getOutputLocation()); } // Override with the existing configuration final ClassPathDetector detector = new ClassPathDetector( this.currProject, subMonitor.newChild(1)); entries = detector.getClasspath(); outLocation = detector.getOutputLocation(); if (entries.length == 0) { entries = null; } } subMonitor.worked(2); if (classpath != null && entries != null) { classpath.set(entries); } if (outputLocation != null && outLocation != null) { outputLocation.set(outLocation); } subMonitor.done(); } private void buildNewBuildPath(IProject project, OutParameter<IClasspathEntry[]> classpath, OutParameter<IPath> outputLocation, IProgressMonitor monitor) throws CoreException { final List<IClasspathEntry> cpEntries = new ArrayList<>(); final IWorkspaceRoot root = project.getWorkspace().getRoot(); final Collection<IClasspathEntry> originalEntries = SARLProjectConfigurator.getDefaultSourceClassPathEntries( new Path(this.firstPage.getProjectName()).makeAbsolute()); final SubMonitor subMonitor = SubMonitor.convert(monitor, originalEntries.size() + 1); for (final IClasspathEntry sourceClasspathEntry : originalEntries) { final IPath path = sourceClasspathEntry.getPath(); if (path.segmentCount() > 1) { final IFolder folder = root.getFolder(path); CoreUtility.createFolder( folder, true, true, subMonitor.newChild(1)); } cpEntries.add(sourceClasspathEntry); } this.firstPage.putDefaultClasspathEntriesIn(cpEntries); final IClasspathEntry[] entries = cpEntries.toArray(new IClasspathEntry[cpEntries.size()]); final IPath outLocation = this.firstPage.getOutputLocation(); if (outLocation.segmentCount() > 1) { final IFolder folder = root.getFolder(outLocation); CoreUtility.createDerivedFolder( folder, true, true, subMonitor.newChild(1)); } if (classpath != null) { classpath.set(entries); } if (outputLocation != null) { outputLocation.set(outLocation); } } /** * Evaluates the new build path and output folder according to the settings on the first page. * The resulting build path is set by calling * {@link #init(IJavaProject, IPath, IClasspathEntry[], boolean)}. * Clients can override this method. * * @param javaProject the new project which is already created when this method is called. * @param monitor the progress monitor * @throws CoreException thrown when initializing the build path failed */ protected void initializeBuildPath(IJavaProject javaProject, IProgressMonitor monitor) throws CoreException { IProgressMonitor theMonitor = monitor; if (theMonitor == null) { theMonitor = new NullProgressMonitor(); } theMonitor.beginTask(NewWizardMessages.NewJavaProjectWizardPageTwo_monitor_init_build_path, 2); try { final OutParameter<IClasspathEntry[]> entries = new OutParameter<>(); final OutParameter<IPath> outputLocation = new OutParameter<>(); final IProject project = javaProject.getProject(); if (this.keepContent) { keepExistingBuildPath(project, entries, outputLocation, theMonitor); } else { buildNewBuildPath(project, entries, outputLocation, theMonitor); } if (theMonitor.isCanceled()) { throw new OperationCanceledException(); } init(javaProject, outputLocation.get(), entries.get(), false); } catch (CoreException e) { throw e; } catch (Throwable e) { if (e.getCause() instanceof CoreException) { throw (CoreException) e.getCause(); } final Throwable ee; if (e.getCause() != null && e.getCause() != e) { ee = e.getCause(); } else { ee = e; } throw new CoreException(SARLEclipsePlugin.getDefault().createStatus(IStatus.ERROR, ee)); } finally { theMonitor.done(); } } private static void deleteProjectFile(URI projectLocation) throws CoreException { final IFileStore file = EFS.getStore(projectLocation); if (file.fetchInfo().exists()) { final IFileStore projectFile = file.getChild(FILENAME_PROJECT); if (projectFile.fetchInfo().exists()) { projectFile.delete(EFS.NONE, null); } } } private void rememberExisitingFolders(URI projectLocation) { this.orginalFolders = new HashSet<>(); try { final IFileStore[] children = EFS.getStore(projectLocation).childStores(EFS.NONE, null); for (int i = 0; i < children.length; i++) { final IFileStore child = children[i]; final IFileInfo info = child.fetchInfo(); if (info.isDirectory() && info.exists() && !this.orginalFolders.contains(child.getName())) { this.orginalFolders.add(child); } } } catch (CoreException e) { SARLEclipsePlugin.getDefault().log(e); } } private void restoreExistingFolders(URI projectLocation) { final Set<IFileStore> foldersToKeep = new HashSet<>(this.orginalFolders); // workaround for bug 319054: Eclipse deletes all files when I cancel // a project creation (symlink in project location path) for (final IFileStore originalFileStore : this.orginalFolders) { try { final File localFile = originalFileStore.toLocalFile(EFS.NONE, null); if (localFile != null) { final File canonicalFile = localFile.getCanonicalFile(); final IFileStore canonicalFileStore = originalFileStore.getFileSystem().fromLocalFile(canonicalFile); if (!originalFileStore.equals(canonicalFileStore)) { foldersToKeep.add(canonicalFileStore); } } } catch (IOException e) { // } catch (CoreException e) { // } } try { final IFileStore[] children = EFS.getStore(projectLocation).childStores(EFS.NONE, null); for (int i = 0; i < children.length; i++) { final IFileStore child = children[i]; final IFileInfo info = child.fetchInfo(); if (info.isDirectory() && info.exists() && !foldersToKeep.contains(child)) { child.delete(EFS.NONE, null); this.orginalFolders.remove(child); } } for (Iterator<IFileStore> iterator = this.orginalFolders.iterator(); iterator.hasNext();) { final IFileStore deleted = iterator.next(); deleted.mkdir(EFS.NONE, null); } } catch (CoreException e) { SARLEclipsePlugin.getDefault().log(e); } } private void rememberExistingFiles(URI projectLocation) throws CoreException { this.dotProjectBackup = null; this.dotClasspathBackup = null; final IFileStore file = EFS.getStore(projectLocation); if (file.fetchInfo().exists()) { final IFileStore projectFile = file.getChild(FILENAME_PROJECT); if (projectFile.fetchInfo().exists()) { this.dotProjectBackup = createBackup(projectFile, "project-desc"); //$NON-NLS-1$ } final IFileStore classpathFile = file.getChild(FILENAME_CLASSPATH); if (classpathFile.fetchInfo().exists()) { this.dotClasspathBackup = createBackup(classpathFile, "classpath-desc"); //$NON-NLS-1$ } } } private void restoreExistingFiles(URI projectLocation, IProgressMonitor monitor) throws CoreException { final SubMonitor subMonitor = SubMonitor.convert(monitor, 4); final int ticks = ((this.dotProjectBackup != null ? 1 : 0) + (this.dotClasspathBackup != null ? 1 : 0)) * 2; monitor.beginTask("", ticks); //$NON-NLS-1$ try { final IFileStore projectFile = EFS.getStore(projectLocation).getChild(FILENAME_PROJECT); projectFile.delete(EFS.NONE, subMonitor.newChild(1)); if (this.dotProjectBackup != null) { copyFile(this.dotProjectBackup, projectFile, subMonitor.newChild(1)); } } catch (IOException e) { final IStatus status = new Status( IStatus.ERROR, JavaUI.ID_PLUGIN, IStatus.ERROR, NewWizardMessages.NewJavaProjectWizardPageTwo_problem_restore_project, e); throw new CoreException(status); } try { final IFileStore classpathFile = EFS.getStore(projectLocation).getChild(FILENAME_CLASSPATH); classpathFile.delete(EFS.NONE, subMonitor.newChild(1)); if (this.dotClasspathBackup != null) { copyFile(this.dotClasspathBackup, classpathFile, subMonitor.newChild(1)); } } catch (IOException e) { final IStatus status = new Status( IStatus.ERROR, JavaUI.ID_PLUGIN, IStatus.ERROR, NewWizardMessages.NewJavaProjectWizardPageTwo_problem_restore_classpath, e); throw new CoreException(status); } subMonitor.done(); } private static File createBackup(IFileStore source, String name) throws CoreException { try { final File bak = File.createTempFile("eclipse-" + name, ".bak"); //$NON-NLS-1$//$NON-NLS-2$ copyFile(source, bak); return bak; } catch (IOException e) { final IStatus status = new Status( IStatus.ERROR, JavaUI.ID_PLUGIN, IStatus.ERROR, MessageFormat.format( NewWizardMessages.NewJavaProjectWizardPageTwo_problem_backup, name), e); throw new CoreException(status); } } private static void copyFile(IFileStore source, File target) throws IOException, CoreException { try (InputStream is = source.openInputStream(EFS.NONE, null)) { try (FileOutputStream os = new FileOutputStream(target)) { unsecureCopyFile(is, os); } } } private static void copyFile(File source, IFileStore target, IProgressMonitor monitor) throws IOException, CoreException { try (FileInputStream is = new FileInputStream(source)) { try (OutputStream os = target.openOutputStream(EFS.NONE, monitor)) { unsecureCopyFile(is, os); } } } private static void unsecureCopyFile(InputStream is, OutputStream os) throws IOException { final byte[] buffer = new byte[FILE_COPY_BLOCK_SIZE]; int bytesRead = is.read(buffer); while (bytesRead > 0) { os.write(buffer, 0, bytesRead); bytesRead = is.read(buffer); } os.flush(); } /** * Called from the wizard on finish. * * @param monitor the progress monitor * @throws CoreException thrown when the project creation or configuration failed * @throws InterruptedException thrown when the user cancelled the project creation */ public void performFinish(IProgressMonitor monitor) throws CoreException, InterruptedException { final SubMonitor subMonitor = SubMonitor.convert(monitor, 4); try { monitor.beginTask(NewWizardMessages.NewJavaProjectWizardPageTwo_operation_create, 3); if (this.currProject == null) { updateProject(subMonitor.newChild(1)); } final String newProjectCompliance = this.keepContent ? null : this.firstPage.getCompilerCompliance(); configureJavaProject(newProjectCompliance, subMonitor.newChild(1)); } catch (Throwable e) { if (this.currProject != null) { removeProvisonalProject(); } throw e; } finally { subMonitor.done(); this.currProject = null; if (this.isAutobuild != null) { CoreUtility.setAutoBuilding(this.isAutobuild.booleanValue()); this.isAutobuild = null; } } } /** * Creates the provisional project on which the wizard is working on. * The provisional project is typically created when the page is entered * the first time. The early project creation is required to configure * linked folders. * * @return the provisional project */ protected IProject createProvisonalProject() { final IStatus status = changeToNewProject(); if (status != null) { updateStatus(status); if (!status.isOK()) { ErrorDialog.openError( getShell(), NewWizardMessages.NewJavaProjectWizardPageTwo_error_title, null, status); } } return this.currProject; } /** * Removes the provisional project. The provisional project is typically * removed when the user cancels the wizard or goes back to the first page. */ protected void removeProvisonalProject() { if (!this.currProject.exists()) { this.currProject = null; return; } final IRunnableWithProgress op = new IRunnableWithProgress() { @SuppressWarnings("synthetic-access") @Override public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { doRemoveProject(monitor); } }; try { getContainer().run(true, true, new WorkspaceModifyDelegatingOperation(op)); } catch (InvocationTargetException e) { final String title = NewWizardMessages.NewJavaProjectWizardPageTwo_error_remove_title; final String message = NewWizardMessages.NewJavaProjectWizardPageTwo_error_remove_message; ExceptionHandler.handle(e, getShell(), title, message); } catch (InterruptedException e) { // cancel pressed } } private void doRemoveProject(IProgressMonitor monitor) throws InvocationTargetException { // Test if the project is inside the workspace final boolean noProgressMonitor = this.currProjectLocation == null; final SubMonitor subMonitor = SubMonitor.convert(noProgressMonitor ? null : monitor, 3); subMonitor.beginTask(NewWizardMessages.NewJavaProjectWizardPageTwo_operation_remove, 3); try { try { final URI projLoc = this.currProject.getLocationURI(); final boolean removeContent = !this.keepContent && this.currProject.isSynchronized(IResource.DEPTH_INFINITE); if (!removeContent) { restoreExistingFolders(projLoc); } this.currProject.delete(removeContent, false, subMonitor.newChild(2)); restoreExistingFiles(projLoc, subMonitor.newChild(1)); } finally { // fIsAutobuild must be set CoreUtility.setAutoBuilding(this.isAutobuild.booleanValue()); this.isAutobuild = null; } } catch (CoreException e) { throw new InvocationTargetException(e); } finally { subMonitor.done(); this.currProject = null; this.keepContent = false; } } /** * Called from the wizard on cancel. */ public void performCancel() { if (this.currProject != null) { removeProvisonalProject(); } } /** Task for updating the project. * * @author $Author: sgalland$ * @version $FullVersion$ * @mavengroupid $GroupId$ * @mavenartifactid $ArtifactId$ */ private class UpdateRunnable implements IRunnableWithProgress { private IStatus infoStatus = Status.OK_STATUS; UpdateRunnable() { // } public IStatus getInfoStatus() { return this.infoStatus; } @SuppressWarnings("synthetic-access") @Override public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { try { if (BuildSettingWizardPage.this.isAutobuild == null) { BuildSettingWizardPage.this.isAutobuild = Boolean.valueOf(CoreUtility.setAutoBuilding(false)); } this.infoStatus = updateProject(monitor); } catch (CoreException e) { throw new InvocationTargetException(e); } catch (OperationCanceledException e) { throw new InterruptedException(); } finally { monitor.done(); } } } }