AbstractInstantiationEngine.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.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResourceRuleFactory;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.resources.WorkspaceJob;
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.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.IJobManager;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.JobGroup;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.ide.IDE;
import org.osate.aadl2.instance.SystemInstance;
import org.osate.aadl2.instantiation.InstantiateModel;
import org.osate.aadl2.instantiation.RootMissingException;
import org.osate.core.AadlNature;
import org.osate.core.OsateCorePlugin;
import org.osate.ui.OsateUiPlugin;

/**
 * @since 3.0
 */
abstract class AbstractInstantiationEngine<T> {
	protected final Collection<?> selectionAsList;

	public AbstractInstantiationEngine(final Collection<?> selectionAsList) {
		this.selectionAsList = selectionAsList;
	}

	/**
	 * Instantiate models based on the selected items.  Blocks the current job/thread until it is finished, so this should
	 * not be called from the ui thread.
	 *
	 * XXX: Say something about the abstract methods here
	 */
	public final List<IFile> instantiate(final IProgressMonitor monitor) {
		boolean cancelled = false;

		/* Make sure the resources are saved if they are open in an editor */
		if (!saveDirtyEditors()) {
			cancelled = true;
		}

		if (!cancelled) {
			final Set<T> inputs = getInputsFromSelection(selectionAsList);
			final int size = inputs.size();

			if (size > 0) {
				/*
				 * This map is shared by all the jobs to build the set of results. It is created here,
				 * given to all the jobs, and then used to build te final method result.
				 */
				final Map<T, InternalJobResult> results = new ConcurrentHashMap<>(size);

				final PrereqHelper helper = getPrereqHelper(size, ResourcesPlugin.getWorkspace().getRuleFactory());
				for (final T input : inputs) {
					if (input != null) {
						helper.handleInput(input);

						/*
						 * Init each result as cancelled because if the job is cancelled before it starts, it will never
						 * add a new result record to the map. This way those jobs that never run are accounted for.
						 */
						results.put(input, InternalJobResult.NOT_EXECUTED);
					}
				}

				/*
				 * NB. Chances are the prerequisites (helper.performPrereqs()) will run code using Workspace.run(). This will cause
				 * the auto build thread to be interrupted and rescheduled if it is currently running.
				 */
				if (helper.performPrereqs()) {
					final SubMonitor subMonitor = SubMonitor.convert(monitor, 3);
					subMonitor.subTask("Waiting for build to finish");
					/* Wait for any builds to finish: Taken from DebugUIPlugin.launchInBackground(). */
					final IJobManager jobManager = Job.getJobManager();
					try {
						jobManager.join(ResourcesPlugin.FAMILY_MANUAL_BUILD, subMonitor.split(1));
						jobManager.join(ResourcesPlugin.FAMILY_AUTO_BUILD, subMonitor.split(1));
					} catch (OperationCanceledException | InterruptedException e) {
						cancelled = true;
					}

					/* NB. THere really isn't any way to guarantee that a new build doesn't start here */
					if (!cancelled) {
						final JobGroup jobGroup = new JobGroup("Instantiation", 0, 0);
						for (int i = 0; i < results.size(); i++) {
							final Job job = helper.getJob(i, results);
							job.setUser(true);
							job.setJobGroup(jobGroup);
							job.schedule();
							/*
							 * NB. These jobs will interrupt the build job, if there is a new one. More accurately,
							 * the auto buisizeld job periodically checks to see if its rule blocks any other jobs, and
							 * if so, it interrupts and reschedules itself.
							 */
						}

						final Job resultJob = new ResultJob(jobGroup, results);
						resultJob.setRule(null); // doesn't use resources
						resultJob.setUser(false); // background helper job, don't let the user see it
						resultJob.setSystem(true); // background helper job, don't let the user see it
						resultJob.schedule();

						// Wait for the whole thing to complete
						try {
							subMonitor.subTask("Waiting for (re)instantiations to finish");
							resultJob.join(0L, subMonitor.split(1));
						} catch (final OperationCanceledException | InterruptedException e) {
							cancelled = true;
						}
					}
				}

				if (cancelled) {
					return Collections.emptyList();
				} else {
					final List<IFile> successfullyInstantiated = new ArrayList<>(size);
					for (final InternalJobResult ijr : results.values()) {
						if (ijr.aaxlFile != null) {
							successfullyInstantiated.add(ijr.aaxlFile);
						}
					}
					return Collections.unmodifiableList(successfullyInstantiated);
				}
			}
		}

		// Nothing was selected, so we didn't do anything
		return Collections.emptyList();
	}

	/**
	 * This a class so that it can have persistent state.  {@link #handleInput(Object)} can build up state that is
	 * used by the other two methods.  This is meant to determine what needs to be set up in the workspace before the
	 * instantiation can actually run.  Generally, this is expected to be determining what output folders need to exist.
	 * These would be computed for each input item on a call to {@link #handleInput(Object), and then actually created
	 * on a call to {@link #performPrereqs()}.  Finally {@link #getJob(int, Map)} is called for each of the input files,
	 * in the same order as {@link #handleInput(Object)} was called.  It is expected to return a job that can
	 * perform the instantiation and to set the rules on the job based on the prerequisites that have been accumulating.
	 */
	protected abstract class PrereqHelper {
		public abstract void handleInput(final T input);

