This is an automated email from the ASF dual-hosted git repository. fmariani pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel.git
commit 33b30df35ad3b9c80b75fec534dbb361a237d411 Author: Croway <[email protected]> AuthorDate: Mon Feb 16 18:16:52 2026 +0100 (feat): Camel MCP - Migration/Upgrades tools --- dsl/camel-jbang/camel-jbang-mcp/pom.xml | 60 +++ .../dsl/jbang/core/commands/mcp/MigrationData.java | 553 +++++++++++++++++++++ .../core/commands/mcp/MigrationResources.java | 102 ++++ .../jbang/core/commands/mcp/MigrationTools.java | 395 +++++++++++++++ .../commands/mcp/MigrationWildflyKarafTools.java | 179 +++++++ .../core/commands/mcp/MigrationDataSearchTest.java | 64 +++ 6 files changed, 1353 insertions(+) diff --git a/dsl/camel-jbang/camel-jbang-mcp/pom.xml b/dsl/camel-jbang/camel-jbang-mcp/pom.xml index be3855f34d2b..629840ad673d 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/pom.xml +++ b/dsl/camel-jbang/camel-jbang-mcp/pom.xml @@ -106,6 +106,13 @@ <artifactId>camel-yaml-dsl</artifactId> </dependency> + <!-- Apache Commons Text for fuzzy string matching (Levenshtein distance) --> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-text</artifactId> + <version>${commons-text-version}</version> + </dependency> + <!-- test dependencies --> <dependency> <groupId>io.quarkus</groupId> @@ -136,6 +143,59 @@ </execution> </executions> </plugin> + <!-- Copy migration guide .adoc files into the uber-JAR for runtime search --> + <plugin> + <artifactId>maven-resources-plugin</artifactId> + <executions> + <execution> + <id>copy-migration-guides</id> + <phase>generate-resources</phase> + <goals> + <goal>copy-resources</goal> + </goals> + <configuration> + <outputDirectory>${project.build.outputDirectory}/migration-guides</outputDirectory> + <resources> + <resource> + <directory>${project.basedir}/../../../docs/user-manual/modules/ROOT/pages</directory> + <filtering>false</filtering> + <includes> + <include>camel-3-migration-guide.adoc</include> + <include>camel-4-migration-guide.adoc</include> + <include>camel-3x-upgrade-guide-3_*.adoc</include> + <include>camel-4x-upgrade-guide-4_*.adoc</include> + </includes> + </resource> + </resources> + </configuration> + </execution> + </executions> + </plugin> + <!-- Generate index.txt listing all copied migration guide filenames --> + <plugin> + <artifactId>maven-antrun-plugin</artifactId> + <executions> + <execution> + <id>generate-migration-guide-index</id> + <phase>process-resources</phase> + <goals> + <goal>run</goal> + </goals> + <configuration> + <target> + <fileset id="guide-files" dir="${project.build.outputDirectory}/migration-guides"> + <include name="*.adoc"/> + </fileset> + <pathconvert property="guide-list" refid="guide-files" pathsep="${line.separator}"> + <flattenmapper/> + </pathconvert> + <echo file="${project.build.outputDirectory}/migration-guides/index.txt" + message="${guide-list}"/> + </target> + </configuration> + </execution> + </executions> + </plugin> <plugin> <artifactId>maven-compiler-plugin</artifactId> <configuration> diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationData.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationData.java new file mode 100644 index 000000000000..11ebb293e3eb --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationData.java @@ -0,0 +1,553 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.commands.mcp; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import org.apache.commons.text.similarity.LevenshteinDistance; + +/** + * Shared holder for migration reference data used by {@link MigrationTools}, {@link MigrationWildflyKarafTools}, and + * {@link MigrationResources}. + * <p> + * Contains migration guide metadata, WildFly/Karaf detection markers, and POM parsing utilities. Component-level + * migration details (renames, discontinued components, API changes) are intentionally not included here — the LLM + * should consult the migration guides directly for that information. + */ +@ApplicationScoped +public class MigrationData { + + // Migration guides + + private static final List<MigrationGuide> MIGRATION_GUIDES = Arrays.asList( + new MigrationGuide( + "migration-and-upgrade", + "Migration and Upgrade", + "https://camel.apache.org/manual/migration-and-upgrade.html", + "Overview of Camel migration and upgrade options across all versions."), + new MigrationGuide( + "camel-3-migration", + "Camel 3 Migration Guide", + "https://camel.apache.org/manual/camel-3-migration-guide.html", + "Guide for migrating from Camel 2.x to Camel 3.0."), + new MigrationGuide( + "camel-4-migration", + "Camel 4 Migration Guide", + "https://camel.apache.org/manual/camel-4-migration-guide.html", + "Guide for migrating from Camel 3.x to Camel 4.0."), + new MigrationGuide( + "camel-3x-upgrade", + "Camel 3.x Upgrade Guide", + "https://camel.apache.org/manual/camel-3x-upgrade-guide.html", + "Detailed upgrade notes for Camel 3.x minor version upgrades."), + new MigrationGuide( + "camel-4x-upgrade", + "Camel 4.x Upgrade Guide", + "https://camel.apache.org/manual/camel-4x-upgrade-guide.html", + "Detailed upgrade notes for Camel 4.x minor version upgrades.")); + + // WildFly/Karaf detection markers + + private static final List<String> WILDFLY_MARKERS = Arrays.asList( + "org.wildfly.camel", "org.wildfly", + "org.wildfly.plugins", + "javax.ejb", "jakarta.ejb", + "camel-cdi", "maven-war-plugin"); + + private static final List<String> KARAF_MARKERS = Arrays.asList( + "org.apache.karaf", "camel-blueprint", + "org.ops4j.pax", "camel-core-osgi", + "maven-bundle-plugin"); + + // Accessors + + public List<MigrationGuide> getMigrationGuides() { + return MIGRATION_GUIDES; + } + + public MigrationGuide getMigrationGuide(String name) { + return MIGRATION_GUIDES.stream() + .filter(g -> g.name().equals(name)) + .findFirst() + .orElse(null); + } + + public List<String> getWildflyMarkers() { + return WILDFLY_MARKERS; + } + + public List<String> getKarafMarkers() { + return KARAF_MARKERS; + } + + /** + * Get relevant migration guides based on the detected Camel major version. + */ + public List<MigrationGuide> getGuidesForVersion(int majorVersion) { + List<MigrationGuide> guides = new ArrayList<>(); + guides.add(getMigrationGuide("migration-and-upgrade")); + if (majorVersion <= 2) { + guides.add(getMigrationGuide("camel-3-migration")); + guides.add(getMigrationGuide("camel-4-migration")); + } else if (majorVersion == 3) { + guides.add(getMigrationGuide("camel-3x-upgrade")); + guides.add(getMigrationGuide("camel-4-migration")); + } else { + guides.add(getMigrationGuide("camel-4x-upgrade")); + } + return guides; + } + + // POM parsing + + /** + * Parse pom.xml content and extract project analysis data. + */ + public static PomAnalysis parsePomContent(String pomContent) throws Exception { + DocumentBuilderFactory factory = createSecureDocumentBuilderFactory(); + Document doc = factory.newDocumentBuilder() + .parse(new ByteArrayInputStream(pomContent.getBytes(StandardCharsets.UTF_8))); + + String camelVersion = null; + String springBootVersion = null; + String quarkusVersion = null; + String javaVersion = null; + boolean isWildfly = false; + boolean isKaraf = false; + List<String> dependencies = new ArrayList<>(); + + // Detect packaging type (war packaging is a strong WildFly/app-server signal) + String packaging = getElementText(doc.getDocumentElement(), "packaging"); + if ("war".equalsIgnoreCase(packaging)) { + isWildfly = true; + } + + // Extract properties + NodeList propertiesNodes = doc.getElementsByTagName("properties"); + if (propertiesNodes.getLength() > 0) { + Element properties = (Element) propertiesNodes.item(0); + camelVersion = getElementText(properties, "camel.version"); + if (camelVersion == null) { + camelVersion = getElementText(properties, "camel-version"); + } + springBootVersion = getElementText(properties, "spring-boot.version"); + if (springBootVersion == null) { + springBootVersion = getElementText(properties, "spring-boot-version"); + } + quarkusVersion = getElementText(properties, "quarkus.platform.version"); + if (quarkusVersion == null) { + quarkusVersion = getElementText(properties, "quarkus-plugin.version"); + } + javaVersion = getElementText(properties, "maven.compiler.release"); + if (javaVersion == null) { + javaVersion = getElementText(properties, "maven.compiler.source"); + } + if (javaVersion == null) { + javaVersion = getElementText(properties, "maven.compiler.target"); + } + } + + // Scan dependencyManagement and dependencies for version and runtime detection + NodeList allDeps = doc.getElementsByTagName("dependency"); + for (int i = 0; i < allDeps.getLength(); i++) { + Element dep = (Element) allDeps.item(i); + String groupId = getElementText(dep, "groupId"); + String artifactId = getElementText(dep, "artifactId"); + String version = getElementText(dep, "version"); + + if (groupId == null || artifactId == null) { + continue; + } + + // Detect Camel version from BOM + if (camelVersion == null && version != null && !version.startsWith("$")) { + if ("camel-bom".equals(artifactId) || "camel-spring-boot-bom".equals(artifactId) + || "camel-quarkus-bom".equals(artifactId)) { + camelVersion = version; + } + } + + // Detect Spring Boot + if (springBootVersion == null && version != null && !version.startsWith("$")) { + if ("spring-boot-dependencies".equals(artifactId) + || "spring-boot-starter-parent".equals(artifactId)) { + springBootVersion = version; + } + } + + // Detect Quarkus + if (quarkusVersion == null && version != null && !version.startsWith("$")) { + if ("quarkus-bom".equals(artifactId)) { + quarkusVersion = version; + } + } + + // Detect WildFly + for (String marker : WILDFLY_MARKERS) { + if (groupId.contains(marker) || artifactId.contains(marker)) { + isWildfly = true; + } + } + + // Detect Karaf + for (String marker : KARAF_MARKERS) { + if (groupId.contains(marker) || artifactId.contains(marker)) { + isKaraf = true; + } + } + + // Collect Camel dependencies + if (artifactId.startsWith("camel-")) { + dependencies.add(artifactId); + } + } + + return new PomAnalysis( + camelVersion, springBootVersion, quarkusVersion, javaVersion, + dependencies, isWildfly, isKaraf); + } + + // XML helper + + private static DocumentBuilderFactory createSecureDocumentBuilderFactory() { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setIgnoringElementContentWhitespace(true); + factory.setIgnoringComments(true); + try { + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, Boolean.TRUE); + } catch (ParserConfigurationException e) { + // ignore + } + try { + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + } catch (ParserConfigurationException e) { + // ignore + } + try { + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + } catch (ParserConfigurationException e) { + // ignore + } + return factory; + } + + private static String getElementText(Element parent, String tagName) { + NodeList nodes = parent.getElementsByTagName(tagName); + if (nodes.getLength() > 0) { + String text = nodes.item(0).getTextContent(); + if (text != null && !text.isBlank()) { + return text.trim(); + } + } + return null; + } + + // Guide search + + private static final String GUIDES_RESOURCE_DIR = "migration-guides/"; + private static final String GUIDES_INDEX_FILE = GUIDES_RESOURCE_DIR + "index.txt"; + + private volatile List<GuideSection> guideIndex; + + /** + * Get the guide index, loading lazily on first access. + */ + public synchronized List<GuideSection> getGuideIndex() { + if (guideIndex == null) { + guideIndex = loadGuideSections(); + } + return guideIndex; + } + + /** + * Search migration guides for sections matching the query using fuzzy matching. + */ + public List<GuideSection> searchGuides(String query, int maxResults) { + List<String> queryTokens = tokenize(query); + String lowerQuery = query.toLowerCase(Locale.ROOT); + + List<GuideSection> index = getGuideIndex(); + List<ScoredSection> scored = new ArrayList<>(); + + for (GuideSection section : index) { + double score = scoreSection(section, queryTokens, lowerQuery); + if (score > 0) { + scored.add(new ScoredSection(section, score)); + } + } + + scored.sort(Comparator.comparingDouble(ScoredSection::score).reversed()); + + return scored.stream() + .limit(maxResults) + .map(ScoredSection::section) + .collect(Collectors.toList()); + } + + private double scoreSection(GuideSection section, List<String> queryTokens, String lowerQuery) { + double score = 0; + String lowerContent = section.content().toLowerCase(Locale.ROOT); + + // Bonus for exact substring match of the full query + if (lowerContent.contains(lowerQuery)) { + score += 10; + } + + // Token-level scoring + for (String qt : queryTokens) { + boolean exactFound = false; + boolean fuzzyFound = false; + boolean substringFound = false; + + for (String dt : section.tokens()) { + if (dt.equals(qt)) { + exactFound = true; + break; + } + int maxDist = Math.max(1, Math.min(qt.length(), dt.length()) / 4); + if (levenshteinDistance(qt, dt) <= maxDist) { + fuzzyFound = true; + } + } + + if (!exactFound && !fuzzyFound && lowerContent.contains(qt)) { + substringFound = true; + } + + if (exactFound) { + score += 3; + } else if (fuzzyFound) { + score += 2; + } else if (substringFound) { + score += 1; + } + } + + return score; + } + + private List<GuideSection> loadGuideSections() { + List<GuideSection> sections = new ArrayList<>(); + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + + // Read index.txt to discover guide filenames + List<String> guideFiles = new ArrayList<>(); + InputStream indexStream = cl.getResourceAsStream(GUIDES_INDEX_FILE); + if (indexStream != null) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(indexStream, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + String trimmed = line.trim(); + if (!trimmed.isEmpty() && trimmed.endsWith(".adoc")) { + guideFiles.add(trimmed); + } + } + } catch (IOException e) { + // fall through with empty list + } + } + + for (String filename : guideFiles) { + InputStream is = cl.getResourceAsStream(GUIDES_RESOURCE_DIR + filename); + if (is == null) { + continue; + } + try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + String content = reader.lines().collect(Collectors.joining("\n")); + String guideName = guideNameFromFilename(filename); + String version = versionFromFilename(filename); + String url = urlFromFilename(filename); + + splitIntoSections(content, guideName, version, url, sections); + } catch (IOException e) { + // skip unreadable files + } + } + + return sections; + } + + private void splitIntoSections( + String content, String guideName, String version, String url, + List<GuideSection> sections) { + String[] lines = content.split("\n"); + StringBuilder currentContent = new StringBuilder(); + String currentTitle = guideName; + + for (String line : lines) { + if (line.startsWith("== ") && !line.startsWith("===")) { + // Flush previous section + if (currentContent.length() > 0) { + String sectionText = currentContent.toString().trim(); + if (!sectionText.isEmpty()) { + sections.add(new GuideSection( + guideName, version, currentTitle, sectionText, url, + tokenize(sectionText))); + } + } + currentTitle = line.substring(3).trim(); + currentContent = new StringBuilder(); + } else { + currentContent.append(line).append("\n"); + } + } + + // Flush last section + if (currentContent.length() > 0) { + String sectionText = currentContent.toString().trim(); + if (!sectionText.isEmpty()) { + sections.add(new GuideSection( + guideName, version, currentTitle, sectionText, url, + tokenize(sectionText))); + } + } + } + + /** + * Tokenize text into lowercase words, preserving hyphens within words (e.g., direct-vm). + */ + static List<String> tokenize(String text) { + String[] parts = text.toLowerCase(Locale.ROOT).split("[^a-zA-Z0-9\\-]+"); + List<String> tokens = new ArrayList<>(); + for (String part : parts) { + if (!part.isBlank() && part.length() > 1) { + tokens.add(part); + } + } + return tokens; + } + + private static final LevenshteinDistance LEVENSHTEIN = LevenshteinDistance.getDefaultInstance(); + + static int levenshteinDistance(String a, String b) { + return LEVENSHTEIN.apply(a, b); + } + + private static String guideNameFromFilename(String filename) { + String name = filename.replace(".adoc", ""); + if ("camel-3-migration-guide".equals(name)) { + return "Camel 3 Migration Guide (2.x to 3.0)"; + } + if ("camel-4-migration-guide".equals(name)) { + return "Camel 4 Migration Guide (3.x to 4.0)"; + } + String version = versionFromFilename(filename); + return "Camel " + version + " Upgrade Guide"; + } + + private static String versionFromFilename(String filename) { + String name = filename.replace(".adoc", ""); + if ("camel-3-migration-guide".equals(name)) { + return "3.0"; + } + if ("camel-4-migration-guide".equals(name)) { + return "4.0"; + } + int lastDash = name.lastIndexOf('-'); + if (lastDash > 0) { + return name.substring(lastDash + 1).replace('_', '.'); + } + return name; + } + + private static String urlFromFilename(String filename) { + String name = filename.replace(".adoc", ""); + return "https://camel.apache.org/manual/" + name + ".html"; + } + + // Records + + public record MigrationGuide(String name, String title, String url, String summary) { + } + + public record GuideSection( + String guide, + String version, + String sectionTitle, + String content, + String url, + List<String> tokens) { + } + + private record ScoredSection(GuideSection section, double score) { + } + + public record PomAnalysis( + String camelVersion, + String springBootVersion, + String quarkusVersion, + String javaVersion, + List<String> dependencies, + boolean isWildfly, + boolean isKaraf) { + + /** + * Determine the runtime type from the POM analysis. + */ + public String runtimeType() { + if (isWildfly) { + return "wildfly"; + } + if (isKaraf) { + return "karaf"; + } + if (springBootVersion != null || dependencies.stream().anyMatch(d -> d.contains("spring-boot"))) { + return "spring-boot"; + } + if (quarkusVersion != null || dependencies.stream().anyMatch(d -> d.contains("quarkus"))) { + return "quarkus"; + } + return "main"; + } + + /** + * Get the major version number from the Camel version string. + */ + public int majorVersion() { + if (camelVersion == null) { + return 0; + } + try { + return Integer.parseInt(camelVersion.split("\\.")[0]); + } catch (NumberFormatException e) { + return 0; + } + } + } +} diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationResources.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationResources.java new file mode 100644 index 000000000000..cd4790ef31b1 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationResources.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.commands.mcp; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import io.quarkiverse.mcp.server.Resource; +import io.quarkiverse.mcp.server.ResourceTemplate; +import io.quarkiverse.mcp.server.ResourceTemplateArg; +import io.quarkiverse.mcp.server.TextResourceContents; +import org.apache.camel.util.json.JsonArray; +import org.apache.camel.util.json.JsonObject; + +/** + * MCP Resources exposing migration guide reference data. + * <p> + * These resources provide browseable migration guide metadata that clients can pull into context for Camel version + * upgrades and runtime migrations. + */ +@ApplicationScoped +public class MigrationResources { + + @Inject + MigrationData migrationData; + + /** + * All Camel migration guides with titles, URLs, and summaries. + */ + @Resource(uri = "camel://migration/guides", + name = "camel_migration_guides", + title = "Camel Migration Guides", + description = "List of all Camel migration and upgrade guides with titles, URLs, and summaries " + + "covering Camel 2.x to 3.x, 3.x to 4.x, and minor version upgrades.", + mimeType = "application/json") + public TextResourceContents migrationGuides() { + JsonObject result = new JsonObject(); + + JsonArray guides = new JsonArray(); + for (MigrationData.MigrationGuide guide : migrationData.getMigrationGuides()) { + JsonObject guideJson = new JsonObject(); + guideJson.put("name", guide.name()); + guideJson.put("title", guide.title()); + guideJson.put("url", guide.url()); + guideJson.put("summary", guide.summary()); + guides.add(guideJson); + } + + result.put("guides", guides); + result.put("totalCount", guides.size()); + + return new TextResourceContents("camel://migration/guides", result.toJson(), "application/json"); + } + + /** + * Detail for a specific migration guide by name. + */ + @ResourceTemplate(uriTemplate = "camel://migration/guide/{name}", + name = "camel_migration_guide_detail", + title = "Migration Guide Detail", + description = "Detail for a specific Camel migration guide including title, URL, and summary.", + mimeType = "application/json") + public TextResourceContents migrationGuideDetail( + @ResourceTemplateArg(name = "name") String name) { + + String uri = "camel://migration/guide/" + name; + + MigrationData.MigrationGuide guide = migrationData.getMigrationGuide(name); + if (guide == null) { + JsonObject result = new JsonObject(); + result.put("name", name); + result.put("found", false); + result.put("message", "Migration guide '" + name + "' not found. " + + "Available guides: migration-and-upgrade, camel-3-migration, " + + "camel-4-migration, camel-3x-upgrade, camel-4x-upgrade."); + return new TextResourceContents(uri, result.toJson(), "application/json"); + } + + JsonObject result = new JsonObject(); + result.put("name", guide.name()); + result.put("found", true); + result.put("title", guide.title()); + result.put("url", guide.url()); + result.put("summary", guide.summary()); + + return new TextResourceContents(uri, result.toJson(), "application/json"); + } +} diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationTools.java new file mode 100644 index 000000000000..24654dd5f4c0 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationTools.java @@ -0,0 +1,395 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.commands.mcp; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import io.quarkiverse.mcp.server.Tool; +import io.quarkiverse.mcp.server.ToolArg; +import io.quarkiverse.mcp.server.ToolCallException; + +/** + * MCP Tools for Camel migration and upgrade workflows. + * <p> + * Provides a guided, step-by-step migration pipeline: analyze → compatibility → recipes → guide search. Each tool + * returns a {@code nextStep} hint that guides the LLM to the next action in the workflow. For migration summaries, use + * {@code git diff --shortstat} directly. + */ +@ApplicationScoped +public class MigrationTools { + + private static final String OPENREWRITE_VERSION = "6.29.0"; + private static final String CAMEL_UPGRADE_RECIPES_ARTIFACT = "camel-upgrade-recipes"; + private static final String CAMEL_SPRING_BOOT_UPGRADE_RECIPES_ARTIFACT = "camel-spring-boot-upgrade-recipes"; + + @Inject + MigrationData migrationData; + + /** + * Step 1: Analyze a project's pom.xml to detect runtime, Camel version, Java version, and components. + */ + @Tool(description = "Analyze a Camel project's pom.xml to detect the runtime type (main, spring-boot, quarkus, " + + "wildfly, karaf), Camel version, Java version, and Camel component dependencies. " + + "This is the first step in a migration workflow.") + public ProjectAnalysisResult camel_migration_analyze( + @ToolArg(description = "The pom.xml file content") String pomContent) { + + if (pomContent == null || pomContent.isBlank()) { + throw new ToolCallException("pomContent is required", null); + } + + try { + MigrationData.PomAnalysis pom = MigrationData.parsePomContent(pomContent); + + String runtimeType = pom.runtimeType(); + int majorVersion = pom.majorVersion(); + + List<String> warnings = new ArrayList<>(); + if (pom.camelVersion() == null) { + warnings.add("Could not detect Camel version from pom.xml. " + + "Check if the version is defined in a parent POM."); + } + if (pom.javaVersion() == null) { + warnings.add("Could not detect Java version from pom.xml."); + } + + List<String> guideUrls = migrationData.getGuidesForVersion(majorVersion).stream() + .map(MigrationData.MigrationGuide::url) + .collect(Collectors.toList()); + + String nextStep; + if ("wildfly".equals(runtimeType) || "karaf".equals(runtimeType)) { + nextStep = "Call camel_migration_wildfly_karaf to get migration guidance for " + runtimeType + "."; + } else { + nextStep = "Call camel_migration_compatibility with the detected components and target version."; + } + + return new ProjectAnalysisResult( + pom.camelVersion(), majorVersion, runtimeType, pom.javaVersion(), + pom.dependencies(), warnings, guideUrls, nextStep); + } catch (ToolCallException e) { + throw e; + } catch (Exception e) { + throw new ToolCallException("Failed to parse pom.xml: " + e.getMessage(), e); + } + } + + /** + * Step 2: Check compatibility and provide relevant migration guide references. + */ + @Tool(description = "Check migration compatibility for Camel components by providing relevant migration guide " + + "URLs and Java version requirements. The LLM should consult the migration guides for " + + "detailed component rename mappings and API changes.") + public CompatibilityResult camel_migration_compatibility( + @ToolArg(description = "Comma-separated list of Camel component artifactIds") String camelComponents, + @ToolArg(description = "Current Camel version (e.g., 3.20.0)") String currentVersion, + @ToolArg(description = "Target Camel version (e.g., 4.18.0)") String targetVersion, + @ToolArg(description = "Runtime type: main, spring-boot, or quarkus") String runtime, + @ToolArg(description = "Current Java version (e.g., 11, 17, 21)") String javaVersion) { + + if (camelComponents == null || camelComponents.isBlank()) { + throw new ToolCallException("camelComponents is required", null); + } + if (currentVersion == null || currentVersion.isBlank()) { + throw new ToolCallException("currentVersion is required", null); + } + if (targetVersion == null || targetVersion.isBlank()) { + throw new ToolCallException("targetVersion is required", null); + } + + int currentMajor = parseMajorVersion(currentVersion); + int targetMajor = parseMajorVersion(targetVersion); + boolean crossesMajorVersion = currentMajor != targetMajor; + + // Java version check + List<String> blockers = new ArrayList<>(); + List<String> warnings = new ArrayList<>(); + JavaCompatibility javaCompat = null; + + if (javaVersion != null && !javaVersion.isBlank()) { + int javaVer = parseJavaVersion(javaVersion); + boolean javaCompatible = true; + String requiredVersion = "17"; + + if (targetMajor >= 4 && javaVer < 17) { + javaCompatible = false; + blockers.add("Camel 4.x requires Java 17+. Current Java version is " + javaVersion + "."); + } + + javaCompat = new JavaCompatibility(javaVersion, requiredVersion, javaCompatible); + } + + // Collect relevant migration guides + List<MigrationData.MigrationGuide> guides = new ArrayList<>(); + guides.add(migrationData.getMigrationGuide("migration-and-upgrade")); + if (crossesMajorVersion) { + if (currentMajor <= 2 && targetMajor >= 3) { + guides.add(migrationData.getMigrationGuide("camel-3-migration")); + } + if (currentMajor <= 3 && targetMajor >= 4) { + guides.add(migrationData.getMigrationGuide("camel-4-migration")); + } + } else { + if (targetMajor == 3) { + guides.add(migrationData.getMigrationGuide("camel-3x-upgrade")); + } else if (targetMajor >= 4) { + guides.add(migrationData.getMigrationGuide("camel-4x-upgrade")); + } + } + + List<String> guideUrls = guides.stream() + .map(MigrationData.MigrationGuide::url) + .collect(Collectors.toList()); + + if (crossesMajorVersion) { + warnings.add("This is a major version upgrade (" + currentMajor + ".x → " + targetMajor + + ".x). Review the migration guides for component renames, " + + "discontinued components, and API changes."); + } + + String nextStep; + if (!blockers.isEmpty()) { + nextStep = "Resolve blockers, then call camel_migration_recipes for OpenRewrite commands."; + } else { + nextStep = "Call camel_migration_recipes to get the OpenRewrite upgrade commands."; + } + + return new CompatibilityResult( + blockers.isEmpty(), guideUrls, javaCompat, warnings, blockers, nextStep); + } + + /** + * Step 3: Get Maven commands to run OpenRewrite migration recipes. + */ + @Tool(description = "Get Maven commands to run Camel OpenRewrite migration recipes for upgrading between versions. " + + "Returns the exact Maven commands to execute on the project. " + + "PREREQUISITE: The project MUST compile successfully ('mvn clean compile' must pass) " + + "BEFORE running the OpenRewrite recipes. If the project does not compile, fix the build " + + "errors first. OpenRewrite requires a compilable project to parse and transform the code.") + public MigrationRecipesResult camel_migration_recipes( + @ToolArg(description = "Runtime type: main, spring-boot, or quarkus") String runtime, + @ToolArg(description = "Current Camel version (e.g., 4.4.0)") String currentVersion, + @ToolArg(description = "Target Camel version (e.g., 4.18.0)") String targetVersion, + @ToolArg(description = "Current Java version (e.g., 11, 17)") String javaVersion, + @ToolArg(description = "If true, perform a dry run without making changes (default: true)") Boolean dryRun) { + + if (runtime == null || runtime.isBlank()) { + throw new ToolCallException("runtime is required", null); + } + if (targetVersion == null || targetVersion.isBlank()) { + throw new ToolCallException("targetVersion is required", null); + } + + boolean isDryRun = dryRun == null || dryRun; + String runMode = isDryRun ? "dryRun" : "run"; + List<String> mavenCommands = new ArrayList<>(); + List<String> notes = new ArrayList<>(); + + String resolvedRuntime = runtime.toLowerCase().trim(); + + if ("quarkus".equals(resolvedRuntime)) { + // The quarkus-maven-plugin:update command internally uses OpenRewrite recipes + // including Camel-specific recipes (e.g., io.quarkus.updates.camel.camel44.CamelQuarkusMigrationRecipe) + // to handle both Quarkus platform and Camel Quarkus upgrades. + String quarkusCommand; + if (targetVersion != null && !targetVersion.isBlank()) { + quarkusCommand = String.format( + "mvn --no-transfer-progress io.quarkus.platform:quarkus-maven-plugin:%s:update %s", + targetVersion, isDryRun ? "-DrewriteDryRun" : "-DrewriteFullRun"); + } else { + quarkusCommand = String.format( + "mvn --no-transfer-progress io.quarkus.platform:quarkus-maven-plugin:update %s", + isDryRun ? "-DrewriteDryRun" : "-DrewriteFullRun"); + } + mavenCommands.add(quarkusCommand); + notes.add("The quarkus-maven-plugin:update command uses OpenRewrite under the hood. " + + "It automatically applies both Quarkus platform upgrade recipes and Camel Quarkus " + + "migration recipes (e.g., CamelQuarkusMigrationRecipe). " + + "Do NOT use the standalone camel-upgrade-recipes artifact for Camel Quarkus projects — " + + "the Quarkus update command already includes the appropriate Camel recipes."); + notes.add("IMPORTANT: This command only works on existing Quarkus projects (version 2.13+). " + + "If migrating from a non-Quarkus runtime (WildFly, Karaf, WAR), " + + "you must first create a new Quarkus project using the archetype from " + + "camel_migration_wildfly_karaf, then run this update command."); + } else { + String artifact = "spring-boot".equals(resolvedRuntime) + ? CAMEL_SPRING_BOOT_UPGRADE_RECIPES_ARTIFACT + : CAMEL_UPGRADE_RECIPES_ARTIFACT; + + String command = String.format( + "mvn --no-transfer-progress org.openrewrite.maven:rewrite-maven-plugin:%s:%s " + + "-Drewrite.recipeArtifactCoordinates=org.apache.camel.upgrade:%s:%s " + + "-Drewrite.activeRecipes=org.apache.camel.upgrade.CamelMigrationRecipe", + OPENREWRITE_VERSION, runMode, artifact, targetVersion); + mavenCommands.add(command); + } + + // Java upgrade suggestion + List<String> javaUpgradeSuggestions = new ArrayList<>(); + if (javaVersion != null && !javaVersion.isBlank()) { + int javaVer = parseJavaVersion(javaVersion); + if (javaVer < 17) { + javaUpgradeSuggestions.add( + "Consider upgrading to Java 17. " + + "OpenRewrite recipe: org.openrewrite.java.migrate.UpgradeToJava17"); + } else if (javaVer < 21) { + javaUpgradeSuggestions.add( + "Consider upgrading to Java 21 for virtual threads support. " + + "OpenRewrite recipe: org.openrewrite.java.migrate.UpgradeToJava21"); + } + } + + String nextStep = "Verify the project compiles with 'mvn clean compile' before and after running the recipes."; + + return new MigrationRecipesResult( + resolvedRuntime, currentVersion, targetVersion, + mavenCommands, javaUpgradeSuggestions, notes, isDryRun, nextStep); + } + + /** + * Search migration guides for a specific term. + */ + @Tool(description = "Search Camel migration and upgrade guides for a specific term or component name. " + + "Returns matching snippets from the official guides with version info and URLs. " + + "Supports fuzzy matching for typo tolerance. " + + "Use this instead of web search when looking up migration-related changes, " + + "removed components, API renames, or breaking changes.") + public GuideSearchResult camel_migration_guide_search( + @ToolArg(description = "Search query — component name, API class, method, or keyword " + + "(e.g., direct-vm, getOut, camel-http4, ExchangePattern)") String query, + @ToolArg(description = "Maximum number of results to return (default: 3)") Integer limit) { + + if (query == null || query.isBlank()) { + throw new ToolCallException("query is required", null); + } + + int maxResults = limit != null && limit > 0 ? limit : 3; + List<MigrationData.GuideSection> matches = migrationData.searchGuides(query, maxResults); + + List<GuideSnippet> snippets = new ArrayList<>(); + for (MigrationData.GuideSection section : matches) { + // Truncate content to avoid huge responses + String snippet = truncateSnippet(section.content(), 30); + snippets.add(new GuideSnippet( + section.guide(), section.version(), section.sectionTitle(), + snippet, section.url())); + } + + String nextStep = "Call camel_migration_guide_search again for other terms, or call camel_migration_recipes."; + + return new GuideSearchResult(query, snippets.size(), snippets, nextStep); + } + + private String truncateSnippet(String content, int maxLines) { + String[] lines = content.split("\n"); + if (lines.length <= maxLines) { + return content; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < maxLines; i++) { + sb.append(lines[i]).append("\n"); + } + sb.append("... (truncated, ").append(lines.length - maxLines).append(" more lines)"); + return sb.toString(); + } + + // Helpers + + private int parseMajorVersion(String version) { + if (version == null) { + return 0; + } + try { + return Integer.parseInt(version.split("\\.")[0]); + } catch (NumberFormatException e) { + return 0; + } + } + + private int parseJavaVersion(String version) { + if (version == null) { + return 0; + } + try { + if (version.startsWith("1.")) { + return Integer.parseInt(version.substring(2)); + } + return Integer.parseInt(version.split("\\.")[0]); + } catch (NumberFormatException e) { + return 0; + } + } + + // Result records + + public record ProjectAnalysisResult( + String camelVersion, + int majorVersion, + String runtimeType, + String javaVersion, + List<String> camelComponents, + List<String> warnings, + List<String> migrationGuideUrls, + String nextStep) { + } + + public record CompatibilityResult( + boolean compatible, + List<String> migrationGuideUrls, + JavaCompatibility javaCompatibility, + List<String> warnings, + List<String> blockers, + String nextStep) { + } + + public record JavaCompatibility( + String currentVersion, + String requiredVersion, + boolean compatible) { + } + + public record MigrationRecipesResult( + String runtime, + String currentVersion, + String targetVersion, + List<String> mavenCommands, + List<String> javaUpgradeSuggestions, + List<String> notes, + boolean dryRun, + String nextStep) { + } + + public record GuideSearchResult( + String query, + int resultCount, + List<GuideSnippet> results, + String nextStep) { + } + + public record GuideSnippet( + String guide, + String version, + String sectionTitle, + String snippet, + String url) { + } +} diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationWildflyKarafTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationWildflyKarafTools.java new file mode 100644 index 000000000000..17f938204dc2 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationWildflyKarafTools.java @@ -0,0 +1,179 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.commands.mcp; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import io.quarkiverse.mcp.server.Tool; +import io.quarkiverse.mcp.server.ToolArg; +import io.quarkiverse.mcp.server.ToolCallException; + +/** + * MCP Tool for migrating Camel projects from WildFly or Karaf to modern runtimes. + * <p> + * Handles the special case where projects running on legacy runtimes need to be replatformed to Spring Boot or Quarkus. + * Uses Maven archetypes for new project creation and migration guides for component mapping details. + */ +@ApplicationScoped +public class MigrationWildflyKarafTools { + + @Inject + MigrationData migrationData; + + /** + * WildFly/Karaf migration guidance with archetype-based project creation. + */ + @Tool(description = "Get migration guidance for Camel projects running on WildFly, Karaf, or WAR-based " + + "application servers. Returns the Maven archetype command to create a new target project, " + + "migration steps, and relevant migration guide URLs. " + + "IMPORTANT: When migrating to a different runtime (e.g., WildFly to Quarkus, Karaf to Spring Boot), " + + "you MUST use the archetype command returned by this tool to create a new project. " + + "Do NOT manually rewrite the pom.xml — always generate a new project with the archetype first, " + + "then migrate routes and source files into it.") + public WildflyKarafMigrationResult camel_migration_wildfly_karaf( + @ToolArg(description = "The pom.xml file content of the WildFly/Karaf project") String pomContent, + @ToolArg(description = "Target runtime: spring-boot or quarkus (default: quarkus)") String targetRuntime, + @ToolArg(description = "Target Camel version (e.g., 4.18.0)") String targetVersion) { + + if (pomContent == null || pomContent.isBlank()) { + throw new ToolCallException("pomContent is required", null); + } + + try { + MigrationData.PomAnalysis pom = MigrationData.parsePomContent(pomContent); + + String sourceRuntime = pom.isWildfly() ? "wildfly" : pom.isKaraf() ? "karaf" : "unknown"; + String resolvedTarget = targetRuntime != null && !targetRuntime.isBlank() + ? targetRuntime.toLowerCase().trim() : "quarkus"; + + // Build archetype command for new project creation + String archetypeCommand = buildArchetypeCommand(resolvedTarget, targetVersion); + + // Collect migration steps + List<String> migrationSteps = buildMigrationSteps(sourceRuntime, resolvedTarget); + + // Collect relevant migration guides (always include 2→3 and 3→4 for legacy projects) + List<MigrationData.MigrationGuide> guides = new ArrayList<>(); + guides.add(migrationData.getMigrationGuide("migration-and-upgrade")); + guides.add(migrationData.getMigrationGuide("camel-3-migration")); + guides.add(migrationData.getMigrationGuide("camel-4-migration")); + guides.add(migrationData.getMigrationGuide("camel-4x-upgrade")); + + List<String> guideUrls = guides.stream() + .map(MigrationData.MigrationGuide::url) + .collect(Collectors.toList()); + + // Warnings specific to the source runtime + List<String> warnings = new ArrayList<>(); + if ("karaf".equals(sourceRuntime)) { + warnings.add("Blueprint XML is not supported in Camel 3.x+. " + + "Routes must be converted to YAML DSL, XML DSL, or Java DSL."); + warnings.add("OSGi-specific features (bundle classloading, service registry) " + + "have no equivalent in Spring Boot or Quarkus."); + } + if ("wildfly".equals(sourceRuntime)) { + warnings.add("WildFly Camel subsystem is discontinued. " + + "Migrate to Camel Spring Boot or Camel Quarkus."); + } + + String nextStep = "Create a new project using the archetype command, migrate routes and source files, " + + "then call camel_migration_recipes for OpenRewrite upgrade commands."; + + return new WildflyKarafMigrationResult( + sourceRuntime, pom.camelVersion(), resolvedTarget, targetVersion, + pom.dependencies(), archetypeCommand, migrationSteps, + guideUrls, warnings, nextStep); + } catch (ToolCallException e) { + throw e; + } catch (Exception e) { + throw new ToolCallException("Failed to analyze WildFly/Karaf project: " + e.getMessage(), e); + } + } + + private String buildArchetypeCommand(String targetRuntime, String targetVersion) { + if ("spring-boot".equals(targetRuntime)) { + // Camel Spring Boot archetype from camel-spring-boot repo + StringBuilder sb = new StringBuilder(); + sb.append("mvn archetype:generate "); + sb.append("-DarchetypeGroupId=org.apache.camel.springboot "); + sb.append("-DarchetypeArtifactId=camel-archetype-spring-boot "); + if (targetVersion != null && !targetVersion.isBlank()) { + sb.append("-DarchetypeVersion=").append(targetVersion).append(" "); + } + sb.append("-DgroupId=com.example "); + sb.append("-DartifactId=camel-migration-project "); + sb.append("-Dversion=1.0-SNAPSHOT "); + sb.append("-DinteractiveMode=false"); + return sb.toString(); + } else { + // Quarkus project creation with Camel extensions + StringBuilder sb = new StringBuilder(); + sb.append("mvn io.quarkus.platform:quarkus-maven-plugin:create "); + sb.append("-DprojectGroupId=com.example "); + sb.append("-DprojectArtifactId=camel-migration-project "); + sb.append("-Dextensions=camel-quarkus-core"); + return sb.toString(); + } + } + + private List<String> buildMigrationSteps(String sourceRuntime, String targetRuntime) { + List<String> steps = new ArrayList<>(); + steps.add("Create a new " + targetRuntime + " project using the archetype command provided."); + steps.add("Review the migration guides for component renames between Camel 2.x and 4.x."); + + if ("karaf".equals(sourceRuntime)) { + steps.add("Convert Blueprint XML routes to YAML DSL, XML DSL, or Java DSL. " + + "Use the camel_transform_route tool for YAML/XML conversions."); + steps.add("Remove OSGi-specific code (bundle activators, service trackers, " + + "MANIFEST.MF Import-Package directives)."); + } + + if ("wildfly".equals(sourceRuntime)) { + steps.add("Remove WildFly Camel subsystem configuration."); + steps.add("Convert CDI-based Camel configuration to Spring Boot or Quarkus patterns."); + } + + steps.add("Add required Camel component dependencies to the new project's pom.xml, " + + "using updated artifact names from the migration guides."); + steps.add("Migrate Java source files, updating package imports and API calls " + + "per the migration guides."); + steps.add("Run 'mvn clean compile' to check for build errors."); + steps.add("Fix any remaining build errors using camel_migration_guide_search for reference."); + steps.add("Run 'mvn clean test' to validate the migration."); + return steps; + } + + // Result record + + public record WildflyKarafMigrationResult( + String sourceRuntime, + String sourceCamelVersion, + String targetRuntime, + String targetCamelVersion, + List<String> detectedDependencies, + String archetypeCommand, + List<String> migrationSteps, + List<String> migrationGuideUrls, + List<String> warnings, + String nextStep) { + } +} diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationDataSearchTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationDataSearchTest.java new file mode 100644 index 000000000000..a3d2d637d9c6 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationDataSearchTest.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.commands.mcp; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests that the migration guide fuzzy search works end-to-end: loads the real .adoc guide files from the classpath, + * indexes them, and returns relevant results for exact, fuzzy, and nonsense queries. + */ +class MigrationDataSearchTest { + + private final MigrationData migrationData = new MigrationData(); + + @Test + void exactSearchFindsMatchingGuideSection() { + // "camel-vm" was removed in Camel 4 — this is documented in the migration guides + List<MigrationData.GuideSection> results = migrationData.searchGuides("camel-vm", 3); + + assertThat(results).isNotEmpty(); + assertThat(results.get(0).content().toLowerCase()).contains("camel-vm"); + assertThat(results.get(0).url()).startsWith("https://camel.apache.org/manual/"); + } + + @Test + void fuzzySearchFindsResultDespiteTypo() { + // "directvm" (missing hyphen) should still match "direct-vm" sections via fuzzy matching + List<MigrationData.GuideSection> results = migrationData.searchGuides("directvm", 3); + + assertThat(results).isNotEmpty(); + } + + @Test + void nonsenseQueryReturnsNoResults() { + List<MigrationData.GuideSection> results = migrationData.searchGuides("xyznonexistent12345", 3); + + assertThat(results).isEmpty(); + } + + @Test + void searchRespectsLimit() { + List<MigrationData.GuideSection> results = migrationData.searchGuides("camel", 2); + + assertThat(results).hasSizeLessThanOrEqualTo(2); + } +}
