AbstractAnalysisHandler.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.handlers;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
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.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.MultiRule;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.ui.IWorkingSet;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.handlers.HandlerUtil;
import org.osate.aadl2.Element;
import org.osate.aadl2.instance.SystemInstance;
import org.osate.aadl2.instance.SystemOperationMode;
import org.osate.aadl2.modelsupport.Activator;
import org.osate.aadl2.modelsupport.FileNameConstants;
import org.osate.aadl2.modelsupport.errorreporting.AnalysisErrorReporterManager;
import org.osate.aadl2.modelsupport.resources.OsateResourceUtil;
import org.osate.aadl2.util.Aadl2Util;
import org.osate.core.AadlNature;
import org.osate.result.AnalysisResult;
import org.osate.result.Diagnostic;
import org.osate.result.Result;
import org.osate.ui.OsateUiPlugin;
import org.osate.ui.internal.instantiate.InstantiationEngine;

/**
 * Working replacement for {@link #AaxlReadOnlyHandlerAsJob}.  Not at all "ready for prime-time", which
 * is why it is currently package visibility.  Also consider integrating with
 * {@code org.osate.ui.handlers.AbstractMultiJobHandler}.
 *
 * <p>Assumptions: The analysis selection is a bunch of working sets, AADL projects, folders, and aaxl files.
 * There is a method for getting a list of unique aaxl files from that.  IT is assumed analysis can
 * run on each aaxl file independently.  Analysis may produce markers on each aaxl file, and may create a
 * single output file for each input aaxl file.  For each input file, the output file is located
 * in "reports/subdir/" rooted in the same directory as the input file.  The analysis names "subdir"
 * using an abstract method.  The output file is named using an abstract method.
 *
 * <p>We need more experience writing some "modern" osate analyses.  This class is expected to evolve as
 * it sees more use.  It's possible we will find some analysis that doesn't fit
 * the assumptions at all.
 *
 * @author aarong
 * @since 6.2
 */
public abstract class AbstractAnalysisHandler extends AbstractHandler {
	private static final String REPORTS_DIR = "/reports";

	@Override
	public final Object execute(final ExecutionEvent event) throws ExecutionException {
		/*
		 * Do as little as possible here so we don't hold up the UI thread. Just get the
		 * selected items in a raw list and start a new job to figure everything else out.
		 */
		@SuppressWarnings("unchecked")
		final List<Object> selectionAsList = HandlerUtil.getCurrentStructuredSelection(event).toList();
		final Job job = new KickoffJob(selectionAsList);
		job.setRule(null); // doesn't use resources
		job.schedule();

		// Supposed to always return null
		return null;
	}

	/*
	 * Job that starts everything else out. Goes through the selection and finds all the
	 * selected Aaxl files. Figures out the resource locking and output files needed
	 * to analyze each instance model.
	 *
	 * AS STATED ABOVE, THE ASSUMPTION HERE IS THAT EACH INSTANCE MODEL CAN BE ANALYZED INDEPENDENT
	 * OF THE OTHER ONES.
	 *
	 * WE ALSO ASSUME THAT EACH INPUT FILE CREATES A SINGLE OUTPUT FILE (probably a .csv file).
	 */
	private final class KickoffJob extends Job {
		private final List<Object> selectionAsList;

		public KickoffJob(final List<Object> selectionAsList) {
			super("Analysis kickoff");
			this.selectionAsList = selectionAsList;
		}

