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..d0a7f59dc207 --- /dev/null +++ b/java/maven.hints/src/org/netbeans/modules/maven/hints/pom/UpdateDependencyHint.java @@ -0,0 +1,237 @@ +/* + * 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.apache.maven.artifact.versioning.ComparableVersion; +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.PluginManagement; +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) { + if (build.getPlugins() != null) { + addHintsTo(build.getPlugins(), hints); + } + + PluginManagement plugman = build.getPluginManagement(); + if (plugman != null && plugman.getPlugins() != null) { + addHintsTo(plugman.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) { + + // don't upgrade numerical versions to timestamps or non-numerical versions (other way around is ok) + boolean allow_qualifier = !isNumerical(version); + boolean allow_timestamp = !noTimestamp(version); + + Optional latest = RepositoryQueries.getVersionsResult(groupId, artifactId, null) + .getResults().stream() + .map(NBVersionInfo::getVersion) + .filter((v) -> !v.isEmpty()) + .filter((v) -> allow_qualifier || !Character.isDigit(v.charAt(0)) || isNumerical(v)) + .filter((v) -> allow_timestamp || !Character.isDigit(v.charAt(0)) || noTimestamp(v)) + .map(ComparableVersion::new) + .max(ComparableVersion::compareTo); + + if (latest.isPresent() && latest.get().compareTo(new ComparableVersion(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().toString())); + } + } + } + } + } + } + } + + private static boolean isNumerical(String v) { + for (char c : v.toCharArray()) { + if (!(Character.isDigit(c) || c == '.')) { + return false; + } + } + return true; + } + + private boolean noTimestamp(String v) { + char[] chars = v.toCharArray(); + for (int i = 0; i < chars.length; i++) { + if (!(Character.isDigit(chars[i]))) { + return i == 0 || Integer.parseInt(v.substring(0, i)) < 10_000; + } + } + return Integer.parseInt(v) < 10_000; + } + + private static int compare(String version1, String version2) { + return new ComparableVersion(version1).compareTo(new ComparableVersion(version2)); + } + + 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..b51a3b913936 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 @@ -25,6 +25,7 @@ import java.util.prefs.Preferences; import javax.swing.JComponent; import javax.xml.namespace.QName; +import org.apache.maven.artifact.versioning.ComparableVersion; import org.netbeans.api.project.Project; import org.netbeans.modules.editor.NbEditorUtilities; import org.netbeans.modules.maven.hints.pom.spi.Configuration; @@ -61,6 +62,8 @@ public class UseReleaseOptionHint implements POMErrorFixProvider { private static final String SOURCE_TAG = "source"; private static final String RELEASE_TAG = "release"; + private static final ComparableVersion MIN_VERSION = new ComparableVersion("3.6"); + private static final Configuration config = new Configuration(UseReleaseOptionHint.class.getName(), TIT_UseReleaseVersionHint(), DESC_UseReleaseVersionHint(), true, Configuration.HintSeverity.WARNING); @@ -113,15 +116,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); @@ -157,35 +160,11 @@ private ErrorDescription createHintForComponent(String prefix, POMComponent comp * maven-compiler-plugin version must be >= 3.6 */ private boolean isPluginCompatible(Plugin plugin) { - String string = plugin.getVersion(); - if (string == null) { - return false; - } - String[] version = string.split("-")[0].split("\\."); - try { - int major = version.length > 0 ? Integer.parseInt(version[0]) : 0; - int minor = version.length > 1 ? Integer.parseInt(version[1]) : 0; - if (major < 3 || (major == 3 && minor < 6)) { - return false; - } - } catch (NumberFormatException ignored) { + String version = plugin.getVersion(); + if (version == null || version.isEmpty()) { return false; } - 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; + return new ComparableVersion(version).compareTo(MIN_VERSION) >= 0; } private static class ConvertToReleaseOptionFix implements Fix {