diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/util/FindBuildGradleFilesUtil.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/buildfile/BuildFileLocator.java similarity index 90% rename from src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/util/FindBuildGradleFilesUtil.java rename to src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/buildfile/BuildFileLocator.java index ba175ea..92462ac 100644 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/util/FindBuildGradleFilesUtil.java +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/buildfile/BuildFileLocator.java @@ -1,4 +1,4 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.util; +package com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.project.Project; @@ -12,7 +12,7 @@ import java.util.ArrayList; import java.util.List; -public class FindBuildGradleFilesUtil { +public class BuildFileLocator { public static List findBuildGradleFilesInCurrentProject(Project project) { return ApplicationManager.getApplication().runReadAction((Computable>) () -> { List files = new ArrayList<>(); @@ -22,13 +22,13 @@ public static List findBuildGradleFilesInCurrentProject(Project pro VfsUtilCore.visitChildrenRecursively(baseDir, new VirtualFileVisitor() { @Override public boolean visitFile(@NotNull VirtualFile file) { - // Skip common directories + // Skip common big directories if (file.isDirectory()) { String name = file.getName(); return !name.startsWith(".") && !name.equals("build") && !name.equals("node_modules") && !name.equals("target"); } - if (SupportedFilesUtil.isSupportedFile(file.getName())) { + if (SupportedBuildFile.isSupportedFile(file.getName())) { files.add(file); } return true; diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/psi/DependencyParser.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/buildfile/BuildFileParser.java similarity index 83% rename from src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/psi/DependencyParser.java rename to src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/buildfile/BuildFileParser.java index 9ed33be..1c21ff7 100644 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/psi/DependencyParser.java +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/buildfile/BuildFileParser.java @@ -1,6 +1,6 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.psi; +package com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.DependencyInfo; +import com.github.clementherve.intellijjavadependencyupdaterplugin.dependency.Dependency; import com.intellij.psi.PsiFile; import org.jetbrains.annotations.NotNull; @@ -9,7 +9,7 @@ /** * Interface for parsing dependency declarations from Gradle build files. */ -public interface DependencyParser { +public interface BuildFileParser { /** * Parses all dependency declarations from a PSI file. @@ -18,7 +18,7 @@ public interface DependencyParser { * @return a list of dependency information objects */ @NotNull - List parseDependencies(@NotNull PsiFile psiFile); + List parseDependencies(@NotNull PsiFile psiFile); /** * Checks if this parser can handle the given file. diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/psi/DependencyParserFactory.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/buildfile/BuildFileParserFactory.java similarity index 68% rename from src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/psi/DependencyParserFactory.java rename to src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/buildfile/BuildFileParserFactory.java index d77af64..c9a4c80 100644 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/psi/DependencyParserFactory.java +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/buildfile/BuildFileParserFactory.java @@ -1,5 +1,6 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.psi; +package com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile; +import com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile.gradle.GradleBuildFileParser; import com.intellij.psi.PsiFile; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -7,7 +8,7 @@ /** * Factory for creating appropriate dependency parsers based on file type. */ -public class DependencyParserFactory { +public class BuildFileParserFactory { /** * Gets the appropriate parser for the given file. @@ -16,11 +17,11 @@ public class DependencyParserFactory { * @return a parser instance, or null if no parser is available for this file type */ @Nullable - public static DependencyParser getParser(@NotNull PsiFile file) { + public static BuildFileParser getParser(@NotNull PsiFile file) { String fileName = file.getName(); if ("build.gradle".equals(fileName)) { - return new GradlePsiParser(); + return new GradleBuildFileParser(); } return null; diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/util/SupportedFilesUtil.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/buildfile/SupportedBuildFile.java similarity index 83% rename from src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/util/SupportedFilesUtil.java rename to src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/buildfile/SupportedBuildFile.java index 920fe8e..e329872 100644 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/util/SupportedFilesUtil.java +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/buildfile/SupportedBuildFile.java @@ -1,6 +1,6 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.util; +package com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile; -public class SupportedFilesUtil { +public class SupportedBuildFile { private static final String SUPPORTED_FILE = "build.gradle"; public static boolean isSupportedFile(String fileName) { diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/buildfile/gradle/GradleBuildFileParser.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/buildfile/gradle/GradleBuildFileParser.java new file mode 100644 index 0000000..6fd4bd5 --- /dev/null +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/buildfile/gradle/GradleBuildFileParser.java @@ -0,0 +1,55 @@ +package com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile.gradle; + +import com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile.BuildFileParser; +import com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile.SupportedBuildFile; +import com.github.clementherve.intellijjavadependencyupdaterplugin.dependency.Dependency; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.psi.PsiFile; +import com.intellij.psi.util.PsiTreeUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall; + +import java.util.ArrayList; +import java.util.List; + +/** + * Parses dependency and plugin declarations from Groovy build.gradle files by walking every + * method call and delegating each to the dependency and plugin extractors. + */ +public class GradleBuildFileParser implements BuildFileParser { + + private static final Logger LOGGER = Logger.getInstance(GradleBuildFileParser.class); + + @Override + public boolean canParse(@NotNull PsiFile psiFile) { + return SupportedBuildFile.isSupportedFile(psiFile.getName()); + } + + @NotNull + @Override + public List parseDependencies(@NotNull PsiFile psiFile) { + List dependencies = new ArrayList<>(); + + for (GrMethodCall methodCall : PsiTreeUtil.findChildrenOfType(psiFile, GrMethodCall.class)) { + try { + Dependency dependency = GradleDependencyExtractor.extract(methodCall, psiFile); + if (dependency != null) { + dependencies.add(dependency); + } + + Dependency plugin = GradlePluginExtractor.extract(methodCall); + if (plugin != null) { + dependencies.add(plugin); + } + } catch (ProcessCanceledException exception) { + // Rethrow - this is a control flow exception, not an error + throw exception; + } catch (Exception exception) { + LOGGER.debug("Failed to parse dependency from method call", exception); + } + } + + return dependencies; + } +} diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/buildfile/gradle/GradleDependencyExtractor.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/buildfile/gradle/GradleDependencyExtractor.java new file mode 100644 index 0000000..1afedb7 --- /dev/null +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/buildfile/gradle/GradleDependencyExtractor.java @@ -0,0 +1,256 @@ +package com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile.gradle; + +import com.github.clementherve.intellijjavadependencyupdaterplugin.dependency.Dependency; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.SmartPointerManager; +import com.intellij.psi.SmartPsiElementPointer; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentList; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrNamedArgument; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrLiteral; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrString; + +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Extracts a dependency declaration from a single Groovy method call, supporting the three + * notations Gradle accepts: string ({@code "group:artifact:version"}), interpolated GString + * ({@code "group:artifact:$version"}) and map ({@code group: 'g', name: 'a', version: 'v'}). + */ +final class GradleDependencyExtractor { + + private static final List KNOWN_CONFIGURATIONS = Arrays.asList( + "implementation", "api", "compileOnly", "runtimeOnly", + "testImplementation", "testCompileOnly", "testRuntimeOnly", + "annotationProcessor", "kapt" + ); + + // group:artifact:version + private static final Pattern DEPENDENCY_PATTERN = Pattern.compile("([^:]+):([^:]+):([^:]+)?"); + + private GradleDependencyExtractor() { + } + + /** + * Attempts to parse a dependency from a method call expression. + * In Groovy, everything is a function, i.e.: {@code }. + */ + @Nullable + static Dependency extract(@NotNull GrMethodCall methodCall, @NotNull PsiFile psiFile) { + PsiElement methodElement = methodCall.getInvokedExpression(); + + String methodName = methodElement.getText(); + final boolean unknownMethodName = !KNOWN_CONFIGURATIONS.contains(methodName); + if (unknownMethodName) { + return null; + } + + GrArgumentList argumentList = methodCall.getArgumentList(); + + final boolean emptyMethodArguments = argumentList.getAllArguments().length == 0; + if (emptyMethodArguments) { + return null; + } + + // cas: implementation 'org.springframework:spring-core:6.0' + GrExpression[] expressionArguments = argumentList.getExpressionArguments(); + final boolean hasExpressionArguments = expressionArguments.length > 0; + if (hasExpressionArguments) { + if (expressionArguments[0] instanceof final GrLiteral literal) { + Object value = literal.getValue(); + + if (value instanceof String) { + // string notation, no variable + return parseStringNotation((String) value, methodName, literal, psiFile); + } else if (literal instanceof final GrString gstring) { + // GString notation, with variables + return parseGStringNotation(gstring, methodName, psiFile); + } + } + } + + // cas: implementation(group: 'org.springframework', name: 'spring-core', version: '6.0') + GrNamedArgument[] namedArguments = argumentList.getNamedArguments(); + final boolean hasNamedArguments = namedArguments.length > 0; + if (hasNamedArguments) { + return parseMapNotation(namedArguments, methodName, psiFile); + } + + return null; + } + + /** + * Parses GString notation with variable interpolation: {@code "group:artifact:$version"} + */ + @Nullable + private static Dependency parseGStringNotation(@NotNull GrString gstring, + @NotNull String configurationName, + @NotNull PsiFile psiFile) { + StringBuilder fullString = new StringBuilder(); + String variableName; + boolean hasVariable = false; + + for (PsiElement child : gstring.getChildren()) { + fullString.append(child.getText()); + } + + String dependencyString = fullString.toString(); + Matcher matcher = DEPENDENCY_PATTERN.matcher(dependencyString); + if (!matcher.matches()) { + return null; + } + + String group = matcher.group(1); + String artifact = matcher.group(2); + variableName = matcher.group(3); + + if (variableName == null) { + return null; + } + + String resolvedVariable; + + if (variableName.startsWith("$")) { + variableName = GradleVariableResolver.extractVariableName(variableName); + resolvedVariable = GradleVariableResolver.resolveVariable(variableName, psiFile); + hasVariable = true; + } else { + resolvedVariable = variableName; + } + + if (StringUtils.isBlank(resolvedVariable)) { + resolvedVariable = ""; + hasVariable = false; + } + + SmartPsiElementPointer pointer = SmartPointerManager.createPointer(gstring); + + return new Dependency( + group, + artifact, + resolvedVariable, + configurationName, + pointer, + hasVariable, + variableName + ); + } + + /** + * Parses string notation: {@code "group:artifact:version"} + */ + @Nullable + private static Dependency parseStringNotation(@NotNull String dependencyString, + @NotNull String configurationName, + @NotNull PsiElement versionElement, + @NotNull PsiFile psiFile) { + Matcher matcher = DEPENDENCY_PATTERN.matcher(dependencyString); + if (!matcher.matches()) { + return null; + } + + String group = matcher.group(1); + String artifact = matcher.group(2); + String version = matcher.group(3); + + boolean isVersionVariable = version.startsWith("$"); + String variableName = null; + String resolvedVersion = version; + + if (isVersionVariable) { + variableName = GradleVariableResolver.extractVariableName(version); + String resolved = GradleVariableResolver.resolveVariable(variableName, psiFile); + if (resolved != null) { + resolvedVersion = resolved; + } + } + + SmartPsiElementPointer pointer = SmartPointerManager.createPointer(versionElement); + + return new Dependency( + group, + artifact, + resolvedVersion, + configurationName, + pointer, + isVersionVariable, + variableName + ); + } + + /** + * Parses map notation: {@code group: 'g', name: 'a', version: 'v'} + */ + @Nullable + private static Dependency parseMapNotation(@NotNull GrNamedArgument[] namedArgs, + @NotNull String configurationName, + @NotNull PsiFile psiFile) { + String group = null; + String artifact = null; + String version = null; + PsiElement versionElement = null; + + for (GrNamedArgument arg : namedArgs) { + String argName = arg.getLabelName(); + GrExpression expression = arg.getExpression(); + + if (expression instanceof GrLiteral) { + Object value = ((GrLiteral) expression).getValue(); + if (value instanceof String) { + switch (argName) { + case "group": + group = (String) value; + break; + case "name": + artifact = (String) value; + break; + case "version": + version = (String) value; + versionElement = expression; + break; + case null: + break; + default: + throw new IllegalStateException("Unexpected value: " + argName); + } + } + } + } + + if (group == null || artifact == null || version == null) { + return null; + } + + boolean isVersionVariable = version.startsWith("$"); + String variableName = null; + String resolvedVersion = version; + + if (isVersionVariable) { + variableName = GradleVariableResolver.extractVariableName(version); + String resolved = GradleVariableResolver.resolveVariable(variableName, psiFile); + if (resolved != null) { + resolvedVersion = resolved; + } + } + + SmartPsiElementPointer pointer = SmartPointerManager.createPointer(versionElement); + + return new Dependency( + group, + artifact, + resolvedVersion, + configurationName, + pointer, + isVersionVariable, + variableName + ); + } +} diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/buildfile/gradle/GradlePluginExtractor.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/buildfile/gradle/GradlePluginExtractor.java new file mode 100644 index 0000000..ef2ea4e --- /dev/null +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/buildfile/gradle/GradlePluginExtractor.java @@ -0,0 +1,115 @@ +package com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile.gradle; + +import com.github.clementherve.intellijjavadependencyupdaterplugin.dependency.Dependency; +import com.intellij.psi.PsiElement; +import com.intellij.psi.SmartPointerManager; +import com.intellij.psi.SmartPsiElementPointer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentList; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrLiteral; + +/** + * Extracts a plugin declaration from a {@code plugins { }} block. + *

+ * Format: {@code id 'plugin.id' version 'version'}. In Groovy this is a chained method call + * where {@code version} is invoked on the result of {@code id}, so the PSI structure is + * {@code GrMethodCall(version) -> GrReferenceExpression(id(...))}. + */ +final class GradlePluginExtractor { + + private GradlePluginExtractor() { + } + + @Nullable + static Dependency extract(@NotNull GrMethodCall methodCall) { + PsiElement invokedExpression = methodCall.getInvokedExpression(); + String methodName; + + // Extract just the method name from the reference (not the full chained text) + if (invokedExpression instanceof GrReferenceExpression) { + methodName = ((GrReferenceExpression) invokedExpression).getReferenceName(); + } else { + methodName = invokedExpression.getText(); + } + + // Only the outer 'version' call in: id 'plugin.id' version 'version' + if (!"version".equals(methodName)) { + return null; + } + + GrArgumentList argumentList = methodCall.getArgumentList(); + GrExpression[] versionArguments = argumentList.getExpressionArguments(); + + if (versionArguments.length == 0 || !(versionArguments[0] instanceof GrLiteral)) { + return null; + } + + Object versionValue = ((GrLiteral) versionArguments[0]).getValue(); + if (!(versionValue instanceof final String version)) { + return null; + } + + PsiElement versionElement = versionArguments[0]; + + // The invoked expression should be a reference with a qualifier that is the 'id' call + if (!(invokedExpression instanceof final GrReferenceExpression referenceExpression)) { + return null; + } + + GrExpression qualifier = referenceExpression.getQualifierExpression(); + if (!(qualifier instanceof final GrMethodCall idCall)) { + return null; + } + + PsiElement idInvoked = idCall.getInvokedExpression(); + if (!"id".equals(idInvoked.getText())) { + return null; + } + + if (!isInsidePluginsBlock(methodCall)) { + return null; + } + + GrArgumentList idArgumentList = idCall.getArgumentList(); + GrExpression[] idArguments = idArgumentList.getExpressionArguments(); + + if (idArguments.length == 0 || !(idArguments[0] instanceof GrLiteral)) { + return null; + } + + Object pluginIdValue = ((GrLiteral) idArguments[0]).getValue(); + if (!(pluginIdValue instanceof final String pluginId)) { + return null; + } + + SmartPsiElementPointer pointer = SmartPointerManager.createPointer(versionElement); + + return new Dependency( + "", // empty group for plugins + pluginId, // artifact is the plugin ID + version, + "plugin", + pointer, + false, + null + ); + } + + private static boolean isInsidePluginsBlock(@NotNull GrMethodCall methodCall) { + PsiElement ancestor = methodCall.getParent(); + while (ancestor != null) { + if (ancestor instanceof GrMethodCall) { + PsiElement ancestorMethod = ((GrMethodCall) ancestor).getInvokedExpression(); + if ("plugins".equals(ancestorMethod.getText())) { + return true; + } + } + ancestor = ancestor.getParent(); + } + return false; + } +} diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/buildfile/gradle/GradleVariableResolver.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/buildfile/gradle/GradleVariableResolver.java new file mode 100644 index 0000000..dd878c4 --- /dev/null +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/buildfile/gradle/GradleVariableResolver.java @@ -0,0 +1,84 @@ +package com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile.gradle; + +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.util.PsiTreeUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Resolves Gradle build-script variables (those declared in an {@code ext { }} block) to + * their literal values, and normalises variable references such as {@code ${name}} or + * {@code $name} to the bare variable name. + */ +final class GradleVariableResolver { + + private static final String EXT_BLOCK_CONSTANT = "ext"; + private static final Pattern INTERPOLATION_PATTERN = Pattern.compile("\\$\\{([^}]+)}"); + + private GradleVariableResolver() { + } + + /** + * Resolves a Gradle variable to its value by searching the {@code ext} block. + * + * @param variableName the name of the variable (without the {@code $} prefix) + * @param psiFile the Gradle build file + * @return the resolved value, or {@code null} if not found + */ + @Nullable + static String resolveVariable(@NotNull String variableName, @NotNull PsiFile psiFile) { + for (GrMethodCall methodCall : PsiTreeUtil.findChildrenOfType(psiFile, GrMethodCall.class)) { + PsiElement methodElement = methodCall.getInvokedExpression(); + + final boolean hasExtBlock = EXT_BLOCK_CONSTANT.equals(methodElement.getText()); + if (hasExtBlock) { + String value = findVariableInExtBlock(variableName, methodCall); + if (value != null) { + return value; + } + } + } + + return null; + } + + /** + * Searches for a variable assignment within an {@code ext { }} block. + */ + @Nullable + private static String findVariableInExtBlock(@NotNull String variableName, @NotNull GrMethodCall extCall) { + for (PsiElement child : extCall.getChildren()) { + String text = child.getText(); + Pattern pattern = Pattern.compile(Pattern.quote(variableName) + "\\s*=\\s*['\"]([^'\"]+)['\"]"); + Matcher matcher = pattern.matcher(text); + if (matcher.find()) { + return matcher.group(1); + } + } + return null; + } + + /** + * Extracts the bare variable name from a reference: {@code ${spring_version}} or + * {@code $spring_version} both yield {@code spring_version}. Returns {@code null} when the + * value is not a variable reference. + */ + @Nullable + static String extractVariableName(@NotNull String version) { + if (version.startsWith("${")) { + Matcher matcher = INTERPOLATION_PATTERN.matcher(version); + return matcher.find() ? matcher.group(1) : version; + } + + if (version.startsWith("$")) { + return version.substring(1); + } + + return null; + } +} diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/model/DependencyInfo.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/dependency/Dependency.java similarity index 91% rename from src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/model/DependencyInfo.java rename to src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/dependency/Dependency.java index d612365..b3548ec 100644 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/model/DependencyInfo.java +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/dependency/Dependency.java @@ -1,4 +1,4 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.model; +package com.github.clementherve.intellijjavadependencyupdaterplugin.dependency; import com.intellij.psi.PsiElement; import com.intellij.psi.SmartPsiElementPointer; @@ -10,10 +10,10 @@ /** * Represents a dependency declaration found in a Gradle build file. */ -public record DependencyInfo(String group, String artifact, String currentVersion, String configurationName, +public record Dependency(String group, String artifact, String currentVersion, String configurationName, SmartPsiElementPointer psiElementPointer, boolean isVersionVariable, String variableName) { - public DependencyInfo(@NotNull String group, + public Dependency(@NotNull String group, @NotNull String artifact, @NotNull String currentVersion, @NotNull String configurationName, @@ -79,7 +79,7 @@ public String getFullCoordinates() { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - DependencyInfo that = (DependencyInfo) o; + Dependency that = (Dependency) o; return Objects.equals(group, that.group) && Objects.equals(artifact, that.artifact) && Objects.equals(currentVersion, that.currentVersion) && @@ -94,7 +94,7 @@ public int hashCode() { @NotNull @Override public String toString() { - return "DependencyInfo{" + + return "Dependency{" + configurationName + "('" + group + ":" + artifact + ":" + currentVersion + "'" + (isVersionVariable ? ", variable=" + variableName : "") + diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/actions/UpdateAllDependenciesAction.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/action/UpdateAllDependenciesAction.java similarity index 77% rename from src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/actions/UpdateAllDependenciesAction.java rename to src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/action/UpdateAllDependenciesAction.java index 0dcfa67..561dd8f 100644 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/actions/UpdateAllDependenciesAction.java +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/action/UpdateAllDependenciesAction.java @@ -1,12 +1,12 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.actions; - -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.DependencyInfo; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.VersionCandidate; -import com.github.clementherve.intellijjavadependencyupdaterplugin.psi.DependencyParser; -import com.github.clementherve.intellijjavadependencyupdaterplugin.psi.DependencyParserFactory; -import com.github.clementherve.intellijjavadependencyupdaterplugin.services.DependencyUpdateService; -import com.github.clementherve.intellijjavadependencyupdaterplugin.util.SupportedFilesUtil; -import com.github.clementherve.intellijjavadependencyupdaterplugin.util.VersionReplacer; +package com.github.clementherve.intellijjavadependencyupdaterplugin.ide.action; + +import com.github.clementherve.intellijjavadependencyupdaterplugin.dependency.Dependency; +import com.github.clementherve.intellijjavadependencyupdaterplugin.version.VersionCandidate; +import com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile.BuildFileParser; +import com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile.BuildFileParserFactory; +import com.github.clementherve.intellijjavadependencyupdaterplugin.service.DependencyUpdateService; +import com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile.SupportedBuildFile; +import com.github.clementherve.intellijjavadependencyupdaterplugin.update.DependencyVersionWriter; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.CommonDataKeys; @@ -31,15 +31,15 @@ public class UpdateAllDependenciesAction extends AnAction { @Override - public void actionPerformed(@NotNull AnActionEvent e) { - Project project = e.getProject(); - PsiFile file = e.getData(CommonDataKeys.PSI_FILE); + public void actionPerformed(@NotNull AnActionEvent event) { + Project project = event.getProject(); + PsiFile file = event.getData(CommonDataKeys.PSI_FILE); if (project == null || file == null) { return; } - if (!SupportedFilesUtil.isSupportedFile(file.getName())) { + if (!SupportedBuildFile.isSupportedFile(file.getName())) { Messages.showInfoMessage( project, "This action only works on build.gradle files.", @@ -49,13 +49,13 @@ public void actionPerformed(@NotNull AnActionEvent e) { } ProgressManager.getInstance().run(new Task.Backgroundable(project, "Checking for dependency updates", true) { - private final Map dependenciesWithUpdateCandidates = new HashMap<>(); + private final Map dependenciesWithUpdateCandidates = new HashMap<>(); @Override public void run(@NotNull ProgressIndicator indicator) { indicator.setText("Parsing dependencies..."); - List dependencies = safelyParseDependencies(file); + List dependencies = safelyParseDependencies(file); if (dependencies.isEmpty()) { return; @@ -69,7 +69,7 @@ public void run(@NotNull ProgressIndicator indicator) { return; } - DependencyInfo dependency = dependencies.get(i); + Dependency dependency = dependencies.get(i); indicator.setFraction((double) i / dependencies.size()); indicator.setText2("Checking " + dependency.artifact() + "..."); @@ -106,7 +106,7 @@ public void onSuccess() { ); if (result == Messages.OK) { - List> sortedUpdates = sortRowsToUpdateInReverseOrder(dependenciesWithUpdateCandidates); + List> sortedUpdates = sortUpdatesInReverseDocumentOrder(dependenciesWithUpdateCandidates); applyUpdateDependenciesToDocument(sortedUpdates, project); @@ -119,7 +119,7 @@ public void onSuccess() { } @NotNull - private List> sortRowsToUpdateInReverseOrder(Map updates) { + private List> sortUpdatesInReverseDocumentOrder(Map updates) { return updates.entrySet().stream() .sorted((e1, e2) -> { final SmartPsiElementPointer psiElementPointer1 = e1.getKey().psiElementPointer(); @@ -139,13 +139,13 @@ private List> sortRowsToUpdateInReve private StringBuilder getMessage() { StringBuilder message = new StringBuilder("The following dependencies will be updated:\n\n"); - for (Map.Entry entry : dependenciesWithUpdateCandidates.entrySet()) { - DependencyInfo dependencyInfo = entry.getKey(); + for (Map.Entry entry : dependenciesWithUpdateCandidates.entrySet()) { + Dependency dependency = entry.getKey(); VersionCandidate versionCandidate = entry.getValue(); final String informationMessage = String.format("%s: %s → %s\n", - dependencyInfo.artifact(), - dependencyInfo.currentVersion(), + dependency.artifact(), + dependency.currentVersion(), versionCandidate.version()); message.append(informationMessage); @@ -165,9 +165,9 @@ public void onThrowable(@NotNull Throwable error) { }); } - private static List safelyParseDependencies(final PsiFile file) { - return ApplicationManager.getApplication().runReadAction((Computable>) () -> { - DependencyParser dependencyParser = DependencyParserFactory.getParser(file); + private static List safelyParseDependencies(final PsiFile file) { + return ApplicationManager.getApplication().runReadAction((Computable>) () -> { + BuildFileParser dependencyParser = BuildFileParserFactory.getParser(file); if (dependencyParser == null) { return new ArrayList<>(); } @@ -175,10 +175,10 @@ private static List safelyParseDependencies(final PsiFile file) }); } - private static void applyUpdateDependenciesToDocument(final List> sortedUpdates, final Project project) { + private static void applyUpdateDependenciesToDocument(final List> sortedUpdates, final Project project) { WriteCommandAction.runWriteCommandAction(project, "Update All Dependencies", null, () -> { - for (Map.Entry entry : sortedUpdates) { - VersionReplacer.applyUpdateInWriteAction( + for (Map.Entry entry : sortedUpdates) { + DependencyVersionWriter.applyUpdateInWriteAction( project, entry.getKey(), entry.getValue().version() @@ -188,9 +188,9 @@ private static void applyUpdateDependenciesToDocument(final List dependencies = CachedValuesManager.getCachedValue(file, + List dependencies = CachedValuesManager.getCachedValue(file, () -> CachedValueProvider.Result.create(parser.parseDependencies(file), file)); DependencyUpdateService service = DependencyUpdateService.getInstance(project); - for (DependencyInfo dependency : dependencies) { + for (Dependency dependency : dependencies) { final SmartPsiElementPointer elementPointer = dependency.psiElementPointer(); if (elementPointer == null) { continue; diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/intention/UpdateDependencyToPatchIntention.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/intention/AbstractUpdateDependencyIntention.java similarity index 51% rename from src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/intention/UpdateDependencyToPatchIntention.java rename to src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/intention/AbstractUpdateDependencyIntention.java index 343447d..f69d3bf 100644 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/intention/UpdateDependencyToPatchIntention.java +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/intention/AbstractUpdateDependencyIntention.java @@ -1,15 +1,14 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.intention; - -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.DependencyInfo; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.VersionCandidate; -import com.github.clementherve.intellijjavadependencyupdaterplugin.psi.DependencyParser; -import com.github.clementherve.intellijjavadependencyupdaterplugin.psi.DependencyParserFactory; -import com.github.clementherve.intellijjavadependencyupdaterplugin.services.DependencyUpdateService; -import com.github.clementherve.intellijjavadependencyupdaterplugin.util.SemanticVersion; -import com.github.clementherve.intellijjavadependencyupdaterplugin.util.SupportedFilesUtil; -import com.github.clementherve.intellijjavadependencyupdaterplugin.util.VersionReplacer; -import com.github.clementherve.intellijjavadependencyupdaterplugin.util.VersionUpdateType; +package com.github.clementherve.intellijjavadependencyupdaterplugin.ide.intention; + import com.github.clementherve.intellijjavadependencyupdaterplugin.DependencyUpdaterBundle; +import com.github.clementherve.intellijjavadependencyupdaterplugin.dependency.Dependency; +import com.github.clementherve.intellijjavadependencyupdaterplugin.version.VersionCandidate; +import com.github.clementherve.intellijjavadependencyupdaterplugin.service.DependencyUpdateService; +import com.github.clementherve.intellijjavadependencyupdaterplugin.version.SemanticVersion; +import com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile.SupportedBuildFile; +import com.github.clementherve.intellijjavadependencyupdaterplugin.update.DependencyVersionWriter; +import com.github.clementherve.intellijjavadependencyupdaterplugin.version.VersionChangeClassifier; +import com.github.clementherve.intellijjavadependencyupdaterplugin.version.VersionChangeKind; import com.intellij.codeInsight.intention.IntentionAction; import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction; import com.intellij.openapi.editor.Editor; @@ -19,13 +18,22 @@ import com.intellij.util.IncorrectOperationException; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.List; /** - * Intention action to update a dependency to the latest patch version. + * Shared behaviour for the intentions that bump a dependency to the latest version of a + * specific {@link VersionChangeKind} (major, minor or patch). Subclasses only declare which + * change kind they target and how they label themselves. */ -public class UpdateDependencyToPatchIntention extends PsiElementBaseIntentionAction implements IntentionAction { +public abstract class AbstractUpdateDependencyIntention extends PsiElementBaseIntentionAction implements IntentionAction { + + /** + * The change kind this intention bumps to. + */ + @NotNull + protected abstract VersionChangeKind changeKind(); @Override public boolean startInWriteAction() { @@ -40,37 +48,36 @@ public void invoke(@NotNull Project project, Editor editor, @NotNull PsiElement return; } - DependencyInfo dependency = findDependencyAtElement(file, element); + Dependency dependency = DependencyAtCaretFinder.find(file, element); if (dependency == null) { return; } - VersionCandidate candidate = findPatchUpdate(project, dependency); + VersionCandidate candidate = findUpdate(project, dependency); if (candidate == null) { return; } - VersionReplacer.applyUpdateInWriteAction(project, dependency, candidate.version()); + DependencyVersionWriter.applyUpdateInWriteAction(project, dependency, candidate.version()); } @Override public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull PsiElement element) { - // todo: extract in a service PsiFile file = element.getContainingFile(); if (file == null) { return false; } - if (!SupportedFilesUtil.isSupportedFile(file.getName())) { + if (!SupportedBuildFile.isSupportedFile(file.getName())) { return false; } - DependencyInfo dependency = findDependencyAtElement(file, element); + Dependency dependency = DependencyAtCaretFinder.find(file, element); if (dependency == null) { return false; } - return findPatchUpdate(project, dependency) != null; + return findUpdate(project, dependency) != null; } @Nls @@ -80,39 +87,8 @@ public String getFamilyName() { return DependencyUpdaterBundle.message("intention.familyName"); } - @NotNull - @Override - public String getText() { - return DependencyUpdaterBundle.message("intention.updateDependencyToPatch"); - } - - private DependencyInfo findDependencyAtElement(@NotNull PsiFile file, @NotNull PsiElement element) { - // todo: extract in a service - DependencyParser parser = DependencyParserFactory.getParser(file); - if (parser == null) { - return null; - } - - List dependencies = parser.parseDependencies(file); - int offset = element.getTextOffset(); - - for (DependencyInfo dependency : dependencies) { - if (dependency.psiElementPointer() != null) { - PsiElement depElement = dependency.psiElementPointer().getElement(); - if (depElement != null) { - int start = depElement.getTextRange().getStartOffset(); - int end = depElement.getTextRange().getEndOffset(); - if (offset >= start && offset <= end) { - return dependency; - } - } - } - } - - return null; - } - - private VersionCandidate findPatchUpdate(@NotNull Project project, @NotNull DependencyInfo dependency) { + @Nullable + private VersionCandidate findUpdate(@NotNull Project project, @NotNull Dependency dependency) { SemanticVersion currentVersion = SemanticVersion.parse(dependency.currentVersion()); if (currentVersion == null) { return null; @@ -121,6 +97,6 @@ private VersionCandidate findPatchUpdate(@NotNull Project project, @NotNull Depe DependencyUpdateService service = DependencyUpdateService.getInstance(project); List candidates = service.getAllCandidatesFromCache(dependency); - return VersionUpdateType.findLatestByType(currentVersion, candidates, VersionUpdateType.UpdateType.PATCH); + return VersionChangeClassifier.findLatestChange(currentVersion, candidates, changeKind()); } } diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/intention/DependencyAtCaretFinder.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/intention/DependencyAtCaretFinder.java new file mode 100644 index 0000000..8d1e186 --- /dev/null +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/intention/DependencyAtCaretFinder.java @@ -0,0 +1,49 @@ +package com.github.clementherve.intellijjavadependencyupdaterplugin.ide.intention; + +import com.github.clementherve.intellijjavadependencyupdaterplugin.dependency.Dependency; +import com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile.BuildFileParser; +import com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile.BuildFileParserFactory; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * Locates the dependency declaration that contains a given caret position, by parsing the + * build file and matching the element offset against each declaration's range. + */ +public final class DependencyAtCaretFinder { + + private DependencyAtCaretFinder() { + } + + @Nullable + public static Dependency find(@NotNull PsiFile file, @NotNull PsiElement element) { + BuildFileParser parser = BuildFileParserFactory.getParser(file); + if (parser == null) { + return null; + } + + List dependencies = parser.parseDependencies(file); + int offset = element.getTextOffset(); + + for (Dependency dependency : dependencies) { + if (dependency.psiElementPointer() == null) { + continue; + } + PsiElement declarationElement = dependency.psiElementPointer().getElement(); + if (declarationElement == null) { + continue; + } + int start = declarationElement.getTextRange().getStartOffset(); + int end = declarationElement.getTextRange().getEndOffset(); + if (offset >= start && offset <= end) { + return dependency; + } + } + + return null; + } +} diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/intention/UpdateDependencyIntention.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/intention/UpdateDependencyIntention.java similarity index 66% rename from src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/intention/UpdateDependencyIntention.java rename to src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/intention/UpdateDependencyIntention.java index 8a7aa7a..43c2d0d 100644 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/intention/UpdateDependencyIntention.java +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/intention/UpdateDependencyIntention.java @@ -1,13 +1,11 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.intention; +package com.github.clementherve.intellijjavadependencyupdaterplugin.ide.intention; import com.github.clementherve.intellijjavadependencyupdaterplugin.DependencyUpdaterBundle; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.DependencyInfo; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.VersionCandidate; -import com.github.clementherve.intellijjavadependencyupdaterplugin.psi.DependencyParser; -import com.github.clementherve.intellijjavadependencyupdaterplugin.psi.DependencyParserFactory; -import com.github.clementherve.intellijjavadependencyupdaterplugin.services.DependencyUpdateService; -import com.github.clementherve.intellijjavadependencyupdaterplugin.util.SupportedFilesUtil; -import com.github.clementherve.intellijjavadependencyupdaterplugin.util.VersionReplacer; +import com.github.clementherve.intellijjavadependencyupdaterplugin.dependency.Dependency; +import com.github.clementherve.intellijjavadependencyupdaterplugin.version.VersionCandidate; +import com.github.clementherve.intellijjavadependencyupdaterplugin.service.DependencyUpdateService; +import com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile.SupportedBuildFile; +import com.github.clementherve.intellijjavadependencyupdaterplugin.update.DependencyVersionWriter; import com.intellij.codeInsight.intention.IntentionAction; import com.intellij.codeInsight.intention.PriorityAction; import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction; @@ -19,15 +17,13 @@ import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; -import java.util.List; - /** * Intention action to update a dependency to the latest version. */ public class UpdateDependencyIntention extends PsiElementBaseIntentionAction implements IntentionAction, PriorityAction { private VersionCandidate cachedCandidate; - private DependencyInfo cachedDependency; + private Dependency cachedDependency; @NotNull @Override @@ -48,7 +44,7 @@ public void invoke(@NotNull Project project, Editor editor, @NotNull PsiElement return; } - DependencyInfo dependency = findDependencyAtElement(file, element); + Dependency dependency = DependencyAtCaretFinder.find(file, element); if (dependency == null) { return; } @@ -62,7 +58,7 @@ public void invoke(@NotNull Project project, Editor editor, @NotNull PsiElement return; } - VersionReplacer.applyUpdateInWriteAction(project, dependency, candidate.version()); + DependencyVersionWriter.applyUpdateInWriteAction(project, dependency, candidate.version()); } @Override @@ -74,13 +70,13 @@ public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull Psi return false; } - if (!SupportedFilesUtil.isSupportedFile(file.getName())) { + if (!SupportedBuildFile.isSupportedFile(file.getName())) { cachedCandidate = null; cachedDependency = null; return false; } - DependencyInfo dependency = findDependencyAtElement(file, element); + Dependency dependency = DependencyAtCaretFinder.find(file, element); if (dependency == null) { cachedCandidate = null; cachedDependency = null; @@ -117,33 +113,4 @@ public String getText() { } return DependencyUpdaterBundle.message("intention.checkForDependencyUpdates"); } - - /** - * Finds the dependency info at the given element position. - */ - private DependencyInfo findDependencyAtElement(@NotNull PsiFile file, @NotNull PsiElement element) { - // todo: extract in a service - DependencyParser parser = DependencyParserFactory.getParser(file); - if (parser == null) { - return null; - } - - List dependencies = parser.parseDependencies(file); - - int offset = element.getTextOffset(); - for (DependencyInfo dependency : dependencies) { - if (dependency.psiElementPointer() != null) { - PsiElement depElement = dependency.psiElementPointer().getElement(); - if (depElement != null) { - int start = depElement.getTextRange().getStartOffset(); - int end = depElement.getTextRange().getEndOffset(); - if (offset >= start && offset <= end) { - return dependency; - } - } - } - } - - return null; - } } diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/intention/UpdateDependencyToMajorIntention.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/intention/UpdateDependencyToMajorIntention.java new file mode 100644 index 0000000..d3f8169 --- /dev/null +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/intention/UpdateDependencyToMajorIntention.java @@ -0,0 +1,23 @@ +package com.github.clementherve.intellijjavadependencyupdaterplugin.ide.intention; + +import com.github.clementherve.intellijjavadependencyupdaterplugin.DependencyUpdaterBundle; +import com.github.clementherve.intellijjavadependencyupdaterplugin.version.VersionChangeKind; +import org.jetbrains.annotations.NotNull; + +/** + * Intention action to update a dependency to the latest major version. + */ +public class UpdateDependencyToMajorIntention extends AbstractUpdateDependencyIntention { + + @NotNull + @Override + protected VersionChangeKind changeKind() { + return VersionChangeKind.MAJOR; + } + + @NotNull + @Override + public String getText() { + return DependencyUpdaterBundle.message("intention.updateDependencyToMajor"); + } +} diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/intention/UpdateDependencyToMinorIntention.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/intention/UpdateDependencyToMinorIntention.java new file mode 100644 index 0000000..2d9b4a5 --- /dev/null +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/intention/UpdateDependencyToMinorIntention.java @@ -0,0 +1,23 @@ +package com.github.clementherve.intellijjavadependencyupdaterplugin.ide.intention; + +import com.github.clementherve.intellijjavadependencyupdaterplugin.DependencyUpdaterBundle; +import com.github.clementherve.intellijjavadependencyupdaterplugin.version.VersionChangeKind; +import org.jetbrains.annotations.NotNull; + +/** + * Intention action to update a dependency to the latest minor version. + */ +public class UpdateDependencyToMinorIntention extends AbstractUpdateDependencyIntention { + + @NotNull + @Override + protected VersionChangeKind changeKind() { + return VersionChangeKind.MINOR; + } + + @NotNull + @Override + public String getText() { + return DependencyUpdaterBundle.message("intention.updateDependencyToMinor"); + } +} diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/intention/UpdateDependencyToPatchIntention.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/intention/UpdateDependencyToPatchIntention.java new file mode 100644 index 0000000..e4d1acf --- /dev/null +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/intention/UpdateDependencyToPatchIntention.java @@ -0,0 +1,23 @@ +package com.github.clementherve.intellijjavadependencyupdaterplugin.ide.intention; + +import com.github.clementherve.intellijjavadependencyupdaterplugin.DependencyUpdaterBundle; +import com.github.clementherve.intellijjavadependencyupdaterplugin.version.VersionChangeKind; +import org.jetbrains.annotations.NotNull; + +/** + * Intention action to update a dependency to the latest patch version. + */ +public class UpdateDependencyToPatchIntention extends AbstractUpdateDependencyIntention { + + @NotNull + @Override + protected VersionChangeKind changeKind() { + return VersionChangeKind.PATCH; + } + + @NotNull + @Override + public String getText() { + return DependencyUpdaterBundle.message("intention.updateDependencyToPatch"); + } +} diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/settings/DependencyUpdaterConfigurable.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/settings/DependencyUpdaterConfigurable.java similarity index 98% rename from src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/settings/DependencyUpdaterConfigurable.java rename to src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/settings/DependencyUpdaterConfigurable.java index 5c946d1..b526258 100644 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/settings/DependencyUpdaterConfigurable.java +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/settings/DependencyUpdaterConfigurable.java @@ -1,4 +1,4 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.settings; +package com.github.clementherve.intellijjavadependencyupdaterplugin.ide.settings; import com.intellij.openapi.options.Configurable; import org.jetbrains.annotations.Nls; diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/settings/DependencyUpdaterSettings.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/settings/DependencyUpdaterSettings.java similarity index 99% rename from src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/settings/DependencyUpdaterSettings.java rename to src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/settings/DependencyUpdaterSettings.java index 4116be3..41b610b 100644 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/settings/DependencyUpdaterSettings.java +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/settings/DependencyUpdaterSettings.java @@ -1,6 +1,6 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.settings; +package com.github.clementherve.intellijjavadependencyupdaterplugin.ide.settings; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.VersionPolicy; +import com.github.clementherve.intellijjavadependencyupdaterplugin.policy.VersionPolicy; import com.intellij.credentialStore.CredentialAttributes; import com.intellij.credentialStore.CredentialAttributesKt; import com.intellij.credentialStore.Credentials; diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/settings/DependencyUpdaterSettingsPanel.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/settings/DependencyUpdaterSettingsPanel.java similarity index 99% rename from src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/settings/DependencyUpdaterSettingsPanel.java rename to src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/settings/DependencyUpdaterSettingsPanel.java index 6a8407a..9b31f31 100644 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/settings/DependencyUpdaterSettingsPanel.java +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/settings/DependencyUpdaterSettingsPanel.java @@ -1,4 +1,4 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.settings; +package com.github.clementherve.intellijjavadependencyupdaterplugin.ide.settings; import com.github.clementherve.intellijjavadependencyupdaterplugin.DependencyUpdaterBundle; import com.intellij.openapi.application.ApplicationManager; diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/toolwindow/DependencyOverviewPanel.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/toolwindow/DependencyOverviewPanel.java new file mode 100644 index 0000000..dcad706 --- /dev/null +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/toolwindow/DependencyOverviewPanel.java @@ -0,0 +1,273 @@ +package com.github.clementherve.intellijjavadependencyupdaterplugin.ide.toolwindow; + +import com.github.clementherve.intellijjavadependencyupdaterplugin.DependencyUpdaterBundle; +import com.github.clementherve.intellijjavadependencyupdaterplugin.service.DependencyUpdateService; +import com.github.clementherve.intellijjavadependencyupdaterplugin.ide.toolwindow.DependencyRow; +import com.github.clementherve.intellijjavadependencyupdaterplugin.update.DependencyVersionWriter; +import com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile.SupportedBuildFile; +import com.intellij.icons.AllIcons; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.ActionToolbar; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.DefaultActionGroup; +import com.intellij.openapi.command.WriteCommandAction; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.fileEditor.FileDocumentManagerListener; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiElement; +import com.intellij.psi.SmartPsiElementPointer; +import org.jetbrains.annotations.NotNull; + +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; +import java.awt.BorderLayout; +import java.awt.CardLayout; +import java.awt.GridBagLayout; +import java.util.ArrayList; +import java.util.List; + +/** + * Main panel for the dependency overview tool window. Wires together the {@link DependencyTable} + * view, the {@link DependencyScanController} background scan and the loading/table card switch, + * and hosts the toolbar update commands. + */ +public class DependencyOverviewPanel extends JPanel { + + private static final Logger LOGGER = Logger.getInstance(DependencyOverviewPanel.class); + private static final String CARD_LOADING = "loading"; + private static final String CARD_TABLE = "table"; + + private final Project project; + private final DependencyTable dependencyTable; + private final DependencyScanController scanController; + private final CardLayout cardLayout; + private final JPanel contentPanel; + private final JLabel loadingLabel; + + public DependencyOverviewPanel(@NotNull Project project) { + super(new BorderLayout()); + this.project = project; + this.dependencyTable = new DependencyTable(project, this::pickAndApplyVersion); + this.scanController = new DependencyScanController(project, new ScanListener()); + this.cardLayout = new CardLayout(); + this.contentPanel = new JPanel(cardLayout); + this.loadingLabel = new JLabel(DependencyUpdaterBundle.message("toolWindow.loading"), SwingConstants.CENTER); + + setupLoadingPanel(); + contentPanel.add(dependencyTable.getComponent(), CARD_TABLE); + setupToolbar(); + setupFileListener(); + + add(contentPanel, BorderLayout.CENTER); + + showLoading(); + refreshDependencies(false); + } + + private void setupLoadingPanel() { + JPanel loadingPanel = new JPanel(new GridBagLayout()); + loadingPanel.add(loadingLabel); + contentPanel.add(loadingPanel, CARD_LOADING); + } + + private void setupToolbar() { + DefaultActionGroup actionGroup = new DefaultActionGroup(); + actionGroup.add(new RefreshAction()); + actionGroup.add(new UpdateAllAction()); + actionGroup.add(new UpdateSelectedAction()); + + ActionToolbar toolbar = ActionManager.getInstance().createActionToolbar("DependencyOverview", actionGroup, true); + toolbar.setTargetComponent(this); + + add(toolbar.getComponent(), BorderLayout.NORTH); + } + + private void setupFileListener() { + // Listen for document saves to auto-refresh when build files change + project.getMessageBus().connect().subscribe(FileDocumentManagerListener.TOPIC, new FileDocumentManagerListener() { + @Override + public void beforeDocumentSaving(@NotNull Document document) { + VirtualFile file = FileDocumentManager.getInstance().getFile(document); + if (file != null && SupportedBuildFile.isSupportedFile(file.getName())) { + SwingUtilities.invokeLater(() -> refreshDependencies(false)); + } + } + }); + } + + private void refreshDependencies(boolean forceRefresh) { + showLoading(); + loadingLabel.setText(DependencyUpdaterBundle.message("toolWindow.scanning")); + scanController.refresh(forceRefresh); + } + + private void showLoading() { + SwingUtilities.invokeLater(() -> cardLayout.show(contentPanel, CARD_LOADING)); + } + + private void showTable() { + SwingUtilities.invokeLater(() -> cardLayout.show(contentPanel, CARD_TABLE)); + } + + /** + * Receives scan progress and results and drives the loading/table card switch. + */ + private class ScanListener implements DependencyScanController.Listener { + @Override + public void onStatus(@NotNull String message) { + SwingUtilities.invokeLater(() -> loadingLabel.setText(message)); + } + + @Override + public void onScanned(@NotNull List rows) { + SwingUtilities.invokeLater(() -> { + if (rows.isEmpty()) { + loadingLabel.setText(DependencyUpdaterBundle.message("toolWindow.noDependencies")); + showLoading(); + return; + } + dependencyTable.setRows(rows); + showTable(); + }); + } + + @Override + public void onError(@NotNull Throwable error) { + LOGGER.error("Failed to scan dependencies", error); + SwingUtilities.invokeLater(() -> { + loadingLabel.setText(DependencyUpdaterBundle.message("toolWindow.error", error.getMessage())); + showLoading(); + }); + } + } + + private void pickAndApplyVersion(@NotNull DependencyRow row) { + DependencyUpdateService service = DependencyUpdateService.getInstance(project); + String selectedVersion = VersionPickerDialog.pickVersion(project, row.dependency(), service); + if (selectedVersion != null) { + DependencyVersionWriter.applyUpdate(project, row.dependency(), selectedVersion); + PsiDocumentManager.getInstance(project).commitAllDocuments(); + refreshDependencies(false); + } + } + + private void updateSelectedDependencies(boolean pickVersion) { + if (!dependencyTable.hasSelection()) { + Messages.showInfoMessage(project, DependencyUpdaterBundle.message("toolWindow.dialog.selectToUpdate"), DependencyUpdaterBundle.message("toolWindow.dialog.selectToUpdateTitle")); + return; + } + + List rowsToUpdate = new ArrayList<>(); + for (DependencyRow row : dependencyTable.getSelectedRows()) { + if (row.latestVersion() != null || pickVersion) { + rowsToUpdate.add(row); + } + } + + if (rowsToUpdate.isEmpty()) { + Messages.showInfoMessage(project, DependencyUpdaterBundle.message("toolWindow.dialog.alreadyUpToDate"), DependencyUpdaterBundle.message("toolWindow.dialog.selectToUpdateTitle")); + return; + } + + if (pickVersion && rowsToUpdate.size() == 1) { + pickAndApplyVersion(rowsToUpdate.getFirst()); + } else if (pickVersion) { + Messages.showInfoMessage(project, DependencyUpdaterBundle.message("toolWindow.dialog.singleSelectionOnly"), DependencyUpdaterBundle.message("toolWindow.dialog.pickVersionTitle")); + } else { + confirmAndApply(rowsToUpdate, DependencyUpdaterBundle.message("toolWindow.dialog.confirmUpdateTitle", rowsToUpdate.size()), DependencyUpdaterBundle.message("toolWindow.dialog.updateButton")); + } + } + + private void updateAllDependencies() { + List outdatedRows = dependencyTable.getAllRows().stream() + .filter(row -> row.latestVersion() != null) + .toList(); + + if (outdatedRows.isEmpty()) { + Messages.showInfoMessage(project, DependencyUpdaterBundle.message("toolWindow.dialog.allUpToDate"), DependencyUpdaterBundle.message("toolWindow.dialog.allUpToDateTitle")); + return; + } + + confirmAndApply(outdatedRows, DependencyUpdaterBundle.message("toolWindow.dialog.confirmUpdateTitle", outdatedRows.size()), DependencyUpdaterBundle.message("toolWindow.dialog.updateAllButton")); + } + + private void confirmAndApply(@NotNull List rowsToUpdate, @NotNull String title, @NotNull String confirmButton) { + StringBuilder message = new StringBuilder(DependencyUpdaterBundle.message("toolWindow.dialog.confirmUpdateQuestion") + "\n\n"); + for (DependencyRow row : rowsToUpdate) { + message.append(String.format("%s: %s → %s\n", row.dependency().artifact(), row.dependency().currentVersion(), row.latestVersion().version())); + } + + int result = Messages.showOkCancelDialog(project, message.toString(), title, confirmButton, DependencyUpdaterBundle.message("toolWindow.dialog.cancelButton"), Messages.getQuestionIcon()); + + if (result != Messages.OK) { + return; + } + + List sortedRows = sortInReverseDocumentOrder(rowsToUpdate); + + WriteCommandAction.runWriteCommandAction(project, "Update Dependencies", null, () -> { + for (DependencyRow row : sortedRows) { + DependencyVersionWriter.applyUpdateInWriteAction(project, row.dependency(), row.latestVersion().version()); + } + }); + + PsiDocumentManager.getInstance(project).commitAllDocuments(); + refreshDependencies(false); + } + + // Sort updates from bottom to top of the document to avoid invalidating offsets as we edit. + private List sortInReverseDocumentOrder(@NotNull List rowsToUpdate) { + return rowsToUpdate.stream().sorted((first, second) -> { + int firstOffset = offsetOf(first); + int secondOffset = offsetOf(second); + return Integer.compare(secondOffset, firstOffset); + }).toList(); + } + + private int offsetOf(@NotNull DependencyRow row) { + SmartPsiElementPointer pointer = row.dependency().psiElementPointer(); + PsiElement element = pointer != null ? pointer.getElement() : null; + return element != null ? element.getTextOffset() : 0; + } + + private class RefreshAction extends AnAction { + RefreshAction() { + super(DependencyUpdaterBundle.message("toolWindow.refresh"), DependencyUpdaterBundle.message("toolWindow.action.refresh.description"), AllIcons.Actions.Refresh); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent event) { + refreshDependencies(true); + } + } + + private class UpdateAllAction extends AnAction { + UpdateAllAction() { + super(DependencyUpdaterBundle.message("toolWindow.updateAll"), DependencyUpdaterBundle.message("toolWindow.action.updateAll.description"), AllIcons.Diff.MagicResolve); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent event) { + updateAllDependencies(); + } + } + + private class UpdateSelectedAction extends AnAction { + UpdateSelectedAction() { + super(DependencyUpdaterBundle.message("toolWindow.updateSelected"), DependencyUpdaterBundle.message("toolWindow.action.updateSelected.description"), AllIcons.Actions.Edit); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent event) { + updateSelectedDependencies(false); + } + } +} diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/toolwindow/DependencyOverviewToolWindowFactory.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/toolwindow/DependencyOverviewToolWindowFactory.java similarity index 97% rename from src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/toolwindow/DependencyOverviewToolWindowFactory.java rename to src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/toolwindow/DependencyOverviewToolWindowFactory.java index 9e17f84..122fad5 100644 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/toolwindow/DependencyOverviewToolWindowFactory.java +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/toolwindow/DependencyOverviewToolWindowFactory.java @@ -1,4 +1,4 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.toolwindow; +package com.github.clementherve.intellijjavadependencyupdaterplugin.ide.toolwindow; import com.intellij.openapi.project.DumbAware; import com.intellij.openapi.project.Project; diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/toolwindow/DependencyRow.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/toolwindow/DependencyRow.java new file mode 100644 index 0000000..4f7dd99 --- /dev/null +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/toolwindow/DependencyRow.java @@ -0,0 +1,50 @@ +package com.github.clementherve.intellijjavadependencyupdaterplugin.ide.toolwindow; + +import com.github.clementherve.intellijjavadependencyupdaterplugin.dependency.Dependency; +import com.github.clementherve.intellijjavadependencyupdaterplugin.version.VersionCandidate; +import com.github.clementherve.intellijjavadependencyupdaterplugin.version.VersionChangeClassifier; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Represents a row in the dependency table. + */ +public record DependencyRow(Dependency dependency, VersionCandidate latestVersion, String status, + String updateType, String projectName) { + + private static final String UP_TO_DATE = "Up to date"; + private static final String OUTDATED = "Outdated"; + + public DependencyRow(@NotNull Dependency dependency, + @Nullable VersionCandidate latestVersion, + @NotNull String status, + @Nullable String updateType, + @NotNull String projectName) { + this.dependency = dependency; + this.latestVersion = latestVersion; + this.status = status; + this.updateType = updateType; + this.projectName = projectName; + } + + /** + * Builds a row for a dependency, deriving its status and change kind from the latest + * available version ({@code null} latest version means the dependency is up to date). + */ + @NotNull + public static DependencyRow from(@NotNull Dependency dependency, + @Nullable VersionCandidate latestVersion, + @NotNull String projectName) { + String status; + String updateType = null; + + if (latestVersion == null) { + status = UP_TO_DATE; + } else { + status = OUTDATED; + updateType = VersionChangeClassifier.describe(dependency.currentVersion(), latestVersion.version()); + } + + return new DependencyRow(dependency, latestVersion, status, updateType, projectName); + } +} diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/toolwindow/DependencyScanController.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/toolwindow/DependencyScanController.java new file mode 100644 index 0000000..35dee41 --- /dev/null +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/toolwindow/DependencyScanController.java @@ -0,0 +1,142 @@ +package com.github.clementherve.intellijjavadependencyupdaterplugin.ide.toolwindow; + +import com.github.clementherve.intellijjavadependencyupdaterplugin.DependencyUpdaterBundle; +import com.github.clementherve.intellijjavadependencyupdaterplugin.dependency.Dependency; +import com.github.clementherve.intellijjavadependencyupdaterplugin.version.VersionCandidate; +import com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile.BuildFileParser; +import com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile.BuildFileParserFactory; +import com.github.clementherve.intellijjavadependencyupdaterplugin.service.DependencyUpdateService; +import com.github.clementherve.intellijjavadependencyupdaterplugin.ide.toolwindow.DependencyRow; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.progress.Task; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Computable; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiManager; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile.BuildFileLocator.findBuildGradleFilesInCurrentProject; + +/** + * Runs the background scan that discovers build files, parses their dependencies and checks + * each for an available update. Progress and results are reported to a {@link Listener}; the + * controller itself performs no UI work beyond driving the progress indicator. + */ +class DependencyScanController { + + /** + * Receives scan progress and results. {@link #onStatus} may be called from a background + * thread; {@link #onScanned} and {@link #onError} are called on the EDT. + */ + interface Listener { + void onStatus(@NotNull String message); + + void onScanned(@NotNull List rows); + + void onError(@NotNull Throwable error); + } + + private static final Logger LOGGER = Logger.getInstance(DependencyScanController.class); + + private final Project project; + private final Listener listener; + + DependencyScanController(@NotNull Project project, @NotNull Listener listener) { + this.project = project; + this.listener = listener; + } + + void refresh(boolean forceRefresh) { + ProgressManager.getInstance().run(new Task.Backgroundable(project, DependencyUpdaterBundle.message("toolWindow.scanning"), false) { + private final List rows = new ArrayList<>(); + + @Override + public void run(@NotNull ProgressIndicator indicator) { + report(indicator, DependencyUpdaterBundle.message("toolWindow.findingFiles")); + + List buildFiles = findBuildGradleFilesInCurrentProject(project); + if (buildFiles.isEmpty()) { + listener.onStatus(DependencyUpdaterBundle.message("toolWindow.noFiles")); + return; + } + + listener.onStatus(DependencyUpdaterBundle.message("toolWindow.foundFiles", buildFiles.size())); + + DependencyUpdateService service = DependencyUpdateService.getInstance(project); + PsiManager psiManager = PsiManager.getInstance(project); + + for (int i = 0; i < buildFiles.size(); i++) { + if (indicator.isCanceled()) { + return; + } + + VirtualFile file = buildFiles.get(i); + String projectName = file.getParent() != null ? file.getParent().getName() : file.getName(); + indicator.setFraction((double) i / buildFiles.size()); + report(indicator, DependencyUpdaterBundle.message("toolWindow.processingFile", file.getName())); + + try { + scanFile(file, projectName, psiManager, service, indicator, forceRefresh); + } catch (Exception exception) { + LOGGER.warn("Failed to process " + file.getName(), exception); + } + } + } + + private void scanFile(@NotNull VirtualFile file, @NotNull String projectName, + @NotNull PsiManager psiManager, @NotNull DependencyUpdateService service, + @NotNull ProgressIndicator indicator, boolean forceRefresh) throws IOException { + List dependencies = ApplicationManager.getApplication().runReadAction((Computable>) () -> { + PsiFile psiFile = psiManager.findFile(file); + if (psiFile == null) { + return List.of(); + } + + BuildFileParser parser = BuildFileParserFactory.getParser(psiFile); + if (parser == null) { + return List.of(); + } + + return parser.parseDependencies(psiFile); + }); + + for (Dependency dependency : dependencies) { + if (indicator.isCanceled()) { + return; + } + + indicator.setText2(DependencyUpdaterBundle.message("toolWindow.checkingDependency", dependency.artifact())); + listener.onStatus(DependencyUpdaterBundle.message("toolWindow.checkingDependency", dependency.artifact()) + "..."); + + VersionCandidate latest = forceRefresh + ? service.forceCheckForUpdate(dependency) + : service.checkForUpdate(dependency); + rows.add(DependencyRow.from(dependency, latest, projectName)); + } + } + + @Override + public void onSuccess() { + listener.onScanned(rows); + } + + @Override + public void onThrowable(@NotNull Throwable error) { + listener.onError(error); + } + + private void report(@NotNull ProgressIndicator indicator, @NotNull String message) { + indicator.setText(message); + listener.onStatus(message); + } + }); + } +} diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/toolwindow/DependencyTable.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/toolwindow/DependencyTable.java new file mode 100644 index 0000000..7e4c4f0 --- /dev/null +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/toolwindow/DependencyTable.java @@ -0,0 +1,175 @@ +package com.github.clementherve.intellijjavadependencyupdaterplugin.ide.toolwindow; + +import com.github.clementherve.intellijjavadependencyupdaterplugin.ide.toolwindow.DependencyRow; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.fileEditor.OpenFileDescriptor; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.SmartPsiElementPointer; +import com.intellij.ui.SearchTextField; +import com.intellij.ui.components.JBScrollPane; +import com.intellij.ui.table.JBTable; +import org.jetbrains.annotations.NotNull; + +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.ListSelectionModel; +import javax.swing.RowFilter; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.table.TableColumn; +import javax.swing.table.TableRowSorter; +import java.awt.BorderLayout; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +/** + * The dependency table view: owns the table, its model, sorting, the search/filter field and + * the scroll component, and handles in-table interactions (double-click navigation to the + * declaration and right-click version picking). Data-mutating commands live in the panel. + */ +class DependencyTable { + + /** + * Receives in-table interactions that require coordination outside the view. + */ + interface Listener { + void onPickVersion(@NotNull DependencyRow row); + } + + private static final int PROJECT_COLUMN_INDEX = 5; + + private final Project project; + private final Listener listener; + private final DependencyTableModel model = new DependencyTableModel(); + private final JBTable table = new JBTable(model); + private final TableRowSorter sorter; + private final JComponent component; + + DependencyTable(@NotNull Project project, @NotNull Listener listener) { + this.project = project; + this.listener = listener; + + this.sorter = new TableRowSorter<>(model); + table.setRowSorter(sorter); + table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + installMouseListener(); + + this.component = buildComponent(); + } + + @NotNull + JComponent getComponent() { + return component; + } + + void setRows(@NotNull List rows) { + model.setRows(rows); + updateProjectColumnVisibility(); + } + + @NotNull + List getAllRows() { + return model.getAllRows(); + } + + boolean hasSelection() { + return table.getSelectedRowCount() > 0; + } + + @NotNull + List getSelectedRows() { + List selected = new ArrayList<>(); + for (int viewRow : table.getSelectedRows()) { + selected.add(model.getRow(table.convertRowIndexToModel(viewRow))); + } + return selected; + } + + @NotNull + private JComponent buildComponent() { + SearchTextField searchField = new SearchTextField(); + searchField.getTextEditor().getDocument().addDocumentListener(new DocumentListener() { + @Override public void insertUpdate(DocumentEvent event) { applyFilter(searchField.getText()); } + @Override public void removeUpdate(DocumentEvent event) { applyFilter(searchField.getText()); } + @Override public void changedUpdate(DocumentEvent event) { applyFilter(searchField.getText()); } + }); + + JBScrollPane scrollPane = new JBScrollPane(table); + JPanel panel = new JPanel(new BorderLayout()); + panel.add(searchField, BorderLayout.NORTH); + panel.add(scrollPane, BorderLayout.CENTER); + return panel; + } + + private void applyFilter(String text) { + if (text == null || text.isBlank()) { + sorter.setRowFilter(null); + } else { + sorter.setRowFilter(RowFilter.regexFilter("(?i)" + Pattern.quote(text), 0)); + } + } + + private void installMouseListener() { + table.addMouseListener(new java.awt.event.MouseAdapter() { + @Override + public void mouseClicked(java.awt.event.MouseEvent event) { + if (event.getClickCount() == 2 && event.getButton() == java.awt.event.MouseEvent.BUTTON1) { + navigateToSelectedDependency(); + } + + if (event.getClickCount() == 1 && event.getButton() == java.awt.event.MouseEvent.BUTTON3) { + int clickedRow = table.rowAtPoint(event.getPoint()); + if (clickedRow < 0) { + return; + } + table.setRowSelectionInterval(clickedRow, clickedRow); + DependencyRow row = model.getRow(table.convertRowIndexToModel(clickedRow)); + listener.onPickVersion(row); + } + } + }); + } + + private void navigateToSelectedDependency() { + int selectedRow = table.getSelectedRow(); + if (selectedRow < 0) { + return; + } + + DependencyRow row = model.getRow(table.convertRowIndexToModel(selectedRow)); + + SmartPsiElementPointer pointer = row.dependency().psiElementPointer(); + if (pointer == null) { + return; + } + + PsiElement element = pointer.getElement(); + if (element == null) { + return; + } + + PsiFile containingFile = element.getContainingFile(); + if (containingFile == null || containingFile.getVirtualFile() == null) { + return; + } + + OpenFileDescriptor descriptor = new OpenFileDescriptor(project, containingFile.getVirtualFile(), element.getTextOffset()); + FileEditorManager.getInstance(project).openTextEditor(descriptor, true); + } + + private void updateProjectColumnVisibility() { + TableColumn projectColumn = table.getColumnModel().getColumn(PROJECT_COLUMN_INDEX); + if (model.hasMultipleProjects()) { + projectColumn.setMinWidth(50); + projectColumn.setMaxWidth(Integer.MAX_VALUE); + projectColumn.setPreferredWidth(120); + } else { + projectColumn.setMinWidth(0); + projectColumn.setMaxWidth(0); + projectColumn.setPreferredWidth(0); + } + } +} diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/toolwindow/DependencyTableModel.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/toolwindow/DependencyTableModel.java similarity index 78% rename from src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/toolwindow/DependencyTableModel.java rename to src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/toolwindow/DependencyTableModel.java index ff3c1e2..8f5e1cc 100644 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/toolwindow/DependencyTableModel.java +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/toolwindow/DependencyTableModel.java @@ -1,9 +1,9 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.toolwindow; +package com.github.clementherve.intellijjavadependencyupdaterplugin.ide.toolwindow; import com.github.clementherve.intellijjavadependencyupdaterplugin.DependencyUpdaterBundle; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.DependencyInfo; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.VersionCandidate; -import com.github.clementherve.intellijjavadependencyupdaterplugin.toolwindow.model.DependencyRow; +import com.github.clementherve.intellijjavadependencyupdaterplugin.dependency.Dependency; +import com.github.clementherve.intellijjavadependencyupdaterplugin.version.VersionCandidate; +import com.github.clementherve.intellijjavadependencyupdaterplugin.ide.toolwindow.DependencyRow; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -12,16 +12,12 @@ import java.util.ArrayList; import java.util.List; -import static com.github.clementherve.intellijjavadependencyupdaterplugin.toolwindow.util.UpdateTypeUtil.determineUpdateType; - /** * Table model for displaying dependencies in the tool window. */ public class DependencyTableModel extends AbstractTableModel { private static final int COLUMN_COUNT = 6; - private static final String UP_TO_DATE = "Up to date"; - private static final String OUTDATED = "Outdated"; private static final String NO_VALUE = "-"; private final List rows = new ArrayList<>(); @@ -52,7 +48,7 @@ public String getColumnName(int column) { @Override public Object getValueAt(int rowIndex, int columnIndex) { DependencyRow row = rows.get(rowIndex); - final DependencyInfo dependency = row.dependency(); + final Dependency dependency = row.dependency(); return switch (columnIndex) { case 0 -> { @@ -75,19 +71,18 @@ public Object getValueAt(int rowIndex, int columnIndex) { }; } - public void addRow(@NotNull DependencyInfo dependency, @Nullable VersionCandidate latestVersion, + public void addRow(@NotNull Dependency dependency, @Nullable VersionCandidate latestVersion, @NotNull String projectName) { - String status; - String updateType = null; - - if (latestVersion == null) { - status = UP_TO_DATE; - } else { - status = OUTDATED; - updateType = determineUpdateType(dependency.currentVersion(), latestVersion.version()); - } + rows.add(DependencyRow.from(dependency, latestVersion, projectName)); + } - rows.add(new DependencyRow(dependency, latestVersion, status, updateType, projectName)); + /** + * Replaces every row with the given rows and refreshes the table. + */ + public void setRows(@NotNull List newRows) { + rows.clear(); + rows.addAll(newRows); + fireTableDataChanged(); } public void clear() { diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/toolwindow/VersionPickerDialog.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/toolwindow/VersionPickerDialog.java similarity index 90% rename from src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/toolwindow/VersionPickerDialog.java rename to src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/toolwindow/VersionPickerDialog.java index de74e1c..3f6c43a 100644 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/toolwindow/VersionPickerDialog.java +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/toolwindow/VersionPickerDialog.java @@ -1,8 +1,8 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.toolwindow; +package com.github.clementherve.intellijjavadependencyupdaterplugin.ide.toolwindow; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.DependencyInfo; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.VersionCandidate; -import com.github.clementherve.intellijjavadependencyupdaterplugin.services.DependencyUpdateService; +import com.github.clementherve.intellijjavadependencyupdaterplugin.dependency.Dependency; +import com.github.clementherve.intellijjavadependencyupdaterplugin.version.VersionCandidate; +import com.github.clementherve.intellijjavadependencyupdaterplugin.service.DependencyUpdateService; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.DialogWrapper; import com.intellij.ui.components.JBLabel; @@ -22,12 +22,12 @@ */ public class VersionPickerDialog extends DialogWrapper { - private final DependencyInfo dependency; + private final Dependency dependency; private final List availableVersions; private JBList versionList; private String selectedVersion; - public VersionPickerDialog(@NotNull Project project, @NotNull DependencyInfo dependency, @NotNull List availableVersions) { + public VersionPickerDialog(@NotNull Project project, @NotNull Dependency dependency, @NotNull List availableVersions) { super(project); this.dependency = dependency; this.availableVersions = availableVersions; @@ -50,8 +50,8 @@ protected JComponent createCenterPanel() { versionList.addMouseListener(new java.awt.event.MouseAdapter() { @Override - public void mouseClicked(java.awt.event.MouseEvent evt) { - if (evt.getClickCount() == 2) { + public void mouseClicked(java.awt.event.MouseEvent event) { + if (event.getClickCount() == 2) { doOKAction(); } } @@ -94,7 +94,7 @@ public String getSelectedVersion() { } @Nullable - public static String pickVersion(@NotNull Project project, @NotNull DependencyInfo dependency, @NotNull DependencyUpdateService service) { + public static String pickVersion(@NotNull Project project, @NotNull Dependency dependency, @NotNull DependencyUpdateService service) { List versions = service.getAllCandidatesFromCache(dependency); if (versions.isEmpty()) { diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/listeners/BuildFileChangeListener.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/trigger/BuildFileSaveListener.java similarity index 89% rename from src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/listeners/BuildFileChangeListener.java rename to src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/trigger/BuildFileSaveListener.java index 220d628..662241c 100644 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/listeners/BuildFileChangeListener.java +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/trigger/BuildFileSaveListener.java @@ -1,11 +1,11 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.listeners; - -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.DependencyInfo; -import com.github.clementherve.intellijjavadependencyupdaterplugin.psi.DependencyParser; -import com.github.clementherve.intellijjavadependencyupdaterplugin.psi.DependencyParserFactory; -import com.github.clementherve.intellijjavadependencyupdaterplugin.services.DependencyUpdateService; -import com.github.clementherve.intellijjavadependencyupdaterplugin.settings.DependencyUpdaterSettings; -import com.github.clementherve.intellijjavadependencyupdaterplugin.util.SupportedFilesUtil; +package com.github.clementherve.intellijjavadependencyupdaterplugin.ide.trigger; + +import com.github.clementherve.intellijjavadependencyupdaterplugin.dependency.Dependency; +import com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile.BuildFileParser; +import com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile.BuildFileParserFactory; +import com.github.clementherve.intellijjavadependencyupdaterplugin.service.DependencyUpdateService; +import com.github.clementherve.intellijjavadependencyupdaterplugin.ide.settings.DependencyUpdaterSettings; +import com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile.SupportedBuildFile; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.util.Computable; import com.intellij.openapi.diagnostic.Logger; @@ -28,9 +28,9 @@ * Listens for build file saves and refreshes the dependency cache. * Only active when trigger mode is set to ON_SAVE. */ -public class BuildFileChangeListener implements FileDocumentManagerListener { +public class BuildFileSaveListener implements FileDocumentManagerListener { - private static final Logger LOG = Logger.getInstance(BuildFileChangeListener.class); + private static final Logger LOG = Logger.getInstance(BuildFileSaveListener.class); @Override public void beforeDocumentSaving(@NotNull Document document) { @@ -41,7 +41,7 @@ public void beforeDocumentSaving(@NotNull Document document) { return; } - if (!SupportedFilesUtil.isSupportedFile(file.getName())) { + if (!SupportedBuildFile.isSupportedFile(file.getName())) { return; } @@ -78,13 +78,13 @@ private void refreshCacheForFile(@NotNull Project project, @NotNull VirtualFile try { // Parse dependencies in read action - List dependencies = ApplicationManager.getApplication().runReadAction((Computable>) () -> { + List dependencies = ApplicationManager.getApplication().runReadAction((Computable>) () -> { PsiFile psiFile = PsiManager.getInstance(project).findFile(file); if (psiFile == null) { return List.of(); } - DependencyParser parser = DependencyParserFactory.getParser(psiFile); + BuildFileParser parser = BuildFileParserFactory.getParser(psiFile); if (parser == null) { return List.of(); } @@ -107,7 +107,7 @@ private void refreshCacheForFile(@NotNull Project project, @NotNull VirtualFile return; } - DependencyInfo dependency = dependencies.get(i); + Dependency dependency = dependencies.get(i); indicator.setFraction((double) i / dependencies.size()); indicator.setText2("Checking " + dependency.artifact()); diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/startup/DependencyStartupActivity.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/trigger/ProjectOpenWarmupActivity.java similarity index 86% rename from src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/startup/DependencyStartupActivity.java rename to src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/trigger/ProjectOpenWarmupActivity.java index 6770cb3..9ac3982 100644 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/startup/DependencyStartupActivity.java +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/trigger/ProjectOpenWarmupActivity.java @@ -1,11 +1,11 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.startup; +package com.github.clementherve.intellijjavadependencyupdaterplugin.ide.trigger; import com.github.clementherve.intellijjavadependencyupdaterplugin.DependencyUpdaterBundle; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.DependencyInfo; -import com.github.clementherve.intellijjavadependencyupdaterplugin.psi.DependencyParser; -import com.github.clementherve.intellijjavadependencyupdaterplugin.psi.DependencyParserFactory; -import com.github.clementherve.intellijjavadependencyupdaterplugin.services.DependencyUpdateService; -import com.github.clementherve.intellijjavadependencyupdaterplugin.settings.DependencyUpdaterSettings; +import com.github.clementherve.intellijjavadependencyupdaterplugin.dependency.Dependency; +import com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile.BuildFileParser; +import com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile.BuildFileParserFactory; +import com.github.clementherve.intellijjavadependencyupdaterplugin.service.DependencyUpdateService; +import com.github.clementherve.intellijjavadependencyupdaterplugin.ide.settings.DependencyUpdaterSettings; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.util.Computable; import com.intellij.openapi.diagnostic.Logger; @@ -25,15 +25,15 @@ import java.util.ArrayList; import java.util.List; -import static com.github.clementherve.intellijjavadependencyupdaterplugin.util.FindBuildGradleFilesUtil.findBuildGradleFilesInCurrentProject; +import static com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile.BuildFileLocator.findBuildGradleFilesInCurrentProject; /** * Startup activity that pre-populates the version cache when the project opens. * This improves user experience by having version information ready before opening build files. */ -public class DependencyStartupActivity implements ProjectActivity { +public class ProjectOpenWarmupActivity implements ProjectActivity { - private static final Logger LOGGER = Logger.getInstance(DependencyStartupActivity.class); + private static final Logger LOGGER = Logger.getInstance(ProjectOpenWarmupActivity.class); @Nullable @Override @@ -83,13 +83,13 @@ private void fetchDependenciesAndSaveThemInCache(@NotNull Project project, @NotN indicator.setText(DependencyUpdaterBundle.message("cache.processingFile", file.getName())); try { - List dependencies = ApplicationManager.getApplication().runReadAction((Computable>) () -> { + List dependencies = ApplicationManager.getApplication().runReadAction((Computable>) () -> { PsiFile psiFile = psiManager.findFile(file); if (psiFile == null) { return new ArrayList<>(); } - DependencyParser parser = DependencyParserFactory.getParser(psiFile); + BuildFileParser parser = BuildFileParserFactory.getParser(psiFile); if (parser == null) { return new ArrayList<>(); } @@ -97,7 +97,7 @@ private void fetchDependenciesAndSaveThemInCache(@NotNull Project project, @NotN return parser.parseDependencies(psiFile); }); - for (DependencyInfo dependency : dependencies) { + for (Dependency dependency : dependencies) { if (indicator.isCanceled()) { return; } diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/intention/UpdateDependencyToMajorIntention.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/intention/UpdateDependencyToMajorIntention.java deleted file mode 100644 index 0f79a3e..0000000 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/intention/UpdateDependencyToMajorIntention.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.intention; - -import com.github.clementherve.intellijjavadependencyupdaterplugin.DependencyUpdaterBundle; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.DependencyInfo; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.VersionCandidate; -import com.github.clementherve.intellijjavadependencyupdaterplugin.psi.DependencyParser; -import com.github.clementherve.intellijjavadependencyupdaterplugin.psi.DependencyParserFactory; -import com.github.clementherve.intellijjavadependencyupdaterplugin.services.DependencyUpdateService; -import com.github.clementherve.intellijjavadependencyupdaterplugin.util.SemanticVersion; -import com.github.clementherve.intellijjavadependencyupdaterplugin.util.SupportedFilesUtil; -import com.github.clementherve.intellijjavadependencyupdaterplugin.util.VersionReplacer; -import com.github.clementherve.intellijjavadependencyupdaterplugin.util.VersionUpdateType; -import com.intellij.codeInsight.intention.IntentionAction; -import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.project.Project; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; -import com.intellij.util.IncorrectOperationException; -import org.jetbrains.annotations.Nls; -import org.jetbrains.annotations.NotNull; - -import java.util.List; - -/** - * Intention action to update a dependency to the latest major version. - */ -public class UpdateDependencyToMajorIntention extends PsiElementBaseIntentionAction implements IntentionAction { - - @Override - public boolean startInWriteAction() { - return true; - } - - @Override - public void invoke(@NotNull Project project, Editor editor, @NotNull PsiElement element) - throws IncorrectOperationException { - PsiFile file = element.getContainingFile(); - if (file == null) { - return; - } - - DependencyInfo dependency = findDependencyAtElement(file, element); - if (dependency == null) { - return; - } - - VersionCandidate candidate = findMajorUpdate(project, dependency); - if (candidate == null) { - return; - } - - VersionReplacer.applyUpdateInWriteAction(project, dependency, candidate.version()); - } - - @Override - public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull PsiElement element) { - // todo: extract in a service - PsiFile file = element.getContainingFile(); - if (file == null) { - return false; - } - - if (!SupportedFilesUtil.isSupportedFile(file.getName())) { - return false; - } - - DependencyInfo dependency = findDependencyAtElement(file, element); - if (dependency == null) { - return false; - } - - return findMajorUpdate(project, dependency) != null; - } - - @Nls - @NotNull - @Override - public String getFamilyName() { - return DependencyUpdaterBundle.message("intention.familyName"); - } - - @NotNull - @Override - public String getText() { - return DependencyUpdaterBundle.message("intention.updateDependencyToMajor"); - } - - private DependencyInfo findDependencyAtElement(@NotNull PsiFile file, @NotNull PsiElement element) { - // todo: extract in a service - DependencyParser parser = DependencyParserFactory.getParser(file); - if (parser == null) { - return null; - } - - List dependencies = parser.parseDependencies(file); - int offset = element.getTextOffset(); - - for (DependencyInfo dependency : dependencies) { - if (dependency.psiElementPointer() != null) { - PsiElement depElement = dependency.psiElementPointer().getElement(); - if (depElement != null) { - int start = depElement.getTextRange().getStartOffset(); - int end = depElement.getTextRange().getEndOffset(); - if (offset >= start && offset <= end) { - return dependency; - } - } - } - } - - return null; - } - - private VersionCandidate findMajorUpdate(@NotNull Project project, @NotNull DependencyInfo dependency) { - SemanticVersion currentVersion = SemanticVersion.parse(dependency.currentVersion()); - if (currentVersion == null) { - return null; - } - - DependencyUpdateService service = DependencyUpdateService.getInstance(project); - List candidates = service.getAllCandidatesFromCache(dependency); - - return VersionUpdateType.findLatestByType(currentVersion, candidates, VersionUpdateType.UpdateType.MAJOR); - } -} diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/intention/UpdateDependencyToMinorIntention.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/intention/UpdateDependencyToMinorIntention.java deleted file mode 100644 index 5f0898d..0000000 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/intention/UpdateDependencyToMinorIntention.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.intention; - -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.DependencyInfo; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.VersionCandidate; -import com.github.clementherve.intellijjavadependencyupdaterplugin.psi.DependencyParser; -import com.github.clementherve.intellijjavadependencyupdaterplugin.psi.DependencyParserFactory; -import com.github.clementherve.intellijjavadependencyupdaterplugin.services.DependencyUpdateService; -import com.github.clementherve.intellijjavadependencyupdaterplugin.util.SemanticVersion; -import com.github.clementherve.intellijjavadependencyupdaterplugin.util.SupportedFilesUtil; -import com.github.clementherve.intellijjavadependencyupdaterplugin.util.VersionReplacer; -import com.github.clementherve.intellijjavadependencyupdaterplugin.util.VersionUpdateType; -import com.github.clementherve.intellijjavadependencyupdaterplugin.DependencyUpdaterBundle; -import com.intellij.codeInsight.intention.IntentionAction; -import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.project.Project; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; -import com.intellij.util.IncorrectOperationException; -import org.jetbrains.annotations.Nls; -import org.jetbrains.annotations.NotNull; - -import java.util.List; - -/** - * Intention action to update a dependency to the latest minor version. - */ -public class UpdateDependencyToMinorIntention extends PsiElementBaseIntentionAction implements IntentionAction { - - @Override - public boolean startInWriteAction() { - return true; - } - - @Override - public void invoke(@NotNull Project project, Editor editor, @NotNull PsiElement element) throws IncorrectOperationException { - PsiFile file = element.getContainingFile(); - if (file == null) { - return; - } - - DependencyInfo dependency = findDependencyAtElement(file, element); - if (dependency == null) { - return; - } - - VersionCandidate candidate = findMinorUpdate(project, dependency); - if (candidate == null) { - return; - } - - VersionReplacer.applyUpdateInWriteAction(project, dependency, candidate.version()); - } - - @Override - public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull PsiElement element) { - // todo: extract in a service - PsiFile file = element.getContainingFile(); - if (file == null) { - return false; - } - - if (!SupportedFilesUtil.isSupportedFile(file.getName())) { - return false; - } - - DependencyInfo dependency = findDependencyAtElement(file, element); - if (dependency == null) { - return false; - } - - return findMinorUpdate(project, dependency) != null; - } - - @Nls - @NotNull - @Override - public String getFamilyName() { - return DependencyUpdaterBundle.message("intention.familyName"); - } - - @NotNull - @Override - public String getText() { - return DependencyUpdaterBundle.message("intention.updateDependencyToMinor"); - } - - private DependencyInfo findDependencyAtElement(@NotNull PsiFile file, @NotNull PsiElement element) { - // todo: extract in a service - DependencyParser parser = DependencyParserFactory.getParser(file); - if (parser == null) { - return null; - } - - List dependencies = parser.parseDependencies(file); - int offset = element.getTextOffset(); - - for (DependencyInfo dependency : dependencies) { - if (dependency.psiElementPointer() != null) { - PsiElement depElement = dependency.psiElementPointer().getElement(); - if (depElement != null) { - int start = depElement.getTextRange().getStartOffset(); - int end = depElement.getTextRange().getEndOffset(); - if (offset >= start && offset <= end) { - return dependency; - } - } - } - } - - return null; - } - - private VersionCandidate findMinorUpdate(@NotNull Project project, @NotNull DependencyInfo dependency) { - SemanticVersion currentVersion = SemanticVersion.parse(dependency.currentVersion()); - if (currentVersion == null) { - return null; - } - - DependencyUpdateService service = DependencyUpdateService.getInstance(project); - List candidates = service.getAllCandidatesFromCache(dependency); - - return VersionUpdateType.findLatestByType(currentVersion, candidates, VersionUpdateType.UpdateType.MINOR); - } -} diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/model/VersionPolicy.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/policy/VersionPolicy.java similarity index 99% rename from src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/model/VersionPolicy.java rename to src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/policy/VersionPolicy.java index 88f6efe..ac960c7 100644 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/model/VersionPolicy.java +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/policy/VersionPolicy.java @@ -1,4 +1,4 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.model; +package com.github.clementherve.intellijjavadependencyupdaterplugin.policy; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/policy/VersionPolicyEvaluator.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/policy/VersionPolicyEvaluator.java index d942f72..7b905e5 100644 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/policy/VersionPolicyEvaluator.java +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/policy/VersionPolicyEvaluator.java @@ -1,8 +1,8 @@ package com.github.clementherve.intellijjavadependencyupdaterplugin.policy; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.VersionCandidate; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.VersionPolicy; -import com.github.clementherve.intellijjavadependencyupdaterplugin.util.SemanticVersion; +import com.github.clementherve.intellijjavadependencyupdaterplugin.version.VersionCandidate; +import com.github.clementherve.intellijjavadependencyupdaterplugin.policy.VersionPolicy; +import com.github.clementherve.intellijjavadependencyupdaterplugin.version.SemanticVersion; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/psi/GradlePsiParser.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/psi/GradlePsiParser.java deleted file mode 100644 index 8deeac8..0000000 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/psi/GradlePsiParser.java +++ /dev/null @@ -1,439 +0,0 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.psi; - -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.DependencyInfo; -import com.github.clementherve.intellijjavadependencyupdaterplugin.util.SupportedFilesUtil; -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.progress.ProcessCanceledException; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; -import com.intellij.psi.SmartPointerManager; -import com.intellij.psi.SmartPsiElementPointer; -import com.intellij.psi.util.PsiTreeUtil; -import org.apache.commons.collections.CollectionUtils; -import org.apache.commons.lang3.StringUtils; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentList; -import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrNamedArgument; -import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression; -import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall; -import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression; -import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrLiteral; -import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrString; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Parses dependency declarations from Groovy build.gradle files. - */ -public class GradlePsiParser implements DependencyParser { - - private static final Logger LOGGER = Logger.getInstance(GradlePsiParser.class); - - private static final List KNOWN_CONFIGURATIONS = Arrays.asList( - "implementation", "api", "compileOnly", "runtimeOnly", - "testImplementation", "testCompileOnly", "testRuntimeOnly", - "annotationProcessor", "kapt" - ); - - // group:artifact:version - private static final Pattern DEPENDENCY_PATTERN = Pattern.compile( - "([^:]+):([^:]+):([^:]+)?" - ); - - private static final String EXT_BLOCK_CONSTANT = "ext"; - private static final String VARIABLE_PATTERN = "\\$\\{([^}]+)}"; - - @Override - public boolean canParse(@NotNull PsiFile psiFile) { - return SupportedFilesUtil.isSupportedFile(psiFile.getName()); - } - - @NotNull - @Override - public List parseDependencies(@NotNull PsiFile psiFile) { - List dependencies = new ArrayList<>(); - - for (GrMethodCall methodCall : PsiTreeUtil.findChildrenOfType(psiFile, GrMethodCall.class)) { - try { - DependencyInfo dependency = parseDependencyFromMethodCall(methodCall, psiFile); - if (dependency != null) { - dependencies.add(dependency); - } - - DependencyInfo plugin = parsePluginFromMethodCall(methodCall, psiFile); - if (plugin != null) { - dependencies.add(plugin); - } - } catch (ProcessCanceledException e) { - // Rethrow - this is a control flow exception, not an error - throw e; - } catch (Exception e) { - LOGGER.debug("Failed to parse dependency from method call", e); - } - } - - return dependencies; - } - - /** - * Attempts to parse a plugin declaration from the plugins block. - * Format: id 'plugin.id' version 'version' - *

- * In Groovy, this syntax is a chained method call where 'version' is called on the result of 'id'. - * The PSI structure looks like: GrMethodCall(version) -> GrReferenceExpression(id(...)) - */ - private DependencyInfo parsePluginFromMethodCall(@NotNull GrMethodCall methodCall, @NotNull PsiFile psiFile) { - // Get the actual method name (not the full text which includes the chain) - PsiElement invokedExpression = methodCall.getInvokedExpression(); - String methodName; - - // Extract just the method name from the reference - if (invokedExpression instanceof GrReferenceExpression) { - methodName = ((GrReferenceExpression) invokedExpression).getReferenceName(); - } else { - methodName = invokedExpression.getText(); - } - - // Check if this is a 'version' method call that wraps an 'id' call - if ("version".equals(methodName)) { - // This is the outer 'version' call in: id 'plugin.id' version 'version' - GrArgumentList argumentList = methodCall.getArgumentList(); - GrExpression[] versionArguments = argumentList.getExpressionArguments(); - - if (versionArguments.length > 0 && versionArguments[0] instanceof GrLiteral) { - Object versionValue = ((GrLiteral) versionArguments[0]).getValue(); - - if (versionValue instanceof final String version) { - PsiElement versionElement = versionArguments[0]; - - // The invoked expression should be a reference expression with a qualifier that is the 'id' call - if (invokedExpression instanceof final GrReferenceExpression refExpr) { - GrExpression qualifier = refExpr.getQualifierExpression(); - - if (qualifier instanceof final GrMethodCall idCall) { - PsiElement idInvoked = idCall.getInvokedExpression(); - String idMethodName = idInvoked.getText(); - - if ("id".equals(idMethodName)) { - // Check if inside plugins block - boolean insidePluginsBlock = false; - PsiElement ancestor = methodCall.getParent(); - while (ancestor != null) { - if (ancestor instanceof GrMethodCall) { - PsiElement ancestorMethod = ((GrMethodCall) ancestor).getInvokedExpression(); - String ancestorName = ancestorMethod.getText(); - if ("plugins".equals(ancestorName)) { - insidePluginsBlock = true; - break; - } - } - ancestor = ancestor.getParent(); - } - - if (!insidePluginsBlock) { - return null; - } - - // Extract plugin ID from the 'id' call - GrArgumentList idArgList = idCall.getArgumentList(); - GrExpression[] idArgs = idArgList.getExpressionArguments(); - - if (idArgs.length > 0 && idArgs[0] instanceof GrLiteral) { - Object pluginIdValue = ((GrLiteral) idArgs[0]).getValue(); - if (pluginIdValue instanceof final String pluginId) { - - SmartPsiElementPointer pointer = SmartPointerManager.createPointer(versionElement); - - return new DependencyInfo( - "", // empty group for plugins - pluginId, // artifact is the plugin ID - version, - "plugin", - pointer, - false, - null - ); - } - } - } - } - } - } - } - } - - return null; - } - - /** - * Attempts to parse a dependency from a method call expression. - * In Groovy, everything is a function, i.e.: . - */ - private DependencyInfo parseDependencyFromMethodCall(@NotNull GrMethodCall methodCall, @NotNull PsiFile psiFile) { - PsiElement methodElement = methodCall.getInvokedExpression(); - - String methodName = methodElement.getText(); - final boolean unknownMethodName = !KNOWN_CONFIGURATIONS.contains(methodName); - if (unknownMethodName) { - return null; - } - - GrArgumentList argumentList = methodCall.getArgumentList(); - - final boolean emptyMethodArguments = argumentList.getAllArguments().length == 0; - if (emptyMethodArguments) { - return null; - } - - // cas: implementation 'org.springframework:spring-core:6.0' - GrExpression[] expressionArguments = argumentList.getExpressionArguments(); - final boolean hasExpressionArguments = expressionArguments.length > 0; - if (hasExpressionArguments) { - // si c'est un litéral - if (expressionArguments[0] instanceof final GrLiteral literal) { - Object value = literal.getValue(); - - if (value instanceof String) { - // sans variable - return parseStringNotation((String) value, methodName, literal, psiFile); - } else if (literal instanceof final GrString gstring) { - // avec variables - return parseGStringNotation(gstring, methodName, psiFile); - } - } - - } - - // cas: implementation(group: 'org.springframework', name: 'spring-core', version: '6.0') - GrNamedArgument[] namedArguments = argumentList.getNamedArguments(); - final boolean hasNamedArguments = namedArguments.length > 0; - if (hasNamedArguments) { - return parseMapNotation(namedArguments, methodName, psiFile); - } - - return null; - } - - /** - * Parses GString notation with variable interpolation: "group:artifact:$version" - */ - private DependencyInfo parseGStringNotation(@NotNull GrString gstring, - @NotNull String configurationName, - @NotNull PsiFile psiFile) { - StringBuilder fullString = new StringBuilder(); - String variableName; - boolean hasVariable = false; - - for (PsiElement child : gstring.getChildren()) { - fullString.append(child.getText()); - } - - String dependencyString = fullString.toString(); - Matcher matcher = DEPENDENCY_PATTERN.matcher(dependencyString); - if (!matcher.matches()) { - return null; - } - - String group = matcher.group(1); - String artifact = matcher.group(2); - variableName = matcher.group(3); - - if (variableName == null) { - return null; - } - - String resolvedVariable; - - if (variableName.startsWith("$")) { - variableName = extractVariableName(variableName); - resolvedVariable = resolveVariable(variableName, psiFile); - hasVariable = true; - } else { - resolvedVariable = variableName; - } - - if (StringUtils.isBlank(resolvedVariable)) { - resolvedVariable = ""; - hasVariable = false; - } - - SmartPsiElementPointer pointer = SmartPointerManager.createPointer(gstring); - - return new DependencyInfo( - group, - artifact, - resolvedVariable, - configurationName, - pointer, - hasVariable, - variableName - ); - } - - /** - * Parses string notation: "group:artifact:version" - */ - private DependencyInfo parseStringNotation(@NotNull String dependencyString, - @NotNull String configurationName, - @NotNull PsiElement versionElement, - @NotNull PsiFile psiFile) { - Matcher matcher = DEPENDENCY_PATTERN.matcher(dependencyString); - if (!matcher.matches()) { - return null; - } - - String group = matcher.group(1); - String artifact = matcher.group(2); - String version = matcher.group(3); - - // todo: remove this: string notation can't have variables - boolean isVersionVariable = version.startsWith("$"); - String variableName = null; - String resolvedVersion = version; - - if (isVersionVariable) { - variableName = extractVariableName(version); - String resolved = resolveVariable(variableName, psiFile); - if (resolved != null) { - resolvedVersion = resolved; - } - } - - SmartPsiElementPointer pointer = - SmartPointerManager.createPointer(versionElement); - - return new DependencyInfo( - group, - artifact, - resolvedVersion, - configurationName, - pointer, - isVersionVariable, - variableName - ); - } - - /** - * Parses map notation: group: 'g', name: 'a', version: 'v' - */ - private DependencyInfo parseMapNotation(@NotNull GrNamedArgument[] namedArgs, - @NotNull String configurationName, - @NotNull PsiFile psiFile) { - String group = null; - String artifact = null; - String version = null; - PsiElement versionElement = null; - - for (GrNamedArgument arg : namedArgs) { - String argName = arg.getLabelName(); - GrExpression expression = arg.getExpression(); - - if (expression instanceof GrLiteral) { - Object value = ((GrLiteral) expression).getValue(); - if (value instanceof String) { - switch (argName) { - case "group": - group = (String) value; - break; - case "name": - artifact = (String) value; - break; - case "version": - version = (String) value; - versionElement = expression; - break; - case null: - break; - default: - throw new IllegalStateException("Unexpected value: " + argName); - } - } - } - } - - if (group == null || artifact == null || version == null) { - return null; - } - - boolean isVersionVariable = version.startsWith("$"); - String variableName = null; - String resolvedVersion = version; - - if (isVersionVariable) { - variableName = extractVariableName(version); - String resolved = resolveVariable(variableName, psiFile); - if (resolved != null) { - resolvedVersion = resolved; - } - } - - SmartPsiElementPointer pointer = SmartPointerManager.createPointer(versionElement); - - return new DependencyInfo( - group, - artifact, - resolvedVersion, - configurationName, - pointer, - isVersionVariable, - variableName - ); - } - - /** - * Resolves a Gradle variable to its value by searching the ext block and project properties. - * - * @param variableName the name of the variable (without the $ prefix) - * @param psiFile the Gradle build file - * @return the resolved value, or null if not found - */ - private String resolveVariable(@NotNull String variableName, @NotNull PsiFile psiFile) { - for (GrMethodCall methodCall : PsiTreeUtil.findChildrenOfType(psiFile, GrMethodCall.class)) { - PsiElement methodElement = methodCall.getInvokedExpression(); - - final boolean hasExtBlock = EXT_BLOCK_CONSTANT.equals(methodElement.getText()); - if (hasExtBlock) { - String value = findVariableInExtBlock(variableName, methodCall); - if (value != null) { - return value; - } - } - } - - return null; - } - - /** - * Searches for a variable assignment within an ext{} block. - * todo: optimization: parse the ext block once and cache the result - */ - private String findVariableInExtBlock(@NotNull String variableName, @NotNull GrMethodCall extCall) { - for (PsiElement child : extCall.getChildren()) { - String text = child.getText(); - Pattern pattern = Pattern.compile(Pattern.quote(variableName) + "\\s*=\\s*['\"]([^'\"]+)['\"]"); - Matcher matcher = pattern.matcher(text); - if (matcher.find()) { - return matcher.group(1); - } - } - return null; - } - - // handles ${spring_version} -> spring_version - private String extractVariableName(String version) { - if (version.startsWith("${")) { - Matcher matcher = Pattern.compile(VARIABLE_PATTERN).matcher(version); - return matcher.find() ? matcher.group(1) : version; - } - - if (version.startsWith("$")) { - return version.substring(1); - } - - return null; - } -} diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/GradlePluginPortalClient.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/GradlePluginPortalRepository.java similarity index 96% rename from src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/GradlePluginPortalClient.java rename to src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/GradlePluginPortalRepository.java index f0f2d91..84e0d66 100644 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/GradlePluginPortalClient.java +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/GradlePluginPortalRepository.java @@ -22,12 +22,12 @@ * Client for fetching plugin versions from Gradle Plugin Portal. * Plugins are backed by Maven artifacts at https://plugins.gradle.org/m2/ */ -public class GradlePluginPortalClient implements VersionRepository { +public class GradlePluginPortalRepository implements VersionRepository { private static final String PLUGIN_PORTAL_MAVEN = "https://plugins.gradle.org/m2/"; private final HttpClient httpClient; - public GradlePluginPortalClient() { + public GradlePluginPortalRepository() { this.httpClient = HttpClient.newBuilder() .followRedirects(HttpClient.Redirect.NORMAL) .build(); diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/MavenCentralClient.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/MavenCentralRepository.java similarity index 57% rename from src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/MavenCentralClient.java rename to src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/MavenCentralRepository.java index e092ff7..28c5ca9 100644 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/MavenCentralClient.java +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/MavenCentralRepository.java @@ -5,16 +5,15 @@ import org.jetbrains.annotations.NotNull; import java.io.IOException; -import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * Client for fetching versions from Maven Central using the maven-metadata.xml endpoint. */ -public class MavenCentralClient implements VersionRepository { +public class MavenCentralRepository implements VersionRepository { - private static final Logger LOG = Logger.getInstance(MavenCentralClient.class); + private static final Logger LOG = Logger.getInstance(MavenCentralRepository.class); private static final String MAVEN_CENTRAL_BASE_URL = "https://repo1.maven.org/maven2"; private static final int TIMEOUT_MS = 10000; @@ -34,7 +33,7 @@ public List fetchVersions(@NotNull String group, @NotNull String artifac .readTimeout(TIMEOUT_MS) .readString(null); - return parseVersionsFromMetadata(xmlContent); + return MavenMetadataParser.parseVersions(xmlContent); } catch (HttpRequests.HttpStatusException e) { if (e.getStatusCode() == 404) { LOG.warn("Artifact not found in Maven Central: " + group + ":" + artifact); @@ -52,44 +51,4 @@ public List fetchVersions(@NotNull String group, @NotNull String artifac public String getSourceName() { return "Maven Central"; } - - /** - * Parses version strings from Maven metadata XML. - * Expected format: - * - * - * - * 1.0.0 - * 1.1.0 - * - * - * - */ - @NotNull - private List parseVersionsFromMetadata(@NotNull String xmlContent) { - List versions = new ArrayList<>(); - - try { - // Simple XML parsing - extract ... tags - int pos = 0; - while (true) { - int versionStart = xmlContent.indexOf("", pos); - if (versionStart == -1) break; - - int versionEnd = xmlContent.indexOf("", versionStart); - if (versionEnd == -1) break; - - String version = xmlContent.substring(versionStart + 9, versionEnd).trim(); - if (!version.isEmpty()) { - versions.add(version); - } - - pos = versionEnd + 10; - } - } catch (Exception e) { - LOG.warn("Failed to parse Maven metadata XML", e); - } - - return versions; - } } diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/MavenMetadataParser.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/MavenMetadataParser.java new file mode 100644 index 0000000..1afa098 --- /dev/null +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/MavenMetadataParser.java @@ -0,0 +1,62 @@ +package com.github.clementherve.intellijjavadependencyupdaterplugin.repository; + +import com.intellij.openapi.diagnostic.Logger; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +/** + * Extracts the version strings from a Maven {@code maven-metadata.xml} document. + * Shared by every repository that exposes the standard Maven metadata layout: + *

+ * <metadata>
+ *   <versioning>
+ *     <versions>
+ *       <version>1.0.0</version>
+ *       <version>1.1.0</version>
+ *     </versions>
+ *   </versioning>
+ * </metadata>
+ * 
+ */ +public final class MavenMetadataParser { + + private static final Logger LOGGER = Logger.getInstance(MavenMetadataParser.class); + private static final String OPEN_TAG = ""; + private static final String CLOSE_TAG = ""; + + private MavenMetadataParser() { + } + + /** + * Returns the versions declared in the given metadata document, in document order. + * Returns an empty list when the document contains no versions or cannot be read. + */ + @NotNull + public static List parseVersions(@NotNull String xmlContent) { + List versions = new ArrayList<>(); + + try { + int position = 0; + while (true) { + int versionStart = xmlContent.indexOf(OPEN_TAG, position); + if (versionStart == -1) break; + + int versionEnd = xmlContent.indexOf(CLOSE_TAG, versionStart); + if (versionEnd == -1) break; + + String version = xmlContent.substring(versionStart + OPEN_TAG.length(), versionEnd).trim(); + if (!version.isEmpty()) { + versions.add(version); + } + + position = versionEnd + CLOSE_TAG.length(); + } + } catch (Exception exception) { + LOGGER.warn("Failed to parse Maven metadata XML", exception); + } + + return versions; + } +} diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/NexusClient.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/NexusRepository.java similarity index 69% rename from src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/NexusClient.java rename to src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/NexusRepository.java index a646ef8..5e54ca5 100644 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/NexusClient.java +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/NexusRepository.java @@ -7,7 +7,6 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; import java.util.Base64; import java.util.Collections; import java.util.List; @@ -16,16 +15,16 @@ * Client for fetching versions from Nexus/Artifactory repositories. * Supports both Nexus 2.x and 3.x REST APIs. */ -public class NexusClient implements VersionRepository { +public class NexusRepository implements VersionRepository { - private static final Logger LOGGER = Logger.getInstance(NexusClient.class); + private static final Logger LOGGER = Logger.getInstance(NexusRepository.class); private static final int TIMEOUT_MS = 10_000; private final String baseUrl; private final String username; private final String password; - public NexusClient(@NotNull String baseUrl, + public NexusRepository(@NotNull String baseUrl, @Nullable String username, @Nullable String password) { this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; @@ -59,7 +58,7 @@ public List fetchVersions(@NotNull String group, @NotNull String artifac }) .readString(null); - return parseVersionsFromMetadata(xmlContent); + return MavenMetadataParser.parseVersions(xmlContent); } catch (HttpRequests.HttpStatusException e) { if (e.getStatusCode() == 401) { @@ -81,36 +80,4 @@ public List fetchVersions(@NotNull String group, @NotNull String artifac public String getSourceName() { return "Nexus (" + baseUrl + ")"; } - - /** - * Parses version strings from Maven metadata XML. - */ - @NotNull - private List parseVersionsFromMetadata(@NotNull String xmlContent) { - List versions = new ArrayList<>(); - - try { - // todo: use a proper XML parser and make it common with the maven central client - // Simple XML parsing - extract ... tags - int pos = 0; - while (true) { - int versionStart = xmlContent.indexOf("", pos); - if (versionStart == -1) break; - - int versionEnd = xmlContent.indexOf("", versionStart); - if (versionEnd == -1) break; - - String version = xmlContent.substring(versionStart + 9, versionEnd).trim(); - if (!version.isEmpty()) { - versions.add(version); - } - - pos = versionEnd + 10; - } - } catch (Exception e) { - LOGGER.warn("Failed to parse Nexus metadata XML", e); - } - - return versions; - } } diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/RepositorySource.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/RepositorySource.java new file mode 100644 index 0000000..a8bcaf0 --- /dev/null +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/RepositorySource.java @@ -0,0 +1,24 @@ +package com.github.clementherve.intellijjavadependencyupdaterplugin.repository; + +import org.jetbrains.annotations.NotNull; + +/** + * The repository a dependency's versions are resolved from. The display name is what the + * tool window and version candidates show to the user. + */ +public enum RepositorySource { + GRADLE_PLUGIN_PORTAL("Gradle Plugin Portal"), + NEXUS("Nexus"), + MAVEN_CENTRAL("Maven Central"); + + private final String displayName; + + RepositorySource(@NotNull String displayName) { + this.displayName = displayName; + } + + @NotNull + public String getDisplayName() { + return displayName; + } +} diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/cache/VersionCache.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/VersionCache.java similarity index 99% rename from src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/cache/VersionCache.java rename to src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/VersionCache.java index e58d49c..26f1a56 100644 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/cache/VersionCache.java +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/VersionCache.java @@ -1,4 +1,4 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.cache; +package com.github.clementherve.intellijjavadependencyupdaterplugin.repository; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.Service; diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/services/DependencyUpdateService.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/service/DependencyUpdateService.java similarity index 52% rename from src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/services/DependencyUpdateService.java rename to src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/service/DependencyUpdateService.java index abf68c2..4c9bd01 100644 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/services/DependencyUpdateService.java +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/service/DependencyUpdateService.java @@ -1,23 +1,17 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.services; +package com.github.clementherve.intellijjavadependencyupdaterplugin.service; -import com.github.clementherve.intellijjavadependencyupdaterplugin.cache.VersionCache; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.DependencyInfo; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.VersionCandidate; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.VersionPolicy; +import com.github.clementherve.intellijjavadependencyupdaterplugin.repository.VersionCache; +import com.github.clementherve.intellijjavadependencyupdaterplugin.dependency.Dependency; +import com.github.clementherve.intellijjavadependencyupdaterplugin.version.VersionCandidate; +import com.github.clementherve.intellijjavadependencyupdaterplugin.policy.VersionPolicy; import com.github.clementherve.intellijjavadependencyupdaterplugin.policy.VersionPolicyEvaluator; -import com.github.clementherve.intellijjavadependencyupdaterplugin.repository.GradlePluginPortalClient; -import com.github.clementherve.intellijjavadependencyupdaterplugin.repository.MavenCentralClient; -import com.github.clementherve.intellijjavadependencyupdaterplugin.repository.NexusClient; -import com.github.clementherve.intellijjavadependencyupdaterplugin.repository.VersionRepository; -import com.github.clementherve.intellijjavadependencyupdaterplugin.settings.DependencyUpdaterSettings; +import com.github.clementherve.intellijjavadependencyupdaterplugin.ide.settings.DependencyUpdaterSettings; import com.intellij.openapi.components.Service; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.progress.Task; import com.intellij.openapi.project.Project; -import org.apache.commons.collections.CollectionUtils; -import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -26,7 +20,7 @@ /** * Main service for checking dependency updates. - * Coordinates cache, repository access, and policy evaluation. + * Coordinates the cache, version resolution, and policy evaluation. */ @Service(Service.Level.PROJECT) public final class DependencyUpdateService { @@ -36,15 +30,13 @@ public final class DependencyUpdateService { private final Project project; private final VersionCache cache; private final VersionPolicyEvaluator policyEvaluator; - private final MavenCentralClient mavenCentralClient; - private final GradlePluginPortalClient pluginPortalClient; + private final VersionResolver versionResolver; public DependencyUpdateService(@NotNull Project project) { this.project = project; this.cache = VersionCache.getInstance(); this.policyEvaluator = new VersionPolicyEvaluator(); - this.mavenCentralClient = new MavenCentralClient(); - this.pluginPortalClient = new GradlePluginPortalClient(); + this.versionResolver = new VersionResolver(); } public static DependencyUpdateService getInstance(@NotNull Project project) { @@ -59,26 +51,16 @@ public static DependencyUpdateService getInstance(@NotNull Project project) { * @return the best version candidate, or null if no update is available */ @Nullable - public VersionCandidate checkForUpdate(@NotNull DependencyInfo dependency) { + public VersionCandidate checkForUpdate(@NotNull Dependency dependency) { try { List versions = fetchVersionsAndSaveThemToCache(dependency.group(), dependency.artifact()); if (versions.isEmpty()) { return null; } - DependencyUpdaterSettings settings = DependencyUpdaterSettings.getInstance(); - VersionPolicy policy = getFirstPolicy(); - String excludeRegex = settings.getVersionFilterRegex(); - - return policyEvaluator.findBestCandidate( - versions, - dependency.currentVersion(), - policy, - getRepositorySource(dependency.group(), dependency.artifact()), - excludeRegex - ); - } catch (Exception e) { - LOGGER.warn("Failed to check for update: " + dependency.getFullCoordinates(), e); + return findBestCandidate(dependency, versions); + } catch (Exception exception) { + LOGGER.warn("Failed to check for update: " + dependency.getFullCoordinates(), exception); return null; } } @@ -91,30 +73,15 @@ public VersionCandidate checkForUpdate(@NotNull DependencyInfo dependency) { * @return the best version candidate from cache, or null if not cached */ @Nullable - public VersionCandidate getFromCache(@NotNull DependencyInfo dependency) { - DependencyUpdaterSettings settings = DependencyUpdaterSettings.getInstance(); - - List cachedVersions = cache.getVersions( - dependency.group(), - dependency.artifact(), - settings.getCacheTtlMinutes() - ); + public VersionCandidate getFromCache(@NotNull Dependency dependency) { + List cachedVersions = getCachedVersions(dependency); final boolean isNotInCache = cachedVersions == null; if (isNotInCache) { return null; } - VersionPolicy policy = getFirstPolicy(); - String excludeRegex = settings.getVersionFilterRegex(); - - return policyEvaluator.findBestCandidate( - cachedVersions, - dependency.currentVersion(), - policy, - getRepositorySource(dependency.group(), dependency.artifact()), - excludeRegex - ); + return findBestCandidate(dependency, cachedVersions); } /** @@ -125,21 +92,15 @@ public VersionCandidate getFromCache(@NotNull DependencyInfo dependency) { * @return list of all version candidates from cache (sorted descending), or empty list if not cached */ @NotNull - public List getAllCandidatesFromCache(@NotNull DependencyInfo dependency) { - DependencyUpdaterSettings settings = DependencyUpdaterSettings.getInstance(); - - List cachedVersions = cache.getVersions( - dependency.group(), - dependency.artifact(), - settings.getCacheTtlMinutes() - ); + public List getAllCandidatesFromCache(@NotNull Dependency dependency) { + List cachedVersions = getCachedVersions(dependency); if (cachedVersions == null) { return List.of(); // Not in cache } VersionPolicy policy = getFirstPolicy(); - return policyEvaluator.evaluate(cachedVersions, policy, getRepositorySource(dependency.group(), dependency.artifact())); + return policyEvaluator.evaluate(cachedVersions, policy, repositorySourceName(dependency)); } /** @@ -148,7 +109,7 @@ public List getAllCandidatesFromCache(@NotNull DependencyInfo * * @param dependency the dependency to fetch versions for */ - public void scheduleCacheWarmup(@NotNull DependencyInfo dependency) { + public void scheduleCacheWarmup(@NotNull Dependency dependency) { ProgressManager.getInstance().run(new Task.Backgroundable(project, "Fetching versions for " + dependency.artifact(), false) { @Override public void run(@NotNull ProgressIndicator indicator) { @@ -156,8 +117,8 @@ public void run(@NotNull ProgressIndicator indicator) { indicator.setText("Fetching versions from repository..."); fetchVersionsAndSaveThemToCache(dependency.group(), dependency.artifact()); // next highlighting pass will show the marker - } catch (Exception e) { - LOGGER.debug("Background cache warmup failed for " + dependency.getFullCoordinates(), e); + } catch (Exception exception) { + LOGGER.debug("Background cache warmup failed for " + dependency.getFullCoordinates(), exception); } } }); @@ -179,8 +140,7 @@ public List fetchVersionsAndSaveThemToCache(@NotNull String group, @NotN return cachedVersions; } - - List versions = fetchVersionsFromRepositories(group, artifact); + List versions = versionResolver.fetchVersions(group, artifact); if (!versions.isEmpty()) { cache.putVersions(group, artifact, versions); @@ -189,87 +149,6 @@ public List fetchVersionsAndSaveThemToCache(@NotNull String group, @NotN return versions; } - /** - * Fetches versions from configured repositories. - * For plugins (empty group), uses Gradle Plugin Portal. - * For regular dependencies, uses Nexus first, then Maven Central if fallback is enabled. - */ - @NotNull - private List fetchVersionsFromRepositories(@NotNull String group, @NotNull String artifact) throws IOException { - if (group.isEmpty()) { - return pluginPortalClient.fetchVersions(group, artifact); - } - - DependencyUpdaterSettings settings = DependencyUpdaterSettings.getInstance(); - - final boolean isNexusConfigured = StringUtils.isNotBlank(settings.getNexusBaseUrl()); - - if (isNexusConfigured) { - String nexusDependencyRegex = settings.getNexusDependencyRegex(); - boolean shouldUseNexus = nexusDependencyRegex.isEmpty() || (group + ":" + artifact).matches(nexusDependencyRegex); - - if (shouldUseNexus) { - try { - VersionRepository nexusClient = createNexusClient(); - List versions = nexusClient.fetchVersions(group, artifact); - - if (CollectionUtils.isNotEmpty(versions)) { - return versions; - } - } catch (IOException e) { - LOGGER.error("Nexus fetch failed for " + group + ":" + artifact + ", error: " + e.getMessage()); - if (!settings.isFallbackToMavenCentral()) { - throw e; - } - } - } - } - - return mavenCentralClient.fetchVersions(group, artifact); - } - - /** - * Creates a Nexus client with current settings. - */ - @NotNull - private VersionRepository createNexusClient() { - DependencyUpdaterSettings settings = DependencyUpdaterSettings.getInstance(); - return new NexusClient( - settings.getNexusBaseUrl(), - settings.getNexusUsername(), - settings.getNexusPassword() - ); - } - - /** - * Gets the first version policy from settings. - */ - @NotNull - private VersionPolicy getFirstPolicy() { - List policies = DependencyUpdaterSettings.getInstance().getVersionPolicies(); - return policies.isEmpty() ? VersionPolicy.createDefaultStablePolicy() : policies.getFirst(); - } - - @NotNull - private String getRepositorySource(@NotNull String group, @NotNull String artifact) { - if (group.isEmpty()) { - return "Gradle Plugin Portal"; // todo: create a enum - } - - DependencyUpdaterSettings settings = DependencyUpdaterSettings.getInstance(); - if (StringUtils.isNotBlank(settings.getNexusBaseUrl())) { - String nexusDependencyRegex = settings.getNexusDependencyRegex(); - - boolean matchesNexusFilter = nexusDependencyRegex.isEmpty() || (group + ":" + artifact).matches(nexusDependencyRegex); - - if (matchesNexusFilter) { - return "Nexus"; // todo: create a enum - } - } - - return "Maven Central"; // todo: create a enum - } - /** * Invalidates the cache for a specific dependency. */ @@ -284,12 +163,22 @@ public void invalidateAllCache() { cache.invalidateAll(); } - public VersionCandidate forceCheckForUpdate(final DependencyInfo dependency) throws IOException { - + public VersionCandidate forceCheckForUpdate(final Dependency dependency) throws IOException { invalidateCache(dependency.group(), dependency.artifact()); final List versions = fetchVersionsAndSaveThemToCache(dependency.group(), dependency.artifact()); + return findBestCandidate(dependency, versions); + } + + @Nullable + private List getCachedVersions(@NotNull Dependency dependency) { + DependencyUpdaterSettings settings = DependencyUpdaterSettings.getInstance(); + return cache.getVersions(dependency.group(), dependency.artifact(), settings.getCacheTtlMinutes()); + } + + @Nullable + private VersionCandidate findBestCandidate(@NotNull Dependency dependency, @NotNull List versions) { DependencyUpdaterSettings settings = DependencyUpdaterSettings.getInstance(); VersionPolicy policy = getFirstPolicy(); String excludeRegex = settings.getVersionFilterRegex(); @@ -298,8 +187,22 @@ public VersionCandidate forceCheckForUpdate(final DependencyInfo dependency) thr versions, dependency.currentVersion(), policy, - getRepositorySource(dependency.group(), dependency.artifact()), + repositorySourceName(dependency), excludeRegex ); } + + @NotNull + private String repositorySourceName(@NotNull Dependency dependency) { + return versionResolver.resolveSource(dependency.group(), dependency.artifact()).getDisplayName(); + } + + /** + * Gets the first version policy from settings. + */ + @NotNull + private VersionPolicy getFirstPolicy() { + List policies = DependencyUpdaterSettings.getInstance().getVersionPolicies(); + return policies.isEmpty() ? VersionPolicy.createDefaultStablePolicy() : policies.getFirst(); + } } diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/service/VersionResolver.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/service/VersionResolver.java new file mode 100644 index 0000000..e377d64 --- /dev/null +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/service/VersionResolver.java @@ -0,0 +1,95 @@ +package com.github.clementherve.intellijjavadependencyupdaterplugin.service; + +import com.github.clementherve.intellijjavadependencyupdaterplugin.repository.GradlePluginPortalRepository; +import com.github.clementherve.intellijjavadependencyupdaterplugin.repository.MavenCentralRepository; +import com.github.clementherve.intellijjavadependencyupdaterplugin.repository.NexusRepository; +import com.github.clementherve.intellijjavadependencyupdaterplugin.repository.RepositorySource; +import com.github.clementherve.intellijjavadependencyupdaterplugin.ide.settings.DependencyUpdaterSettings; +import com.intellij.openapi.diagnostic.Logger; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.util.List; + +/** + * Decides which repository a dependency's versions come from and fetches them. + *
    + *
  • Plugins (empty group) resolve from the Gradle Plugin Portal.
  • + *
  • Regular dependencies resolve from Nexus when it is configured and the dependency + * matches the configured filter, otherwise (or as a fallback) from Maven Central.
  • + *
+ * The same Nexus-filter decision drives both the fetch and the {@link RepositorySource} + * reported for display, so the two can never disagree. + */ +final class VersionResolver { + + private static final Logger LOGGER = Logger.getInstance(VersionResolver.class); + + private final MavenCentralRepository mavenCentral = new MavenCentralRepository(); + private final GradlePluginPortalRepository pluginPortal = new GradlePluginPortalRepository(); + + /** + * Fetches all available versions for a dependency from the appropriate repository. + */ + @NotNull + List fetchVersions(@NotNull String group, @NotNull String artifact) throws IOException { + if (group.isEmpty()) { + return pluginPortal.fetchVersions(group, artifact); + } + + DependencyUpdaterSettings settings = DependencyUpdaterSettings.getInstance(); + + if (shouldUseNexus(group, artifact, settings)) { + try { + List versions = createNexusRepository(settings).fetchVersions(group, artifact); + if (CollectionUtils.isNotEmpty(versions)) { + return versions; + } + } catch (IOException exception) { + LOGGER.error("Nexus fetch failed for " + group + ":" + artifact + ", error: " + exception.getMessage()); + if (!settings.isFallbackToMavenCentral()) { + throw exception; + } + } + } + + return mavenCentral.fetchVersions(group, artifact); + } + + /** + * Returns the repository source reported for display for the given dependency. + */ + @NotNull + RepositorySource resolveSource(@NotNull String group, @NotNull String artifact) { + if (group.isEmpty()) { + return RepositorySource.GRADLE_PLUGIN_PORTAL; + } + + DependencyUpdaterSettings settings = DependencyUpdaterSettings.getInstance(); + if (shouldUseNexus(group, artifact, settings)) { + return RepositorySource.NEXUS; + } + + return RepositorySource.MAVEN_CENTRAL; + } + + private boolean shouldUseNexus(@NotNull String group, @NotNull String artifact, + @NotNull DependencyUpdaterSettings settings) { + if (StringUtils.isBlank(settings.getNexusBaseUrl())) { + return false; + } + String nexusDependencyRegex = settings.getNexusDependencyRegex(); + return nexusDependencyRegex.isEmpty() || (group + ":" + artifact).matches(nexusDependencyRegex); + } + + @NotNull + private NexusRepository createNexusRepository(@NotNull DependencyUpdaterSettings settings) { + return new NexusRepository( + settings.getNexusBaseUrl(), + settings.getNexusUsername(), + settings.getNexusPassword() + ); + } +} diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/toolwindow/DependencyOverviewPanel.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/toolwindow/DependencyOverviewPanel.java deleted file mode 100644 index ba9a71e..0000000 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/toolwindow/DependencyOverviewPanel.java +++ /dev/null @@ -1,477 +0,0 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.toolwindow; - -import com.github.clementherve.intellijjavadependencyupdaterplugin.DependencyUpdaterBundle; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.DependencyInfo; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.VersionCandidate; -import com.github.clementherve.intellijjavadependencyupdaterplugin.psi.DependencyParser; -import com.github.clementherve.intellijjavadependencyupdaterplugin.psi.DependencyParserFactory; -import com.github.clementherve.intellijjavadependencyupdaterplugin.services.DependencyUpdateService; -import com.github.clementherve.intellijjavadependencyupdaterplugin.toolwindow.model.DependencyRow; -import com.github.clementherve.intellijjavadependencyupdaterplugin.toolwindow.model.DependencyWithVersion; -import com.github.clementherve.intellijjavadependencyupdaterplugin.util.SupportedFilesUtil; -import com.github.clementherve.intellijjavadependencyupdaterplugin.util.VersionReplacer; -import com.intellij.icons.AllIcons; -import com.intellij.openapi.actionSystem.ActionManager; -import com.intellij.openapi.actionSystem.ActionToolbar; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.actionSystem.DefaultActionGroup; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.util.Computable; -import com.intellij.openapi.command.WriteCommandAction; -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.editor.Document; -import com.intellij.openapi.fileEditor.FileDocumentManager; -import com.intellij.openapi.fileEditor.FileDocumentManagerListener; -import com.intellij.openapi.fileEditor.FileEditorManager; -import com.intellij.openapi.fileEditor.OpenFileDescriptor; -import com.intellij.openapi.progress.ProgressIndicator; -import com.intellij.openapi.progress.ProgressManager; -import com.intellij.openapi.progress.Task; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.Messages; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.psi.PsiDocumentManager; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; -import com.intellij.psi.PsiManager; -import com.intellij.psi.SmartPsiElementPointer; -import com.intellij.ui.SearchTextField; -import com.intellij.ui.components.JBScrollPane; -import com.intellij.ui.table.JBTable; -import org.jetbrains.annotations.NotNull; - -import javax.swing.*; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import javax.swing.table.TableColumn; -import javax.swing.table.TableRowSorter; -import java.awt.*; -import java.awt.event.MouseEvent; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Pattern; - -import static com.github.clementherve.intellijjavadependencyupdaterplugin.util.FindBuildGradleFilesUtil.findBuildGradleFilesInCurrentProject; - -/** - * Main panel for the dependency overview tool window. - */ -public class DependencyOverviewPanel extends JPanel { - - private static final Logger LOGGER = Logger.getInstance(DependencyOverviewPanel.class); - private static final String CARD_LOADING = "loading"; - private static final String CARD_TABLE = "table"; - - private final Project project; - private final DependencyTableModel tableModel; - private final JBTable table; - private final JPanel contentPanel; - private final CardLayout cardLayout; - private final JLabel loadingLabel; - private TableRowSorter rowSorter; - - public DependencyOverviewPanel(@NotNull Project project) { - super(new BorderLayout()); - this.project = project; - this.tableModel = new DependencyTableModel(); - this.table = new JBTable(tableModel); - this.cardLayout = new CardLayout(); - this.contentPanel = new JPanel(cardLayout); - this.loadingLabel = new JLabel(DependencyUpdaterBundle.message("toolWindow.loading"), SwingConstants.CENTER); - - setupLoadingPanel(); - setupTable(); - setupToolbar(); - setupFileListener(); - - add(contentPanel, BorderLayout.CENTER); - - showLoading(); - refreshDependencies(false); - } - - private void setupFileListener() { - // Listen for document saves to auto-refresh when build files change - project.getMessageBus().connect().subscribe(FileDocumentManagerListener.TOPIC, new FileDocumentManagerListener() { - @Override - public void beforeDocumentSaving(@NotNull Document document) { - VirtualFile file = FileDocumentManager.getInstance().getFile(document); - if (file != null && SupportedFilesUtil.isSupportedFile(file.getName())) { - SwingUtilities.invokeLater(() -> refreshDependencies(false)); - } - } - }); - } - - - private void setupLoadingPanel() { - JPanel loadingPanel = new JPanel(new GridBagLayout()); - loadingPanel.add(loadingLabel); - contentPanel.add(loadingPanel, CARD_LOADING); - } - - private void applyFilter(String text) { - if (text == null || text.isBlank()) { - rowSorter.setRowFilter(null); - } else { - rowSorter.setRowFilter(RowFilter.regexFilter("(?i)" + Pattern.quote(text), 0)); - } - } - - private void setupTable() { - rowSorter = new TableRowSorter<>(tableModel); - table.setRowSorter(rowSorter); - table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); - - table.addMouseListener(new java.awt.event.MouseAdapter() { - @Override - public void mouseClicked(java.awt.event.MouseEvent evt) { - if (evt.getClickCount() == 2 && evt.getButton() == MouseEvent.BUTTON1) { - navigateToSelectedDependency(); - } - - if (evt.getClickCount() == 1 && evt.getButton() == MouseEvent.BUTTON3) { - int clickedRow = table.rowAtPoint(evt.getPoint()); - if (clickedRow < 0) { - return; - } - table.setRowSelectionInterval(clickedRow, clickedRow); - int modelRow = table.convertRowIndexToModel(clickedRow); - DependencyRow row = tableModel.getRow(modelRow); - DependencyUpdateService service = DependencyUpdateService.getInstance(project); - String selectedVersion = VersionPickerDialog.pickVersion(project, row.dependency(), service); - if (selectedVersion != null) { - VersionReplacer.applyUpdate(project, row.dependency(), selectedVersion); - PsiDocumentManager.getInstance(project).commitAllDocuments(); - refreshDependencies(false); - } - } - } - }); - - SearchTextField searchField = new SearchTextField(); - searchField.getTextEditor().getDocument().addDocumentListener(new DocumentListener() { - @Override public void insertUpdate(DocumentEvent e) { applyFilter(searchField.getText()); } - @Override public void removeUpdate(DocumentEvent e) { applyFilter(searchField.getText()); } - @Override public void changedUpdate(DocumentEvent e) { applyFilter(searchField.getText()); } - }); - - JBScrollPane scrollPane = new JBScrollPane(table); - JPanel tablePanel = new JPanel(new BorderLayout()); - tablePanel.add(searchField, BorderLayout.NORTH); - tablePanel.add(scrollPane, BorderLayout.CENTER); - contentPanel.add(tablePanel, CARD_TABLE); - } - - private void showLoading() { - SwingUtilities.invokeLater(() -> cardLayout.show(contentPanel, CARD_LOADING)); - } - - private void showTable() { - SwingUtilities.invokeLater(() -> cardLayout.show(contentPanel, CARD_TABLE)); - } - - private void setupToolbar() { - DefaultActionGroup actionGroup = new DefaultActionGroup(); - actionGroup.add(new RefreshAction()); - actionGroup.add(new UpdateAllAction()); - actionGroup.add(new UpdateSelectedAction()); - - ActionToolbar toolbar = ActionManager.getInstance().createActionToolbar("DependencyOverview", actionGroup, true); - toolbar.setTargetComponent(this); - - add(toolbar.getComponent(), BorderLayout.NORTH); - } - - private void refreshDependencies(boolean forceRefresh) { - showLoading(); - loadingLabel.setText(DependencyUpdaterBundle.message("toolWindow.scanning")); - - ProgressManager.getInstance().run(new Task.Backgroundable(project, DependencyUpdaterBundle.message("toolWindow.scanning"), false) { - private final List results = new ArrayList<>(); - - @Override - public void run(@NotNull ProgressIndicator indicator) { - SwingUtilities.invokeLater(() -> loadingLabel.setText(DependencyUpdaterBundle.message("toolWindow.findingFiles"))); - indicator.setText(DependencyUpdaterBundle.message("toolWindow.findingFiles")); - - List buildFiles = findBuildGradleFilesInCurrentProject(project); - - if (buildFiles.isEmpty()) { - SwingUtilities.invokeLater(() -> loadingLabel.setText(DependencyUpdaterBundle.message("toolWindow.noFiles"))); - return; - } - - SwingUtilities.invokeLater(() -> loadingLabel.setText(DependencyUpdaterBundle.message("toolWindow.foundFiles", buildFiles.size()))); - - DependencyUpdateService service = DependencyUpdateService.getInstance(project); - PsiManager psiManager = PsiManager.getInstance(project); - - for (int i = 0; i < buildFiles.size(); i++) { - if (indicator.isCanceled()) { - return; - } - - VirtualFile file = buildFiles.get(i); - String projectName = file.getParent() != null ? file.getParent().getName() : file.getName(); - indicator.setFraction((double) i / buildFiles.size()); - String statusText = DependencyUpdaterBundle.message("toolWindow.processingFile", file.getName()); - indicator.setText(statusText); - SwingUtilities.invokeLater(() -> loadingLabel.setText(statusText)); - - try { - List dependencies = ApplicationManager.getApplication().runReadAction((Computable>) () -> { - PsiFile psiFile = psiManager.findFile(file); - if (psiFile == null) { - return List.of(); - } - - DependencyParser parser = DependencyParserFactory.getParser(psiFile); - if (parser == null) { - return List.of(); - } - - return parser.parseDependencies(psiFile); - }); - - // Check for updates - for (DependencyInfo dependency : dependencies) { - if (indicator.isCanceled()) { - return; - } - - String checkingText = DependencyUpdaterBundle.message("toolWindow.checkingDependency", dependency.artifact()); - indicator.setText2(checkingText); - SwingUtilities.invokeLater(() -> loadingLabel.setText(checkingText + "...")); - VersionCandidate latest; - if (forceRefresh) { - latest = service.forceCheckForUpdate(dependency); - } else { - latest = service.checkForUpdate(dependency); - } - results.add(new DependencyWithVersion(dependency, latest, projectName)); - } - - } catch (Exception e) { - LOGGER.warn("Failed to process " + file.getName(), e); - } - } - } - - @Override - public void onSuccess() { - SwingUtilities.invokeLater(() -> { - tableModel.clear(); - for (DependencyWithVersion result : results) { - tableModel.addRow(result.dependency(), result.latestVersion(), result.projectName()); - } - tableModel.refresh(); - updateProjectColumnVisibility(); - showTable(); - - if (results.isEmpty()) { - loadingLabel.setText(DependencyUpdaterBundle.message("toolWindow.noDependencies")); - showLoading(); - } - }); - } - - @Override - public void onThrowable(@NotNull Throwable error) { - LOGGER.error("Failed to scan dependencies", error); - SwingUtilities.invokeLater(() -> { - loadingLabel.setText(DependencyUpdaterBundle.message("toolWindow.error", error.getMessage())); - showLoading(); - }); - } - }); - } - - - private void navigateToSelectedDependency() { - int selectedRow = table.getSelectedRow(); - if (selectedRow < 0) { - return; - } - - int modelRow = table.convertRowIndexToModel(selectedRow); - DependencyRow row = tableModel.getRow(modelRow); - - SmartPsiElementPointer pointer = row.dependency().psiElementPointer(); - if (pointer == null) { - return; - } - - PsiElement element = pointer.getElement(); - if (element == null) { - return; - } - - PsiFile containingFile = element.getContainingFile(); - if (containingFile == null || containingFile.getVirtualFile() == null) { - return; - } - - OpenFileDescriptor descriptor = new OpenFileDescriptor(project, containingFile.getVirtualFile(), element.getTextOffset()); - - FileEditorManager.getInstance(project).openTextEditor(descriptor, true); - } - - private void updateSelectedDependencies(boolean pickVersion) { - int[] selectedRows = table.getSelectedRows(); - if (selectedRows.length == 0) { - Messages.showInfoMessage(project, DependencyUpdaterBundle.message("toolWindow.dialog.selectToUpdate"), DependencyUpdaterBundle.message("toolWindow.dialog.selectToUpdateTitle")); - return; - } - - List rowsToUpdate = new ArrayList<>(); - for (int selectedRow : selectedRows) { - int modelRow = table.convertRowIndexToModel(selectedRow); - DependencyRow row = tableModel.getRow(modelRow); - if (row.latestVersion() != null || pickVersion) { - rowsToUpdate.add(row); - } - } - - if (rowsToUpdate.isEmpty()) { - Messages.showInfoMessage(project, DependencyUpdaterBundle.message("toolWindow.dialog.alreadyUpToDate"), DependencyUpdaterBundle.message("toolWindow.dialog.selectToUpdateTitle")); - return; - } - - if (pickVersion && rowsToUpdate.size() == 1) { - DependencyRow row = rowsToUpdate.getFirst(); - DependencyUpdateService service = DependencyUpdateService.getInstance(project); - String selectedVersion = VersionPickerDialog.pickVersion(project, row.dependency(), service); - - if (selectedVersion != null) { - VersionReplacer.applyUpdate(project, row.dependency(), selectedVersion); - PsiDocumentManager.getInstance(project).commitAllDocuments(); - refreshDependencies(false); - } - } else if (pickVersion) { - Messages.showInfoMessage(project, DependencyUpdaterBundle.message("toolWindow.dialog.singleSelectionOnly"), DependencyUpdaterBundle.message("toolWindow.dialog.pickVersionTitle")); - } else { - StringBuilder message = new StringBuilder(DependencyUpdaterBundle.message("toolWindow.dialog.confirmUpdateQuestion") + "\n\n"); - for (DependencyRow row : rowsToUpdate) { - message.append(String.format("%s: %s → %s\n", row.dependency().artifact(), row.dependency().currentVersion(), row.latestVersion().version())); - } - - int result = Messages.showOkCancelDialog(project, message.toString(), DependencyUpdaterBundle.message("toolWindow.dialog.confirmUpdateTitle", rowsToUpdate.size()), DependencyUpdaterBundle.message("toolWindow.dialog.updateButton"), DependencyUpdaterBundle.message("toolWindow.dialog.cancelButton"), Messages.getQuestionIcon()); - - if (result == Messages.OK) { - List sortedRows = sortUpdatedRowsReversed(rowsToUpdate); - - WriteCommandAction.runWriteCommandAction(project, "Update Dependencies", null, () -> { - for (DependencyRow row : sortedRows) { - VersionReplacer.applyUpdateInWriteAction(project, row.dependency(), row.latestVersion().version()); - } - }); - - PsiDocumentManager.getInstance(project).commitAllDocuments(); - refreshDependencies(false); - } - } - } - - // Sort updates in reverse order (bottom to top) to avoid position invalidation - private List sortUpdatedRowsReversed(final List rowsToUpdate) { - return rowsToUpdate.stream().sorted((r1, r2) -> { - final SmartPsiElementPointer psiElementPointer1 = r1.dependency().psiElementPointer(); - final SmartPsiElementPointer psiElementPointer2 = r2.dependency().psiElementPointer(); - - int offset1 = psiElementPointer1 != null && psiElementPointer1.getElement() != null ? psiElementPointer1.getElement().getTextOffset() : 0; - int offset2 = psiElementPointer2 != null && psiElementPointer2.getElement() != null ? psiElementPointer2.getElement().getTextOffset() : 0; - - return Integer.compare(offset2, offset1); - }).toList(); - } - - private void updateAllDependencies() { - List allRows = tableModel.getAllRows(); - List outdatedRows = allRows.stream().filter(row -> row.latestVersion() != null).toList(); - - if (outdatedRows.isEmpty()) { - Messages.showInfoMessage(project, DependencyUpdaterBundle.message("toolWindow.dialog.allUpToDate"), DependencyUpdaterBundle.message("toolWindow.dialog.allUpToDateTitle")); - return; - } - - StringBuilder message = new StringBuilder(DependencyUpdaterBundle.message("toolWindow.dialog.confirmUpdateQuestion") + "\n\n"); - for (DependencyRow row : outdatedRows) { - message.append(String.format("%s: %s → %s\n", row.dependency().artifact(), row.dependency().currentVersion(), row.latestVersion().version())); - } - - int result = Messages.showOkCancelDialog(project, message.toString(), DependencyUpdaterBundle.message("toolWindow.dialog.confirmUpdateTitle", outdatedRows.size()), DependencyUpdaterBundle.message("toolWindow.dialog.updateAllButton"), DependencyUpdaterBundle.message("toolWindow.dialog.cancelButton"), Messages.getQuestionIcon()); - - if (result == Messages.OK) { - final List sortedRows = sortUpdatedRowsReversed(outdatedRows); - - WriteCommandAction.runWriteCommandAction(project, "Update All Dependencies", null, () -> { - for (DependencyRow row : sortedRows) { - VersionReplacer.applyUpdateInWriteAction(project, row.dependency(), row.latestVersion().version()); - } - }); - - PsiDocumentManager.getInstance(project).commitAllDocuments(); - refreshDependencies(false); - } - } - - private void updateProjectColumnVisibility() { - TableColumn projectColumn = table.getColumnModel().getColumn(5); - if (tableModel.hasMultipleProjects()) { - projectColumn.setMinWidth(50); - projectColumn.setMaxWidth(Integer.MAX_VALUE); - projectColumn.setPreferredWidth(120); - } else { - projectColumn.setMinWidth(0); - projectColumn.setMaxWidth(0); - projectColumn.setPreferredWidth(0); - } - } - - private class RefreshAction extends AnAction { - RefreshAction() { - super(DependencyUpdaterBundle.message("toolWindow.refresh"), DependencyUpdaterBundle.message("toolWindow.action.refresh.description"), AllIcons.Actions.Refresh); - } - - @Override - public void actionPerformed(@NotNull AnActionEvent e) { - refreshDependencies(true); - } - } - - private class UpdateAllAction extends AnAction { - UpdateAllAction() { - super(DependencyUpdaterBundle.message("toolWindow.updateAll"), DependencyUpdaterBundle.message("toolWindow.action.updateAll.description"), AllIcons.Diff.MagicResolve); - } - - @Override - public void actionPerformed(@NotNull AnActionEvent e) { - updateAllDependencies(); - } - } - - private class UpdateSelectedAction extends AnAction { - UpdateSelectedAction() { - super(DependencyUpdaterBundle.message("toolWindow.updateSelected"), DependencyUpdaterBundle.message("toolWindow.action.updateSelected.description"), AllIcons.Actions.Edit); - } - - @Override - public void actionPerformed(@NotNull AnActionEvent e) { - updateSelectedDependencies(false); - } - } - - private class PickVersionAction extends AnAction { - PickVersionAction() { - super(DependencyUpdaterBundle.message("toolWindow.pickVersion"), DependencyUpdaterBundle.message("toolWindow.action.pickVersion.description"), AllIcons.Actions.ShortcutFilter); - } - - @Override - public void actionPerformed(@NotNull AnActionEvent e) { - updateSelectedDependencies(true); - } - } - -} diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/toolwindow/model/DependencyRow.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/toolwindow/model/DependencyRow.java deleted file mode 100644 index 8770e66..0000000 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/toolwindow/model/DependencyRow.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.toolwindow.model; - -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.DependencyInfo; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.VersionCandidate; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * Represents a row in the dependency table. - */ -public record DependencyRow(DependencyInfo dependency, VersionCandidate latestVersion, String status, - String updateType, String projectName) { - public DependencyRow(@NotNull DependencyInfo dependency, - @Nullable VersionCandidate latestVersion, - @NotNull String status, - @Nullable String updateType, - @NotNull String projectName) { - this.dependency = dependency; - this.latestVersion = latestVersion; - this.status = status; - this.updateType = updateType; - this.projectName = projectName; - } -} diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/toolwindow/model/DependencyWithVersion.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/toolwindow/model/DependencyWithVersion.java deleted file mode 100644 index 436cdfd..0000000 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/toolwindow/model/DependencyWithVersion.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.toolwindow.model; - -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.DependencyInfo; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.VersionCandidate; - -public record DependencyWithVersion(DependencyInfo dependency, VersionCandidate latestVersion, String projectName) { -} diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/toolwindow/util/UpdateTypeUtil.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/toolwindow/util/UpdateTypeUtil.java deleted file mode 100644 index 649d1d6..0000000 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/toolwindow/util/UpdateTypeUtil.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.toolwindow.util; - -public class UpdateTypeUtil { - public static String determineUpdateType(String currentVersion, String latestVersion) { - String[] currentParts = currentVersion.split("\\."); - String[] latestParts = latestVersion.split("\\."); - - if (currentParts.length < 1 || latestParts.length < 1) { - return "Unknown"; - } - - try { - int currentMajor = Integer.parseInt(currentParts[0]); - int latestMajor = Integer.parseInt(latestParts[0]); - - if (latestMajor > currentMajor) { - return "Major"; - } - - if (currentParts.length >= 2 && latestParts.length >= 2) { - int currentMinor = Integer.parseInt(currentParts[1]); - int latestMinor = Integer.parseInt(latestParts[1]); - - if (latestMinor > currentMinor) { - return "Minor"; - } - } - - if (currentParts.length >= 3 && latestParts.length >= 3) { - int currentPatch = Integer.parseInt(currentParts[2].split("-")[0]); // Handle versions like 1.2.3-beta - int latestPatch = Integer.parseInt(latestParts[2].split("-")[0]); - - if (latestPatch > currentPatch) { - return "Patch"; - } - } - - return "Other"; - } catch (NumberFormatException e) { - return "Unknown"; - } - } - -} diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/util/VersionReplacer.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/update/DependencyVersionWriter.java similarity index 94% rename from src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/util/VersionReplacer.java rename to src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/update/DependencyVersionWriter.java index 016dec1..9028696 100644 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/util/VersionReplacer.java +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/update/DependencyVersionWriter.java @@ -1,6 +1,6 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.util; +package com.github.clementherve.intellijjavadependencyupdaterplugin.update; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.DependencyInfo; +import com.github.clementherve.intellijjavadependencyupdaterplugin.dependency.Dependency; import com.intellij.openapi.command.WriteCommandAction; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; @@ -14,12 +14,12 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -public class VersionReplacer { +public class DependencyVersionWriter { - private static final Logger LOGGER = Logger.getInstance(VersionReplacer.class); + private static final Logger LOGGER = Logger.getInstance(DependencyVersionWriter.class); public static void applyUpdate(@NotNull Project project, - @NotNull DependencyInfo dependency, + @NotNull Dependency dependency, @NotNull String newVersion) { if (dependency.isVersionVariable()) { int result = Messages.showOkCancelDialog( @@ -41,12 +41,12 @@ public static void applyUpdate(@NotNull Project project, } public static void applyUpdateInWriteAction(@NotNull Project project, - @NotNull DependencyInfo dependency, + @NotNull Dependency dependency, @NotNull String newVersion) { performUpdate(dependency, newVersion); } - private static void performUpdate(@NotNull DependencyInfo dependency, + private static void performUpdate(@NotNull Dependency dependency, @NotNull String newVersion) { if (dependency.psiElementPointer() == null) { return; diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/util/VersionUpdateType.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/util/VersionUpdateType.java deleted file mode 100644 index 6c8e21c..0000000 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/util/VersionUpdateType.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.util; - -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.VersionCandidate; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.List; - -/** - * Utility for categorizing version updates by type (patch, minor, major). - */ -public class VersionUpdateType { - - public enum UpdateType { - PATCH, - MINOR, - MAJOR - } - - /** - * Finds the latest version candidate for a specific update type. - * - * @param currentVersion the current version - * @param candidates all available version candidates (should be sorted descending) - * @param type the update type to find - * @return the latest candidate of the specified type, or null if none found - */ - @Nullable - public static VersionCandidate findLatestByType(@NotNull SemanticVersion currentVersion, - @NotNull List candidates, - @NotNull UpdateType type) { - for (VersionCandidate candidate : candidates) { - SemanticVersion candidateVersion = SemanticVersion.parse(candidate.version()); - if (candidateVersion == null) { - continue; - } - - // Skip if not newer than current - if (candidateVersion.compareTo(currentVersion) <= 0) { - continue; - } - - // Check if this candidate matches the update type - switch (type) { - case PATCH: - // Same major and minor, different patch - if (candidateVersion.getMajor() == currentVersion.getMajor() && - candidateVersion.getMinor() == currentVersion.getMinor() && - candidateVersion.getPatch() > currentVersion.getPatch()) { - return candidate; - } - break; - - case MINOR: - // Same major, different minor - if (candidateVersion.getMajor() == currentVersion.getMajor() && - candidateVersion.getMinor() > currentVersion.getMinor()) { - return candidate; - } - break; - - case MAJOR: - // Different major - if (candidateVersion.getMajor() > currentVersion.getMajor()) { - return candidate; - } - break; - } - } - - return null; - } -} diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/util/SemanticVersion.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/version/SemanticVersion.java similarity index 99% rename from src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/util/SemanticVersion.java rename to src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/version/SemanticVersion.java index 440ed08..e9f9bee 100644 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/util/SemanticVersion.java +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/version/SemanticVersion.java @@ -1,4 +1,4 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.util; +package com.github.clementherve.intellijjavadependencyupdaterplugin.version; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/model/VersionCandidate.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/version/VersionCandidate.java similarity index 98% rename from src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/model/VersionCandidate.java rename to src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/version/VersionCandidate.java index 6752ff6..0625bb3 100644 --- a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/model/VersionCandidate.java +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/version/VersionCandidate.java @@ -1,6 +1,6 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.model; +package com.github.clementherve.intellijjavadependencyupdaterplugin.version; -import com.github.clementherve.intellijjavadependencyupdaterplugin.util.SemanticVersion; +import com.github.clementherve.intellijjavadependencyupdaterplugin.version.SemanticVersion; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/version/VersionChangeClassifier.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/version/VersionChangeClassifier.java new file mode 100644 index 0000000..cbe4b4b --- /dev/null +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/version/VersionChangeClassifier.java @@ -0,0 +1,125 @@ +package com.github.clementherve.intellijjavadependencyupdaterplugin.version; + +import com.github.clementherve.intellijjavadependencyupdaterplugin.version.VersionCandidate; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * Classifies the difference between version strings: it can both pick the latest + * candidate of a given {@link VersionChangeKind} and describe the change between + * two versions for display. + */ +public final class VersionChangeClassifier { + + private static final String UNKNOWN = "Unknown"; + private static final String MAJOR = "Major"; + private static final String MINOR = "Minor"; + private static final String PATCH = "Patch"; + private static final String OTHER = "Other"; + + private VersionChangeClassifier() { + } + + /** + * Finds the latest version candidate that represents the given change kind relative + * to the current version. + * + * @param currentVersion the current version + * @param candidates all available version candidates (should be sorted descending) + * @param kind the change kind to find + * @return the latest candidate of the given kind, or {@code null} if none qualifies + */ + @Nullable + public static VersionCandidate findLatestChange(@NotNull SemanticVersion currentVersion, + @NotNull List candidates, + @NotNull VersionChangeKind kind) { + for (VersionCandidate candidate : candidates) { + SemanticVersion candidateVersion = SemanticVersion.parse(candidate.version()); + if (candidateVersion == null) { + continue; + } + + // Skip if not newer than current + if (candidateVersion.compareTo(currentVersion) <= 0) { + continue; + } + + switch (kind) { + case PATCH: + // Same major and minor, different patch + if (candidateVersion.getMajor() == currentVersion.getMajor() && + candidateVersion.getMinor() == currentVersion.getMinor() && + candidateVersion.getPatch() > currentVersion.getPatch()) { + return candidate; + } + break; + + case MINOR: + // Same major, higher minor + if (candidateVersion.getMajor() == currentVersion.getMajor() && + candidateVersion.getMinor() > currentVersion.getMinor()) { + return candidate; + } + break; + + case MAJOR: + // Higher major + if (candidateVersion.getMajor() > currentVersion.getMajor()) { + return candidate; + } + break; + } + } + + return null; + } + + /** + * Describes, for display, how the latest version differs from the current one + * (e.g. {@code "Major"}, {@code "Minor"}, {@code "Patch"}). Returns {@code "Other"} + * when the versions differ in a way that is not a clean major/minor/patch bump, and + * {@code "Unknown"} when either version cannot be parsed as numeric components. + */ + @NotNull + public static String describe(@NotNull String currentVersion, @NotNull String latestVersion) { + String[] currentParts = currentVersion.split("\\."); + String[] latestParts = latestVersion.split("\\."); + + if (currentParts.length < 1 || latestParts.length < 1) { + return UNKNOWN; + } + + try { + int currentMajor = Integer.parseInt(currentParts[0]); + int latestMajor = Integer.parseInt(latestParts[0]); + + if (latestMajor > currentMajor) { + return MAJOR; + } + + if (currentParts.length >= 2 && latestParts.length >= 2) { + int currentMinor = Integer.parseInt(currentParts[1]); + int latestMinor = Integer.parseInt(latestParts[1]); + + if (latestMinor > currentMinor) { + return MINOR; + } + } + + if (currentParts.length >= 3 && latestParts.length >= 3) { + int currentPatch = Integer.parseInt(currentParts[2].split("-")[0]); // Handle versions like 1.2.3-beta + int latestPatch = Integer.parseInt(latestParts[2].split("-")[0]); + + if (latestPatch > currentPatch) { + return PATCH; + } + } + + return OTHER; + } catch (NumberFormatException exception) { + return UNKNOWN; + } + } +} diff --git a/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/version/VersionChangeKind.java b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/version/VersionChangeKind.java new file mode 100644 index 0000000..c16ef70 --- /dev/null +++ b/src/main/java/com/github/clementherve/intellijjavadependencyupdaterplugin/version/VersionChangeKind.java @@ -0,0 +1,10 @@ +package com.github.clementherve.intellijjavadependencyupdaterplugin.version; + +/** + * The kind of version increment between two versions, following semantic versioning. + */ +public enum VersionChangeKind { + MAJOR, + MINOR, + PATCH +} diff --git a/src/main/kotlin/com/github/clementherve/intellijjavadependencyupdaterplugin/DependencyUpdaterBundle.kt b/src/main/kotlin/com/github/clementherve/intellijjavadependencyupdaterplugin/DependencyUpdaterBundle.kt deleted file mode 100644 index 8b3d5d0..0000000 --- a/src/main/kotlin/com/github/clementherve/intellijjavadependencyupdaterplugin/DependencyUpdaterBundle.kt +++ /dev/null @@ -1,2 +0,0 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin -// Bundle class moved to DependencyUpdaterBundle.java diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index c15ea26..ea1c0d4 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -22,12 +22,12 @@ - + - @@ -35,40 +35,40 @@ + implementationClass="com.github.clementherve.intellijjavadependencyupdaterplugin.ide.annotator.DependencyUpdateAnnotator"/> + factoryClass="com.github.clementherve.intellijjavadependencyupdaterplugin.ide.toolwindow.DependencyOverviewToolWindowFactory"/> Groovy - com.github.clementherve.intellijjavadependencyupdaterplugin.intention.UpdateDependencyIntention + com.github.clementherve.intellijjavadependencyupdaterplugin.ide.intention.UpdateDependencyIntention Dependency updater Groovy - com.github.clementherve.intellijjavadependencyupdaterplugin.intention.UpdateDependencyToPatchIntention + com.github.clementherve.intellijjavadependencyupdaterplugin.ide.intention.UpdateDependencyToPatchIntention Dependency updater Groovy - com.github.clementherve.intellijjavadependencyupdaterplugin.intention.UpdateDependencyToMinorIntention + com.github.clementherve.intellijjavadependencyupdaterplugin.ide.intention.UpdateDependencyToMinorIntention Dependency updater Groovy - com.github.clementherve.intellijjavadependencyupdaterplugin.intention.UpdateDependencyToMajorIntention + com.github.clementherve.intellijjavadependencyupdaterplugin.ide.intention.UpdateDependencyToMajorIntention Dependency updater diff --git a/src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/psi/GradlePsiParserTest.java b/src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/buildfile/gradle/GradleBuildFileParserTest.java similarity index 83% rename from src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/psi/GradlePsiParserTest.java rename to src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/buildfile/gradle/GradleBuildFileParserTest.java index a7337b7..444284b 100644 --- a/src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/psi/GradlePsiParserTest.java +++ b/src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/buildfile/gradle/GradleBuildFileParserTest.java @@ -1,6 +1,6 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.psi; +package com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile.gradle; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.DependencyInfo; +import com.github.clementherve.intellijjavadependencyupdaterplugin.dependency.Dependency; import com.intellij.psi.PsiFile; import com.intellij.testFramework.fixtures.BasePlatformTestCase; import org.jetbrains.plugins.groovy.GroovyFileType; @@ -8,9 +8,9 @@ import java.util.List; /** - * Platform tests for GradlePsiParser. + * Platform tests for GradleBuildFileParser. */ -public class GradlePsiParserTest extends BasePlatformTestCase { +public class GradleBuildFileParserTest extends BasePlatformTestCase { @Override protected void setUp() throws Exception { @@ -35,25 +35,25 @@ public void test_parse_simple_string_notation() { PsiFile file = myFixture.configureByText(GroovyFileType.GROOVY_FILE_TYPE, content); myFixture.setTestDataPath("src/test/testData"); - GradlePsiParser parser = new GradlePsiParser(); - List dependencies = parser.parseDependencies(file); + GradleBuildFileParser parser = new GradleBuildFileParser(); + List dependencies = parser.parseDependencies(file); assertEquals(3, dependencies.size()); - DependencyInfo guava = dependencies.getFirst(); + Dependency guava = dependencies.getFirst(); assertEquals("com.google.guava", guava.group()); assertEquals("guava", guava.artifact()); assertEquals("31.1-jre", guava.currentVersion()); assertEquals("implementation", guava.configurationName()); assertFalse(guava.isVersionVariable()); - DependencyInfo commonsLang = dependencies.get(1); + Dependency commonsLang = dependencies.get(1); assertEquals("org.apache.commons", commonsLang.group()); assertEquals("commons-lang3", commonsLang.artifact()); assertEquals("3.12.0", commonsLang.currentVersion()); assertEquals("api", commonsLang.configurationName()); - DependencyInfo junit = dependencies.get(2); + Dependency junit = dependencies.get(2); assertEquals("junit", junit.group()); assertEquals("junit", junit.artifact()); assertEquals("4.13.2", junit.currentVersion()); @@ -68,19 +68,19 @@ public void test_parse_map_notation() { } """; PsiFile file = myFixture.configureByText(GroovyFileType.GROOVY_FILE_TYPE, content); - GradlePsiParser parser = new GradlePsiParser(); + GradleBuildFileParser parser = new GradleBuildFileParser(); - List dependencies = parser.parseDependencies(file); + List dependencies = parser.parseDependencies(file); assertEquals(2, dependencies.size()); - DependencyInfo guava = dependencies.getFirst(); + Dependency guava = dependencies.getFirst(); assertEquals("com.google.guava", guava.group()); assertEquals("guava", guava.artifact()); assertEquals("31.1-jre", guava.currentVersion()); assertEquals("implementation", guava.configurationName()); - DependencyInfo springBoot = dependencies.get(1); + Dependency springBoot = dependencies.get(1); assertEquals("org.springframework.boot", springBoot.group()); assertEquals("spring-boot-starter", springBoot.artifact()); assertEquals("2.7.0", springBoot.currentVersion()); @@ -102,27 +102,27 @@ public void test_parse_with_variables() { """; PsiFile file = myFixture.configureByText(GroovyFileType.GROOVY_FILE_TYPE, content); - GradlePsiParser parser = new GradlePsiParser(); + GradleBuildFileParser parser = new GradleBuildFileParser(); - List dependencies = parser.parseDependencies(file); + List dependencies = parser.parseDependencies(file); assertEquals(3, dependencies.size()); - DependencyInfo firstDependency = dependencies.getFirst(); + Dependency firstDependency = dependencies.getFirst(); assertEquals("com.google.guava", firstDependency.group()); assertEquals("guava", firstDependency.artifact()); assertTrue(firstDependency.isVersionVariable()); assertEquals("guavaVersion", firstDependency.variableName()); assertEquals("31.1-jre", firstDependency.currentVersion()); - DependencyInfo secondDependency = dependencies.get(1); + Dependency secondDependency = dependencies.get(1); assertEquals("fr.test.extension", secondDependency.group()); assertEquals("extension", secondDependency.artifact()); assertTrue(secondDependency.isVersionVariable()); assertEquals("test_version", secondDependency.variableName()); assertEquals("3.12.0", secondDependency.currentVersion()); - DependencyInfo thirdDependency = dependencies.get(2); + Dependency thirdDependency = dependencies.get(2); assertEquals("org.apache.commons", thirdDependency.group()); assertEquals("commons-lang3", thirdDependency.artifact()); assertEquals("3.12.0", thirdDependency.currentVersion()); @@ -142,15 +142,15 @@ public void test_parse_plugins_block() { """; PsiFile file = myFixture.configureByText(GroovyFileType.GROOVY_FILE_TYPE, content); - GradlePsiParser parser = new GradlePsiParser(); + GradleBuildFileParser parser = new GradleBuildFileParser(); - List plugins = parser.parseDependencies(file); + List plugins = parser.parseDependencies(file); // Should find 4 plugins (java plugin has no version, so it won't be parsed) assertEquals(4, plugins.size()); // Test Spring Boot plugin - DependencyInfo springBoot = plugins.getFirst(); + Dependency springBoot = plugins.getFirst(); assertEquals("", springBoot.group()); // Empty group for plugins assertEquals("org.springframework.boot", springBoot.artifact()); assertEquals("3.5.6", springBoot.currentVersion()); @@ -158,21 +158,21 @@ public void test_parse_plugins_block() { assertFalse(springBoot.isVersionVariable()); // Test Dependency Management plugin - DependencyInfo depMgmt = plugins.get(1); + Dependency depMgmt = plugins.get(1); assertEquals("", depMgmt.group()); assertEquals("io.spring.dependency-management", depMgmt.artifact()); assertEquals("1.1.7", depMgmt.currentVersion()); assertEquals("plugin", depMgmt.configurationName()); // Test GraalVM plugin - DependencyInfo graalvm = plugins.get(2); + Dependency graalvm = plugins.get(2); assertEquals("", graalvm.group()); assertEquals("org.graalvm.buildtools.native", graalvm.artifact()); assertEquals("0.10.5", graalvm.currentVersion()); assertEquals("plugin", graalvm.configurationName()); // Test Versions plugin (uses double quotes) - DependencyInfo versions = plugins.get(3); + Dependency versions = plugins.get(3); assertEquals("", versions.group()); assertEquals("com.github.ben-manes.versions", versions.artifact()); assertEquals("0.53.0", versions.currentVersion()); @@ -193,9 +193,9 @@ public void test_parse_plugins_and_dependencies_together() { """; PsiFile file = myFixture.configureByText(GroovyFileType.GROOVY_FILE_TYPE, content); - GradlePsiParser parser = new GradlePsiParser(); + GradleBuildFileParser parser = new GradleBuildFileParser(); - List all = parser.parseDependencies(file); + List all = parser.parseDependencies(file); // Should find 2 plugins + 2 dependencies = 4 total assertEquals(4, all.size()); diff --git a/src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/toolwindow/DependencyTableModelTest.java b/src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/toolwindow/DependencyTableModelTest.java similarity index 92% rename from src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/toolwindow/DependencyTableModelTest.java rename to src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/toolwindow/DependencyTableModelTest.java index e7f751c..b87f726 100644 --- a/src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/toolwindow/DependencyTableModelTest.java +++ b/src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/ide/toolwindow/DependencyTableModelTest.java @@ -1,7 +1,7 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.toolwindow; +package com.github.clementherve.intellijjavadependencyupdaterplugin.ide.toolwindow; import com.github.clementherve.intellijjavadependencyupdaterplugin.DependencyUpdaterBundle; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.DependencyInfo; +import com.github.clementherve.intellijjavadependencyupdaterplugin.dependency.Dependency; import com.intellij.testFramework.fixtures.BasePlatformTestCase; /** @@ -9,8 +9,8 @@ */ public class DependencyTableModelTest extends BasePlatformTestCase { - private static DependencyInfo dependency() { - return new DependencyInfo( + private static Dependency dependency() { + return new Dependency( "com.google.guava", "guava", "31.1-jre", "implementation", null, false, null); } diff --git a/src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/policy/VersionPolicyEvaluatorTest.java b/src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/policy/VersionPolicyEvaluatorTest.java index b681d6d..077cdc1 100644 --- a/src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/policy/VersionPolicyEvaluatorTest.java +++ b/src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/policy/VersionPolicyEvaluatorTest.java @@ -1,7 +1,7 @@ package com.github.clementherve.intellijjavadependencyupdaterplugin.policy; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.VersionCandidate; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.VersionPolicy; +import com.github.clementherve.intellijjavadependencyupdaterplugin.version.VersionCandidate; +import com.github.clementherve.intellijjavadependencyupdaterplugin.policy.VersionPolicy; import org.junit.Before; import org.junit.Test; diff --git a/src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/cache/VersionCacheTest.java b/src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/VersionCacheTest.java similarity index 99% rename from src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/cache/VersionCacheTest.java rename to src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/VersionCacheTest.java index 96f1a73..cbff20e 100644 --- a/src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/cache/VersionCacheTest.java +++ b/src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/repository/VersionCacheTest.java @@ -1,4 +1,4 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.cache; +package com.github.clementherve.intellijjavadependencyupdaterplugin.repository; import org.junit.Before; import org.junit.Test; diff --git a/src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/util/VersionReplacerTest.java b/src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/update/DependencyVersionWriterTest.java similarity index 82% rename from src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/util/VersionReplacerTest.java rename to src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/update/DependencyVersionWriterTest.java index 08d63ed..fabe798 100644 --- a/src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/util/VersionReplacerTest.java +++ b/src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/update/DependencyVersionWriterTest.java @@ -1,7 +1,7 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.util; +package com.github.clementherve.intellijjavadependencyupdaterplugin.update; -import com.github.clementherve.intellijjavadependencyupdaterplugin.model.DependencyInfo; -import com.github.clementherve.intellijjavadependencyupdaterplugin.psi.GradlePsiParser; +import com.github.clementherve.intellijjavadependencyupdaterplugin.dependency.Dependency; +import com.github.clementherve.intellijjavadependencyupdaterplugin.buildfile.gradle.GradleBuildFileParser; import com.intellij.openapi.command.WriteCommandAction; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiElement; @@ -14,9 +14,9 @@ import java.util.List; /** - * Platform tests for {@link VersionReplacer}. + * Platform tests for {@link DependencyVersionWriter}. */ -public class VersionReplacerTest extends BasePlatformTestCase { +public class DependencyVersionWriterTest extends BasePlatformTestCase { private static final String BUILD_GRADLE = """ dependencies { @@ -74,16 +74,16 @@ private PsiFile configureFull() { return myFixture.configureByText(GroovyFileType.GROOVY_FILE_TYPE, FULL_BUILD_GRADLE); } - private DependencyInfo findByArtifact(List dependencies, String artifact) { + private Dependency findByArtifact(List dependencies, String artifact) { return dependencies.stream() .filter(d -> artifact.equals(d.artifact())) .findFirst() .orElseThrow(() -> new AssertionError("dependency not found: " + artifact)); } - private String update(PsiFile file, DependencyInfo dependency, String newVersion) { + private String update(PsiFile file, Dependency dependency, String newVersion) { WriteCommandAction.runWriteCommandAction(getProject(), () -> - VersionReplacer.applyUpdateInWriteAction(getProject(), dependency, newVersion)); + DependencyVersionWriter.applyUpdateInWriteAction(getProject(), dependency, newVersion)); PsiDocumentManager.getInstance(getProject()).commitAllDocuments(); return file.getText(); } @@ -94,10 +94,10 @@ private String update(PsiFile file, DependencyInfo dependency, String newVersion */ public void test_update_version_that_is_a_prefix_of_another_version() { PsiFile file = configure(); - GradlePsiParser parser = new GradlePsiParser(); - List dependencies = parser.parseDependencies(file); + GradleBuildFileParser parser = new GradleBuildFileParser(); + List dependencies = parser.parseDependencies(file); - DependencyInfo annotations = findByArtifact(dependencies, "jackson-annotations"); + Dependency annotations = findByArtifact(dependencies, "jackson-annotations"); String result = update(file, annotations, "2.99"); assertTrue("jackson-annotations should be updated to 2.99", @@ -114,10 +114,10 @@ public void test_update_version_that_is_a_prefix_of_another_version() { */ public void test_update_longer_version_keeps_other_lines_intact() { PsiFile file = configure(); - GradlePsiParser parser = new GradlePsiParser(); - List dependencies = parser.parseDependencies(file); + GradleBuildFileParser parser = new GradleBuildFileParser(); + List dependencies = parser.parseDependencies(file); - DependencyInfo core = findByArtifact(dependencies, "jackson-core"); + Dependency core = findByArtifact(dependencies, "jackson-core"); String result = update(file, core, "2.22.0"); assertTrue("jackson-core should be updated to 2.22.0", @@ -133,7 +133,7 @@ public void test_update_longer_version_keeps_other_lines_intact() { */ public void test_parsing_versions_are_exact() { PsiFile file = configure(); - List dependencies = new GradlePsiParser().parseDependencies(file); + List dependencies = new GradleBuildFileParser().parseDependencies(file); assertEquals("2.2.43", findByArtifact(dependencies, "swagger-annotations").currentVersion()); assertEquals("3.0.2", findByArtifact(dependencies, "jsr305").currentVersion()); @@ -149,21 +149,21 @@ public void test_parsing_versions_are_exact() { */ public void test_update_all_dependencies_in_one_write_action() { PsiFile file = configure(); - List dependencies = new GradlePsiParser().parseDependencies(file); + List dependencies = new GradleBuildFileParser().parseDependencies(file); - DependencyInfo swagger = findByArtifact(dependencies, "swagger-annotations"); - DependencyInfo jsr305 = findByArtifact(dependencies, "jsr305"); - DependencyInfo core = findByArtifact(dependencies, "jackson-core"); - DependencyInfo annotations = findByArtifact(dependencies, "jackson-annotations"); - DependencyInfo databind = findByArtifact(dependencies, "jackson-databind"); + Dependency swagger = findByArtifact(dependencies, "swagger-annotations"); + Dependency jsr305 = findByArtifact(dependencies, "jsr305"); + Dependency core = findByArtifact(dependencies, "jackson-core"); + Dependency annotations = findByArtifact(dependencies, "jackson-annotations"); + Dependency databind = findByArtifact(dependencies, "jackson-databind"); - List reverseOrdered = dependencies.stream() + List reverseOrdered = dependencies.stream() .sorted(Comparator.comparingInt(this::offsetOf).reversed()) .toList(); WriteCommandAction.runWriteCommandAction(getProject(), () -> { - for (DependencyInfo dependency : reverseOrdered) { - VersionReplacer.applyUpdateInWriteAction(getProject(), dependency, newVersionFor(dependency)); + for (Dependency dependency : reverseOrdered) { + DependencyVersionWriter.applyUpdateInWriteAction(getProject(), dependency, newVersionFor(dependency)); } }); PsiDocumentManager.getInstance(getProject()).commitAllDocuments(); @@ -188,7 +188,7 @@ public void test_version_substring_in_artifact_is_not_corrupted() { } """; PsiFile file = myFixture.configureByText(GroovyFileType.GROOVY_FILE_TYPE, content); - DependencyInfo dependency = new GradlePsiParser().parseDependencies(file).getFirst(); + Dependency dependency = new GradleBuildFileParser().parseDependencies(file).getFirst(); String result = update(file, dependency, "3.0"); @@ -207,7 +207,7 @@ public void test_version_substring_in_group_is_not_corrupted() { } """; PsiFile file = myFixture.configureByText(GroovyFileType.GROOVY_FILE_TYPE, content); - DependencyInfo dependency = new GradlePsiParser().parseDependencies(file).getFirst(); + Dependency dependency = new GradleBuildFileParser().parseDependencies(file).getFirst(); String result = update(file, dependency, "3"); @@ -224,7 +224,7 @@ public void test_version_substring_in_group_is_not_corrupted() { */ public void test_full_file_detected_dependencies() { PsiFile file = configureFull(); - List dependencies = new GradlePsiParser().parseDependencies(file); + List dependencies = new GradleBuildFileParser().parseDependencies(file); List coordinates = dependencies.stream() .map(d -> d.getCoordinates() + ":" + d.currentVersion()) @@ -256,7 +256,7 @@ public void test_full_file_detected_dependencies() { */ public void test_full_file_update_jackson_datatype() { PsiFile file = configureFull(); - List dependencies = new GradlePsiParser().parseDependencies(file); + List dependencies = new GradleBuildFileParser().parseDependencies(file); String result = update(file, findByArtifact(dependencies, "jackson-datatype-jsr310"), "2.22.0"); @@ -273,9 +273,9 @@ public void test_full_file_update_jackson_datatype() { */ public void test_full_file_update_variable_version() { PsiFile file = configureFull(); - List dependencies = new GradlePsiParser().parseDependencies(file); + List dependencies = new GradleBuildFileParser().parseDependencies(file); - DependencyInfo junitApi = findByArtifact(dependencies, "junit-jupiter-api"); + Dependency junitApi = findByArtifact(dependencies, "junit-jupiter-api"); assertTrue("junit-jupiter-api should be variable-based", junitApi.isVersionVariable()); assertEquals("junit_version", junitApi.variableName()); assertEquals("5.13.4", junitApi.currentVersion()); @@ -294,9 +294,9 @@ public void test_full_file_update_variable_version() { */ public void test_full_file_update_plugin_version() { PsiFile file = configureFull(); - List dependencies = new GradlePsiParser().parseDependencies(file); + List dependencies = new GradleBuildFileParser().parseDependencies(file); - DependencyInfo plugin = findByArtifact(dependencies, "com.github.ben-manes.versions"); + Dependency plugin = findByArtifact(dependencies, "com.github.ben-manes.versions"); assertEquals("plugin", plugin.configurationName()); String result = update(file, plugin, "0.54.0"); @@ -304,7 +304,7 @@ public void test_full_file_update_plugin_version() { assertTrue(result, result.contains("id \"com.github.ben-manes.versions\" version \"0.54.0\"")); } - private String newVersionFor(DependencyInfo dependency) { + private String newVersionFor(Dependency dependency) { return switch (dependency.artifact()) { case "swagger-annotations" -> "2.2.44"; case "jsr305" -> "3.0.3"; @@ -314,7 +314,7 @@ private String newVersionFor(DependencyInfo dependency) { }; } - private int offsetOf(DependencyInfo dependency) { + private int offsetOf(Dependency dependency) { SmartPsiElementPointer pointer = dependency.psiElementPointer(); PsiElement element = pointer != null ? pointer.getElement() : null; return element != null ? element.getTextOffset() : 0; diff --git a/src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/util/SemanticVersionTest.java b/src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/version/SemanticVersionTest.java similarity index 99% rename from src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/util/SemanticVersionTest.java rename to src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/version/SemanticVersionTest.java index 5f6fa7e..64e28c9 100644 --- a/src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/util/SemanticVersionTest.java +++ b/src/test/java/com/github/clementherve/intellijjavadependencyupdaterplugin/version/SemanticVersionTest.java @@ -1,4 +1,4 @@ -package com.github.clementherve.intellijjavadependencyupdaterplugin.util; +package com.github.clementherve.intellijjavadependencyupdaterplugin.version; import org.junit.Test;