		@Override
		protected IStatus run(final IProgressMonitor monitor) {
			/* Find the aaxl files and the component implementations from the UI selection */
			final Set<IFile> aaxlFiles = new HashSet<>();
			final Set<Object> forEngine = new HashSet<>();
			findAllInstanceFilesAndComponentImpls(selectionAsList, aaxlFiles, forEngine);

			/*
			 * Instantiate the component implementations and add those that were successful to the
			 * set of aaxl files.
			 */
			if (!forEngine.isEmpty()) {
				final InstantiationEngine engine = new InstantiationEngine(forEngine);
				final List<IFile> newAaxlFiles = engine.instantiate(monitor);
				aaxlFiles.addAll(newAaxlFiles);
			}
			final int size = aaxlFiles.size();

			/*
			 * Go through all the input files and find all the output files and associated
			 * scheduling rules.
			 */
			final IResourceRuleFactory ruleFactory = ResourcesPlugin.getWorkspace().getRuleFactory();
			ISchedulingRule prereqRule = null; // Initially empty
			final Set<IFolder> outputFolders = new HashSet<>();
			final List<IFile> outputFiles = new ArrayList<>(size);
			final Job myJobs[] = new Job[size];

			int idx = 0;
			for (final IFile aaxlFile : aaxlFiles) {
				// Get the output file, its location, and the base filename of the input file
				final IPath pathSansExtension = aaxlFile.getFullPath().removeFileExtension();
				final String filename = pathSansExtension.lastSegment();
				final IPath reportsPath = pathSansExtension.removeLastSegments(1).append(REPORTS_DIR);
				final IPath outputPath = reportsPath.append("/" + getSubDirName());

				final IFolder reportsFolder = ResourcesPlugin.getWorkspace().getRoot().getFolder(reportsPath);
				final IFolder outputFolder = ResourcesPlugin.getWorkspace().getRoot().getFolder(outputPath);
				outputFolders.add(outputFolder);

				final IFile outputFile = ResourcesPlugin.getWorkspace().getRoot()
						.getFile(outputPath.append("/" + getOutputFileForAaxlFile(aaxlFile, filename)));
				outputFiles.add(outputFile);
				prereqRule = MultiRule.combine(prereqRule, MultiRule.combine(
						ruleFactory.createRule(reportsFolder),
						MultiRule.combine(ruleFactory.createRule(outputFolder), ruleFactory.createRule(outputFile))));

				final ISchedulingRule jobRule = MultiRule.combine(ruleFactory.modifyRule(outputFile),
						ruleFactory.markerRule(aaxlFile));
				final Job job = createAnalysisJob(aaxlFile, outputFile);
				job.setRule(jobRule);
				job.setUser(true);
				myJobs[idx] = job;

				idx += 1;
			}

			/*
			 * 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.
			 *
			 * We also create (touch) all the output files before hand because creation requires locking the parent container,
			 * whereas modifying only requires locking the file itself.
			 *
			 * This means that the methods in the subclass that output to the file must use IFile.setContents().
			 */
			boolean prereqFailed = false;
			try {
				ResourcesPlugin.getWorkspace().run(m -> {
					for (final IFolder folder : outputFolders) {
						makeSureFoldersExist(folder);
					}
					for (final IFile file : outputFiles) {
						if (!file.exists()) {
							file.create(EmptyInputStream.INSTANCE, true, null);
						}
					}
				}, prereqRule, IWorkspace.AVOID_UPDATE, null);
			} catch (CoreException e) {
				prereqFailed = true;
				Activator.logThrowable(e);

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

			if (!prereqFailed) {
				// now launch the main jobs
				for (final Job job : myJobs) {
					job.schedule();
				}
			}

			return Status.OK_STATUS;
		}
	}

	/**
	 * Return the name of the subdirectory under the "reports" directory that the output files
	 * for this analysis are located in.
	 *
	 * @return The name of the subdirectory, e.g., {@code "BusLoad"}.
	 */
	protected abstract String getSubDirName();

	/**
	 * Given an instance model file that is to be analyzed, return the name of the output file
	 * to generate after analyzing the given file.  This name does not include path information, and
	 * is usually generated by appending a label to the filename and then adding the appropriate file
	 * extension.  For example, the bus load analysis just returns the {@code filename} with the
	 * extension {@code .csv}.
	 *
	 * @param aaxlFile The file bing analyzed.
	 *
	 * @param filename The filename part of the file for convenience.  This does not include the file
	 * extension.
	 *
	 * @return The output file name.
	 */
	protected abstract String getOutputFileForAaxlFile(IFile aaxlFile, String filename);

	/**
	 * Return a job that analyzes the given instance model file and puts the results in the
	 * given output file.  We assume here that the analysis doesn't rely on anything other than
	 * the given instance model file.  More specifically, if multiple instance models were selected
	 * to launch this action, they can all be analyzed independently of each other.
	 *
	 * <p><em>Note: If the job needs IScheudlingRules beyond the ability to create markers on the instance model
	 * file or to write to the output file, it is currently out of luck. This is an area of future
	 * abstraction if needed.</em></p>
	 *
	 * @param aaxlFile The instance model file to analyze.
	 *
	 * @param outputFile The file to write output to.  This file is guaranteed to exist already.  The
	 * existing contents of the file are not guaranteed.  That is, the file may be new and empty, or it
	 * may contain the results from a previous analysis run, or it could just be garbage.
	 *
	 * @return A job class that executes analysis on the given instnace model file.
	 */
	protected abstract Job createAnalysisJob(IFile aaxlFile, IFile outputFile);

