InstantiationEngine.java

/**
 * Copyright (c) 2004-2025 Carnegie Mellon University and others. (see Contributors file).
 * All Rights Reserved.
 *
 * NO WARRANTY. ALL MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY
 * KIND, EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE
 * OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT
 * MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT.
 *
 * 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/
 * SPDX-License-Identifier: EPL-2.0
 *
 * Created, in part, with funding and support from the United States Government. (see Acknowledgments file).
 *
 * This program includes and/or can make use of certain third party source code, object code, documentation and other
 * files ("Third Party Software"). The Third Party Software that is used by this program is dependent upon your system
 * configuration. By using this program, You agree to comply with any and all relevant Third Party Software terms and
 * conditions contained in any such Third Party Software or separate license file distributed with such Third Party
 * Software. The parties who own the Third Party Software ("Third Party Licensors") are intended third party benefici-
 * aries to this license with respect to the terms applicable to their Third Party Software. Third Party Software li-
 * censes only apply to the Third Party Software and not any other portion of this program or this program as a whole.
 */
package org.osate.ui.internal.instantiate;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResourceRuleFactory;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.MultiRule;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.preference.IPersistentPreferenceStore;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dialogs.ElementListSelectionDialog;
import org.eclipse.xtext.ui.editor.outline.impl.EObjectNode;
import org.eclipse.xtext.ui.label.AbstractLabelProvider;
import org.eclipse.xtext.ui.resource.XtextResourceSetProvider;
import org.osate.aadl2.AadlPackage;
import org.osate.aadl2.Classifier;
import org.osate.aadl2.ComponentImplementation;
import org.osate.aadl2.SubprogramGroupImplementation;
import org.osate.aadl2.SubprogramImplementation;
import org.osate.aadl2.SystemImplementation;
import org.osate.aadl2.instance.SystemInstance;
import org.osate.aadl2.instantiation.InstantiateModel;
import org.osate.aadl2.instantiation.RootMissingException;
import org.osate.aadl2.modelsupport.EObjectURIWrapper;
import org.osate.aadl2.modelsupport.resources.OsateResourceUtil;
import org.osate.core.OsateCorePlugin;
import org.osate.ui.OsateUiPlugin;
import org.osate.ui.UiUtil;
import org.osate.xtext.aadl2.ui.internal.Aadl2Activator;

import com.google.inject.Inject;

/**
 * @since 6.0
 */
public final class InstantiationEngine extends AbstractInstantiationEngine<ComponentImplementation> {
	@Inject
	private XtextResourceSetProvider resourceSetProvider;

	public InstantiationEngine(final Collection<?> selectionAsList) {
		super(selectionAsList);
		Aadl2Activator.getInstance().getInjector(Aadl2Activator.ORG_OSATE_XTEXT_AADL2_AADL2).injectMembers(this);
	}

	@Override
	protected PrereqHelper getPrereqHelper(final int size, final IResourceRuleFactory ruleFactory) {
		return new PrereqHelper() {
			private final List<ComponentImplementation> selectedCompImpls = new ArrayList<>(size);
			private final List<IFile> outputFiles = new ArrayList<>(size);
			private final Set<IFolder> outputFolders = new HashSet<>();
			ISchedulingRule prereqRule = null;

			@Override
			public void handleInput(final ComponentImplementation input) {
				if (input != null) {
					selectedCompImpls.add(input);
					URI instanceModelURI = InstantiateModel.getInstanceModelURI(input);
					final IFile outputFile = OsateResourceUtil.toIFile(instanceModelURI);
					outputFiles.add(outputFile);
					// N.B. We KNOW there is an "Instances" folder above the .aaxl file
					final IFolder outputFolder = (IFolder) outputFile.getParent();
					outputFolders.add(outputFolder);
					prereqRule = MultiRule.combine(prereqRule, ruleFactory.createRule(outputFolder));
				}
			}

			@Override
			public boolean performPrereqs() {
				/*
				 * Make sure all the output folders exists before hand. Could add the folder creation rules to the
				 * jobs below, but they would limit the parallelism too much. So we create them atomically here first,
				 * before we launch the main worker jobs.
				 */
				boolean prereqFailed = false;
				try {
					ResourcesPlugin.getWorkspace().run(m -> {
						for (final IFolder folder : outputFolders) {
							if (!folder.exists()) {
								folder.create(false, true, null);
							}
						}
					}, prereqRule, IWorkspace.AVOID_UPDATE, null);
				} catch (CoreException e) {
					prereqFailed = true;
					OsateUiPlugin.log(e);

					PlatformUI.getWorkbench().getDisplay().asyncExec(() -> {
						MessageDialog.openError(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(),
								"Error staring instantiation",
								"Exception starting model instantiation, see the error log: " + e.getMessage());
					});
				}
				return !prereqFailed;
			}

			@Override
			public InstantiationJob getJob(final int i, final Map<ComponentImplementation, InternalJobResult> results) {
				final IFile outputFile = outputFiles.get(i);
				final InstantiationJob job = new InstantiationJob(selectedCompImpls.get(i), outputFile, results);
				/*
				 * NB. According to <https://www.eclipse.org/articles/Article-Concurrency/jobs-api.html> locking
				 * is only needed for modification, not for reading from resources. This seems sketchy to me
				 * but I'm going to go with it. Readers are supposed to written defensively, to expect that
				 * things might go wonky.
				 *
				 * We create and possibly remove the aaxl file in the case of errors.
				 */
				job.setRule(MultiRule.combine(ruleFactory.createRule(outputFile), ruleFactory.deleteRule(outputFile)));
				return job;
			}
		};
	}

