JavaFileStrategy.java

/*******************************************************************************
 * Copyright (c) 2026 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
 *******************************************************************************/
package org.eclipse.jgit.storage.hibernate.search.strategies;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jgit.storage.hibernate.search.BlobIndexData;
import org.eclipse.jgit.storage.hibernate.search.FileTypeStrategy;
import org.eclipse.jgit.storage.hibernate.search.JavaStructureVisitor;

/**
 * Strategy for extracting structural metadata from Java source files using
 * JDT's AST parser.
 */
public class JavaFileStrategy implements FileTypeStrategy {

	private static final Logger LOG = Logger
			.getLogger(JavaFileStrategy.class.getName());

	private static final int MAX_SNIPPET_LENGTH = 65535;

	@Override
	public Set<String> supportedExtensions() {
		return Set.of(".java"); //$NON-NLS-1$
	}

	@Override
	public Set<String> supportedFilenames() {
		return Collections.emptySet();
	}

	@Override
	public BlobIndexData extract(String source, String filePath) {
		BlobIndexData data = new BlobIndexData();
		data.setFileType("java"); //$NON-NLS-1$
		data.setSourceSnippet(truncate(source, MAX_SNIPPET_LENGTH));

		// Extract projectName from path
		data.setProjectName(extractProjectName(filePath));
		// Extract simpleClassName from path
		data.setSimpleClassName(extractSimpleClassName(filePath));
		// Count lines
		data.setLineCount(countLines(source));

		try {
			@SuppressWarnings("deprecation")
			ASTParser parser = ASTParser.newParser(AST.JLS_Latest);
			parser.setResolveBindings(false);
			parser.setSource(source.toCharArray());
			parser.setKind(ASTParser.K_COMPILATION_UNIT);

			CompilationUnit cu = (CompilationUnit) parser.createAST(null);

			if (cu.getPackage() != null) {
				data.setPackageOrNamespace(cu.getPackage().getName()
						.getFullyQualifiedName());
			}

			Map<String, String> importMap = buildImportMap(cu);
			data.setImportStatements(serializeImports(cu));

			JavaStructureVisitor visitor = new JavaStructureVisitor(
					importMap, data.getPackageOrNamespace());
			cu.accept(visitor);

			data.setDeclaredTypes(visitor.getTypes());
			data.setFullyQualifiedNames(visitor.getFQNs());
			data.setDeclaredMethods(visitor.getMethods());
			data.setDeclaredFields(visitor.getFields());
			data.setExtendsTypes(visitor.getSuperTypes());
			data.setImplementsTypes(visitor.getInterfaces());
			data.setAnnotations(visitor.getAnnotations());
			data.setTypeKind(visitor.getTypeKind());
			data.setVisibility(visitor.getVisibility());
			data.setTypeDocumentation(visitor.getTypeDocumentation());
			data.setMethodSignatures(visitor.getMethodSignatures());
			data.setReferencedTypes(visitor.getReferencedTypes());
			data.setStringLiterals(visitor.getStringLiterals());
			data.setHasMainMethod(visitor.hasMainMethod());
		} catch (Exception e) {
			// Graceful degradation: return partial results on parse errors
			LOG.log(Level.WARNING,
					"Failed to parse Java source: {0}: {1} - returning partial results", //$NON-NLS-1$
					new Object[] { filePath, e.getMessage() });
		}

		return data;
	}

	@Override
	public String fileType() {
		return "java"; //$NON-NLS-1$
	}

	private static Map<String, String> buildImportMap(CompilationUnit cu) {
		Map<String, String> importMap = new HashMap<>();
		for (Object imp : cu.imports()) {
			if (imp instanceof ImportDeclaration importDecl) {
				String fqn = importDecl.getName().getFullyQualifiedName();
				if (!importDecl.isOnDemand()) {
					String simpleName = fqn
							.substring(fqn.lastIndexOf('.') + 1);
					importMap.put(simpleName, fqn);
				}
			}
		}
		return importMap;
	}

	private static String serializeImports(CompilationUnit cu) {
		StringBuilder sb = new StringBuilder();
		for (Object imp : cu.imports()) {
			if (imp instanceof ImportDeclaration importDecl) {
				if (sb.length() > 0) {
					sb.append('\n');
				}
				sb.append(importDecl.getName().getFullyQualifiedName());
				if (importDecl.isOnDemand()) {
					sb.append(".*"); //$NON-NLS-1$
				}
			}
		}
		return sb.toString();
	}

	private static String truncate(String text, int maxLength) {
		if (text == null) {
			return null;
		}
		if (text.length() <= maxLength) {
			return text;
		}
		return text.substring(0, maxLength);
	}

	private static String extractProjectName(String filePath) {
		// Try to find project name from path (segment before /src/ or /tst/)
		int srcIdx = filePath.indexOf("/src/"); //$NON-NLS-1$
		if (srcIdx < 0) {
			srcIdx = filePath.indexOf("/tst/"); //$NON-NLS-1$
		}
		if (srcIdx > 0) {
			String beforeSrc = filePath.substring(0, srcIdx);
			int lastSlash = beforeSrc.lastIndexOf('/');
			return lastSlash >= 0 ? beforeSrc.substring(lastSlash + 1)
					: beforeSrc;
		}
		return null;
	}

	private static String extractSimpleClassName(String filePath) {
		int lastSlash = filePath.lastIndexOf('/');
		String filename = lastSlash >= 0 ? filePath.substring(lastSlash + 1)
				: filePath;
		if (filename.endsWith(".java")) { //$NON-NLS-1$
			return filename.substring(0,
					filename.length() - ".java".length()); //$NON-NLS-1$
		}
		return filename;
	}

	private static int countLines(String source) {
		if (source == null || source.isEmpty()) {
			return 0;
		}
		int count = 1;
		for (int i = 0; i < source.length(); i++) {
			if (source.charAt(i) == '\n') {
				count++;
			}
		}
		return count;
	}
}