HintRegistry.java

/*******************************************************************************
 * Copyright (c) 2025 Carsten Hammer.
 *
 * 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
 *
 * Contributors:
 *     Carsten Hammer - initial API and implementation
 *******************************************************************************/
package org.sandbox.jdt.triggerpattern.internal;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.ILog;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.osgi.framework.Bundle;
import org.sandbox.jdt.triggerpattern.api.Hint;
import org.sandbox.jdt.triggerpattern.api.HintContext;
import org.sandbox.jdt.triggerpattern.api.Pattern;
import org.sandbox.jdt.triggerpattern.api.PatternKind;
import org.sandbox.jdt.triggerpattern.api.TriggerPattern;

/**
 * Registry for trigger pattern hints.
 * 
 * <p>This class discovers and loads hint providers from:
 * <ul>
 *   <li>Extension point contributions</li>
 *   <li>Annotated methods in registered provider classes</li>
 * </ul>
 * </p>
 * 
 * @since 1.2.2
 */
public class HintRegistry {
	
	private static final String EXTENSION_POINT_ID = "org.sandbox.jdt.triggerpattern.hints"; //$NON-NLS-1$
	
	private List<HintDescriptor> hints;
	private boolean initialized = false;
	
	/**
	 * Represents a registered hint with its pattern and invocation information.
	 */
	public static class HintDescriptor {
		private final Pattern pattern;
		private final String displayName;
		private final String description;
		private final boolean enabledByDefault;
		private final String severity;
		private final Class<?> providerClass;
		private final Method method;
		
		/**
		 * @param pattern
		 * @param displayName
		 * @param description
		 * @param enabledByDefault
		 * @param severity
		 * @param providerClass
		 * @param method
		 */
		public HintDescriptor(Pattern pattern, String displayName, String description,
				boolean enabledByDefault, String severity, Class<?> providerClass, Method method) {
			this.pattern = pattern;
			this.displayName = displayName;
			this.description = description;
			this.enabledByDefault = enabledByDefault;
			this.severity = severity;
			this.providerClass = providerClass;
			this.method = method;
		}
		
		/**
		 * @return
		 */
		public Pattern getPattern() {
			return pattern;
		}
		
		/**
		 * @return
		 */
		public String getDisplayName() {
			return displayName;
		}
		
		/**
		 * @return
		 */
		public String getDescription() {
			return description;
		}
		
		/**
		 * @return
		 */
		public boolean isEnabledByDefault() {
			return enabledByDefault;
		}
		
		/**
		 * @return
		 */
		public String getSeverity() {
			return severity;
		}
		
		/**
		 * Invokes the hint method with the given context.
		 * 
		 * @param context the hint context
		 * @return the result of the hint method (typically a completion proposal or list of proposals)
		 * @throws Exception if invocation fails
		 */
		public Object invoke(HintContext context) throws Exception {
			if (!Modifier.isStatic(method.getModifiers())) {
				throw new IllegalStateException("Hint method must be static: " + method); //$NON-NLS-1$
			}
			return method.invoke(null, context);
		}
	}
	
	/**
	 * Returns all registered hints.
	 * Lazily initializes the registry on first call.
	 * 
	 * @return list of hint descriptors
	 */
	public synchronized List<HintDescriptor> getHints() {
		if (!initialized) {
			loadHints();
			initialized = true;
		}
		return new ArrayList<>(hints);
	}
	
	/**
	 * Loads hints from extension points and annotations.
	 */
	private synchronized void loadHints() {
		List<HintDescriptor> loadedHints = new ArrayList<>();
		
		IExtensionRegistry registry = Platform.getExtensionRegistry();
		if (registry == null) {
			hints = loadedHints;
			return;
		}
		
		IConfigurationElement[] elements = registry.getConfigurationElementsFor(EXTENSION_POINT_ID);
		
		for (IConfigurationElement element : elements) {
			try {
				if ("hintProvider".equals(element.getName())) { //$NON-NLS-1$
					loadFromProvider(element, loadedHints);
				} else if ("pattern".equals(element.getName())) { //$NON-NLS-1$
					loadDeclarativePattern(element, loadedHints);
				}
			} catch (Exception e) {
				// Log error but continue with other hints
				ILog log = Platform.getLog(HintRegistry.class);
				log.log(Status.error("Error loading hint from extension point", e)); //$NON-NLS-1$
			}
		}
		
		hints = loadedHints;
	}
	
