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.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
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.eclipse.HintContext;
import org.sandbox.jdt.triggerpattern.api.HintKind;
import org.sandbox.jdt.triggerpattern.api.Pattern;
import org.sandbox.jdt.triggerpattern.api.PatternKind;
import org.sandbox.jdt.triggerpattern.api.Severity;
import org.sandbox.jdt.triggerpattern.api.TriggerPattern;
import org.sandbox.jdt.triggerpattern.api.TriggerPatterns;
import org.sandbox.jdt.triggerpattern.api.TriggerTreeKind;
/**
* 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 Severity severity;
private final String id;
private final String category;
private final String[] suppressWarnings;
private final HintKind hintKind;
private final String minSourceVersion;
private final Integer[] treeKinds; // For @TriggerTreeKind
private final Class<?> providerClass;
private final Method method;
/**
* Full constructor with all hint attributes.
*
* @param pattern the pattern to match (may be null for @TriggerTreeKind)
* @param displayName hint display name
* @param description hint description
* @param enabledByDefault whether enabled by default
* @param severity severity level
* @param id hint identifier
* @param category hint category
* @param suppressWarnings suppress warnings keys
* @param hintKind inspection or action
* @param minSourceVersion minimum Java version
* @param treeKinds AST node types to match (for @TriggerTreeKind)
* @param providerClass the provider class
* @param method the hint method
*/
public HintDescriptor(Pattern pattern, String displayName, String description,
boolean enabledByDefault, Severity severity, String id, String category,
String[] suppressWarnings, HintKind hintKind, String minSourceVersion,
Integer[] treeKinds, Class<?> providerClass, Method method) {
this.pattern = pattern;
this.displayName = displayName;
this.description = description;
this.enabledByDefault = enabledByDefault;
this.severity = severity;
this.id = id;
this.category = category;
this.suppressWarnings = suppressWarnings;
this.hintKind = hintKind;
this.minSourceVersion = minSourceVersion;
this.treeKinds = treeKinds;
this.providerClass = providerClass;
this.method = method;
}
/**
* Legacy constructor for backward compatibility.
*
* @deprecated Use the full constructor instead
*/
@Deprecated
public HintDescriptor(Pattern pattern, String displayName, String description,
boolean enabledByDefault, String severity, Class<?> providerClass, Method method) {
this(pattern, displayName, description, enabledByDefault,
parseSeverity(severity), "", "", new String[0], //$NON-NLS-1$ //$NON-NLS-2$
HintKind.INSPECTION, "", null, providerClass, method); //$NON-NLS-1$
}
private static Severity parseSeverity(String severityStr) {
if (severityStr == null) {
return Severity.INFO;
}
try {
return Severity.valueOf(severityStr.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException e) {
// Map common string values to enum
return switch (severityStr.toLowerCase(Locale.ROOT)) {
case "error" -> Severity.ERROR; //$NON-NLS-1$
case "warning" -> Severity.WARNING; //$NON-NLS-1$
case "hint" -> Severity.HINT; //$NON-NLS-1$
default -> Severity.INFO;
};
}
}
/**
* @return the hint pattern (may be null for tree kind hints)
*/
public Pattern getPattern() {
return pattern;
}
/**
* @return the display name
*/
public String getDisplayName() {
return displayName;
}
/**
* @return the description
*/
public String getDescription() {
return description;
}
/**
* @return whether enabled by default
*/
public boolean isEnabledByDefault() {
return enabledByDefault;
}
/**
* @return the severity level
*/
public Severity getSeverity() {
return severity;
}
/**
* @return the severity as string (for backward compatibility)
* @deprecated Use {@link #getSeverity()} instead
*/
@Deprecated
public String getSeverityString() {
return severity.name().toLowerCase(Locale.ROOT);
}
/**
* @return the hint ID
*/
public String getId() {
return id;
}
/**
* @return the category
*/
public String getCategory() {
return category;
}
/**
* @return suppress warnings keys
*/
public String[] getSuppressWarnings() {
return suppressWarnings;
}
/**
* @return the hint kind
*/
public HintKind getHintKind() {
return hintKind;
}
/**
* @return minimum source version
*/
public String getMinSourceVersion() {
return minSourceVersion;
}
/**
* @return tree kinds for @TriggerTreeKind hints (null for pattern hints)
*/
public Integer[] getTreeKinds() {
return treeKinds;
}
/**
* @return the provider class that contains the hint method
*/
public Class<?> getProviderClass() {
return providerClass;
}
/**
* 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);
} else if ("hintFile".equals(element.getName())) { //$NON-NLS-1$
loadHintFile(element);
}
} 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 with hint annotations
for (Method method : providerClass.getDeclaredMethods()) {
// Check for @TriggerPattern
TriggerPattern triggerPattern = method.getAnnotation(TriggerPattern.class);
if (triggerPattern != null) {
loadTriggerPatternHint(method, triggerPattern, providerClass, loadedHints);
}
// Check for @TriggerPatterns (container)
TriggerPatterns triggerPatterns = method.getAnnotation(TriggerPatterns.class);
if (triggerPatterns != null) {
for (TriggerPattern tp : triggerPatterns.value()) {
loadTriggerPatternHint(method, tp, providerClass, loadedHints);
}
}
// Check for @TriggerTreeKind
TriggerTreeKind treeKind = method.getAnnotation(TriggerTreeKind.class);
if (treeKind != null) {
loadTreeKindHint(method, treeKind, providerClass, loadedHints);
}
}
}
/**
* Loads a single @TriggerPattern hint.
*/
private void loadTriggerPatternHint(Method method, TriggerPattern triggerPattern,
Class<?> providerClass, List<HintDescriptor> loadedHints) throws Exception {
validateHintMethod(method);
Pattern pattern = new Pattern(
triggerPattern.value(),
triggerPattern.kind(),
triggerPattern.id().isEmpty() ? null : triggerPattern.id(),
null, null, null, null
);
HintDescriptor descriptor = createHintDescriptor(pattern, null, method, providerClass);
loadedHints.add(descriptor);
}
/**
* Loads a @TriggerTreeKind hint.
*/
private void loadTreeKindHint(Method method, TriggerTreeKind treeKind,
Class<?> providerClass, List<HintDescriptor> loadedHints) throws Exception {
validateHintMethod(method);
// Convert int[] to Integer[]
int[] kinds = treeKind.value();
Integer[] treeKinds = new Integer[kinds.length];
for (int i = 0; i < kinds.length; i++) {
treeKinds[i] = kinds[i];
}
HintDescriptor descriptor = createHintDescriptor(null, treeKinds, method, providerClass);
loadedHints.add(descriptor);
}
/**
* Creates a HintDescriptor from a method and its @Hint annotation.
*/
private HintDescriptor createHintDescriptor(Pattern pattern, Integer[] treeKinds,
Method method, Class<?> providerClass) {
// 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();
Severity severity = hintAnnotation != null ? hintAnnotation.severity() : Severity.INFO;
String id = hintAnnotation != null ? hintAnnotation.id() : ""; //$NON-NLS-1$
String category = hintAnnotation != null ? hintAnnotation.category() : ""; //$NON-NLS-1$
String[] suppressWarnings = hintAnnotation != null ? hintAnnotation.suppressWarnings() : new String[0];
HintKind hintKind = hintAnnotation != null ? hintAnnotation.hintKind() : HintKind.INSPECTION;
String minSourceVersion = hintAnnotation != null ? hintAnnotation.minSourceVersion() : ""; //$NON-NLS-1$
return new HintDescriptor(pattern, displayName, description, enabledByDefault,
severity, id, category, suppressWarnings, hintKind, minSourceVersion,
treeKinds, providerClass, method);
}
/**
* 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, null, null, null);
// 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, //$NON-NLS-1$ //$NON-NLS-2$
Severity.INFO, "", "", new String[0], //$NON-NLS-1$ //$NON-NLS-2$
HintKind.INSPECTION, "", null, providerClass, method //$NON-NLS-1$
);
loadedHints.add(descriptor);
}
/**
* Loads a {@code .sandbox-hint} file registered via the extension point.
*
* <p>The hint file is loaded from the contributing plugin's classpath
* and registered with the {@link HintFileRegistry}.</p>
*
* @param element the configuration element with {@code id} and {@code resource} attributes
*/
private void loadHintFile(IConfigurationElement element) {
String id = element.getAttribute("id"); //$NON-NLS-1$
String resource = element.getAttribute("resource"); //$NON-NLS-1$
if (id == null || resource == null) {
return;
}
Bundle bundle = Platform.getBundle(element.getContributor().getName());
if (bundle == null) {
return;
}
try {
java.net.URL resourceUrl = bundle.getResource(resource);
if (resourceUrl == null) {
ILog log = Platform.getLog(HintRegistry.class);
log.log(Status.warning("Hint file resource not found: " + resource //$NON-NLS-1$
+ " in bundle " + bundle.getSymbolicName())); //$NON-NLS-1$
return;
}
try (InputStream is = resourceUrl.openStream();
Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
HintFileRegistry.getInstance().loadFromReader(id, reader);
}
} catch (HintFileParser.HintParseException | IOException e) {
ILog log = Platform.getLog(HintRegistry.class);
log.log(Status.error("Error loading hint file: " + resource, e)); //$NON-NLS-1$
}
}
/**
* 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$
);
}
}
}