diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d42f5c1f..f1ffe8d0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - [#86](https://github.com/gemoc/ale-lang/issues/86) The XMI model and the ALE entry point to execute can be set from the *Launch Configuration Tab* - [#92](https://github.com/gemoc/ale-lang/issues/92) The editor autocompletes attributes and methods of `self` - [#94](https://github.com/gemoc/ale-lang/issues/94) The editor automatically switches to dark colors when Eclipse IDE is in dark theme +- [#98](https://github.com/gemoc/ale-lang/issues/98) The _New ALE Project_ wizard can be used to create ALE projects ### Changed - [#93](https://github.com/gemoc/ale-lang/issues/93) More tokens are available to tailor editor's syntax coloring diff --git a/plugins/org.eclipse.emf.ecoretools.ale.ide.ui/META-INF/MANIFEST.MF b/plugins/org.eclipse.emf.ecoretools.ale.ide.ui/META-INF/MANIFEST.MF index f4002f316..0eaaa5d45 100644 --- a/plugins/org.eclipse.emf.ecoretools.ale.ide.ui/META-INF/MANIFEST.MF +++ b/plugins/org.eclipse.emf.ecoretools.ale.ide.ui/META-INF/MANIFEST.MF @@ -6,7 +6,9 @@ Bundle-Version: 1.1.0.qualifier Bundle-ActivationPolicy: lazy Bundle-Activator: org.eclipse.emf.ecoretools.ale.ide.ui.Activator Export-Package: org.eclipse.emf.ecoretools.ale.ide.ui.launchconfig, - org.eclipse.emf.ecoretools.ale.ide.ui.services + org.eclipse.emf.ecoretools.ale.ide.ui.project;x-friends:="org.eclipse.emf.ecoretools.ale.ide.ui.tests", + org.eclipse.emf.ecoretools.ale.ide.ui.services, + org.eclipse.emf.ecoretools.ale.ide.ui.wizards Require-Bundle: org.eclipse.emf.ecoretools.ale.ide, org.eclipse.ui.workbench, org.eclipse.ui, @@ -21,7 +23,11 @@ Require-Bundle: org.eclipse.emf.ecoretools.ale.ide, org.eclipse.sirius.common, org.eclipse.sirius.common.acceleo.aql, org.eclipse.emf.ecoretools.ale.xtext, - org.eclipse.emf.ecoretools.design + org.eclipse.emf.ecoretools.design, + org.eclipse.jdt.core;resolution:=optional, + org.eclipse.jdt.launching;resolution:=optional, + org.eclipse.sirius.ext.base, + org.eclipse.sirius.ui Bundle-Vendor: Inria/Obeo Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Automatic-Module-Name: org.eclipse.emf.ecoretools.ale.ide.ui diff --git a/plugins/org.eclipse.emf.ecoretools.ale.ide.ui/plugin.xml b/plugins/org.eclipse.emf.ecoretools.ale.ide.ui/plugin.xml index dba5753c9..41f5db7c6 100644 --- a/plugins/org.eclipse.emf.ecoretools.ale.ide.ui/plugin.xml +++ b/plugins/org.eclipse.emf.ecoretools.ale.ide.ui/plugin.xml @@ -102,4 +102,19 @@ id="org.eclipse.emf.ecoretools.ale.ide.ui.launcher.image"> + + + + + + diff --git a/plugins/org.eclipse.emf.ecoretools.ale.ide.ui/src/org/eclipse/emf/ecoretools/ale/ide/ui/launchconfig/UiUtils.java b/plugins/org.eclipse.emf.ecoretools.ale.ide.ui/src/org/eclipse/emf/ecoretools/ale/ide/ui/launchconfig/UiUtils.java index cd02270ca..9d51d1b06 100644 --- a/plugins/org.eclipse.emf.ecoretools.ale.ide.ui/src/org/eclipse/emf/ecoretools/ale/ide/ui/launchconfig/UiUtils.java +++ b/plugins/org.eclipse.emf.ecoretools.ale.ide.ui/src/org/eclipse/emf/ecoretools/ale/ide/ui/launchconfig/UiUtils.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019 Inria and Obeo. + * Copyright (c) 2019-2020 Inria and Obeo. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -18,7 +18,7 @@ /** * Utility methods for dealing with UI. */ -final class UiUtils { +public final class UiUtils { private UiUtils() { // utility class should not be instantiated diff --git a/plugins/org.eclipse.emf.ecoretools.ale.ide.ui/src/org/eclipse/emf/ecoretools/ale/ide/ui/project/WorkspaceAleProject.java b/plugins/org.eclipse.emf.ecoretools.ale.ide.ui/src/org/eclipse/emf/ecoretools/ale/ide/ui/project/WorkspaceAleProject.java new file mode 100644 index 000000000..766ca653c --- /dev/null +++ b/plugins/org.eclipse.emf.ecoretools.ale.ide.ui/src/org/eclipse/emf/ecoretools/ale/ide/ui/project/WorkspaceAleProject.java @@ -0,0 +1,455 @@ +/******************************************************************************* + * Copyright (c) 2020 Inria and Obeo. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Inria - initial API and implementation + *******************************************************************************/ +package org.eclipse.emf.ecoretools.ale.ide.ui.project; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.emptySet; +import static java.util.Objects.requireNonNull; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; + +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.IWorkspace; +import org.eclipse.core.resources.IWorkspaceRunnable; +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.Status; +import org.eclipse.core.runtime.SubMonitor; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.EPackage; +import org.eclipse.emf.ecore.EcoreFactory; +import org.eclipse.emf.ecore.EcorePackage; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl; +import org.eclipse.emf.ecoretools.ale.core.parser.Dsl; +import org.eclipse.emf.ecoretools.ale.ide.project.AleProject; +import org.eclipse.emf.ecoretools.ale.ide.ui.Activator; +import org.eclipse.emf.transaction.RecordingCommand; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IPackageFragmentRoot; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.launching.JavaRuntime; +import org.eclipse.sirius.business.api.componentization.ViewpointRegistry; +import org.eclipse.sirius.business.api.dialect.DialectManager; +import org.eclipse.sirius.business.api.session.Session; +import org.eclipse.sirius.business.api.session.SessionManager; +import org.eclipse.sirius.business.internal.session.SessionTransientAttachment; +import org.eclipse.sirius.business.internal.session.danalysis.SaveSessionJob; +import org.eclipse.sirius.ui.business.api.viewpoint.ViewpointSelectionCallback; +import org.eclipse.sirius.ui.business.internal.commands.ChangeViewpointSelectionCommand; +import org.eclipse.sirius.viewpoint.DView; +import org.eclipse.sirius.viewpoint.description.RepresentationDescription; +import org.eclipse.sirius.viewpoint.description.Viewpoint; +import org.eclipse.xtext.ui.XtextProjectHelper; + +/** + * An {@link AleProject ALE project} located in the workspace. + */ +public class WorkspaceAleProject implements AleProject { + /** + * The workspace containing the project. + */ + private final IWorkspace workspace; + /** + * The description of the project. + */ + private final Description description; + + /** + * Instantiates a new ALE project located in the workspace. + * + * @param workspace + * The workspace containing the project. + */ + public WorkspaceAleProject(IWorkspace workspace, Description description) { + this.workspace = requireNonNull(workspace, "workspace"); + this.description = requireNonNull(description, "description"); + } + + @Override + public IProject create(String name, IPath path, IProgressMonitor monitor) throws CoreException { + IWorkspaceRunnable createProject = projectMonitor -> { + SubMonitor subMonitor = SubMonitor.convert(projectMonitor, "Creating ALE project", 60); + try { + IProject project = createProject(name, path, subMonitor.split(1)); + EPackage epackage = getOrCreateEcoreModel(project, subMonitor.split(1)); + IPath aleFilePath = createAleSourceFile(project, epackage, subMonitor.split(1)); + + createDslFile(project, asList(URI.createPlatformResourceURI(epackage.eResource().getURI().toString(), true).toString()), asList(URI.createPlatformResourceURI(aleFilePath.toPortableString(), true).toString()), subMonitor.split(1)); + createRepresentation(project, epackage, subMonitor.split(1)); + addJavaNature(project, subMonitor.split(1)); + + project.refreshLocal(IResource.DEPTH_INFINITE, subMonitor.split(1)); + } + catch (IOException e) { + throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e)); + } + finally { + monitor.done(); + } + }; + workspace.run(createProject, monitor); + return workspace.getRoot().getProject(name); + } + + /** + * Creates a new IProject and persists it on disk. + */ + private IProject createProject(String name, IPath path, IProgressMonitor monitor) throws CoreException { + SubMonitor subMonitor = SubMonitor.convert(monitor, "Creating the project...", 3); + IProject project = workspace.getRoot().getProject(name); + + if (project.exists()) { + throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "project " + name + " already exists in the workspace")); + } + IProjectDescription desc = workspace.newProjectDescription(name); + desc.setLocation(path); + desc.setNatureIds(new String[] { XtextProjectHelper.NATURE_ID }); + + subMonitor.subTask("Creating the project " + name + "..."); + project.create(desc, subMonitor.split(1)); + + subMonitor.subTask("Opening the project " + name + "..."); + project.open(subMonitor.split(1)); + + subMonitor.subTask("Creating the model folder..."); + project.getFolder("model").create(false, true, subMonitor.split(1)); + + return project; + } + + private EPackage getOrCreateEcoreModel(IProject project, IProgressMonitor monitor) throws IOException { + SubMonitor subMonitor = SubMonitor.convert(monitor, "Creating the Ecore model...", 1); + + if (!description.useAnExistingEcoreModel) { + return createEcoreModel(project, description.ecorePackageName, subMonitor.split(1)); + } + else { + return getEcoreModel(description.ecoreModelFilePath); + } + } + + private static EPackage createEcoreModel(IProject project, String packageName, IProgressMonitor monitor) throws IOException { + EPackage pkg = EcoreFactory.eINSTANCE.createEPackage(); + pkg.setName(packageName); + pkg.setNsPrefix(packageName); + pkg.setNsURI("http://" + packageName); + + IFile ecoreFile = project.getFolder("model").getFile(project.getName() + ".ecore"); + ResourceSet resources = new ResourceSetImpl(); + Resource resource = resources.createResource(URI.createFileURI(ecoreFile.getFullPath().toOSString())); + + resource.getContents().add(pkg); + resource.save(emptyMap()); + + return pkg; + } + + private IPath createAleSourceFile(IProject project, EPackage epackage, IProgressMonitor monitor) throws CoreException { + SubMonitor subMonitor = SubMonitor.convert(monitor, "Creating ALE source file...", 1); + + String initialContent = "behavior " + epackage.getName() + ";"; + InputStream readableInitialContent = new ByteArrayInputStream(initialContent.getBytes()); + + IFile src = project.getFolder("model").getFile(project.getName() + ".ale"); + src.create(readableInitialContent, false, subMonitor.split(1)); + + return src.getFullPath(); + } + + private static EPackage getEcoreModel(IPath path) { + ResourceSet resources = new ResourceSetImpl(); + Resource resource = resources.getResource(URI.createFileURI(path.toOSString()), true); + return (EPackage) resource.getContents().get(0); + } + + private static void createDslFile(IProject project, List ecoreModels, List aleSourceFiles, IProgressMonitor monitor) { + SubMonitor subMonitor = SubMonitor.convert(monitor, "Creating .dsl file...", 1); + + Dsl dsl = new Dsl(ecoreModels, aleSourceFiles); + dsl.setSourceFile(project.getFolder("model").getFile(project.getName() + ".dsl").getLocation().toOSString()); + dsl.save(); + + subMonitor.worked(1); + } + + private void createRepresentation(IProject project, EObject model, IProgressMonitor monitor) throws CoreException { + if (! description.createRepresentation) { + return; + } + SubMonitor subMonitor = SubMonitor.convert(monitor, "Creating Sirius representation...", 4); + + // Create a new Sirius session to handle the representation + + URI sessionResourceURI = URI.createPlatformResourceURI(project.getFolder("model").getFile(project.getName() + ".aird").getFullPath().toOSString(), true); + Session session = SessionManager.INSTANCE.getSession(sessionResourceURI, new NullProgressMonitor()); + session.open(new NullProgressMonitor()); + + // Allow to create a representation for the model thereafter + + session.getTransactionalEditingDomain().getCommandStack() + .execute(new RecordingCommand(session.getTransactionalEditingDomain()) { + @Override + protected void doExecute() { + session.addSemanticResource(URI.createURI(EcorePackage.eNS_URI), subMonitor); + + // Actually Sirius seems to automatically add model's EResource, so the following is useless +// session.addSemanticResource(URI.createPlatformResourceURI(model.eResource().getURI().toString(), true), subMonitor.split(1)); + } + }); + + // Add the 'Design' viewpoint to the representation + + Optional maybeDesignViewpoint = findDesignViewpoint(ViewpointRegistry.getInstance()); + if (! maybeDesignViewpoint.isPresent()) { + Activator.error("Cannot create Sirius representation in project " + project.getName() + ": 'Design' viewpoint not found", null); + return; + } + Viewpoint designViewpoint = maybeDesignViewpoint.get(); + ChangeViewpointSelectionCommand cc = new ChangeViewpointSelectionCommand( + session, + new ViewpointSelectionCallback(), + new HashSet<>(asList(designViewpoint)), + emptySet(), + subMonitor.split(1) + ); + session.getTransactionalEditingDomain().getCommandStack().execute(cc); + session.getTransactionalEditingDomain().getCommandStack() + .execute(new RecordingCommand(session.getTransactionalEditingDomain()) { + @Override + protected void doExecute() { + session.createView(maybeDesignViewpoint.get(), asList(model), true, subMonitor.split(1)); + } + }); + + // Okay, that's tricky. + // + // For some reason there are several instances of the 'Design' viewpoints. + // However, when we use the one returned by the ViewpointRegistry Sirius doesn't create any representation + // for our model because it is doesn't match the instance it uses internally (the `equals` method should + // have been overridden). + // + // As a result, we retrieve the correct instance of the viewpoint below so that Sirius actually + // creates the diagram representation. + // + // FIXME here: I found out by spending lot of time in the debugger, this solution is awful and will likely + // not work with other versions of Sirius (hopefully it won't even be needed). I may missed something + // so if a better solution exist please improve! + + Collection selectedViews = session.getSelectedViews(); + if (! selectedViews.isEmpty()) { + DView view = selectedViews.iterator().next(); + designViewpoint = view.getViewpoint(); + } + + // Finally create a representation of our Ecore model from the 'Entities' template + + Optional representationDescription = + DialectManager.INSTANCE.getAvailableRepresentationDescriptions(asList(designViewpoint), model) + .stream() + .filter(desc -> desc.getName().equals("Entities")) + .findAny(); + + if (! representationDescription.isPresent()) { + Activator.error("Cannot create Sirius representation in project " + project.getName() + ": 'Entities' representation not found", null); + return; + } + // Otherwise Sirius is not able to find the session from the model + model.eAdapters().add(new SessionTransientAttachment(session)); + + session.getTransactionalEditingDomain().getCommandStack() + .execute(new RecordingCommand(session.getTransactionalEditingDomain()) { + @Override + protected void doExecute() { + DialectManager.INSTANCE.createRepresentation( + project.getName() + " class diagram", + model, + representationDescription.get(), + session, + subMonitor.split(1) + ); + } + }); + + // Another trick to prevent issues when creating a representation for existing Ecore models + // + // Issue description: + // ----------------- + // + // When one attempts to initialize the representation with the EPackage content Sirius throws an error. + // + // Steps to reproduce: + // ------------------ + // + // 1. Create a new ALE project relying on an existing Ecore model + // 2. Open the .aird representation file + // 3. Open the model's representation ('? class diagram') + // 4. Double-click on 'Double-click to initialize ...' + // + // Explanation: + // ----------- + // + // According to [1], when Sirius adds the external Ecore model as a semantic resource of the session it + // actually adds a _copy_ of the resource. It then mistakenly believes that Ecore classes are not part + // of the session's semantic resources and throws a exception. + // + // [1] https://www.eclipse.org/forums/index.php?t=msg&th=1085757&goto=1760173&#msg_1760173 + // + // Workaround: + // ---------- + // + // It looks like closing the session clears some internal state and make everything work as expected. + // + // BUT. + // + // We cannot close the session whenever we want. When we modify the session the changes are not synchronously + // persisted in the .aird file. Instead, a SaveSessionJob is schedule in background (when? I don't really know) + // and we have to wait for it to end before closing the session. + // + // BUT. + // + // For some reason: + // - calling Job.getJobManager().join(SaveSessionJob.FAMILY) while a SaveSessionJob is actually running + // ends up in an infinite loop, + // - using an infinite loop (while or for) and checking periodically whether the job has ended does not + // work either (the job runs forever). + // + // In the end, the only reliable solution I've found is to schedule another Job responsible of closing + // the session and that reschedules itself until the SaveSessionJob is done. + + new Job("Close Sirius session of project " + project.getName()) { + + @Override + protected IStatus run(IProgressMonitor monitor) { + boolean sessionIsSaved = Job.getJobManager().find(SaveSessionJob.FAMILY).length == 0; + + if (! sessionIsSaved) { + // The session has not been saved yet, we cannot close + // Reschedule the job so we can try later + schedule(5); + } + else { + session.close(new NullProgressMonitor()); + } + return Status.OK_STATUS; + } + + }.schedule(); + } + + private static Optional findDesignViewpoint(ViewpointRegistry registry) { + return registry.getViewpoints().stream() + .filter(viewpoint -> registry.isFromPlugin(viewpoint)) + .filter(viewpoint -> "Design".equals(viewpoint.getName())) + .findAny(); + } + + private void addJavaNature(IProject project, IProgressMonitor monitor) throws CoreException { + if (! description.activateJava) { + return; + } + SubMonitor subMonitor = SubMonitor.convert(monitor, 6); + try { + subMonitor.setTaskName("Adding Java nature..."); + + IProjectDescription desc = project.getDescription(); + String[] prevNatures = desc.getNatureIds(); + String[] newNatures = new String[prevNatures.length + 1]; + System.arraycopy(prevNatures, 0, newNatures, 0, prevNatures.length); + newNatures[prevNatures.length] = JavaCore.NATURE_ID; + desc.setNatureIds(newNatures); + project.setDescription(desc, subMonitor.split(1)); + + subMonitor.setTaskName("Configuring classpath..."); + + IJavaProject javaProject = JavaCore.create(project); + + // Set bin/ as output location + + IFolder binFolder = project.getFolder("bin"); + binFolder.create(false, true, subMonitor.split(1)); + javaProject.setOutputLocation(binFolder.getFullPath(), subMonitor.split(1)); + + // Add JRE to classpath + + List entries = new ArrayList<>(); + entries.add(JavaRuntime.getDefaultJREContainerEntry()); + javaProject.setRawClasspath(entries.toArray(new IClasspathEntry[entries.size()]), subMonitor.split(1)); + + // Set src/ as source folder + + IFolder sourceFolder = project.getFolder("src"); + sourceFolder.create(false, true, subMonitor.split(1)); + + IPackageFragmentRoot root = javaProject.getPackageFragmentRoot(sourceFolder); + IClasspathEntry[] oldEntries = javaProject.getRawClasspath(); + IClasspathEntry[] newEntries = new IClasspathEntry[oldEntries.length + 1]; + System.arraycopy(oldEntries, 0, newEntries, 0, oldEntries.length); + newEntries[oldEntries.length] = JavaCore.newSourceEntry(root.getPath()); + javaProject.setRawClasspath(newEntries, subMonitor.split(1)); + } + catch (NoClassDefFoundError e) { + Activator.error("Unable to set Java nature to project '" + project.getName() + "': org.eclipse.jdt.core or org.eclipse.jdt.launching is not available", e); + } + } + + /** + * The properties of an ALE project. + */ + public static final class Description { + public final boolean useAnExistingEcoreModel; + public final IPath ecoreModelFilePath; + public final String ecorePackageName; + public final boolean createRepresentation; + public final boolean activateJava; + + /** + * @param useAnExistingEcoreModel + * Whether the user wants to reuse an existing Ecore model + * @param ecoreModelFilePath + * The path to the existing Ecore model (can be null if the user does not want to use one) + * @param ecorePackageName + * The name of the EPackage to create (can be null if the user does not want to create a new Ecore model) + * @param createRepresentation + * Whether the user wants a Sirius representation to be created for the Ecore model + * @param activateJava + * Whether the user wants to create Java services + */ + public Description(boolean useAnExistingEcoreModel, IPath ecoreModelFilePath, String ecorePackageName, boolean createRepresentation, boolean activateJava) { + this.useAnExistingEcoreModel = useAnExistingEcoreModel; + this.ecoreModelFilePath = ecoreModelFilePath; + this.ecorePackageName = ecorePackageName; + this.createRepresentation = createRepresentation; + this.activateJava = activateJava; + } + + } + +} diff --git a/plugins/org.eclipse.emf.ecoretools.ale.ide.ui/src/org/eclipse/emf/ecoretools/ale/ide/ui/wizards/NewAleProjectConfigurationWizardPage.java b/plugins/org.eclipse.emf.ecoretools.ale.ide.ui/src/org/eclipse/emf/ecoretools/ale/ide/ui/wizards/NewAleProjectConfigurationWizardPage.java new file mode 100644 index 000000000..c0ff01217 --- /dev/null +++ b/plugins/org.eclipse.emf.ecoretools.ale.ide.ui/src/org/eclipse/emf/ecoretools/ale/ide/ui/wizards/NewAleProjectConfigurationWizardPage.java @@ -0,0 +1,347 @@ +/******************************************************************************* + * Copyright (c) 2020 Inria and Obeo. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Inria - initial API and implementation + *******************************************************************************/ +package org.eclipse.emf.ecoretools.ale.ide.ui.wizards; + +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.Path; +import org.eclipse.jface.layout.LayoutConstants; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.sirius.business.api.dialect.DialectManager; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.dialogs.FilteredResourcesSelectionDialog; + +/** + * Wizard page used to configure an ALE project. + */ +public class NewAleProjectConfigurationWizardPage extends WizardPage { + /** + * Key used by SWTBot to reliably identify widgets. + */ + private static final String SWTBOT_ID = "org.eclipse.swtbot.widget.key"; + /** + * Id used by SWTBot to identify the "create new Ecore model" radio button. + */ + public static final String CREATE_ECORE_MODEL_BUTTON_ID = "createEcoreModelRadioButton"; + /** + * Id used by SWTBot to identify the "Package name" text field. + */ + public static final String ECORE_PACKAGE_NAME_TEXT_ID = "ecorePackageName"; + + /** + * Whether the "Create a new Ecore model" option should be activated by default. + */ + private static final boolean CREATE_NEW_ECORE_MODEL_BY_DEFAULT = true; + /** + * Whether the "Create a Sirius representation" option should be activated by default. + */ + private static final boolean CREATE_SIRIUS_REPRESENTATION_BY_DEFAULT = true; + /** + * Whether the "This project will expose Java services" option should be activated by default. + */ + private static final boolean EXPOSE_JAVA_SERVICES_BY_DEFAULT = false; + + /** + * Whether the user wants to use an existing Ecore model instead of creating a new one. + */ + private Button useAnExistingEcoreModelFileRadioButton; + /** + * URI to the Ecore model chosen by the user (if he wants to reuse an existing one). + */ + private Text selectedEcoreModelText; + /** + * Name of the new Ecore model (if the user wants to create a new one). + */ + private Text ecorePackageNameText; + /** + * Whether the user wants a Sirius representation to be created for the Ecore model. + */ + private Button createRepresentationCheckBox; + /** + * Whether the user plans to expose Java services. + */ + private Button useJavaServicesCheckBox; + /** + * Whether this page has been visible at least once. + */ + private boolean hasBeenSeen = false; + + /** + * Instantiates a new wizard page aimed at configuring an ALE project + */ + protected NewAleProjectConfigurationWizardPage() { + super("New ALE Project"); + setTitle("New ALE Project"); + setDescription("Configure the project."); + } + + /** + * @return whether the user wants to reuse an existing Ecore model + */ + public boolean useExistingEcoreModel() { + if (useAnExistingEcoreModelFileRadioButton == null) { + return false; + } + return useAnExistingEcoreModelFileRadioButton.getSelection(); + } + + /** + * @return the path to the Ecore model to reuse (meaningful when the user wants + * to reuse an existing one) + */ + public IPath getEcoreModelFile() { + if (selectedEcoreModelText == null) { + return Path.EMPTY; + } + return new Path(selectedEcoreModelText.getText()); + } + + /** + * @return the name of the new EPackage to create (meaningful when the user wants + * to create a new Ecore model) + */ + public String getEcorePackageName() { + if (ecorePackageNameText == null) { + return ""; + } + return ecorePackageNameText.getText().trim(); + } + + /** + * @return whether the user wants a Sirius representation to be created for the Ecore model + */ + public boolean createRepresentation() { + if (createRepresentationCheckBox == null) { + return false; + } + return createRepresentationCheckBox.getSelection(); + } + + /** + * @return whether the user wants to expose Java services in the new project + */ + public boolean activateJava() { + if (useJavaServicesCheckBox == null) { + return false; + } + return useJavaServicesCheckBox.getSelection(); + } + + public void setDefaultEcorePackage(String projectName) { + ecorePackageNameText.setText(projectName); + } + + @Override + public void createControl(Composite parent) { + Composite container = new Composite(parent, SWT.NONE); + GridLayout layout = new GridLayout(); + container.setLayout(layout); + container.setLayoutData(new GridData(GridData.FILL_BOTH)); + + createEcoreModelGroup(container); + createRepresentationGroup(container); + createServicesGroup(container); + + setControl(container); + } + + @Override + public void setVisible(boolean isVisible) { + super.setVisible(isVisible); + if (isVisible) { + hasBeenSeen = true; + ecorePackageNameText.setFocus(); + setPageComplete(isValid()); + } + } + + /** + * Creates widgets allowing the user to select the king of Ecore model he wants. + */ + private void createEcoreModelGroup(Composite container) { + Group group = new Group(container, SWT.LEFT); + GridLayout layout = new GridLayout(); + layout.numColumns = 3; + group.setLayout(layout); + group.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); + group.setText("Ecore model"); + + // "(x) Create a new Ecore model" button + + Button createNewEcoreModelFileRadioButton = new Button(group, SWT.RADIO | SWT.LEFT); + createNewEcoreModelFileRadioButton.setText("Create a new Ecore model"); + createNewEcoreModelFileRadioButton.setToolTipText("A new Ecore model will be created in the project"); + createNewEcoreModelFileRadioButton.setSelection(CREATE_NEW_ECORE_MODEL_BY_DEFAULT); + createNewEcoreModelFileRadioButton.setData(SWTBOT_ID, CREATE_ECORE_MODEL_BUTTON_ID); + + // Span the button horizontally + GridData createNewEcoreModelFileLayoutData = new GridData(GridData.FILL_HORIZONTAL); + createNewEcoreModelFileLayoutData.horizontalSpan = layout.numColumns; + createNewEcoreModelFileRadioButton.setLayoutData(createNewEcoreModelFileLayoutData); + + // Ecore package name text field + + Label ecorePackageNameLabel = new Label(group, SWT.LEFT); + ecorePackageNameLabel.setText("Package name: "); + + // Span the text displaying the path horizontally + GridData ecorePackageNameLayoutData = new GridData(); + ecorePackageNameLayoutData.horizontalIndent = LayoutConstants.getIndent(); + ecorePackageNameLabel.setLayoutData(ecorePackageNameLayoutData); + + ecorePackageNameText = new Text(group, SWT.BORDER); + // Span the text horizontally + GridData ecorePackageNameTextLayoutData = new GridData(GridData.FILL_HORIZONTAL); + ecorePackageNameTextLayoutData.horizontalSpan = layout.numColumns - 1; + ecorePackageNameText.setLayoutData(ecorePackageNameTextLayoutData); + ecorePackageNameText.setData(SWTBOT_ID, ECORE_PACKAGE_NAME_TEXT_ID); + ecorePackageNameText.addListener(SWT.Modify, e -> setPageComplete(isValid())); + + // "( ) Use an existing Ecore model" button + + useAnExistingEcoreModelFileRadioButton = new Button(group, SWT.RADIO | SWT.LEFT); + useAnExistingEcoreModelFileRadioButton.setText("Use an existing Ecore model"); + useAnExistingEcoreModelFileRadioButton.setText("The project will be configured to reuse an existing Ecore model"); + useAnExistingEcoreModelFileRadioButton.setSelection(! CREATE_NEW_ECORE_MODEL_BY_DEFAULT); + + // Span the button horizontally + GridData useAnExistingEcoreModelFileLayoutData = new GridData(GridData.FILL_HORIZONTAL); + useAnExistingEcoreModelFileLayoutData.horizontalSpan = layout.numColumns; + useAnExistingEcoreModelFileRadioButton.setLayoutData(useAnExistingEcoreModelFileLayoutData); + + // "Browse Ecore models in workspace" button + // Clicking the button opens a dialog that fills 'selectedEcoreModelText' text field + + Button browseExistingEcoreButton = new Button(group, SWT.NONE); + browseExistingEcoreButton.setText("Select model..."); + browseExistingEcoreButton.setToolTipText("Opens a dialog to select an .ecore file within the workspace"); + browseExistingEcoreButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + Shell shell = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(); + FilteredResourcesSelectionDialog dialog = new FilteredResourcesSelectionDialog( + shell, + false, + ResourcesPlugin.getWorkspace().getRoot(), + IResource.FILE + ); + dialog.setTitle("Select an Ecore model"); + dialog.setInitialPattern("*.ecore"); + + dialog.open(); + Object[] selected = dialog.getResult(); + + if (selected != null && selected.length == 1 && selected[0] instanceof IResource) { + IResource selectedModel = (IResource) selected[0]; + selectedEcoreModelText.setText("" + selectedModel.getFullPath().toPortableString()); + } + } + }); + GridData browseExistingEcoreLayoutData = new GridData(); + browseExistingEcoreLayoutData.horizontalIndent = LayoutConstants.getIndent(); + browseExistingEcoreButton.setLayoutData(browseExistingEcoreLayoutData); + + // Text displaying the path to the Ecore model to reuse + + selectedEcoreModelText = new Text(group, SWT.READ_ONLY | SWT.BORDER); + selectedEcoreModelText.addListener(SWT.Modify, e -> { + // Make sure "Use an existing Ecore model" button is checked when the user chooses one + createNewEcoreModelFileRadioButton.setSelection(false); + useAnExistingEcoreModelFileRadioButton.setSelection(true); + setPageComplete(isValid()); + }); + + // Span the text displaying the path horizontally + GridData selectedEcoreModelTextLayoutData = new GridData(GridData.FILL_HORIZONTAL); + selectedEcoreModelText.setLayoutData(selectedEcoreModelTextLayoutData); + } + + /** + * Creates widgets allowing the user to customize Ecore model's Sirius representation. + */ + private void createRepresentationGroup(Composite container) { + Group group = new Group(container, SWT.LEFT); + GridLayout layout = new GridLayout(); + group.setLayout(layout); + group.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); + group.setText("Representation"); + + if (SiriusPluginsAreAvailable()) { + // "[x] Create a Sirius representation" button + + createRepresentationCheckBox = new Button(group,SWT.CHECK); + createRepresentationCheckBox.setText("Create a Sirius representation"); + createRepresentationCheckBox.setToolTipText("Create an .aird representation file linked to the Ecore model in the model/ folder"); + createRepresentationCheckBox.setSelection(CREATE_SIRIUS_REPRESENTATION_BY_DEFAULT); + } + else { + Text siriusIsNotAvailableText = new Text(group, SWT.READ_ONLY); + siriusIsNotAvailableText.setText("Warning: Sirius UI seems not to be installed, we cannot create any representation."); + } + } + + @SuppressWarnings({"squid:S00100", "squid:S3516", "squid:S2159"}) + private static boolean SiriusPluginsAreAvailable() { + try { + DialectManager.INSTANCE.equals(null); + return true; + } + catch (Exception | NoClassDefFoundError e) { + return false; + } + } + + /** + * Creates widgets allowing the user to tailor the services available to ALE. + */ + private void createServicesGroup(Composite container) { + Group group = new Group(container, SWT.LEFT); + GridLayout layout = new GridLayout(); + group.setLayout(layout); + group.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); + group.setText("Services"); + + // "[ ] Expose Java services" button + + useJavaServicesCheckBox = new Button(group, SWT.CHECK); + useJavaServicesCheckBox.setText("This project will expose Java services to ALE"); + useJavaServicesCheckBox.setToolTipText("Add the Java nature to the project"); + useJavaServicesCheckBox.setSelection(EXPOSE_JAVA_SERVICES_BY_DEFAULT); + } + + /** + * @return whether the user has filled in all required info + */ + public boolean isValid() { + if (! hasBeenSeen) { + return false; + } + if (useExistingEcoreModel()) { + return ! getEcoreModelFile().isEmpty(); + } + else { + return ! getEcorePackageName().isEmpty(); + } + } + +} diff --git a/plugins/org.eclipse.emf.ecoretools.ale.ide.ui/src/org/eclipse/emf/ecoretools/ale/ide/ui/wizards/NewAleProjectWizard.java b/plugins/org.eclipse.emf.ecoretools.ale.ide.ui/src/org/eclipse/emf/ecoretools/ale/ide/ui/wizards/NewAleProjectWizard.java new file mode 100644 index 000000000..73e8f6ba3 --- /dev/null +++ b/plugins/org.eclipse.emf.ecoretools.ale.ide.ui/src/org/eclipse/emf/ecoretools/ale/ide/ui/wizards/NewAleProjectWizard.java @@ -0,0 +1,178 @@ +/******************************************************************************* + * Copyright (c) 2020 Inria and Obeo. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Inria - initial API and implementation + *******************************************************************************/ +package org.eclipse.emf.ecoretools.ale.ide.ui.wizards; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.MultiStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.emf.ecoretools.ale.ide.project.AleProject; +import org.eclipse.emf.ecoretools.ale.ide.ui.Activator; +import org.eclipse.emf.ecoretools.ale.ide.ui.project.WorkspaceAleProject; +import org.eclipse.jface.dialogs.ErrorDialog; +import org.eclipse.jface.operation.IRunnableWithProgress; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.wizard.IWizardPage; +import org.eclipse.jface.wizard.Wizard; +import org.eclipse.ui.INewWizard; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.dialogs.WizardNewProjectCreationPage; + +/** + * A wizard used to create new ALE projects. + */ +public class NewAleProjectWizard extends Wizard implements INewWizard { + /** + * Used to choose the name and the location of the project + */ + private WizardNewProjectCreationPage newProjectPage; + /** + * Used to choose the Ecore model file to use + */ + private NewAleProjectConfigurationWizardPage aleConfigurationPage; + + /** + * Instantiates a wizard used to create new ALE projects. + */ + public NewAleProjectWizard() { + super(); + setNeedsProgressMonitor(true); + setWindowTitle("ALE Project Wizard"); + } + + @Override + public void init(IWorkbench workbench, IStructuredSelection selection) { + // nothing to initialize + } + + @Override + public void addPages() { + super.addPages(); + + newProjectPage = new WizardNewProjectCreationPage("ALE Project"); + newProjectPage.setTitle("New ALE Project"); + newProjectPage.setDescription("Create a new ALE project."); + + aleConfigurationPage = new NewAleProjectConfigurationWizardPage(); + + addPage(newProjectPage); + addPage(aleConfigurationPage); + } + + + @Override + public IWizardPage getNextPage(IWizardPage page) { + if (page == newProjectPage) { + aleConfigurationPage.setDefaultEcorePackage(newProjectPage.getProjectName()); + } + return super.getNextPage(page); + } + + @Override + public boolean canFinish() { + return ! newProjectPage.getProjectName().isEmpty() + && ! newProjectPage.getLocationPath().isEmpty() + && aleConfigurationPage.isValid(); + } + + @Override + public boolean performFinish() { + try { + // Gather all the data required to create the project. + // Must be retrieved out of the IRunnable to prevent invalid thread accesses + + String projectName = newProjectPage.getProjectName(); + boolean defaultProjectLocation = newProjectPage.useDefaults(); + IPath projectLocation; + + if (defaultProjectLocation) { + projectLocation = newProjectPage.getLocationPath().append(projectName); + } + else { + projectLocation = newProjectPage.getLocationPath(); + } + + boolean useAnExistingEcoreModel = aleConfigurationPage.useExistingEcoreModel(); + IPath ecoreModelFilePath = aleConfigurationPage.getEcoreModelFile(); + String ecorePackageName = aleConfigurationPage.getEcorePackageName(); + boolean createRepresentation = aleConfigurationPage.createRepresentation(); + boolean activateJava = aleConfigurationPage.activateJava(); + + // Create the new project + + getContainer().run(true, false, new IRunnableWithProgress() { + @Override + public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { + try { + WorkspaceAleProject.Description desc = + new WorkspaceAleProject.Description(useAnExistingEcoreModel, ecoreModelFilePath, ecorePackageName, createRepresentation, activateJava); + AleProject project = new WorkspaceAleProject(ResourcesPlugin.getWorkspace(), desc); + project.create(projectName, projectLocation, monitor); + } + catch (Exception e) { + throw new InvocationTargetException(e, "An error occurred while creating the new ALE project " + projectName); + } + } + }); + return true; + } + catch (InvocationTargetException | InterruptedException e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + Activator.error("An error occurred while creating the new ALE project", e); + showErrorDialog(e, "Project creation failed", e.getMessage()); + } + return false; + } + + /** + * Shows an error dialog displaying an exception's stack trace. + * + * @param e + * The exception to show. + * @param title + * The title of the dialog. + * @param message + * The message of the dialog. + */ + private void showErrorDialog(Exception e, String title, String message) { + // Compute a String containing the whole stack trace + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + + String trace = sw.toString(); + + List childStatuses = new ArrayList<>(); + + for (String line : trace.split(System.getProperty("line.separator"))) { + childStatuses.add(new Status(IStatus.ERROR, Activator.PLUGIN_ID, line)); + } + + // Create a status owning the String stack trace + MultiStatus status = new MultiStatus( + Activator.PLUGIN_ID, IStatus.ERROR, + childStatuses.toArray(new Status[] {}), + e.toString(), e + ); + ErrorDialog.openError(getShell(), title, message, status); + } + +} diff --git a/plugins/org.eclipse.emf.ecoretools.ale.ide/META-INF/MANIFEST.MF b/plugins/org.eclipse.emf.ecoretools.ale.ide/META-INF/MANIFEST.MF index 812a54a1e..c461d23f1 100644 --- a/plugins/org.eclipse.emf.ecoretools.ale.ide/META-INF/MANIFEST.MF +++ b/plugins/org.eclipse.emf.ecoretools.ale.ide/META-INF/MANIFEST.MF @@ -16,5 +16,6 @@ Import-Package: org.eclipse.emf.common.util, org.eclipse.emf.ecore.resource.impl Export-Package: org.eclipse.emf.ecoretools.ale.ide, org.eclipse.emf.ecoretools.ale.ide.listener, + org.eclipse.emf.ecoretools.ale.ide.project, org.eclipse.emf.ecoretools.ale.ide.resource Bundle-Vendor: Inria/Obeo diff --git a/plugins/org.eclipse.emf.ecoretools.ale.ide/src/org/eclipse/emf/ecoretools/ale/ide/project/AleProject.java b/plugins/org.eclipse.emf.ecoretools.ale.ide/src/org/eclipse/emf/ecoretools/ale/ide/project/AleProject.java new file mode 100644 index 000000000..bb41b5fbe --- /dev/null +++ b/plugins/org.eclipse.emf.ecoretools.ale.ide/src/org/eclipse/emf/ecoretools/ale/ide/project/AleProject.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (c) 2020 Inria and Obeo. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Inria - initial API and implementation + *******************************************************************************/ +package org.eclipse.emf.ecoretools.ale.ide.project; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; + +/** + *

An Eclipse IDE project aimed at storing ALE source files.

+ * + *

Such a project usually contains: + *

    + *
  • some *.ale source files, + *
  • a .dsl description file + *
+ * and sometimes: + *
    + *
  • an .ecore model, + *
  • an .aird representation file + *
+ *

+ */ +public interface AleProject { + + /** + * Creates the project. + * + * @param name + * The name of the project + * @param path + * The path of the project + * @param monitor + * The monitor used to follow the creation. Can be null. + * + * @return the new project + * + * @throws CoreException if the project cannot be created + */ + IProject create(String name, IPath path, IProgressMonitor monitor) throws CoreException; + +} diff --git a/tests/org.eclipse.emf.ecoretools.ale.ide.ui.tests/META-INF/MANIFEST.MF b/tests/org.eclipse.emf.ecoretools.ale.ide.ui.tests/META-INF/MANIFEST.MF index ac5d8dd03..ecaaf0c77 100644 --- a/tests/org.eclipse.emf.ecoretools.ale.ide.ui.tests/META-INF/MANIFEST.MF +++ b/tests/org.eclipse.emf.ecoretools.ale.ide.ui.tests/META-INF/MANIFEST.MF @@ -23,4 +23,6 @@ Require-Bundle: org.junit, org.eclipse.emf.ecoretools.ale.xtext.ide, org.eclipse.emf.ecoretools.ale.xtext.ui, org.eclipse.xtext.ui.testing, - org.eclipse.sirius.diagram + org.eclipse.sirius.diagram, + org.eclipse.emf.ecoretools.ale.ide, + org.eclipse.jdt.core diff --git a/tests/org.eclipse.emf.ecoretools.ale.ide.ui.tests/src/org/eclipse/emf/ecoretools/ale/ide/helloworld/tests/WorkspaceAleProjectTest.java b/tests/org.eclipse.emf.ecoretools.ale.ide.ui.tests/src/org/eclipse/emf/ecoretools/ale/ide/helloworld/tests/WorkspaceAleProjectTest.java new file mode 100644 index 000000000..37c6448e4 --- /dev/null +++ b/tests/org.eclipse.emf.ecoretools.ale.ide.ui.tests/src/org/eclipse/emf/ecoretools/ale/ide/helloworld/tests/WorkspaceAleProjectTest.java @@ -0,0 +1,123 @@ +/******************************************************************************* + * Copyright (c) 2020 Inria and Obeo. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Inria - initial API and implementation + *******************************************************************************/ +package org.eclipse.emf.ecoretools.ale.ide.helloworld.tests; + +import static java.util.Arrays.asList; +import static java.util.stream.Collectors.toList; +import static org.eclipse.emf.common.util.URI.createURI; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.List; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.IncrementalProjectBuilder; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.EPackage; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl; +import org.eclipse.emf.ecoretools.ale.core.parser.Dsl; +import org.eclipse.emf.ecoretools.ale.ide.ui.project.WorkspaceAleProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.xtext.ui.XtextProjectHelper; +import org.eclipse.xtext.ui.testing.util.IResourcesSetupUtil; +import org.junit.Ignore; +import org.junit.Test; + +import com.google.common.io.CharStreams; + +/** + * Test the behavior of a {@link WorkspaceAleProject}. + *

+ * Ignored because fails on CI. + */ +@Ignore +public class WorkspaceAleProjectTest { + + @Test + public void testCreateNewStandaloneProject() throws CoreException, IOException { + + // WHEN: a 'minifsm' ALE project is created + + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + WorkspaceAleProject.Description desc = new WorkspaceAleProject.Description( + false, null, + "fsm", + true, + true + ); + WorkspaceAleProject project = new WorkspaceAleProject(workspace, desc); + project.create("minifsm", workspace.getRoot().getProject("minifsm").getFullPath(), new NullProgressMonitor()); + + // AND: the workspace is built + + ResourcesPlugin.getWorkspace().build(IncrementalProjectBuilder.INCREMENTAL_BUILD, new NullProgressMonitor()); + IResourcesSetupUtil.reallyWaitForAutoBuild(); + + // THEN: a valid ALE project exists + + IProject minifsm = workspace.getRoot().getProject("minifsm"); + + assertTrue("The minifsm project should exist", minifsm.exists()); + assertNotNull("The minifsm project should have the Xtext nature " + XtextProjectHelper.NATURE_ID, + minifsm.getNature(XtextProjectHelper.NATURE_ID)); + assertNotNull("The minifsm project should have the Java nature " + XtextProjectHelper.NATURE_ID, + minifsm.getNature(JavaCore.NATURE_ID)); + + IFolder modelFolder = minifsm.getFolder("model"); + assertTrue("model/ should exist", modelFolder.exists()); + assertTrue("model/minifsm.ecore should exist", modelFolder.getFile("minifsm.ecore").exists()); + assertTrue("model/minifsm.dsl should exist", modelFolder.getFile("minifsm.dsl").exists()); + assertTrue("model/minifsm.aird should exist", modelFolder.getFile("minifsm.aird").exists()); + assertTrue("model/minifsm.ale should exist", modelFolder.getFile("minifsm.ale").exists()); + assertEquals("model/minifsm.ale file has the wrong content", "behavior fsm;", contentOf(modelFolder.getFile("minifsm.ale"))); + + EPackage fsm = loadPackage(modelFolder.getFile("minifsm.ecore")); + assertEquals("generated FSM EPackage has the wrong name", "fsm", fsm.getName()); + assertEquals("generated FSM EPackage has the wrong Ns URI", "http://fsm", fsm.getNsURI()); + assertEquals("generated FSM EPackage has the wrong Ns prefix", "fsm", fsm.getNsPrefix()); + + Dsl dsl = new Dsl(modelFolder.getFile("minifsm.dsl").getContents()); + assertEquals("model/minifsm.dsl does not have the right syntax", asList(createURI("platform:/resource/minifsm/model/minifsm.ecore", true)), toURIs(dsl.getAllSyntaxes())); + assertEquals("model/minifsm.dsl does not have the right semantics", asList(createURI("platform:/resource/minifsm/model/minifsm.ale", true)), toURIs(dsl.getAllSemantics())); + } + + private static List toURIs(List uris) { + return uris.stream() + .map(uri -> createURI(uri, true)) + .collect(toList()); + } + + private static EPackage loadPackage(IFile file) { + ResourceSet resources = new ResourceSetImpl(); + Resource model = resources.getResource(URI.createFileURI(file.getLocation().toString()), true); + return (EPackage) model.getContents().get(0); + } + + private static String contentOf(IFile file) throws CoreException, IOException { + InputStream contents = file.getContents(); + try (InputStreamReader reader = new InputStreamReader(contents)) { + return CharStreams.toString(reader).trim(); + } + } + +} diff --git a/tests/org.eclipse.emf.ecoretools.ale.ide.ui.tests/src/org/eclipse/emf/ecoretools/ale/ide/helloworld/tests/commons/NewAleProjectWizardBot.java b/tests/org.eclipse.emf.ecoretools.ale.ide.ui.tests/src/org/eclipse/emf/ecoretools/ale/ide/helloworld/tests/commons/NewAleProjectWizardBot.java new file mode 100644 index 000000000..0766920c9 --- /dev/null +++ b/tests/org.eclipse.emf.ecoretools.ale.ide.ui.tests/src/org/eclipse/emf/ecoretools/ale/ide/helloworld/tests/commons/NewAleProjectWizardBot.java @@ -0,0 +1,93 @@ +/******************************************************************************* + * Copyright (c) 2019 Inria and Obeo. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Inria - initial API and implementation + *******************************************************************************/ +package org.eclipse.emf.ecoretools.ale.ide.helloworld.tests.commons; + +import static org.eclipse.emf.ecoretools.ale.ide.ui.wizards.NewAleProjectConfigurationWizardPage.CREATE_ECORE_MODEL_BUTTON_ID; +import static org.eclipse.emf.ecoretools.ale.ide.ui.wizards.NewAleProjectConfigurationWizardPage.ECORE_PACKAGE_NAME_TEXT_ID; +import static org.eclipse.swtbot.swt.finder.waits.Conditions.shellCloses; +import static org.eclipse.swtbot.swt.finder.waits.Conditions.shellIsActive; + +import org.eclipse.swtbot.eclipse.finder.SWTWorkbenchBot; +import org.eclipse.swtbot.swt.finder.exceptions.WidgetNotFoundException; +import org.eclipse.swtbot.swt.finder.widgets.SWTBotShell; + +/** + * Bot aimed at easing creation of new ALE projects during UI tests. + */ +// TODO Make API fluent to prevent inconsistent order of method calls +public class NewAleProjectWizardBot { + + private final SWTWorkbenchBot bot; + + private SWTBotShell wizard; + + /** + * Instantiates a new bot aimed at easing the creation of new ALE projects. + * + * @param bot + * A valid bot capable of manipulating Eclipse's workbench + */ + public NewAleProjectWizardBot(SWTWorkbenchBot bot) { + this.bot = bot; + } + + /** + * Opens the wizard, sets the name of the project then goes to the second page. + * + * @param projectName + * The name of the project + */ + public void open(String projectName) { + bot.menu("File") + .menu("New") + .menu("Project...") + .click(); + wizard = bot.shell("New Project"); + wizard.activate(); + bot.tree() + .expandNode("ALE") + .select("ALE Project"); + bot.button("Next >").click(); + bot.textWithLabel("Project name:").setText(projectName); + bot.button("Next >").click(); + + // Ensure the wizard is open & visible before moving on + bot.waitUntil(shellIsActive("ALE Project Wizard")); + } + + /** + * Selects the 'create new Ecore model' option. + * + * @param packageName + * The name of the new model to create + * + * @throws WidgetNotFoundException if the node was not found + */ + public void createNewEcoreModel(String packageName) { + bot.radioWithId(CREATE_ECORE_MODEL_BUTTON_ID).click(); + bot.textWithId(ECORE_PACKAGE_NAME_TEXT_ID).setText(packageName); + } + + public boolean canFinish() { + return bot.button("Finish").isEnabled(); + } + + public void finish() { + if (! canFinish()) { + throw new IllegalStateException("The New ALE Project wizard has been asked to finish but the 'Finish' button is disabled"); + } + bot.button("Finish").click(); + + // Ensure the project is fully created before moving on + bot.waitUntil(shellCloses(wizard)); + } + +} \ No newline at end of file