move Spring Cli commands implementation to language server

This commit is contained in:
vudayani
2024-09-28 17:11:58 +05:30
committed by Martin Lippert
parent 3d18619e98
commit f9949d6480
42 changed files with 2099 additions and 144 deletions

View File

@@ -49,6 +49,8 @@ import org.springframework.ide.vscode.boot.java.beans.DependsOnDefinitionProvide
import org.springframework.ide.vscode.boot.java.beans.NamedDefinitionProvider;
import org.springframework.ide.vscode.boot.java.beans.QualifierDefinitionProvider;
import org.springframework.ide.vscode.boot.java.beans.ResourceDefinitionProvider;
import org.springframework.ide.vscode.boot.java.conditionalonresource.ConditionalOnResourceDefinitionProvider;
import org.springframework.ide.vscode.boot.java.copilot.ResponseModifier;
import org.springframework.ide.vscode.boot.java.data.jpa.queries.DataQueryParameterDefinitionProvider;
import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtDataQuerySemanticTokensProvider;
import org.springframework.ide.vscode.boot.java.handlers.BootJavaCodeActionProvider;
@@ -72,7 +74,6 @@ import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.JdtReconciler;
import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache;
import org.springframework.ide.vscode.boot.java.value.ValueDefinitionProvider;
import org.springframework.ide.vscode.boot.java.conditionalonresource.ConditionalOnResourceDefinitionProvider;
import org.springframework.ide.vscode.boot.jdt.ls.JavaProjectsService;
import org.springframework.ide.vscode.boot.jdt.ls.JdtLsProjectCache;
import org.springframework.ide.vscode.boot.metadata.AdHocSpringPropertyIndexProvider;
@@ -417,4 +418,8 @@ public class BootLanguageServerBootApp {
return new ModulithService(server, projectFinder, projectObserver, springIndex, reconciler, config);
}
@Bean ResponseModifier responseModifier() {
return new ResponseModifier();
}
}

View File

