DeclarativeReferenceResolver.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.ge.aadl2.internal;

import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import org.eclipse.core.resources.IProject;
import org.eclipse.e4.core.contexts.EclipseContextFactory;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.xtext.naming.QualifiedName;
import org.eclipse.xtext.resource.IEObjectDescription;
import org.eclipse.xtext.resource.IResourceDescription;
import org.osate.aadl2.Aadl2Package;
import org.osate.aadl2.AadlPackage;
import org.osate.aadl2.Classifier;
import org.osate.aadl2.PackageSection;
import org.osate.ge.CanonicalBusinessObjectReference;
import org.osate.ge.StringUtil;
import org.osate.ge.aadl2.ui.AadlModelAccessUtil;
import org.osate.ge.internal.services.AadlResourceService;
import org.osate.ge.internal.services.AadlResourceService.AadlPackageReference;
import org.osate.ge.internal.services.ModelChangeNotifier;
import org.osate.ge.internal.services.ModelChangeNotifier.ChangeListener;
import org.osate.ge.internal.services.impl.DeclarativeReferenceType;
import org.osate.ge.referencehandling.CreateReferenceResolverFactoryContext;
import org.osate.ge.referencehandling.ReferenceResolver;
import org.osate.ge.referencehandling.ReferenceResolverFactory;
import org.osate.ge.referencehandling.ResolveContext;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;

// Handles resolving references related to the AADL declarative model
public class DeclarativeReferenceResolver implements ReferenceResolver {
	public static class Factory implements ReferenceResolverFactory {
		@Override
		public ReferenceResolver create(final CreateReferenceResolverFactoryContext ctx) {
			final Bundle bundle = FrameworkUtil.getBundle(Factory.class);
			final AadlResourceService aadlResourceService = Objects.requireNonNull(
					EclipseContextFactory.getServiceContext(bundle.getBundleContext()).get(AadlResourceService.class),
					"Unable to get AADL resource service");
			final ModelChangeNotifier modelChangeNotifier = Objects.requireNonNull(
					EclipseContextFactory.getServiceContext(bundle.getBundleContext()).get(ModelChangeNotifier.class),
					"Unable to get model change notifier");

			return new DeclarativeReferenceResolver(ctx.getProject(), aadlResourceService, modelChangeNotifier);
		}
	}

	// Cache for items that should not change unless models have changed.
	private static class DeclarativeCache {
		private static final EClass aadlPackageEClass = Aadl2Package.eINSTANCE
				.getAadlPackage();
		private final IProject project;

		// Map for caching. Not cleared to ensure strong references to the AadlPackageReference objects exist during the lifetime of the service
		private final Map<String, AadlPackageReference> packageNameToPackageMap = new HashMap<>();
		private Set<IResourceDescription> resourceDescriptions = null; // Resource descriptions for resources which are in the project references path
		private AadlResourceService aadlResourceService;

		public DeclarativeCache(final IProject project, final AadlResourceService aadlResourceService) {
			this.project = Objects.requireNonNull(project, "project must not be null");
			this.aadlResourceService = Objects.requireNonNull(aadlResourceService,
					"aadlResourceService must not be null");
		}

		public void dispose() {
			invalidate();

			// Clear map to remove object references
			packageNameToPackageMap.clear();
		}

		private void invalidate() {
			resourceDescriptions = null;
		}

		public AadlPackage getAadlPackage(final String packageName) {
			final String lowerCasePackageName = packageName.toLowerCase(Locale.ROOT);
			AadlPackageReference pkgRef = packageNameToPackageMap.get(lowerCasePackageName);
			final AadlPackage pkg;
			if (pkgRef == null) {
				pkgRef = findAadlPackage(lowerCasePackageName, getCachedResourceDescriptions(), aadlResourceService);
				if (pkgRef == null) {
					return null;
				}

				pkg = pkgRef.getAadlPackage();

				// If the package is valid. Store the reference
				if (pkg != null) {
					// Store a reference to the package in the cache if the reference was valid
					packageNameToPackageMap.put(lowerCasePackageName, pkgRef);
				}

			} else {
				pkg = pkgRef.getAadlPackage();

				// Remove the package reference if the package is not valid.
				if (pkg == null) {
					packageNameToPackageMap.remove(lowerCasePackageName);
				}
			}

			return pkg;

		}