		/**
		 * @return {@code true} if prerequisites succeeded.  Returns {@code false} if they failed,
		 * in which case the instantiation process is over.
		 */
		public abstract boolean performPrereqs();

		public abstract AbstractInstantiationJob getJob(int i, Map<T, InternalJobResult> results);
	}

	protected abstract PrereqHelper getPrereqHelper(int size, IResourceRuleFactory ruleFactory);

	protected abstract class AbstractInstantiationJob extends WorkspaceJob {
		private final Map<T, InternalJobResult> results;

		protected AbstractInstantiationJob(final String name, final Map<T, InternalJobResult> results) {
			super(name);
			this.results = results;
		}

		@Override
		public final IStatus runInWorkspace(final IProgressMonitor monitor) {
			final SubMonitor subMonitor = SubMonitor.convert(monitor, 1);

			/*
			 * Error handling in buildIntanceModel is complicated and probably should not be handled the
			 * way it is, but I don't want to fix that right now, so we are going to capture all the information
			 * we can from it and display it to the user at the end of the operation.
			 */
			boolean successful = false;
			String errorMessage = null;
			Exception exception = null;
			boolean cancelled = false;

			boolean delete = false;
			try {
				final SystemInstance systemInstance = buildSystemInstance(subMonitor.split(1));
				successful = systemInstance != null;
				errorMessage = InstantiateModel.getErrorMessage();
				delete = !successful;
			} catch (final InterruptedException | OperationCanceledException e) {
				// Instantiation was canceled by the user.
				cancelled = true;
				delete = true;
			} catch (final RootMissingException e) {
				successful = false;
				errorMessage = "Root component implementation declaration no longer exists; instance model removed";
				delete = true;
			} catch (final Exception e) {
				OsateUiPlugin.log(e);
				successful = false;
				exception = e;
				delete = true;
			}

			final IFile outputFile = getOutputFile();
			if (delete) {
				// Remove the partially instantiated resource
				try {
					if (outputFile.exists()) {
						outputFile.delete(0, null);
					}
				} catch (final CoreException ce) {
					// eat it
				}
			}

			final InternalJobResult result = new InternalJobResult(successful, cancelled, errorMessage, exception,
					successful ? outputFile : null);
			results.put(getInput(), result);
			return cancelled ? Status.CANCEL_STATUS : Status.OK_STATUS;
		}

		protected abstract SystemInstance buildSystemInstance(IProgressMonitor monitor)
				throws InterruptedException, OperationCanceledException, RootMissingException, Exception;

		protected abstract IFile getOutputFile();

		protected abstract T getInput();
	}

	private final class ResultJob extends Job {
		private final JobGroup instantiationJobs;
		private final Map<T, InternalJobResult> results;

		public ResultJob(final JobGroup intantiationJob, final Map<T, InternalJobResult> results) {
			super("Instantiation Result Job (hidden)");
			this.instantiationJobs = intantiationJob;
			this.results = results;
		}

		@Override
		public IStatus run(final IProgressMonitor monitor) {
			// Wait for the instantiation jobs to finish
			boolean cancelled = false;
			try {
				instantiationJobs.join(0L, null);

				/* User can suppress the dialog if all the results are successful */
				if (OsateCorePlugin.getDefault().getAlwaysShowInstantiationResults()
						|| !InternalJobResult.allSuccessful(results.values())) {
					/* Get the results and display them */
					PlatformUI.getWorkbench().getDisplay().asyncExec(() -> {
						final InstantiationResultsDialog<?> d = new InstantiationResultsDialog<T>(
								PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(), getResultActionName(),
								getResultLabelName(), getResultLabelProvider(), results,
								OsateCorePlugin.getDefault().getPreferenceStore());
						d.open();
					});
				}
			} catch (final InterruptedException | OperationCanceledException e) {
				/*
				 * InterruptedException thrown if we are somehow cancelled. Not sure if
				 * or how this can happen, but if it does, just give up.
				 *
				 * OperationCancelledException is thrown if the progress monitor given
				 * to join() is cancelled, but we didn't give it one, so it should
				 * never occur.
				 */
				cancelled = true;
			}

			return cancelled ? Status.CANCEL_STATUS : Status.OK_STATUS;
		}
	}

	protected abstract Set<T> getInputsFromSelection(Collection<?> selectionAsList);

	protected abstract String getResultActionName();

	protected abstract String getResultLabelName();

	protected abstract Function<T, String> getResultLabelProvider();

	/**
	 * Ask to save all the dirty editors that belong to open AADL projects.
	 * @return {@code true} If the action should continue; {@code false} if the user
	 * selected the cancel option in the save dialog.
	 */
	// XXX: Should this be somewhere else, seems generally useful
	private static boolean saveDirtyEditors() {
		/* Find all the open AADL projects */
		final IProject[] allProjects = ResourcesPlugin.getWorkspace().getRoot().getProjects();
		final List<IProject> openAADLProjects = new ArrayList<>(allProjects.length);
		for (final IProject project : allProjects) {
			if (project.isOpen() && AadlNature.hasNature(project)) {
				openAADLProjects.add(project);
			}
		}

		final AtomicBoolean result = new AtomicBoolean();
		PlatformUI.getWorkbench().getDisplay().syncExec(() -> {
			result.set(IDE.saveAllEditors(openAADLProjects.toArray(new IProject[openAADLProjects.size()]), true));
		});
		return result.get();
	}
}