	private final class InstantiationJob extends AbstractInstantiationJob {
		private final ComponentImplementation compImpl;
		private final IFile outputFile;

		public InstantiationJob(final ComponentImplementation compImpl, final IFile outputFile,
				final Map<ComponentImplementation, InternalJobResult> results) {
			super("Instantiate " + compImpl.getQualifiedName(), results);
			this.compImpl = compImpl;
			this.outputFile = outputFile;
		}

		@Override
		protected SystemInstance buildSystemInstance(final IProgressMonitor monitor)
				throws InterruptedException, OperationCanceledException, RootMissingException, Exception {
			return InstantiateModel.buildInstanceModelFile(compImpl, monitor);
		}

		@Override
		protected IFile getOutputFile() {
			return outputFile;
		}

		@Override
		protected ComponentImplementation getInput() {
			return compImpl;
		}
	}

	@Override
	protected String getResultActionName() {
		return "Instantiation";
	}

	@Override
	protected String getResultLabelName() {
		return "Component Implementation";
	}

	@Override
	protected Function<ComponentImplementation, String> getResultLabelProvider() {
		return compImpl -> compImpl.getQualifiedName();
	}

	@Override
	protected Set<ComponentImplementation> getInputsFromSelection(final Collection<?> selectionAsList) {
		final Set<ComponentImplementation> ciSet = new HashSet<>();
		final List<ComponentImplementation> fromAadl = new ArrayList<>();

		for (final Object selection : selectionAsList) {
			if (selection instanceof IFile) {
				final URI uri = OsateResourceUtil.toResourceURI((IFile) selection);
				final EList<EObject> contents = new ResourceSetImpl().getResource(uri, true).getContents();
				if (null != contents && !contents.isEmpty()) {
					final EObject root = contents.get(0);
					if (!(root instanceof AadlPackage)) {
						throw new AssertionError("Unexpected selection: " + selection + " is not an AADL package file");
					}
					getComponentImplsFromPackage((AadlPackage) root, fromAadl);
				}
			} else {
				URI uri = null;
				if (selection instanceof EObjectNode) {
					uri = ((EObjectNode) selection).getEObjectURI();
				} else if (selection instanceof EObjectURIWrapper) {
					uri = ((EObjectURIWrapper) selection).getUri();
				} else {
					throw new AssertionError("Unexpected selection: " + selection);
				}
				final IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(uri.segment(1));
				final ResourceSet resourceSet = resourceSetProvider.get(project);
				final ComponentImplementation impl = (ComponentImplementation) resourceSet.getEObject(uri, true);
				ciSet.add(impl);
			}
		}

		if (!fromAadl.isEmpty()) {
			final boolean showDialog = OsateCorePlugin.getDefault().getAlwaysShowInstantiationAadlDialog();
			final boolean systemsOnly = OsateCorePlugin.getDefault().getOnlyInstantiateSystemImpls();

			final List<SystemImplementation> systems = new ArrayList<>();
			for (final ComponentImplementation ci : fromAadl) {
				if (ci instanceof SystemImplementation) {
					systems.add((SystemImplementation) ci);
				}
			}

			if (!showDialog) {
				ciSet.addAll(systemsOnly ? systems : fromAadl);
			} else {
				/*
				 * THe label provider is sloppy, as I'm replacing the text portion of the delegate and just using
				 * the image portion of the delegate, but I don't know how else to get the images.
				 */
				PlatformUI.getWorkbench().getDisplay().syncExec(() -> {
					final ElementListSelectionDialog d = new ElementListSelectionDialog(
							PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(),
							new AbstractLabelProvider(UiUtil.getModelElementLabelProvider()) {
								@Override
								public String getText(final Object element) {
									return ((ComponentImplementation) element).getQualifiedName();
								}
							}) {
						private Button dontShowButton;
						private Button systemsOnlyButton;

						@Override
						protected Control createDialogArea(final Composite parent) {
							Composite contents = (Composite) super.createDialogArea(parent);

							dontShowButton = new Button(contents, SWT.CHECK);
							dontShowButton.setText("Don't show this dialog again");
							GridData data = new GridData();
							data.grabExcessVerticalSpace = false;
							data.grabExcessHorizontalSpace = true;
							data.horizontalAlignment = GridData.FILL;
							data.verticalAlignment = GridData.BEGINNING;
							dontShowButton.setLayoutData(data);
							dontShowButton.setFont(parent.getFont());

							systemsOnlyButton = new Button(contents, SWT.CHECK);
							systemsOnlyButton.setText("Only systems by default");
							GridData data2 = new GridData();
							data2.grabExcessVerticalSpace = false;
							data2.grabExcessHorizontalSpace = true;
							data2.horizontalAlignment = GridData.FILL;
							data2.verticalAlignment = GridData.BEGINNING;
							systemsOnlyButton.setLayoutData(data2);
							systemsOnlyButton.setFont(parent.getFont());
							systemsOnlyButton.setSelection(systemsOnly);

							return contents;
						}

						@Override
						protected void okPressed() {
							final IPreferenceStore prefs = OsateCorePlugin.getDefault().getPreferenceStore();
							if (dontShowButton.getSelection()) {
								// User just toggled the "don't show option"
								if (MessageDialog.openQuestion(getShell(), "Confirm change",
										"This results dialog will be hidden in the future.  "
												+ "You can restore it by going to the \"OSATE > Instantiation\" preference pane.  "
												+ "Do you wish to make this change?")) {
									prefs.setValue(OsateCorePlugin.ALWAYS_SHOW_INSTANTIATION_AADL_DIALOG, false);
								}
							}
							prefs.setValue(OsateCorePlugin.ONLY_INSTANTIATE_SYSTEM_IMPLS,
									systemsOnlyButton.getSelection());
							if (prefs.needsSaving()) {
								final Job saveJob = Job.create("Save preferences", monitor -> {
									try {
										((IPersistentPreferenceStore) prefs).save();
									} catch (final IOException e) {
										Display.getDefault().asyncExec(() -> {
											MessageDialog.openError(getShell(), "Error",
													"There was a problem saving the preferences: " + e.getMessage());
										});
									}
								});
								saveJob.schedule();
							}

							super.okPressed();
						}
					};
					d.setTitle("Select Component Implementations");
					d.setMessage("Select the component implementations from the selected .aadl files to instantiate.");
					d.setElements(fromAadl.toArray());
					d.setMultipleSelection(true);
					d.setInitialElementSelections(systemsOnly ? systems : fromAadl);
					d.setBlockOnOpen(true);
					if (d.open() == IStatus.OK) {
						final Object[] result = d.getResult();
						for (final Object ci : result) {
							ciSet.add((ComponentImplementation) ci);
						}
					}
				});
			}
		}

		return ciSet;
	}

	private void getComponentImplsFromPackage(final AadlPackage root, List<ComponentImplementation> ciList) {
		// Get all the public component implementations that are not subprograms or subprogram groups
		for (final Classifier c : root.getPublicSection().getOwnedClassifiers()) {
			if (c instanceof ComponentImplementation
					&& !(c instanceof SubprogramImplementation || c instanceof SubprogramGroupImplementation)) {
				ciList.add((ComponentImplementation) c);
			}
		}
	}
}