	private static final class EmptyInputStream extends InputStream {
		public static final EmptyInputStream INSTANCE = new EmptyInputStream();

		private EmptyInputStream() {
			super();
		}

		@Override
		public int read() throws IOException {
			return -1;
		}
	}

	/**
	 * Given a collection of resources, find all the resources that are instance model ({@code .aaxl})
	 * files.  Also finds all the COmponentImplementations that are selected and adds those to the
	 * set referenced by {@code forEngine}.  If a resource is an AADL project, working set, or folder then the contents are recursively searched.
	 * Duplicates are handled: if a resource is encountered more than once it only appears once in
	 * the returned list.  Hidden folders (those that start with {@code .}) are ignored.
	 *
	 * @param A collection of Eclipse {@link IResource} objects and {@link IWorkingSet} objects.
	 * @return A list of {@link IFile} objects that refer to AADL instance model files.
	 */
	private static void findAllInstanceFilesAndComponentImpls(final Collection<Object> rsrcs,
			final Set<IFile> instanceFiles, final Set<Object> forEngine) {
		findAllInstanceFilesAndComponentImpls(rsrcs.toArray(new Object[rsrcs.size()]), instanceFiles, forEngine);
	}

	private static void findAllInstanceFilesAndComponentImpls(final Object[] rsrcs, final Set<IFile> instanceFiles,
			final Set<Object> forEngine) {
		for (final Object rsrc : rsrcs) {
			if (rsrc instanceof IWorkingSet) {
				findAllInstanceFilesAndComponentImpls(((IWorkingSet) rsrc).getElements(), instanceFiles, forEngine);
			} else if (rsrc instanceof IFile) {
				final IFile file = (IFile) rsrc;
				final String ext = file.getFileExtension();
				if (ext != null && ext.equals(FileNameConstants.INSTANCE_FILE_EXT)) {
					instanceFiles.add(file);
				}
			} else if (rsrc instanceof SystemInstance) {
				instanceFiles.add(OsateResourceUtil.toIFile(((SystemInstance) rsrc).eResource().getURI()));
			} else if (rsrc instanceof IContainer) {
				final IContainer container = (IContainer) rsrc;
				if (container instanceof IProject) {
					final IProject project = (IProject) container;
					if (!project.isOpen() || !AadlNature.hasNature(project)) {
						// Project is closed or is not an AADL project, so ignore it
						continue;
					}
				}

				if (!container.getName().startsWith(".")) {
					try {
						findAllInstanceFilesAndComponentImpls(container.members(), instanceFiles, forEngine);
					} catch (final CoreException e) {
						OsateUiPlugin.getDefault().getLog().error(e.getMessage(), e);
					}
				}
			} else {
				// pass through to the instantiation engine
				forEngine.add(rsrc);
			}
		}
	}

	/**
	 * make sure the folders exist all along the path
	 *
	 * @param path
	 */
	private static void makeSureFoldersExist(IFolder folder) {
		if (!folder.exists()) {
			makeSureFoldersExist((IFolder) folder.getParent());
			try {
				folder.create(true, true, null);
			} catch (final CoreException e) {
				OsateUiPlugin.getDefault().getLog().error(e.getMessage(), e);
			}
		}
	}

	// ============================================================
	// == Generate markers for the whole analysis result tree
	// ============================================================

	/**
	 * Generate an Eclipse marker for each diagnostic in the analysis results tree.  This method
	 * assumes that the first layer of {@link Result} objects is one result per system operation mode,
	 * and the name of the system operation mode is used as the message of that result.
	 * @param analysisResult The analysis result tree to generate markers from
	 * @param errManager The error manager used to generate markers.
	 */
	protected static void generateMarkers(final AnalysisResult analysisResult,
			final AnalysisErrorReporterManager errManager) {
		// Handle each SOM
		analysisResult.getResults().forEach(r -> {
			final String somName = r.getMessage();
			final String somPostfix = somName.isEmpty() ? "" : (" in modes " + somName);
			generateMarkersForResult(r, errManager, somPostfix);
		});
	}

