diff --git a/java/maven.hints/src/org/netbeans/modules/maven/hints/layer.xml b/java/maven.hints/src/org/netbeans/modules/maven/hints/layer.xml index ad64474723e2..812b81420ac4 100644 --- a/java/maven.hints/src/org/netbeans/modules/maven/hints/layer.xml +++ b/java/maven.hints/src/org/netbeans/modules/maven/hints/layer.xml @@ -65,6 +65,9 @@ + + + diff --git a/java/maven.hints/src/org/netbeans/modules/maven/hints/pom/PomModelUtils.java b/java/maven.hints/src/org/netbeans/modules/maven/hints/pom/PomModelUtils.java index bcc6ab6da2ae..400f7d9e6395 100644 --- a/java/maven.hints/src/org/netbeans/modules/maven/hints/pom/PomModelUtils.java +++ b/java/maven.hints/src/org/netbeans/modules/maven/hints/pom/PomModelUtils.java @@ -57,7 +57,9 @@ import org.netbeans.modules.maven.hints.pom.spi.POMErrorFixBase; import org.netbeans.modules.maven.hints.pom.spi.POMErrorFixProvider; import org.netbeans.modules.maven.indexer.api.RepositoryPreferences; +import org.netbeans.modules.maven.model.pom.POMComponent; import org.netbeans.modules.maven.model.pom.POMModel; +import org.netbeans.modules.maven.model.pom.Properties; import org.netbeans.modules.xml.xam.Model; import org.netbeans.spi.editor.hints.ErrorDescription; import org.netbeans.spi.editor.hints.ErrorDescriptionFactory; @@ -151,7 +153,7 @@ static void runMavenValidation(final POMModel model, final List findHints(final @NonNull POMModel model, final Project project) { - final List err = new ArrayList(); + final List err = new ArrayList<>(); //before checkModelValid because of #216093 runMavenValidation(model, err); if (!checkModelValid(model)) { @@ -230,7 +232,7 @@ static List runMavenValidationImpl(final File pom, Document doc) { try { problems = embedder.lookupComponent(ProjectBuilder.class).build(new M2S(pom, doc), req).getProblems(); } catch (ProjectBuildingException x) { - problems = new ArrayList(); + problems = new ArrayList<>(); List results = x.getResults(); if (results != null) { //one code point throwing ProjectBuildingException contains results, for (ProjectBuildingResult result : results) { @@ -244,7 +246,7 @@ static List runMavenValidationImpl(final File pom, Document doc) { } } } - List toRet = new LinkedList(); + List toRet = new LinkedList<>(); for (ModelProblem problem : problems) { if(ModelUtils.checkByCLIMavenValidationLevel(problem)) { toRet.add(problem); @@ -272,5 +274,46 @@ static List hintProviders(Project project, Class< }); return providers; } - + + /** + * Returns true if the given text could be a maven property. + */ + static boolean isPropertyExpression(String expression) { + return expression != null && expression.startsWith("${") && expression.endsWith("}"); + } + + /** + * Returns the property name of a property expression. + */ + static String getPropertyName(String expression) { + if (isPropertyExpression(expression)) { + return expression.substring(2, expression.length() - 1); + } + return expression; + } + + /** + * Returns the value of the maven property or null. + * @param expression the property text, for example: ${java.version}. + */ + static String getProperty(POMModel model, String expression) { + Properties properties = model.getProject().getProperties(); + if (properties != null) { + return properties.getProperty(getPropertyName(expression)); + } + return null; + } + + /** + * Returns the first child component with the given name or null. + */ + static POMComponent getFirstChild(POMComponent parent, String name) { + for (POMComponent child : parent.getChildren()) { + if (name.equals(child.getPeer().getNodeName())) { + return child; + } + } + return null; + } + } diff --git a/java/maven.hints/src/org/netbeans/modules/maven/hints/pom/UpdateDependencyHint.java b/java/maven.hints/src/org/netbeans/modules/maven/hints/pom/UpdateDependencyHint.java new file mode 100644 index 000000000000..704a07c633b3 --- /dev/null +++ b/java/maven.hints/src/org/netbeans/modules/maven/hints/pom/UpdateDependencyHint.java @@ -0,0 +1,226 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.modules.maven.hints.pom; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.prefs.Preferences; +import javax.swing.JComponent; +import javax.swing.text.Document; +import org.netbeans.api.project.Project; +import org.netbeans.modules.editor.NbEditorUtilities; +import org.netbeans.modules.maven.hints.pom.spi.Configuration; +import org.netbeans.modules.maven.hints.pom.spi.POMErrorFixProvider; +import org.netbeans.modules.maven.indexer.api.NBVersionInfo; +import org.netbeans.modules.maven.indexer.api.RepositoryQueries; +import org.netbeans.modules.maven.model.pom.Build; +import org.netbeans.modules.maven.model.pom.Dependency; +import org.netbeans.modules.maven.model.pom.DependencyManagement; +import org.netbeans.modules.maven.model.pom.POMComponent; +import org.netbeans.modules.maven.model.pom.POMExtensibilityElement; +import org.netbeans.modules.maven.model.pom.POMModel; +import org.netbeans.modules.maven.model.pom.Properties; +import org.netbeans.modules.maven.model.pom.VersionablePOMComponent; +import org.netbeans.spi.editor.hints.ChangeInfo; +import org.netbeans.spi.editor.hints.ErrorDescription; +import org.netbeans.spi.editor.hints.ErrorDescriptionFactory; +import org.netbeans.spi.editor.hints.Fix; +import org.netbeans.spi.editor.hints.Severity; +import org.openide.util.NbBundle; + +import static org.netbeans.modules.maven.hints.pom.Bundle.*; + +/** + * Marks the artifact if there is a newer version available and provides a fix + * which updates the version. + * @author mbien + */ +@NbBundle.Messages({ + "TIT_UpdateDependencyHint=Mark artifact update opportunities.", + "DESC_UpdateDependencyHint=Marks the artifact if there is a newer version available.", + "HINT_UpdateDependencyHint=New version available: ", + "FIX_UpdateDependencyHint=update to: "}) +public class UpdateDependencyHint implements POMErrorFixProvider { + + private static final Configuration config = new Configuration(UpdateDependencyHint.class.getName(), + TIT_UpdateDependencyHint(), DESC_UpdateDependencyHint(), true, Configuration.HintSeverity.WARNING); + + private volatile boolean cancelled; + + + @Override + public List getErrorsForDocument(POMModel model, Project prj) { + + cancelled = false; + + Map hints = new HashMap<>(); + + List deps = model.getProject().getDependencies(); + if (deps != null) { + addHintsTo(deps, hints); + } + + DependencyManagement depman = model.getProject().getDependencyManagement(); + if (depman != null && depman.getDependencies() != null) { + addHintsTo(depman.getDependencies(), hints); + } + + Build build = model.getProject().getBuild(); + if (build != null && build.getPlugins() != null) { + addHintsTo(build.getPlugins(), hints); + } + + return new ArrayList<>(hints.values()); + } + + private void addHintsTo(List components, Map hints) { + + for (VersionablePOMComponent comp : components) { + + if (cancelled) { + return; + } + + String groupId = comp.getGroupId(); + String artifactId = comp.getArtifactId(); + + if (groupId != null && artifactId != null && !groupId.isEmpty() && !artifactId.isEmpty()) { + + boolean property = false; + String version = comp.getVersion(); + if (PomModelUtils.isPropertyExpression(version)) { + version = PomModelUtils.getProperty(comp.getModel(), version); + property = true; + } + + if (version != null && isNumerical(version)) { + + Optional latest = RepositoryQueries.getVersionsResult(groupId, artifactId, null) + .getResults().stream() + .map(NBVersionInfo::getVersion) + .filter((v) -> isNumerical(v)) + .filter((v) -> Integer.parseInt(v.split("\\.")[0]) < 10_000) // filter out timestamps or things which don't look like versions + .max((v1, v2) -> compare(v1, v2)); + + if (latest.isPresent() && compare(latest.get(), version) > 0) { + POMComponent version_comp = null; + if (property) { + Properties props = comp.getModel().getProject().getProperties(); + if (props != null) { + version_comp = PomModelUtils.getFirstChild(props, PomModelUtils.getPropertyName(comp.getVersion())); + } + } else { + version_comp = PomModelUtils.getFirstChild(comp, "version"); + } + if (version_comp instanceof POMExtensibilityElement) { + ErrorDescription previous = hints.get(version_comp); + if (previous == null || compare(((UpdateVersionFix) previous.getFixes().getFixes().get(0)).version, version) > 0) { + hints.put(version_comp, createHintForComponent((POMExtensibilityElement) version_comp, latest.get())); + } + } + } + } + } + } + } + + private static boolean isNumerical(String v) { + for (char c : v.toCharArray()) { + if (!(Character.isDigit(c) || c == '.')) { + return false; + } + } + return true; + } + + private static int compare(String version1, String version2) { + String[] v1 = version1.split("\\."); + String[] v2 = version2.split("\\."); + for (int i = 0; i < v1.length; i++) { + if (i >= v2.length) { + return 1; + } + int n1 = Integer.parseInt(v1[i]); + int n2 = Integer.parseInt(v2[i]); + if (n1 > n2) { + return 1; + } else if (n2 > n1) { + return -1; + } + } + return 0; + } + + private ErrorDescription createHintForComponent(POMExtensibilityElement comp, String version) { + Document doc = comp.getModel().getBaseDocument(); + int line = NbEditorUtilities.getLine(doc, comp.findPosition(), false).getLineNumber() + 1; + List fix = Collections.singletonList(new UpdateVersionFix(comp, version)); + return ErrorDescriptionFactory.createErrorDescription(Severity.HINT, HINT_UpdateDependencyHint() + version, fix, doc, line); + } + + private static class UpdateVersionFix implements Fix { + + private final POMExtensibilityElement version_comp; + private final String version; + + private UpdateVersionFix(POMExtensibilityElement component, String version) { + this.version_comp = component; + this.version = version; + } + + @Override + public String getText() { + return FIX_UpdateDependencyHint() + version; + } + + @Override + public ChangeInfo implement() throws Exception { + PomModelUtils.implementInTransaction(version_comp.getModel(), () -> { + version_comp.setElementText(version); + }); + return new ChangeInfo(); + } + + } + + @Override + public void cancel() { + cancelled = true; + } + + @Override + public Configuration getConfiguration() { + return config; + } + + @Override + public String getSavedValue(JComponent customizer, String key) { + return null; + } + + @Override + public JComponent getCustomizer(Preferences preferences) { + return null; + } + +} diff --git a/java/maven.hints/src/org/netbeans/modules/maven/hints/pom/UseReleaseOptionHint.java b/java/maven.hints/src/org/netbeans/modules/maven/hints/pom/UseReleaseOptionHint.java index 737b5bb39bab..1c9342c45cd7 100644 --- a/java/maven.hints/src/org/netbeans/modules/maven/hints/pom/UseReleaseOptionHint.java +++ b/java/maven.hints/src/org/netbeans/modules/maven/hints/pom/UseReleaseOptionHint.java @@ -113,15 +113,15 @@ private List createHintsForParent(String prefix, POMComponent try { String sourceText = parent.getChildElementText(POMQName.createQName(prefix+SOURCE_TAG, true)); - if (isProperty(sourceText)) { + if (PomModelUtils.isPropertyExpression(sourceText)) { release = sourceText; - sourceText = getProperty(sourceText, parent.getModel()); + sourceText = PomModelUtils.getProperty(parent.getModel(), sourceText); } String targetText = parent.getChildElementText(POMQName.createQName(prefix+TARGET_TAG, true)); - if (isProperty(targetText)) { + if (PomModelUtils.isPropertyExpression(targetText)) { release = targetText; - targetText = getProperty(targetText, parent.getModel()); + targetText = PomModelUtils.getProperty(parent.getModel(), targetText); } source = Integer.parseInt(sourceText); @@ -174,20 +174,6 @@ private boolean isPluginCompatible(Plugin plugin) { return true; } - private static boolean isProperty(String property) { - return property != null && property.startsWith("$"); - } - - private static String getProperty(String prop, POMModel model) { - if (prop.length() > 3) { - Properties properties = model.getProject().getProperties(); - if (properties != null) { - return properties.getProperty(prop.substring(2, prop.length()-1)); - } - } - return null; - } - private static class ConvertToReleaseOptionFix implements Fix { private final String prefix;