	/**
	 * Loads hints from a provider class that contains @TriggerPattern annotated methods.
	 */
	private void loadFromProvider(IConfigurationElement element, List<HintDescriptor> loadedHints) throws Exception {
		String className = element.getAttribute("class"); //$NON-NLS-1$
		if (className == null) {
			return;
		}
		
		// Load the class from the contributing bundle
		Bundle bundle = Platform.getBundle(element.getContributor().getName());
		if (bundle == null) {
			return;
		}
		
		Class<?> providerClass = bundle.loadClass(className);
		
		// Find all methods annotated with @TriggerPattern
		for (Method method : providerClass.getDeclaredMethods()) {
			TriggerPattern triggerPattern = method.getAnnotation(TriggerPattern.class);
			if (triggerPattern != null) {
				validateHintMethod(method);
				
				Pattern pattern = new Pattern(
					triggerPattern.value(),
					triggerPattern.kind(),
					triggerPattern.id().isEmpty() ? null : triggerPattern.id(),
					null
				);
				
				// Check for @Hint annotation
				Hint hintAnnotation = method.getAnnotation(Hint.class);
				String displayName = hintAnnotation != null ? hintAnnotation.displayName() : ""; //$NON-NLS-1$
				String description = hintAnnotation != null ? hintAnnotation.description() : ""; //$NON-NLS-1$
				boolean enabledByDefault = hintAnnotation == null || hintAnnotation.enabledByDefault();
				String severity = hintAnnotation != null ? hintAnnotation.severity() : "info"; //$NON-NLS-1$
				
				HintDescriptor descriptor = new HintDescriptor(
					pattern, displayName, description, enabledByDefault, severity, providerClass, method
				);
				loadedHints.add(descriptor);
			}
		}
	}
	
	/**
	 * Loads a declaratively defined pattern hint.
	 */
	private void loadDeclarativePattern(IConfigurationElement element, List<HintDescriptor> loadedHints) throws Exception {
		String id = element.getAttribute("id"); //$NON-NLS-1$
		String value = element.getAttribute("value"); //$NON-NLS-1$
		String kindStr = element.getAttribute("kind"); //$NON-NLS-1$
		String displayName = element.getAttribute("displayName"); //$NON-NLS-1$
		String className = element.getAttribute("class"); //$NON-NLS-1$
		String methodName = element.getAttribute("method"); //$NON-NLS-1$
		
		if (value == null || kindStr == null || className == null || methodName == null) {
			return;
		}
		
		PatternKind kind = PatternKind.valueOf(kindStr);
		Pattern pattern = new Pattern(value, kind, id, displayName);
		
		// Load the class and method
		Bundle bundle = Platform.getBundle(element.getContributor().getName());
		if (bundle == null) {
			return;
		}
		
		Class<?> providerClass = bundle.loadClass(className);
		Method method = providerClass.getMethod(methodName, HintContext.class);
		validateHintMethod(method);
		
		HintDescriptor descriptor = new HintDescriptor(
			pattern, displayName != null ? displayName : "", "", true, "info", providerClass, method //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		);
		loadedHints.add(descriptor);
	}
	
	/**
	 * Validates that a method can be used as a hint method.
	 */
	private void validateHintMethod(Method method) {
		if (!Modifier.isPublic(method.getModifiers())) {
			throw new IllegalArgumentException("Hint method must be public: " + method); //$NON-NLS-1$
		}
		if (!Modifier.isStatic(method.getModifiers())) {
			throw new IllegalArgumentException("Hint method must be static: " + method); //$NON-NLS-1$
		}
		Class<?>[] paramTypes = method.getParameterTypes();
		if (paramTypes.length != 1 || !HintContext.class.isAssignableFrom(paramTypes[0])) {
			throw new IllegalArgumentException(
				"Hint method must have exactly one parameter of type HintContext: " + method //$NON-NLS-1$
			);
		}
	}
}