	private static void generateMarkersForResult(final Result result, final AnalysisErrorReporterManager errManager,
			final String somPostfix) {
		generateMarkersFromDiagnostics(result.getDiagnostics(), errManager, somPostfix);
		result.getSubResults().forEach(r -> generateMarkersForResult(r, errManager, somPostfix));
	}

	private static void generateMarkersFromDiagnostics(final List<Diagnostic> diagnostics,
			final AnalysisErrorReporterManager errManager, final String somPostfix) {
		diagnostics.forEach(issue -> {
			switch (issue.getDiagnosticType()) {
			case ERROR:
				errManager.error((Element) issue.getModelElement(), issue.getMessage() + somPostfix);
				break;
			case INFO:
				errManager.info((Element) issue.getModelElement(), issue.getMessage() + somPostfix);
				break;
			case WARNING:
				errManager.warning((Element) issue.getModelElement(), issue.getMessage() + somPostfix);
				break;
			default:
				// Do nothing.
			}
		});
	}

	// ============================================================
	// == Output the results
	// ============================================================

	/*
	 * XXX: SHould this be moved somewhere else? Maybe org.osate.result.util?? I kept
	 * the methods names more generic, that is, "content" instead of "CSV" in case they
	 * are later moved to a superclass.
	 */

	// Should this have a superclass? Are there other output types we want to deal with?
	protected static abstract class CSVAnalysisResultWriter {
		private final IFile outputFile;

		protected CSVAnalysisResultWriter(final IFile outputFile) {
			this.outputFile = outputFile;
		}

		/**
		 * Write the results from given Analysis Results object.
		 *
		 * @param analysisResult The analysis results to write.
		 * @param monitor The progress monitor to use; may be {@code null}.
		 */
		public void writeAnalysisResults(final AnalysisResult analysisResult, final IProgressMonitor monitor) {
			final SubMonitor subMonitor = SubMonitor.convert(monitor, 3);
			final String csvContent = getContentAsString(analysisResult, subMonitor.split(1));
			final InputStream inputStream = new ByteArrayInputStream(csvContent.getBytes());

			try {
				if (outputFile.exists()) {
					outputFile.setContents(inputStream, true, true, subMonitor.split(1));
				} else {
					outputFile.create(inputStream, true, subMonitor.split(1));
				}
			} catch (final CoreException e) {
				Activator.logThrowable(e);
			}
		}

		private String getContentAsString(final AnalysisResult analysisResult, final IProgressMonitor monitor) {
			final StringWriter writer = new StringWriter();
			final PrintWriter pw = new PrintWriter(writer);
			generateContentforAnalysis(pw, analysisResult, monitor);
			pw.close();
			return writer.toString();
		}

		private void generateContentforAnalysis(final PrintWriter pw, final AnalysisResult analysisResult,
				final IProgressMonitor monitor) {
			pw.println(analysisResult.getMessage());
			pw.println();
			pw.println();

			final SubMonitor subMonitor = SubMonitor.convert(monitor, analysisResult.getResults().size());
			analysisResult.getResults().forEach(somResult -> {
				if (Aadl2Util.isPrintableSOMName((SystemOperationMode) somResult.getModelElement())) {
					printItem(pw, "Analysis results in modes " + somResult.getMessage());
					pw.println();
				}
				generateContentforSOM(pw, somResult, subMonitor.split(1));
			});
		}

		protected abstract void generateContentforSOM(final PrintWriter pw, final Result somResult,
				final IProgressMonitor monitor);

		// ==== Low-level CSV format

		protected final void generateContentforDiagnostics(final PrintWriter pw, final List<Diagnostic> diagnostics,
				final IProgressMonitor monitor) {
			final SubMonitor subMonitor = SubMonitor.convert(monitor, diagnostics.size());
			for (final Diagnostic issue : diagnostics) {
				printItem(pw, issue.getDiagnosticType().getName() + ": " + issue.getMessage());
				pw.println();
				subMonitor.split(1);
			}
		}

		protected final void printItems(final PrintWriter pw, final String item1, final String... items) {
			printItem(pw, item1);
			for (final String nextItem : items) {
				printSeparator(pw);
				printItem(pw, nextItem);
			}
			pw.println();
		}

		protected final void printItem(final PrintWriter pw, final String item) {
			// TODO: Doesn't handle quotes in the item!
			pw.print('"');
			pw.print(item);
			pw.print('"');
		}

		protected final void printSeparator(final PrintWriter pw) {
			pw.print(',');
		}
	}
}