@@ -31,6 +31,8 @@ import org.springframework.ide.vscode.boot.java.beans.NamedReferencesProvider;
import org.springframework.ide.vscode.boot.java.beans.ProfileReferencesProvider;
import org.springframework.ide.vscode.boot.java.beans.QualifierReferencesProvider;
import org.springframework.ide.vscode.boot.java.conditionals.ConditionalsLiveHoverProvider;
import org.springframework.ide.vscode.boot.java.copilot.CopilotAgentCommandHandler;
import org.springframework.ide.vscode.boot.java.copilot.ResponseModifier;
import org.springframework.ide.vscode.boot.java.handlers.BootJavaCodeActionProvider;
import org.springframework.ide.vscode.boot.java.handlers.BootJavaCodeLensEngine;
import org.springframework.ide.vscode.boot.java.handlers.BootJavaDocumentHighlightEngine;
@@ -110,6 +112,7 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent
private final SpringLiveChangeDetectionWatchdog liveChangeDetectionWatchdog;
private final ProjectObserver projectObserver;
private final CompilationUnitCache cuCache;
private final ResponseModifier responseModifier;
private JavaProjectFinder projectFinder;
private BootJavaHoverProvider hoverProvider;
@@ -121,10 +124,11 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent
private JdtSemanticTokensHandler semanticTokensHandler;
private JdtInlayHintsHandler inlayHintsHandler;
private SpelSemanticTokens spelSemanticTokens;
public BootJavaLanguageServerComponents(ApplicationContext appContext) {
this.server = appContext.getBean(SimpleLanguageServer.class);
this.serverParams = appContext.getBean(BootLanguageServerParams.class);
this.responseModifier = appContext.getBean(ResponseModifier.class);
projectFinder = serverParams.projectFinder;
projectObserver = serverParams.projectObserver;
@@ -166,6 +170,8 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent
// create and handle commands
new SpringProcessCommandHandler(server, liveDataService, liveDataLocalProcessConnector, appContext.getBeansOfType(SpringProcessConnectorRemote.class).values());
new CopilotAgentCommandHandler(server, projectFinder,responseModifier);
docSymbolProvider = params -> springSymbolIndex.getSymbols(params.getTextDocument().getUri());
workspaceService.onWorkspaceSymbol(new BootJavaWorkspaceSymbolHandler(springSymbolIndex,

View File

@@ -0,0 +1,95 @@
package org.springframework.ide.vscode.boot.java.copilot;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import org.jetbrains.annotations.NotNull;
import org.openrewrite.ExecutionContext;
import org.openrewrite.InMemoryExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.RecipeRun;
import org.openrewrite.Result;
import org.openrewrite.SourceFile;
import org.openrewrite.internal.InMemoryLargeSourceSet;
import org.openrewrite.maven.MavenParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
public abstract class AbstractInjectMavenActionHandler {
private static final Logger logger = LoggerFactory.getLogger(AbstractInjectMavenActionHandler.class);
protected final TemplateEngine templateEngine;
protected final Map<String, Object> model;
protected final Path cwd;
public AbstractInjectMavenActionHandler(TemplateEngine templateEngine, Map<String, Object> model, Path cwd) {
this.templateEngine = templateEngine;
this.model = model;
this.cwd = cwd;
}
protected static ExecutionContext getExecutionContext() {
Consumer<Throwable> onError = e -> {
logger.error("error in javaParser execution", e);
};
return new InMemoryExecutionContext(onError);
}
protected String getTextToUse(String text, String actionName) {
if (!StringUtils.hasText(text)) {
throw new SpringCliException(actionName + " action does not have a value in the 'text:' field.");
}
if (this.templateEngine != null) {
text = this.templateEngine.process(text, model);
}
return text;
}
@NotNull
protected Path getPomPath() {
Path pomPath = cwd.resolve("pom.xml");
if (Files.notExists(pomPath)) {
throw new SpringCliException("Could not find pom.xml in " + this.cwd
+ ". Make sure you are running the command in the directory that contains a pom.xml file");
}
return pomPath;
}
public void exec() {
Path pomPath = getPomPath();
List<Result> resultList = run().getChangeset().getAllResults();
try {
for (Result result : resultList) {
// write updated file.
try (BufferedWriter sourceFileWriter = Files.newBufferedWriter(pomPath, StandardCharsets.UTF_8)) {
sourceFileWriter.write(result.getAfter().printAllTrimmed());
}
}
}
catch (IOException ex) {
throw new SpringCliException("Error writing to " + pomPath.toAbsolutePath(), ex);
}
}
public RecipeRun run() {
List<Path> paths = new ArrayList<>();
paths.add(getPomPath());
MavenParser mavenParser = MavenParser.builder().build();
List<SourceFile> parsedPomFiles = mavenParser.parse(paths, cwd, getExecutionContext()).toList();
return createRecipe().run(new InMemoryLargeSourceSet(parsedPomFiles), getExecutionContext());
}
protected abstract Recipe createRecipe();
}

View File

@@ -0,0 +1,87 @@
package org.springframework.ide.vscode.boot.java.copilot;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.eclipse.lsp4j.ExecuteCommandParams;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.WorkspaceEdit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
import com.google.gson.JsonElement;
import com.google.j2objc.annotations.Weak;
public class CopilotAgentCommandHandler {
private static final Logger log = LoggerFactory.getLogger(CopilotAgentCommandHandler.class);
private static final String CMD_COPILOT_AGENT_ENHANCERESPONSE = "sts/copilot/agent/enhanceResponse";
private static final String CMD_COPILOT_AGENT_LSPEDITS = "sts/copilot/agent/lspEdits";
private final SimpleLanguageServer server;
private final JavaProjectFinder projectFinder;
private final ResponseModifier responseModifier;
public CopilotAgentCommandHandler(SimpleLanguageServer server, JavaProjectFinder projectFinder, ResponseModifier responseModifier) {
this.server = server;
this.projectFinder = projectFinder;
this.responseModifier = responseModifier;
registerCommands();
}
private void registerCommands() {
server.onCommand(CMD_COPILOT_AGENT_ENHANCERESPONSE, (params) -> {
return enhanceResponseHandler(params);
});
log.info("Registered command handler: {}",CMD_COPILOT_AGENT_ENHANCERESPONSE);
server.onCommand(CMD_COPILOT_AGENT_LSPEDITS, params -> {
try {
return createLspEdits(params);
} catch (IOException e) {
log.error(e.getMessage());
}
return null;
});
}
private CompletableFuture<Object> enhanceResponseHandler(ExecuteCommandParams params) {
log.info("Command Handler: ");
String response = ((JsonElement) params.getArguments().get(0)).getAsString();
String modifiedResp = responseModifier.modify(response);
return CompletableFuture.completedFuture(modifiedResp);
}
private CompletableFuture<WorkspaceEdit> createLspEdits(ExecuteCommandParams params) throws IOException {
log.info("Command Handler for lsp edits: ");
String docURI = ((JsonElement) params.getArguments().get(0)).getAsString();
String path = ((JsonElement) params.getArguments().get(0)).getAsString();
String content = ((JsonElement) params.getArguments().get(2)).getAsString();
TextDocument doc = server.getTextDocumentService().getLatestSnapshot("file:///Users/vudayani/Desktop/spring-petclinic/README-ai-spring-petclinic.md");
IJavaProject project = this.projectFinder.find(new TextDocumentIdentifier(docURI)).get();
System.out.println("Project path "+project.getLocationUri() + " "+project.getLocationUri().getPath().toString());
List<ProjectArtifact> projectArtifacts = computeProjectArtifacts(content);
ProjectArtifactEditGenerator editGenerator = new ProjectArtifactEditGenerator(server.getTextDocumentService(), projectArtifacts,
Paths.get(project.getLocationUri()), docURI);
// Paths.get(project.getLocationUri()), docURI);
WorkspaceEdit we = editGenerator.process().getResult();
System.out.println("Final: \n "+ we.toString());
return CompletableFuture.completedFuture(we);
}
List<ProjectArtifact> computeProjectArtifacts(String response) {
ProjectArtifactCreator projectArtifactCreator = new ProjectArtifactCreator();
List<ProjectArtifact> projectArtifacts = projectArtifactCreator.create(response);
return projectArtifacts;
}
}

View File

@@ -0,0 +1,93 @@
package org.springframework.ide.vscode.boot.java.copilot;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.openrewrite.Recipe;
import org.openrewrite.config.DeclarativeRecipe;
import org.openrewrite.maven.MavenParser;
import org.springframework.ide.vscode.boot.java.copilot.recipe.AddDependencyRecipeFactory;
import org.springframework.ide.vscode.boot.java.copilot.recipe.AddManagedDependencyRecipeFactory;
import org.springframework.ide.vscode.boot.java.copilot.recipe.AddPluginRecipeFactory;
import org.springframework.ide.vscode.boot.java.copilot.recipe.InjectTextMavenRepositoryRecipe;
import org.springframework.ide.vscode.boot.java.copilot.util.MavenBuildPluginReader;
import org.springframework.ide.vscode.boot.java.copilot.util.MavenDependencyReader;
import org.springframework.ide.vscode.boot.java.copilot.util.MavenRepositoryReader;
public class InjectMavenActionHandler extends AbstractInjectMavenActionHandler {
private List<InjectMavenDependency> dependencies;
private List<InjectMavenBuildPlugin> buildPlugins;
private List<InjectMavenRepository> repositories;
private List<InjectMavenDependencyManagement> dependencyManagements;
public InjectMavenActionHandler(TemplateEngine templateEngine, Map<String, Object> model, Path cwd) {
super(templateEngine, model, cwd);
this.dependencies = new ArrayList<>();
this.buildPlugins = new ArrayList<>();
this.repositories = new ArrayList<>();
this.dependencyManagements = new ArrayList<>();
}
public boolean injectBuildPlugin(InjectMavenBuildPlugin buildPlugin) {
return buildPlugins.add(buildPlugin);
}
public boolean injectDependency(InjectMavenDependency dependency) {
return dependencies.add(dependency);
}
public boolean injectRepository(InjectMavenRepository repository) {
return repositories.add(repository);
}
public boolean injectDependencyManagement(InjectMavenDependencyManagement dependencyManagement) {
return dependencyManagements.add(dependencyManagement);
}
protected Recipe createRecipe() {
DeclarativeRecipe aggregateRecipe = new DeclarativeRecipe("spring.cli.ai.MavenUpdates",
"Add Pom changes from AI", "", Collections.emptySet(), null, null, false, Collections.emptyList());
MavenParser mavenParser = MavenParser.builder().build();
for (InjectMavenDependency d : dependencies) {
String text = getTextToUse(d.getText(), "Inject Maven Dependency");
MavenDependencyReader mavenDependencyReader = new MavenDependencyReader();
String[] mavenDependencies = mavenDependencyReader.parseMavenSection(text);
for (String md : mavenDependencies) {
aggregateRecipe.getRecipeList().add(new AddDependencyRecipeFactory().create(md));
}
}
for (InjectMavenBuildPlugin p : buildPlugins) {
String text = getTextToUse(p.getText(), "Inject Maven Build Plugin");
MavenBuildPluginReader mavenBuildPluginReader = new MavenBuildPluginReader();
String[] buildPlugins = mavenBuildPluginReader.parseMavenSection(text);
for (String mp : buildPlugins) {
aggregateRecipe.getRecipeList().add(new AddPluginRecipeFactory().create(mp));
}
}
for (InjectMavenRepository r : repositories) {
String text = getTextToUse(r.getText(), "Inject Maven Repository");
MavenRepositoryReader mavenRepositoryReader = new MavenRepositoryReader();
String[] mavenRepositories = mavenRepositoryReader.parseMavenSection(text);
for (String mr : mavenRepositories) {
aggregateRecipe.getRecipeList().add(new InjectTextMavenRepositoryRecipe(mr));
}
}
for (InjectMavenDependencyManagement dm : dependencyManagements) {
String text = getTextToUse(dm.getText(), "Inject Maven Dependency Management");
MavenDependencyReader mavenDependencyReader = new MavenDependencyReader();
String[] mavenDependencyManagements = mavenDependencyReader.parseMavenSection(text);
for (String mdm : mavenDependencyManagements) {
aggregateRecipe.getRecipeList().add(new AddManagedDependencyRecipeFactory().create(mdm));
}
}
return aggregateRecipe;
}
}

View File

@@ -0,0 +1,29 @@
package org.springframework.ide.vscode.boot.java.copilot;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author Mark Pollack
*/
public class InjectMavenBuildPlugin {
private String text;
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
public InjectMavenBuildPlugin(@JsonProperty("text") String text) {
this.text = Objects.requireNonNull(text);
}
public String getText() {
return text;
}
@Override
public String toString() {
return "InjectMavenBuildPlugin{" + "text='" + text + '\'' + '}';
}
}

View File

@@ -0,0 +1,29 @@
package org.springframework.ide.vscode.boot.java.copilot;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author Mark Pollack
*/
public class InjectMavenDependency {
private String text;
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
public InjectMavenDependency(@JsonProperty("text") String text) {
this.text = Objects.requireNonNull(text);
}
public String getText() {
return text;
}
@Override
public String toString() {
return "InjectMavenDependency{" + "text='" + text + '\'' + '}';
}
}

View File

@@ -0,0 +1,29 @@
package org.springframework.ide.vscode.boot.java.copilot;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author Mark Pollack
*/
public class InjectMavenDependencyManagement {
private String text;
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
public InjectMavenDependencyManagement(@JsonProperty("text") String text) {
this.text = Objects.requireNonNull(text);
}
public String getText() {
return text;
}
@Override
public String toString() {
return "InjectMavenDependency{" + "text='" + text + '\'' + '}';
}
}

View File

@@ -0,0 +1,29 @@
package org.springframework.ide.vscode.boot.java.copilot;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author Mark Pollack
*/
public class InjectMavenRepository {
private String text;
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
public InjectMavenRepository(@JsonProperty("text") String text) {
this.text = Objects.requireNonNull(text);
}
public String getText() {
return text;
}
@Override
public String toString() {
return "InjectMavenRepository{" + "text='" + text + '\'' + '}';
}
}

View File

@@ -0,0 +1,79 @@
package org.springframework.ide.vscode.boot.java.copilot;
import java.util.ArrayList;
import java.util.List;
import org.commonmark.node.AbstractVisitor;
import org.commonmark.node.FencedCodeBlock;
public class MarkdownResponseVisitor extends AbstractVisitor {
private List<ProjectArtifact> projectArtifacts = new ArrayList<>();
@Override
public void visit(FencedCodeBlock fencedCodeBlock) {
String info = fencedCodeBlock.getInfo();
String code = fencedCodeBlock.getLiteral();
if (info.equalsIgnoreCase("java")) {
addJavaCode(code);
}
if (info.equalsIgnoreCase("xml")) {
addMavenDependencies(code);
}
if (info.equalsIgnoreCase("properties")) {
addApplicationProperties(code);
}
if (info.equalsIgnoreCase("html")) {
addHtml(code);
}
if (info.isBlank()) {
// sometimes the response doesn't contain
if (code.contains("package")) {
addJavaCode(code);
}
else if (code.contains("<dependency>")) {
addMavenDependencies(code);
}
}
}
private void addHtml(String code) {
addIfNotPresent(new ProjectArtifact(ProjectArtifactType.HTML, code));
}
private void addApplicationProperties(String code) {
addIfNotPresent(new ProjectArtifact(ProjectArtifactType.APPLICATION_PROPERTIES, code));
}
private void addMavenDependencies(String code) {
addIfNotPresent(new ProjectArtifact(ProjectArtifactType.MAVEN_DEPENDENCIES, code));
}
private void addJavaCode(String code) {
if (code.contains("@SpringBootApplication") && code.contains("SpringApplication.run")) {
addIfNotPresent(new ProjectArtifact(ProjectArtifactType.MAIN_CLASS, code));
}
else if (code.contains("@Test")) {
addIfNotPresent(new ProjectArtifact(ProjectArtifactType.TEST_CODE, code));
}
else {
addIfNotPresent(new ProjectArtifact(ProjectArtifactType.SOURCE_CODE, code));
}
}
private void addIfNotPresent(ProjectArtifact projectArtifact) {
// TODO - investigate why Node has duplicate entries
if (!this.projectArtifacts.contains(projectArtifact)) {
this.projectArtifacts.add(projectArtifact);
}
else {
// System.out.println("duplicate project artifact :(");
}
}
public List<ProjectArtifact> getProjectArtifacts() {
return this.projectArtifacts;
}
}

View File

@@ -0,0 +1,29 @@
package org.springframework.ide.vscode.boot.java.copilot;
import java.util.ArrayList;
import java.util.List;
public class ProcessArtifactResult<T> {
List<ProjectArtifact> notProcessedArtifacts = new ArrayList<>();
private T result;
public List<ProjectArtifact> getNotProcessedArtifacts() {
return notProcessedArtifacts;
}
public void addToNotProcessed(ProjectArtifact projectArtifact) {
notProcessedArtifacts.add(projectArtifact);
}
public T getResult() {
return result;
}
void setResult(T result) {
this.result = result;
}
}

View File

@@ -0,0 +1,42 @@
package org.springframework.ide.vscode.boot.java.copilot;
import java.util.Objects;
public class ProjectArtifact {
private ProjectArtifactType artifactType;
private String text;
public ProjectArtifact(ProjectArtifactType artifactType, String text) {
this.artifactType = artifactType;
this.text = text;
}
public ProjectArtifactType getArtifactType() {
return artifactType;
}
public String getText() {
return text;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ProjectArtifact that = (ProjectArtifact) o;
return artifactType == that.artifactType && Objects.equals(text, that.text);
}
@Override
public int hashCode() {
return Objects.hash(artifactType, text);
}
}

View File

@@ -0,0 +1,20 @@
package org.springframework.ide.vscode.boot.java.copilot;
import java.util.List;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
public class ProjectArtifactCreator {
public List<ProjectArtifact> create(String response) {
Parser parser = Parser.builder().build();
Node node = parser.parse(response);
MarkdownResponseVisitor markdownResponseVisitor = new MarkdownResponseVisitor();
node.accept(markdownResponseVisitor);
return markdownResponseVisitor.getProjectArtifacts();
}
}

View File

@@ -0,0 +1,305 @@
package org.springframework.ide.vscode.boot.java.copilot;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.io.IOUtils;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.Model;
import org.eclipse.lsp4j.ChangeAnnotation;
import org.eclipse.lsp4j.WorkspaceEdit;
import org.openrewrite.Result;
import org.springframework.ide.vscode.boot.java.copilot.util.ClassNameExtractor;
import org.springframework.ide.vscode.boot.java.copilot.util.MavenDependencyReader;
import org.springframework.ide.vscode.boot.java.copilot.util.PomReader;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService;
import org.springframework.ide.vscode.commons.rewrite.ORDocUtils;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
public class ProjectArtifactEditGenerator {
private final List<ProjectArtifact> projectArtifacts;
private final Path projectPath;
private final String readmeFileName;
private final Pattern compiledGroupIdPattern;
private final Pattern compiledArtifactIdPattern;
private final SimpleTextDocumentService simpleTextDocumentService;
public ProjectArtifactEditGenerator(SimpleTextDocumentService simpleTextDocumentService, List<ProjectArtifact> projectArtifacts, Path projectPath, String readmeFileName) {
this.simpleTextDocumentService = simpleTextDocumentService;
this.projectArtifacts = projectArtifacts;
this.projectPath = projectPath;
this.readmeFileName = readmeFileName;
compiledGroupIdPattern = Pattern.compile("<groupId>(.*?)</groupId>");
compiledArtifactIdPattern = Pattern.compile("<artifactId>(.*?)</artifactId>");
}
public ProcessArtifactResult<WorkspaceEdit> process() throws IOException {
return processArtifacts(projectArtifacts, projectPath);
}
private ProcessArtifactResult<WorkspaceEdit> processArtifacts(List<ProjectArtifact> projectArtifacts,
Path projectPath) throws IOException {
ProcessArtifactResult<WorkspaceEdit> processArtifactResult = new ProcessArtifactResult<>();
String changeAnnotationId = UUID.randomUUID().toString();
WorkspaceEdit we = new WorkspaceEdit();
ChangeAnnotation changeAnnotation = new ChangeAnnotation();
changeAnnotation.setNeedsConfirmation(true);
we.setDocumentChanges(new ArrayList<>());
we.setChangeAnnotations(Map.of(changeAnnotationId, changeAnnotation));
for (ProjectArtifact projectArtifact : projectArtifacts) {
// try {
ProjectArtifactType artifactType = projectArtifact.getArtifactType();
switch (artifactType) {
case SOURCE_CODE:
writeSourceCode(projectArtifact, projectPath, changeAnnotationId, we);
break;
case TEST_CODE:
writeTestCode(projectArtifact, projectPath, changeAnnotationId, we);
break;
case MAVEN_DEPENDENCIES:
writeMavenDependencies(projectArtifact, projectPath,changeAnnotationId, we);
break;
case APPLICATION_PROPERTIES:
writeApplicationProperties(projectArtifact, projectPath, changeAnnotationId, we);
break;
case MAIN_CLASS:
updateMainApplicationClassAnnotations(projectArtifact, projectPath,
changeAnnotationId, we);
break;
case HTML:
writeHtml(projectArtifact, projectPath, changeAnnotationId, we);
break;
default:
processArtifactResult.addToNotProcessed(projectArtifact);
break;
}
}
processArtifactResult.setResult(we);
return processArtifactResult;
}
private void writeSourceCode(ProjectArtifact projectArtifact, Path projectPath,
String changeAnnotationId, WorkspaceEdit we) throws IOException {
String packageName = this.calculatePackageForArtifact(projectArtifact);
ClassNameExtractor classNameExtractor = new ClassNameExtractor();
Optional<String> className = classNameExtractor.extractClassName(projectArtifact.getText());
if (className.isPresent()) {
Path output = resolveSourceFile(projectPath, packageName, className.get() + ".java");
ORDocUtils.createWorkspaceEdit(simpleTextDocumentService, output.toUri().toASCIIString(), getFileContent(output), projectArtifact.getText(), changeAnnotationId, we);
}
}
private String getFileContent(Path file) throws IOException {
TextDocument doc = simpleTextDocumentService.getLatestSnapshot(file.toUri().toASCIIString());
if (doc != null) {
return doc.get();
}
return null;
}
private void writeTestCode(ProjectArtifact projectArtifact, Path projectPath,
String changeAnnotationId, WorkspaceEdit we) throws IOException {
// TODO parameterize better to reduce code duplication
String packageName = this.calculatePackageForArtifact(projectArtifact);
ClassNameExtractor classNameExtractor = new ClassNameExtractor();
Optional<String> className = classNameExtractor.extractClassName(projectArtifact.getText());
if (className.isPresent()) {
Path output = resolveTestFile(projectPath, packageName, className.get() + ".java");
ORDocUtils.createWorkspaceEdit(simpleTextDocumentService, output.toUri().toASCIIString(), getFileContent(output), projectArtifact.getText(), changeAnnotationId, we);
}
}
private void writeMavenDependencies(ProjectArtifact projectArtifact, Path projectPath, String changeAnnotationId, WorkspaceEdit we) {
PomReader pomReader = new PomReader();
Path currentProjectPomPath = this.projectPath.resolve("pom.xml");
if (Files.notExists(currentProjectPomPath)) {
throw new SpringCliException("Could not find pom.xml in " + this.projectPath
+ ". Make sure you are running the command in the project's root directory.");
}
Model currentModel = pomReader.readPom(currentProjectPomPath.toFile());
List<Dependency> currentDependencies = currentModel.getDependencies();
MavenDependencyReader mavenDependencyReader = new MavenDependencyReader();
// projectArtifact.getText() contains a list of <dependency> elements
String[] mavenDependencies = mavenDependencyReader.parseMavenSection(projectArtifact.getText());
InjectMavenActionHandler injectMavenActionHandler = new InjectMavenActionHandler(null, new HashMap<>(),
projectPath);
for (String candidateDependencyText : mavenDependencies) {
if (!candidateDependencyAlreadyPresent(getProjectDependency(candidateDependencyText),
currentDependencies)) {
injectMavenActionHandler.injectDependency(new InjectMavenDependency(candidateDependencyText));
}
}
List<Result> res = injectMavenActionHandler.run().getChangeset().getAllResults();
if(!res.isEmpty()) {
WorkspaceEdit workspaceEdit = ORDocUtils.createWorkspaceEdit(simpleTextDocumentService, res, changeAnnotationId).get();
we.getDocumentChanges().addAll(workspaceEdit.getDocumentChanges());
}
// return res.isEmpty() ? Collections.emptyList() :
// ORDocUtils.createWorkspaceEdit(simpleTextDocumentService, res, changeAnnotationId).get();
// convertToEdits(res, changeAnnotationId);
}
// private List<Lsp.ChangeOperation> convertToEdits(List<Result> allResults, String changeAnnotationId) {
// List<Lsp.ChangeOperation> edits = new ArrayList<>();
// for (Result res : allResults) {
// Path p = (res.getBefore() == null) ? res.getAfter().getSourcePath() : res.getBefore().getSourcePath();
// String uri = p.toUri().toASCIIString();
// ConversionUtils
// .computeTextDocEdit(uri, res.getBefore().printAll(), res.getAfter().printAll(), changeAnnotationId)
// .ifPresent(edits::add);
// }
// return edits;
// }
private boolean candidateDependencyAlreadyPresent(ProjectDependency toMergeDependency,
List<Dependency> currentDependencies) {
String candidateGroupId = toMergeDependency.getGroupId();
String candidateArtifactId = toMergeDependency.getArtifactId();
boolean candidateDependencyAlreadyPresent = false;
for (Dependency currentDependency : currentDependencies) {
String currentGroupId = currentDependency.getGroupId();
String currentArtifactId = currentDependency.getArtifactId();
if (candidateGroupId.equals(currentGroupId) && candidateArtifactId.equals(currentArtifactId)) {
candidateDependencyAlreadyPresent = true;
break;
}
}
return candidateDependencyAlreadyPresent;
}
private ProjectDependency getProjectDependency(String xml) {
String groupId = null;
String artifactId = null;
try {
groupId = extractValue(xml, this.compiledGroupIdPattern);
artifactId = extractValue(xml, this.compiledArtifactIdPattern);
}
catch (Exception ex) {
throw new SpringCliException("Exception processing dependency: " + xml, ex);
}
if (groupId == null || artifactId == null) {
throw new SpringCliException("Could not process dependency: " + xml);
}
return new ProjectDependency(groupId, artifactId);
}
private static String extractValue(String xml, Pattern compiledPattern) {
Matcher matcher = compiledPattern.matcher(xml);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
private void writeApplicationProperties(ProjectArtifact projectArtifact, Path projectPath,
String changeAnnotationId, WorkspaceEdit we) throws IOException {
Path applicationPropertiesPath = projectPath.resolve("src")
.resolve("main")
.resolve("resources")
.resolve("application.properties");
Properties srcProperties = new Properties();
Properties destProperties = new Properties();
srcProperties.load(IOUtils.toInputStream(projectArtifact.getText(), StandardCharsets.UTF_8));
if (Files.exists(applicationPropertiesPath)) {
destProperties.load(new FileInputStream(applicationPropertiesPath.toFile()));
}
Properties mergedProperties = PropertyFileUtils.mergeProperties(srcProperties, destProperties);
StringWriter sw = new StringWriter();
mergedProperties.store(sw, "updated by spring ai add");
sw.flush();
String newContent = sw.getBuffer().toString();
ORDocUtils.createWorkspaceEdit(simpleTextDocumentService, applicationPropertiesPath.toUri().toASCIIString(), getFileContent(applicationPropertiesPath), newContent, changeAnnotationId, we);
}
private void updateMainApplicationClassAnnotations(ProjectArtifact projectArtifact,
Path projectPath, String changeAnnotationId, WorkspaceEdit we) {
// TODO mer
// return Collections.emptyList();
}
private void writeHtml(ProjectArtifact projectArtifact, Path projectPath,
String changeAnnotationId, WorkspaceEdit we) throws IOException {
String html = projectArtifact.getText();
String fileName = extractFilenameFromComment(html);
if (fileName != null) {
Path htmlFile = projectPath.resolve(fileName);
ORDocUtils.createWorkspaceEdit(simpleTextDocumentService, fileName, getFileContent(htmlFile), projectArtifact.getText(), changeAnnotationId, we);
}
}
private String calculatePackageForArtifact(ProjectArtifact projectArtifact) {
String packageToUse = "com.example.ai";
try (BufferedReader reader = new BufferedReader(new StringReader(projectArtifact.getText()))) {
String firstLine = reader.readLine();
if (firstLine.contains("package")) {
String regex = "^package\\s+([a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)*);";
Pattern pattern = Pattern.compile(regex, Pattern.MULTILINE);
Matcher matcher = pattern.matcher(firstLine);
// Find the package statement and extract the package name
String packageName = "";
if (matcher.find()) {
packageToUse = matcher.group(1);
}
}
}
catch (IOException ex) {
throw new SpringCliException(
"Could not parse package name from Project Artifact: " + projectArtifact.getText(), ex);
}
return packageToUse;
}
private static String extractFilenameFromComment(String content) {
String commentPattern = "<!--\\s*filename:\\s*(\\S+\\.html)\\s*-->";
Pattern pattern = Pattern.compile(commentPattern);
Matcher matcher = pattern.matcher(content);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
public Path resolveSourceFile(Path projectPath, String packageName, String fileName) {
Path sourceDirectory = projectPath.resolve("src").resolve("main").resolve("java");
return resolvePackage(sourceDirectory, packageName).resolve(fileName);
}
public Path resolveTestFile(Path projectPath, String packageName, String fileName) {
Path sourceDirectory = projectPath.resolve("src").resolve("test").resolve("java");
return resolvePackage(sourceDirectory, packageName).resolve(fileName);
}
private static Path resolvePackage(Path directory, String packageName) {
return directory.resolve(packageName.replace('.', '/'));
}
}

View File

@@ -0,0 +1,20 @@
package org.springframework.ide.vscode.boot.java.copilot;
/**
* The type of artifacts returned from an AI generated response to create code
*/
public enum ProjectArtifactType {
APPLICATION_PROPERTIES,
MAIN_CLASS,
SOURCE_CODE,
TEST_CODE,
MAVEN_DEPENDENCIES,
HTML
}

View File

@@ -0,0 +1,27 @@
package org.springframework.ide.vscode.boot.java.copilot;
public class ProjectDependency {
private final String groupId;
private final String artifactId;
public ProjectDependency(String groupId, String artifactId) {
this.groupId = groupId;
this.artifactId = artifactId;
}
public String getGroupId() {
return groupId;
}
public String getArtifactId() {
return artifactId;
}
@Override
public String toString() {
return "ProjectDependency{" + "groupId='" + groupId + '\'' + ", artifactId='" + artifactId + '\'' + '}';
}
}

View File

@@ -0,0 +1,41 @@
package org.springframework.ide.vscode.boot.java.copilot;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;
import org.apache.commons.io.IOUtils;
public final class PropertyFileUtils {
private PropertyFileUtils() {
}
public static Properties getPropertyFile() {
File homeDir = new File(System.getProperty("user.home"));
File propertyFile = new File(homeDir, ".openai");
Properties props = new Properties();
FileInputStream in = null;
try {
in = new FileInputStream(propertyFile);
props.load(in);
return props;
}
catch (IOException ex) {
throw new SpringCliException("Could not load property file ~/.openai", ex);
}
finally {
IOUtils.closeQuietly(in);
}
}
public static Properties mergeProperties(Properties... properties) {
Properties mergedProperties = new Properties();
for (Properties property : properties) {
mergedProperties.putAll(property);
}
return mergedProperties;
}
}

View File

@@ -0,0 +1,66 @@
package org.springframework.ide.vscode.boot.java.copilot;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
public class ResponseModifier {
public String modify(String response) {
System.out.println("In response modifier !!");
return modifyMsyqlDependency(modifyJavax(response));
}
private String modifyJavax(String response) {
StringBuilder sb = new StringBuilder();
sb.append("*Note: The code provided is just an example and may not be suitable for production use.*");
sb.append(System.lineSeparator());
sb.append(System.lineSeparator());
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM);
sb.append("Generated on " + LocalDateTime.now().format(formatter));
sb.append(System.lineSeparator());
sb.append(System.lineSeparator());
sb.append(response);
String code = sb.toString();
if (!code.contains("import javax")) {
return code;
}
return replaceImportStatements(code);
}
private static String replaceImportStatements(String content) {
String[] lines = content.split(System.lineSeparator());
StringBuilder updatedContent = new StringBuilder();
for (String line : lines) {
if (line.trim().startsWith("import ")) {
String packageName = extractPackageName(line);
if (!packageName.contains("javax.sql.")) {
packageName = packageName.replace("javax", "jakarta");
}
line = "import " + packageName + ";";
}
updatedContent.append(line).append(System.lineSeparator());
}
return updatedContent.toString();
}
private static String extractPackageName(String importStatement) {
return importStatement.replace("import", "").replace(";", "").trim();
}
private String modifyMsyqlDependency(String response) {
if (!response.contains("<artifactId>mysql-connector-java</artifactId>")) {
return response;
}
else {
String s1 = response.replace("<groupId>mysql</groupId>", "<groupId>com.mysql</groupId>");
String s2 = s1.replace("<artifactId>mysql-connector-java</artifactId>",
"<artifactId>mysql-connector-j</artifactId>");
return s2;
}
}
}

View File

@@ -0,0 +1,23 @@
package org.springframework.ide.vscode.boot.java.copilot;
@SuppressWarnings("serial")
public class SpringCliException extends RuntimeException {
/**
* Instantiates a new {@code UpException}.
* @param message the message
*/
public SpringCliException(String message) {
super(message);
}
/**
* Instantiates a new {@code UpException}.
* @param message the message
* @param cause the cause
*/
public SpringCliException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,9 @@
package org.springframework.ide.vscode.boot.java.copilot;
import java.util.Map;
public interface TemplateEngine {
String process(String template, Map context);
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2021-2023 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.ide.vscode.boot.java.copilot.recipe;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
/**
* @author Fabian Krüger
*/
public class AbstractRecipeFactory {
private static XmlMapper mapper = new XmlMapper();
@org.jetbrains.annotations.Nullable
protected static String getNullOrTextValue(JsonNode jsonNode, String field) {
return (jsonNode.get(field) != null) ? AbstractRecipeFactory.getTextValue(jsonNode, field) : null;
}
protected static String getTextValue(JsonNode jsonNode, String field) {
try {
JsonNode xmlNode = jsonNode.get(field);
return xmlNode.textValue();
}
catch (NullPointerException npe) {
throw new RecipeCreationException(
"Could not get text value for field '%s' from: \n%s".formatted(field, jsonNode.toPrettyString()));
}
}
protected static JsonNode getJsonNode(String mavenDependencySnippet) throws JsonProcessingException {
JsonNode jsonNode = mapper.readTree(mavenDependencySnippet);
return jsonNode;
}
protected String getTextOrDefaultValue(JsonNode jsonNode, String version, String defaultValue) {
String nullOrTextValue = getNullOrTextValue(jsonNode, version);
return (nullOrTextValue != null) ? nullOrTextValue : defaultValue;
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2021-2023 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.ide.vscode.boot.java.copilot.recipe;
import java.util.regex.Pattern;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.internal.lang.Nullable;
import org.openrewrite.maven.AddDependencyVisitor;
import org.openrewrite.maven.table.MavenMetadataFailures;
/**
* Alternative to {@link org.openrewrite.maven.AddDependency} not doing any version
* checks. It uses the {@link AddDependencyVisitor} and bypasses all checks and other code
* in {@link org.openrewrite.maven.AddManagedDependency}.
*
* @author Fabian Krüger
*/
public class AddDependencyRecipe extends Recipe {
private final String groupId;
private final String artifactId;
private final String version;
private final String scope;
private final String type;
private final @Nullable String classifier;
private final @Nullable Boolean optional;
private final Pattern familyRegex;
private final MavenMetadataFailures metadataFailures;
public AddDependencyRecipe(String groupId, String artifactId, String version, String scope, String type,
@Nullable String classifier, @Nullable Boolean optional, Pattern familyRegex,
MavenMetadataFailures metadataFailures) {
this.groupId = groupId;
this.artifactId = artifactId;
this.version = version;
this.scope = scope;
this.type = type;
this.classifier = classifier;
this.optional = optional;
this.familyRegex = familyRegex;
this.metadataFailures = metadataFailures;
}
@Override
public String getDisplayName() {
return "Add dependency '%s:%s'".formatted(groupId, artifactId);
}
@Override
public String getDescription() {
return getDisplayName();
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
AddDependencyVisitor addDependencyVisitor = new AddDependencyVisitor(groupId, artifactId, version, null, scope,
true, type, classifier, optional, familyRegex, metadataFailures);
return addDependencyVisitor;
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2021-2024 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.ide.vscode.boot.java.copilot.recipe;
import java.util.regex.Pattern;
import org.openrewrite.internal.lang.Nullable;
import org.openrewrite.maven.AddDependency;
import org.openrewrite.maven.table.MavenMetadataFailures;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
/**
* @author Fabian Krüger
*/
public class AddDependencyRecipeFactory extends AbstractRecipeFactory {
/**
* Create {@link AddDependency} recipe from Maven dependency XML snippet.
*
* <pre>
* {@code
* <dependency>
* <groupId>groupId</groupId>
* <artifactId>artifactId</artifactId>
* <version>${some.version}</version>
* <classifier>classifier</classifier>
* <scope>scope</scope>
* <type>pom</type>
* <optional>true</optional>
* </dependency>
* }
* </pre>
*
*/
public AddDependencyRecipe create(String mavenDependency) {
try {
JsonNode jsonNode = getJsonNode(mavenDependency);
String groupId = getTextValue(jsonNode, "groupId");
String artifactId = getTextValue(jsonNode, "artifactId");
String version = getTextOrDefaultValue(jsonNode, "version", "latest");
@Nullable
String scope = getNullOrTextValue(jsonNode, "scope");
@Nullable
String type = getNullOrTextValue(jsonNode, "type");
@Nullable
String classifier = getNullOrTextValue(jsonNode, "classifier");
@Nullable
Boolean optional = Boolean.parseBoolean(getNullOrTextValue(jsonNode, "optional"));
@Nullable
String familyPattern = null;
Pattern familyRegex = (familyPattern != null) ? Pattern.compile(familyPattern) : null;
MavenMetadataFailures metadataFailures = null;
AddDependencyRecipe recipe = new AddDependencyRecipe(groupId, artifactId, version, scope, type, classifier,
optional, familyRegex, metadataFailures);
return recipe;
}
catch (JsonProcessingException ex) {
throw new RuntimeException(ex);
}
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2021-2023 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.ide.vscode.boot.java.copilot.recipe;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.maven.AddManagedDependencyVisitor;
/**
* Alternative to {@link org.openrewrite.maven.AddManagedDependency} that doesn't verify
* the provided version. It uses the {@link AddManagedDependencyVisitor} and bypasses all
* checks and other code in {@link org.openrewrite.maven.AddManagedDependency}.
*
* @author Fabian Krüger
*/
public class AddManagedDependencyRecipe extends Recipe {
private final String groupId;
private final String artifactId;
private final String version;
private final String scope;
private final String type;
private final String classifier;
public AddManagedDependencyRecipe(String groupId, String artifactId, String version, String scope, String type,
String classifier) {
this.groupId = groupId;
this.artifactId = artifactId;
this.version = version;
this.scope = scope;
this.type = type;
this.classifier = classifier;
}
@Override
public String getDisplayName() {
return "Add managed dependency '%s:%s'".formatted(groupId, artifactId);
}
@Override
public String getDescription() {
return getDisplayName();
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new AddManagedDependencyVisitor(groupId, artifactId, version, scope, type, classifier);
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2021-2023 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.ide.vscode.boot.java.copilot.recipe;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import org.apache.maven.model.Dependency;
import org.openrewrite.internal.lang.Nullable;
/**
* @author Fabian Krüger
*/
public class AddManagedDependencyRecipeFactory extends AbstractRecipeFactory {
/**
* Create a {@link AddManagedDependencyRecipe} from a Maven dependency XML snippet.
*
* <pre>
* {@code
* <dependency>
* <groupId>groupId</groupId>
* <artifactId>artifactId</artifactId>
* <version>${some.version}</version>
* <classifier>classifier</classifier>
* <type>pom</type>
* </dependency>
* }
* </pre>
*/
public AddManagedDependencyRecipe create(String mavenDependencySnippet) {
try {
JsonNode jsonNode = getJsonNode(mavenDependencySnippet);
String groupId = getTextValue(jsonNode, "groupId");
String artifactId = getTextValue(jsonNode, "artifactId");
@Nullable
String version = getNullOrTextValue(jsonNode, "version");
@Nullable
String scope = getNullOrTextValue(jsonNode, "scope");
@Nullable
String classifier = getNullOrTextValue(jsonNode, "classifier");
@Nullable
String type = getNullOrTextValue(jsonNode, "type");
AddManagedDependencyRecipe addManagedDependency = new AddManagedDependencyRecipe(groupId, artifactId,
version, scope, type, classifier);
return addManagedDependency;
}
catch (JsonProcessingException ex) {
throw new RuntimeException(ex);
}
}
/**
* Create a {@link AddManagedDependencyRecipe} from a {@link Dependency}.
*/
public AddManagedDependencyRecipe create(Dependency dependency) {
String groupId = dependency.getGroupId();
String artifactId = dependency.getArtifactId();
String version = dependency.getVersion();
String scope = dependency.getScope();
String type = dependency.getType();
String classifier = dependency.getClassifier();
AddManagedDependencyRecipe addManagedDependency = new AddManagedDependencyRecipe(groupId, artifactId, version,
scope, type, classifier);
return addManagedDependency;
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2021-2023 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.ide.vscode.boot.java.copilot.recipe;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import org.openrewrite.internal.lang.Nullable;
import org.openrewrite.maven.AddPlugin;
/**
* @author Fabian Krüger
*/
public class AddPluginRecipeFactory extends AbstractRecipeFactory {
/**
* Create {@link AddPlugin} from Maven plugin XML snippet.
*/
public AddPlugin create(String buildPlugin) {
try {
JsonNode jsonNode = getJsonNode(buildPlugin);
String groupId = getTextValue(jsonNode, "groupId");
String artifactId = getTextValue(jsonNode, "artifactId");
@Nullable
String version = getNullOrTextValue(jsonNode, "version");
@Nullable
String configuration = getNullOrTextValue(jsonNode, "configuration");
@Nullable
String dependencies = getNullOrTextValue(jsonNode, "dependencies");
@Nullable
String executions = getNullOrTextValue(jsonNode, "executions");
@Nullable
String filePattern = getNullOrTextValue(jsonNode, "filePattern");
AddPlugin addPlugin = new AddPlugin(groupId, artifactId, version, configuration, dependencies, executions,
filePattern);
return addPlugin;
}
catch (JsonProcessingException ex) {
throw new RuntimeException(ex);
}
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.ide.vscode.boot.java.copilot.recipe;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.maven.MavenIsoVisitor;
import org.openrewrite.xml.AddToTagVisitor;
import org.openrewrite.xml.XPathMatcher;
import org.openrewrite.xml.tree.Xml;
import org.openrewrite.xml.tree.Xml.Tag;
public class InjectTextMavenRepositoryRecipe extends Recipe {
private static final XPathMatcher REPOS_MATCHER = new XPathMatcher("/project/repositories");
private String text;
public InjectTextMavenRepositoryRecipe(String text) {
this.text = text;
}
@Override
public String getDisplayName() {
return "Add Repository";
}
@Override
public String getDescription() {
return getDisplayName();
}
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new MavenIsoVisitor<ExecutionContext>() {
public Xml.Document visitDocument(Xml.Document document, ExecutionContext ctx) {
Xml.Tag root = document.getRoot();
if (!root.getChild("repositories").isPresent()) {
document = (Xml.Document) (new AddToTagVisitor(root, Tag.build("<repositories/>")))
.visitNonNull(document, ctx, this.getCursor().getParentOrThrow());
}
return super.visitDocument(document, ctx);
}
public Xml.Tag visitTag(Xml.Tag tag, ExecutionContext ctx) {
Xml.Tag repositories = super.visitTag(tag, ctx);
if (REPOS_MATCHER.matches(this.getCursor())) {
Xml.Tag repositoryTag = Tag.build(text);
repositories = (Xml.Tag) (new AddToTagVisitor(repositories, repositoryTag))
.visitNonNull(repositories, ctx, this.getCursor().getParentOrThrow());
this.maybeUpdateModel();
}
return repositories;
}
};
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2021-2023 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.ide.vscode.boot.java.copilot.recipe;
/**
* @author Fabian Krüger
*/
public class RecipeCreationException extends RuntimeException {
public RecipeCreationException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.ide.vscode.boot.java.copilot.recipe;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.openrewrite.Result;
import org.springframework.ide.vscode.boot.java.copilot.SpringCliException;
/**
* Utilities for recipe execution
*/
public final class RecipeUtils {
private RecipeUtils() {
}
public static void writeResults(String recipeName, Path path, List<Result> resultList) {
try {
for (Result result : resultList) {
try (BufferedWriter sourceFileWriter = Files.newBufferedWriter(path)) {
sourceFileWriter.write(result.getAfter().printAllTrimmed());
}
}
}
catch (IOException ex) {
throw new SpringCliException("Could not write recipe results to path = " + path, ex);
}
}
}

View File

@@ -0,0 +1,128 @@
package org.springframework.ide.vscode.boot.java.copilot.util;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.boot.java.copilot.SpringCliException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
public abstract class AbstractMavenReader {
private static final Logger logger = LoggerFactory.getLogger(AbstractMavenReader.class);
protected String sectionName;
/**
* Reads the Document and populates the provided array with individual dependency text
* values for each dependency element.
* @param document The Document to parse
* @param dependencies a list to populate with each dependency text
* @throws TransformerException if the element can't be converted to text
*/
protected void parseDocument(Document document, List<String> dependencies, String tagName)
throws TransformerException {
Element root = document.getDocumentElement();
NodeList nodeList = root.getElementsByTagName(tagName);
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
for (int i = 0; i < nodeList.getLength(); i++) {
Node dependencyNode = nodeList.item(i);
if (dependencyNode.getNodeType() == Node.ELEMENT_NODE) {
Element element = (Element) dependencyNode;
StringWriter writer = new StringWriter();
transformer.transform(new DOMSource(element), new StreamResult(writer));
String xml = writer.toString();
dependencies.add(xml);
}
}
}
protected Document buildDocument(ErrorHandler handler, InputStream stream)
throws ParserConfigurationException, SAXException, IOException {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
DocumentBuilder parser = dbf.newDocumentBuilder();
parser.setErrorHandler(handler);
return parser.parse(stream);
}
protected abstract String massageText(String text);
public String[] parseMavenSection(String text) {
String textToUse = massageText(text);
ErrorHandler errorHandler = new SimpleErrorHandler(logger);
List<String> dependencies = new ArrayList<>();
try {
InputStream inputStream = IOUtils.toInputStream(textToUse, StandardCharsets.UTF_8);
Document document = buildDocument(errorHandler, inputStream);
parseDocument(document, dependencies, this.sectionName);
}
catch (IOException ex) {
throw new SpringCliException("Cannot parse maven " + this.sectionName + " from string: " + text, ex);
}
catch (SAXException ex) {
throw new SpringCliException("Invalid XML in maven " + this.sectionName + " from string: " + text, ex);
}
catch (ParserConfigurationException ex) {
throw new SpringCliException("Internal error parsing maven " + this.sectionName + " from string:" + text,
ex);
}
catch (TransformerException ex) {
throw new SpringCliException("Internal error transforming Node to text from string:" + text, ex);
}
return dependencies.toArray(new String[0]);
}
protected class SimpleErrorHandler implements ErrorHandler {
private final Logger logger;
public SimpleErrorHandler(Logger logger) {
this.logger = logger;
}
@Override
public void warning(SAXParseException ex) throws SAXException {
logger.warn("Ignored XML validation warning", ex);
}
@Override
public void error(SAXParseException ex) throws SAXException {
throw ex;
}
@Override
public void fatalError(SAXParseException ex) throws SAXException {
throw ex;
}
}
}

View File

@@ -0,0 +1,32 @@
package org.springframework.ide.vscode.boot.java.copilot.util;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ClassNameExtractor {
private String[] patternStrings = { "(?<=\\bclass\\s)\\w+", "(?<=\\binterface\\s)\\w+", "(?<=\\b@interface\\s)\\w+",
"(?<=\\benum\\s)\\w+" };
private List<Pattern> patterns = new ArrayList<>();
public ClassNameExtractor() {
for (String patternString : patternStrings) {
patterns.add(Pattern.compile(patternString));
}
}
public Optional<String> extractClassName(String code) {
for (Pattern pattern : patterns) {
Matcher matcher = pattern.matcher(code);
if (matcher.find()) {
return Optional.of(matcher.group());
}
}
return Optional.empty();
}
}

View File

@@ -0,0 +1,18 @@
package org.springframework.ide.vscode.boot.java.copilot.util;
public class MavenBuildPluginReader extends AbstractMavenReader {
public MavenBuildPluginReader() {
this.sectionName = "plugin";
}
protected String massageText(String text) {
if (text.contains("<plugins>")) {
return text;
}
else {
return "<plugins>" + text + "</plugins>";
}
}
}

View File

@@ -0,0 +1,18 @@
package org.springframework.ide.vscode.boot.java.copilot.util;
public class MavenDependencyReader extends AbstractMavenReader {
public MavenDependencyReader() {
this.sectionName = "dependency";
}
protected String massageText(String text) {
if (text.contains("<dependencies>")) {
return text;
}
else {
return "<dependencies>" + text + "</dependencies>";
}
}
}

View File

@@ -0,0 +1,18 @@
package org.springframework.ide.vscode.boot.java.copilot.util;
public class MavenRepositoryReader extends AbstractMavenReader {
public MavenRepositoryReader() {
this.sectionName = "repository";
}
protected String massageText(String text) {
if (text.contains("<repositories>")) {
return text;
}
else {
return "<repositories>" + text + "</repositories>";
}
}
}

View File

@@ -0,0 +1,45 @@
package org.springframework.ide.vscode.boot.java.copilot.util;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import org.apache.maven.model.Model;
import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
/**
* @author Ryan Baxter
*/
public class PomReader {
/**
* Returns a parsed POM.
*/
public Model readPom(File file) {
File pom = file;
if (file.isDirectory()) {
pom = new File(file, "pom.xml");
}
if (!pom.exists()) {
return null;
}
String fileText = "";
try (Reader reader = new FileReader(pom)) {
if (file.isFile()) {
fileText = new String(Files.readAllBytes(file.toPath()));
}
MavenXpp3Reader xpp3Reader = new MavenXpp3Reader();
return xpp3Reader.read(reader);
}
catch (XmlPullParserException | IOException ex) {
if (file.isFile() && fileText.length() == 0) {
throw new IllegalStateException("File [" + pom.getAbsolutePath() + "] is empty", ex);
}
throw new IllegalStateException("Failed to read file: " + pom.getAbsolutePath(), ex);
}
}
}