		/**
		 * Returns an AadlPackageReference depending on whether the package is retrieved from disk or from an Xtext document
		 * @return
		 */
		private static AadlPackageReference findAadlPackage(final String packageName,
				Set<IResourceDescription> resourceDescriptions, final AadlResourceService aadlResourceService) {
			// Get the Xtext Resource for the package
			final String[] pkgNameSegs = packageName.split("::");
			final QualifiedName packageQualifiedName = QualifiedName.create(pkgNameSegs);
			for (final IResourceDescription resDesc : resourceDescriptions) {
				// If there are multiple objects, only check the first one.
				final Iterator<IEObjectDescription> it = resDesc
						.getExportedObjects(aadlPackageEClass, packageQualifiedName, true).iterator();
				if (it.hasNext()) {
					return aadlResourceService.getPackageReference(it.next().getEObjectURI());
				}
			}

			return null;
		}

		private Set<IResourceDescription> getCachedResourceDescriptions() {
			if (resourceDescriptions == null) {
				// Find resources that should be looked in
				resourceDescriptions = AadlModelAccessUtil.calculateVisibleResourceDescriptions(
						project)
						.collect(Collectors.toSet());
			}

			return resourceDescriptions;
		}
	}

	private final ModelChangeNotifier modelChangeNotifier;
	private final DeclarativeCache declarativeCache;
	private final ChangeListener modelChangeListener = new ChangeListener() {
		@Override
		public void modelChanged(final boolean modelWasLocked) {
			invalidateCache();
		}
	};

	public DeclarativeReferenceResolver(final IProject project, final AadlResourceService aadlResourceService,
			final ModelChangeNotifier modelChangeNotifier) {
		this.modelChangeNotifier = Objects.requireNonNull(modelChangeNotifier, "modelChangeNotifier must not be null");
		this.declarativeCache = new DeclarativeCache(project, aadlResourceService);
		modelChangeNotifier.addChangeListener(modelChangeListener);
	}

	@Override
	public void close() {
		modelChangeNotifier.removeChangeListener(modelChangeListener);
		declarativeCache.dispose();
	}

	@Override
	public Optional<Object> resolve(final ResolveContext ctx) {
		final CanonicalBusinessObjectReference ref = ctx.getReference();
		final List<String> refSegs = ref.getSegments();
		if (refSegs.size() < 2) {
			return Optional.empty();
		}

		Object referencedObject = null; // The object that will be returned
		final String type = refSegs.get(0);

		final String qualifiedName = refSegs.get(1);
		if (type.equals(DeclarativeReferenceType.PACKAGE.getId())) {
			referencedObject = getAadlPackage(qualifiedName);
		} else if (type.equals(DeclarativeReferenceType.CLASSIFIER.getId())) {
			referencedObject = getClassifierByQualifiedName(qualifiedName);
		}

		return Optional.ofNullable(referencedObject);
	}

	void invalidateCache() {
		declarativeCache.invalidate();
	}

	private Classifier getClassifierByQualifiedName(final String qualifiedName) {
		final String[] qualifiedNameSegs = qualifiedName.split("::");
		final String packageName = StringUtil.join(qualifiedNameSegs, 0, qualifiedNameSegs.length - 1, "::");
		final String classifierName = qualifiedNameSegs[qualifiedNameSegs.length - 1];

		final AadlPackage pkg = getAadlPackage(packageName);
		if (pkg == null) {
			return null;
		}

		// Return the package if that is the element that was being retrieved
		Classifier classifier = findClassifierByName(pkg.getPublicSection(), classifierName);
		if (classifier == null) {
			classifier = findClassifierByName(pkg.getPrivateSection(), classifierName);
		}

		return classifier;
	}

	public AadlPackage getAadlPackage(final String packageName) {
		return declarativeCache.getAadlPackage(packageName);
	}

	private Classifier findClassifierByName(final PackageSection section, final String classifierName) {
		if (section == null) {
			return null;
		}

		for (final Classifier classifier : section.getOwnedClassifiers()) {
			if (classifierName.equalsIgnoreCase(classifier.getName())) {
				return classifier;
			}
		}

		return null;
	}
}