EclipseBundleClasspath.java

/*******************************************************************************
 * Copyright (c) 2026 Carsten Hammer and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *******************************************************************************/
package org.sandbox.jdt.ui.tests.quickfix.rules;

import java.io.File;
import java.io.IOException;
import java.util.Map;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.junit.JUnitCore;
import org.osgi.framework.Bundle;

/**
 * Adds bundles from the running Eclipse target platform to a temporary Java
 * project's classpath.
 * <p>
 * Workbench, SWTBot and cleanup tests should use the real target-platform API
 * whenever binding identity or inherited signatures are part of the behavior
 * under test. Source stubs with the same qualified names are not equivalent:
 * they can hide missing dependencies and do not prove the binding contract used
 * by real JDT Core or JDT UI projects.
 * </p>
 * <p>
 * Eclipse packages the JUnit libraries behind JDT classpath containers rather
 * than one OSGi bundle per library package. The well-known JUnit package
 * identifiers below therefore resolve through the official JDT containers so
 * fixtures see the same nested libraries as ordinary Eclipse Java projects.
 * </p>
 */
public final class EclipseBundleClasspath {

	private static final Map<String, IPath> JUNIT_LIBRARY_CONTAINERS= Map.of(
			"org.junit", JUnitCore.JUNIT4_CONTAINER_PATH, //$NON-NLS-1$
			"org.junit.jupiter.api", JUnitCore.JUNIT5_CONTAINER_PATH, //$NON-NLS-1$
			"org.apiguardian.api", JUnitCore.JUNIT5_CONTAINER_PATH, //$NON-NLS-1$
			"org.opentest4j", JUnitCore.JUNIT5_CONTAINER_PATH, //$NON-NLS-1$
			"org.junit.platform.commons", JUnitCore.JUNIT5_CONTAINER_PATH); //$NON-NLS-1$

	private EclipseBundleClasspath() {
	}

	/**
	 * Adds all named bundles and their installed fragments. Well-known JUnit
	 * library package identifiers use the corresponding Eclipse JUnit container.
	 *
	 * @param javaProject the temporary Java project
	 * @param bundleSymbolicNames exact bundle symbolic names or supported JUnit
	 *                            library package identifiers
	 * @throws CoreException if a dependency cannot be resolved or added
	 */
	public static void addBundles(IJavaProject javaProject, String... bundleSymbolicNames) throws CoreException {
		for (String bundleSymbolicName : bundleSymbolicNames) {
			IPath junitContainer= JUNIT_LIBRARY_CONTAINERS.get(bundleSymbolicName);
			if (junitContainer != null) {
				addToClasspath(javaProject, JavaCore.newContainerEntry(junitContainer));
			} else {
				addBundle(javaProject, bundleSymbolicName);
			}
		}
	}

	/**
	 * Adds one bundle and its installed fragments.
	 *
	 * @param javaProject the temporary Java project
	 * @param bundleSymbolicName the exact bundle symbolic name
	 * @throws CoreException if the bundle cannot be resolved or added
	 */
	public static void addBundle(IJavaProject javaProject, String bundleSymbolicName) throws CoreException {
		Bundle bundle= Platform.getBundle(bundleSymbolicName);
		if (bundle == null) {
			throw new CoreException(Status.error("Bundle not found: " + bundleSymbolicName)); //$NON-NLS-1$
		}

		addBundleFile(javaProject, bundle, bundleSymbolicName);
		Bundle[] fragments= Platform.getFragments(bundle);
		if (fragments != null) {
			for (Bundle fragment : fragments) {
				addBundleFile(javaProject, fragment, fragment.getSymbolicName());
			}
		}
	}

	private static void addBundleFile(IJavaProject javaProject, Bundle bundle, String displayName)
			throws CoreException {
		File bundleFile;
		try {
			bundleFile= FileLocator.getBundleFile(bundle);
		} catch (IOException exception) {
			throw new CoreException(Status.error("Cannot locate bundle file: " + displayName, exception)); //$NON-NLS-1$
		}
		if (bundleFile == null || !bundleFile.exists()) {
			throw new CoreException(Status.error("Bundle file does not exist: " + displayName)); //$NON-NLS-1$
		}

		File classpathFile= bundleFile;
		if (bundleFile.isDirectory()) {
			File binDirectory= new File(bundleFile, "bin"); //$NON-NLS-1$
			if (binDirectory.isDirectory()) {
				classpathFile= binDirectory;
			}
		}
		IPath path= Path.fromOSString(classpathFile.getAbsolutePath());
		addToClasspath(javaProject, JavaCore.newLibraryEntry(path, null, null));
	}

	private static void addToClasspath(IJavaProject javaProject, IClasspathEntry entry)
			throws JavaModelException {
		IClasspathEntry[] current= javaProject.getRawClasspath();
		for (IClasspathEntry existing : current) {
			if (existing.equals(entry)) {
				return;
			}
		}

		IClasspathEntry[] updated= new IClasspathEntry[current.length + 1];
		System.arraycopy(current, 0, updated, 0, current.length);
		updated[current.length]= entry;
		javaProject.setRawClasspath(updated, null);
	}
}