This is an automated email from the ASF dual-hosted git repository.
mbien pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/netbeans.git
The following commit(s) were added to refs/heads/master by this push:
new 5ac30ecc66 Maven dependency update hint and version completion for pom
profiles.
new 775f0f0ea7 Merge pull request #6765 from mbien/maven-dep-hint-profiles
5ac30ecc66 is described below
commit 5ac30ecc66bf81355ec981a6408ba13cc59052d7
Author: Michael Bien <[email protected]>
AuthorDate: Tue Nov 28 20:59:33 2023 +0100
Maven dependency update hint and version completion for pom profiles.
- properties set in profiles should be taken into account by the
dependency update hint
- implemented auto completion for property values in profiles which
are used for artifact version fields
Still has the same limitation as before: property and usage has to be
in the same pom.
---
.../modules/maven/grammar/MavenProjectGrammar.java | 75 +++++------
.../maven/hints/pom/UpdateDependencyHint.java | 140 ++++++++++++++-------
2 files changed, 133 insertions(+), 82 deletions(-)
diff --git
a/java/maven.grammar/src/org/netbeans/modules/maven/grammar/MavenProjectGrammar.java
b/java/maven.grammar/src/org/netbeans/modules/maven/grammar/MavenProjectGrammar.java
index 794232596b..17bffd7386 100644
---
a/java/maven.grammar/src/org/netbeans/modules/maven/grammar/MavenProjectGrammar.java
+++
b/java/maven.grammar/src/org/netbeans/modules/maven/grammar/MavenProjectGrammar.java
@@ -55,8 +55,6 @@ import org.jdom2.Content;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
-import org.jdom2.filter.AbstractFilter;
-import org.jdom2.filter.Filter;
import org.jdom2.input.SAXBuilder;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectManager;
@@ -79,7 +77,6 @@ import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
-import org.openide.util.RequestProcessor;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
@@ -98,7 +95,6 @@ public class MavenProjectGrammar extends
AbstractSchemaBasedGrammar {
"system" //NOI18N
};
- private static RequestProcessor RP = new
RequestProcessor(MavenProjectGrammar.class.getName(), 3);
private final Project owner;
@@ -123,7 +119,7 @@ public class MavenProjectGrammar extends
AbstractSchemaBasedGrammar {
@Override
protected List<GrammarResult> getDynamicCompletion(String path,
HintContext hintCtx, org.jdom2.Element parent) {
- List<GrammarResult> result = new ArrayList<GrammarResult>();
+ List<GrammarResult> result = new ArrayList<>();
if (path.endsWith("plugins/plugin/configuration") || //NOI18N
path.endsWith("plugins/plugin/executions/execution/configuration")) { //NOI18N
// assuming we have the configuration node as parent..
@@ -296,8 +292,8 @@ public class MavenProjectGrammar extends
AbstractSchemaBasedGrammar {
private List<GrammarResult> collectPluginParams(Document pluginDoc,
HintContext hintCtx) {
Iterator<Content> it = pluginDoc.getRootElement().getDescendants();
- List<GrammarResult> toReturn = new ArrayList<GrammarResult>();
- Collection<String> params = new HashSet<String>();
+ List<GrammarResult> toReturn = new ArrayList<>();
+ Collection<String> params = new HashSet<>();
while (it.hasNext()) {
Content c = it.next();
@@ -336,7 +332,7 @@ public class MavenProjectGrammar extends
AbstractSchemaBasedGrammar {
Exceptions.printStackTrace(ex);
return null;
}
- List<GrammarResult> toReturn = new ArrayList<GrammarResult>();
+ List<GrammarResult> toReturn = new ArrayList<>();
for (PluginIndexManager.ParameterDetail plg : params) {
if (plg.getName().startsWith(hintCtx.getCurrentPrefix())) {
@@ -367,7 +363,7 @@ public class MavenProjectGrammar extends
AbstractSchemaBasedGrammar {
}
FileObject fo = getEnvironment().getFileObject();
if (fo != null) {
- List<String> set = new ArrayList<String>();
+ List<String> set = new ArrayList<>();
set.add("basedir");
set.add("project.build.finalName");
set.add("project.version");
@@ -389,7 +385,7 @@ public class MavenProjectGrammar extends
AbstractSchemaBasedGrammar {
} catch (IllegalArgumentException ex) {
//Exceptions.printStackTrace(ex);
}
- Collection<GrammarResult> elems = new
ArrayList<GrammarResult>();
+ Collection<GrammarResult> elems = new ArrayList<>();
Collections.sort(set);
String suffix =
virtualTextCtx.getNodeValue().substring(prefix.length());
int pplen = propPrefix.length();
@@ -433,7 +429,7 @@ public class MavenProjectGrammar extends
AbstractSchemaBasedGrammar {
}
}
if (path.endsWith("executions/execution/phase")) { //NOI18N
- return
super.createTextValueList(Constants.DEFAULT_PHASES.toArray(new
String[Constants.DEFAULT_PHASES.size()]), virtualTextCtx);
+ return
super.createTextValueList(Constants.DEFAULT_PHASES.toArray(new String[0]),
virtualTextCtx);
}
if (path.endsWith("dependencies/dependency/version") || //NOI18N
path.endsWith("plugins/plugin/version") || //NOI18N
@@ -450,8 +446,8 @@ public class MavenProjectGrammar extends
AbstractSchemaBasedGrammar {
if (hold.getGroupId() != null && hold.getArtifactId() != null) {
Result<NBVersionInfo> result =
RepositoryQueries.getVersionsResult(hold.getGroupId(), hold.getArtifactId(),
RepositoryPreferences.getInstance().getRepositoryInfos());
List<NBVersionInfo> verStrings = result.getResults();
- Collection<GrammarResult> elems = new
ArrayList<GrammarResult>();
- Set<String> uniques = new HashSet<String>();
+ Collection<GrammarResult> elems = new ArrayList<>();
+ Set<String> uniques = new HashSet<>();
for (NBVersionInfo vers : verStrings) {
if (!uniques.contains(vers.getVersion()) &&
vers.getVersion().startsWith(virtualTextCtx.getCurrentPrefix())) {
uniques.add(vers.getVersion());
@@ -466,37 +462,44 @@ public class MavenProjectGrammar extends
AbstractSchemaBasedGrammar {
}
// version property completion
String propXPath = "/project/properties/"; //NOI18N
- if (path.startsWith(propXPath) && path.indexOf("/",
propXPath.length()) == -1) { //NOI18N
- String propName = path.substring(propXPath.length());
- String decorated = "${"+propName+"}"; //NOI18N
-
- Set<ArtifactInfoHolder> usages = new HashSet<>();
+ String profPropXPath = "/project/profiles/profile/properties/";
//NOI18N
+ if ( (path.startsWith(propXPath) && path.indexOf("/",
propXPath.length()) == -1)
+ || (path.startsWith(profPropXPath) && path.indexOf("/",
profPropXPath.length()) == -1)) { //NOI18N
- NodeList pomNodes;
+ Node projectNode; // /project
if (virtualTextCtx.getCurrentPrefix().isEmpty()) {
- pomNodes =
virtualTextCtx.getParentNode().getParentNode().getChildNodes();
+ projectNode = virtualTextCtx.getParentNode().getParentNode();
} else {
- pomNodes =
virtualTextCtx.getParentNode().getParentNode().getParentNode().getChildNodes();
+ projectNode =
virtualTextCtx.getParentNode().getParentNode().getParentNode();
}
+ String property;
+ if (path.startsWith(profPropXPath)) {
+ property = path.substring(profPropXPath.length());
+ projectNode = projectNode.getParentNode().getParentNode();
+ } else {
+ property = path.substring(propXPath.length());
+ }
+ property = "${"+property+"}"; //NOI18N
+ Set<ArtifactInfoHolder> usages = new HashSet<>();
- for (Node node : iterate(pomNodes)) {
+ for (Node node : iterate(projectNode.getChildNodes())) {
if ("dependencies".equals(node.getNodeName())) { //NOI18N
- collectArtifacts("dependency", node, decorated, usages);
//NOI18N
+ collectArtifacts("dependency", node, property, usages);
//NOI18N
} else if ("dependencyManagement".equals(node.getNodeName()))
{ //NOI18N
for (Node dmChild : iterate(node.getChildNodes())) {
if ("dependencies".equals(dmChild.getNodeName())) {
//NOI18N
- collectArtifacts("dependency", dmChild, decorated,
usages); //NOI18N
+ collectArtifacts("dependency", dmChild, property,
usages); //NOI18N
break;
}
}
} else if ("build".equals(node.getNodeName())) { //NOI18N
for (Node buildChild : iterate(node.getChildNodes())) {
if ("plugins".equals(buildChild.getNodeName())) {
//NOI18N
- collectArtifacts("plugin", buildChild, decorated,
usages); //NOI18N
+ collectArtifacts("plugin", buildChild, property,
usages); //NOI18N
} else if
("pluginManagement".equals(buildChild.getNodeName())) { //NOI18N
for (Node pmChild :
iterate(buildChild.getChildNodes())) {
if ("plugins".equals(pmChild.getNodeName())) {
//NOI18N
- collectArtifacts("plugin", pmChild,
decorated, usages); //NOI18N
+ collectArtifacts("plugin", pmChild,
property, usages); //NOI18N
break;
}
}
@@ -542,7 +545,7 @@ public class MavenProjectGrammar extends
AbstractSchemaBasedGrammar {
path.endsWith("extensions/extension/groupId")) { //NOI18N
Result<String> result =
RepositoryQueries.getGroupsResult(RepositoryPreferences.getInstance().getRepositoryInfos());
List<String> elems = result.getResults();
- ArrayList<GrammarResult> texts = new
ArrayList<GrammarResult>();
+ ArrayList<GrammarResult> texts = new ArrayList<>();
for (String elem : elems) {
if (elem.startsWith(virtualTextCtx.getCurrentPrefix())) {
texts.add(new MyTextElement(elem,
virtualTextCtx.getCurrentPrefix()));
@@ -557,7 +560,7 @@ public class MavenProjectGrammar extends
AbstractSchemaBasedGrammar {
if (path.endsWith("plugins/plugin/groupId")) { //NOI18N
Result<String> result =
RepositoryQueries.filterPluginGroupIdsResult(virtualTextCtx.getCurrentPrefix(),
RepositoryPreferences.getInstance().getRepositoryInfos());
// elems.addAll(getRelevant(virtualTextCtx.getCurrentPrefix(),
getCachedPluginGroupIds()));
- ArrayList<GrammarResult> texts = new
ArrayList<GrammarResult>();
+ ArrayList<GrammarResult> texts = new ArrayList<>();
for (String elem : result.getResults()) {
texts.add(new MyTextElement(elem,
virtualTextCtx.getCurrentPrefix()));
}
@@ -579,7 +582,7 @@ public class MavenProjectGrammar extends
AbstractSchemaBasedGrammar {
if (hold.getGroupId() != null) {
Result<String> result =
RepositoryQueries.getArtifactsResult(hold.getGroupId(),
RepositoryPreferences.getInstance().getRepositoryInfos());
List<String> elems = result.getResults();
- ArrayList<GrammarResult> texts = new
ArrayList<GrammarResult>();
+ ArrayList<GrammarResult> texts = new ArrayList<>();
String currprefix = virtualTextCtx.getCurrentPrefix();
for (String elem : elems) {
if (elem.startsWith(currprefix)) {
@@ -605,9 +608,9 @@ public class MavenProjectGrammar extends
AbstractSchemaBasedGrammar {
Result<NBVersionInfo> result =
RepositoryQueries.getRecordsResult(hold.getGroupId(), hold.getArtifactId(),
hold.getVersion(), RepositoryPreferences.getInstance().getRepositoryInfos());
List<NBVersionInfo> elems = result.getResults();
- List<GrammarResult> texts = new ArrayList<GrammarResult>();
+ List<GrammarResult> texts = new ArrayList<>();
String currprefix = virtualTextCtx.getCurrentPrefix();
- Set<String> uniques = new HashSet<String>();
+ Set<String> uniques = new HashSet<>();
for (NBVersionInfo elem : elems) {
if (!uniques.contains(elem.getClassifier()) &&
elem.getClassifier() != null && elem.getClassifier().startsWith(currprefix)) {
texts.add(new MyTextElement(elem.getClassifier(),
currprefix));
@@ -632,7 +635,7 @@ public class MavenProjectGrammar extends
AbstractSchemaBasedGrammar {
ArtifactInfoHolder hold = findArtifactInfo(previous);
if (hold.getGroupId() != null) {
Result<String> result =
RepositoryQueries.filterPluginArtifactIdsResult(hold.getGroupId(),
virtualTextCtx.getCurrentPrefix(),
RepositoryPreferences.getInstance().getRepositoryInfos());
- ArrayList<GrammarResult> texts = new
ArrayList<GrammarResult>();
+ ArrayList<GrammarResult> texts = new ArrayList<>();
for (String elem : result.getResults()) {
texts.add(new MyTextElement(elem,
virtualTextCtx.getCurrentPrefix()));
}
@@ -706,7 +709,7 @@ public class MavenProjectGrammar extends
AbstractSchemaBasedGrammar {
return pathname.isDirectory() && new File(pathname,
"pom.xml").exists(); //NOI18N
}
});
- Collection<GrammarResult> elems = new
ArrayList<GrammarResult>();
+ Collection<GrammarResult> elems = new ArrayList<>();
for (int i = 0; i < modules.length; i++) {
if (modules[i].getName().startsWith(prefix)) {
elems.add(new MyTextElement(modules[i].getName(),
prefix));
@@ -720,7 +723,7 @@ public class MavenProjectGrammar extends
AbstractSchemaBasedGrammar {
/*Return repo url's*/
private List<String> getRepoUrls() {
- List<String> repos = new ArrayList<String>();
+ List<String> repos = new ArrayList<>();
List<RepositoryInfo> ris =
RepositoryPreferences.getInstance().getRepositoryInfos();
for (RepositoryInfo ri : ris) {
@@ -791,7 +794,7 @@ public class MavenProjectGrammar extends
AbstractSchemaBasedGrammar {
private Enumeration<GrammarResult> collectGoals(Document pluginDoc,
HintContext virtualTextCtx) {
Iterator<Content> it = pluginDoc.getRootElement().getDescendants();
- Collection<GrammarResult> toReturn = new ArrayList<GrammarResult>();
+ Collection<GrammarResult> toReturn = new ArrayList<>();
while (it.hasNext()) {
Content c = it.next();
if (!(c instanceof Element)) {
@@ -828,7 +831,7 @@ public class MavenProjectGrammar extends
AbstractSchemaBasedGrammar {
Exceptions.printStackTrace(ex);
return null;
}
- Collection<GrammarResult> toReturn = new ArrayList<GrammarResult>();
+ Collection<GrammarResult> toReturn = new ArrayList<>();
for (String name : goals) {
if (name.startsWith(virtualTextCtx.getCurrentPrefix())) {
toReturn.add(new MyTextElement(name,
virtualTextCtx.getCurrentPrefix()));
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
index cba4ce60ea..2241f9e196 100644
---
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
@@ -35,6 +35,7 @@ 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.BuildBase;
import org.netbeans.modules.maven.model.pom.Dependency;
import org.netbeans.modules.maven.model.pom.DependencyManagement;
import org.netbeans.modules.maven.model.pom.POMComponent;
@@ -42,6 +43,7 @@ import
org.netbeans.modules.maven.model.pom.POMExtensibilityElement;
import org.netbeans.modules.maven.model.pom.POMModel;
import org.netbeans.modules.maven.model.pom.Plugin;
import org.netbeans.modules.maven.model.pom.PluginManagement;
+import org.netbeans.modules.maven.model.pom.Profile;
import org.netbeans.modules.maven.model.pom.Properties;
import org.netbeans.modules.maven.model.pom.ReportPlugin;
import org.netbeans.modules.maven.model.pom.Reporting;
@@ -82,39 +84,54 @@ public class UpdateDependencyHint implements
POMErrorFixProvider {
noMajorUpgrde = getNoMajorUpgradeOption();
Map<POMComponent, ErrorDescription> hints = new HashMap<>();
+ org.netbeans.modules.maven.model.pom.Project project =
model.getProject();
- List<Dependency> deps = model.getProject().getDependencies();
- if (deps != null) {
- addHintsTo(deps, hints);
- }
+ addHintsToDependencies(project.getDependencies(),
project.getDependencyManagement(), hints);
- DependencyManagement depman =
model.getProject().getDependencyManagement();
- if (depman != null && depman.getDependencies() != null) {
- addHintsTo(depman.getDependencies(), hints);
- }
-
- Build build = model.getProject().getBuild();
+ Build build = project.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);
- }
+ addHintsToPlugins(build.getPlugins(), build.getPluginManagement(),
hints);
}
- Reporting reporting = model.getProject().getReporting();
+ Reporting reporting = project.getReporting();
if (reporting != null) {
if (reporting.getReportPlugins() != null) {
addHintsTo(reporting.getReportPlugins(), hints);
}
}
+ List<Profile> profiles = project.getProfiles();
+ if (profiles != null) {
+ for (Profile profile : profiles) {
+ addHintsToDependencies(profile.getDependencies(),
profile.getDependencyManagement(), hints);
+ BuildBase base = profile.getBuildBase();
+ if (base != null) {
+ addHintsToPlugins(base.getPlugins(),
base.getPluginManagement(), hints);
+ }
+ }
+ }
+
return new ArrayList<>(hints.values());
}
+ private void addHintsToDependencies(List<Dependency> deps,
DependencyManagement depman, Map<POMComponent, ErrorDescription> hints) {
+ if (deps != null) {
+ addHintsTo(deps, hints);
+ }
+ if (depman != null && depman.getDependencies() != null) {
+ addHintsTo(depman.getDependencies(), hints);
+ }
+ }
+
+ private void addHintsToPlugins(List<Plugin> plugins, PluginManagement
plugman, Map<POMComponent, ErrorDescription> hints) {
+ if (plugins != null) {
+ addHintsTo(plugins, hints);
+ }
+ if (plugman != null && plugman.getPlugins() != null) {
+ addHintsTo(plugman.getPlugins(), hints);
+ }
+ }
+
private void addHintsTo(List<? extends VersionablePOMComponent>
components, Map<POMComponent, ErrorDescription> hints) {
for (VersionablePOMComponent comp : components) {
@@ -127,24 +144,66 @@ public class UpdateDependencyHint implements
POMErrorFixProvider {
groupId = Constants.GROUP_APACHE_PLUGINS;
}
- if (artifactId != null && groupId != null) {
+ if (artifactId != null && groupId != null && comp.getVersion() !=
null) {
+
+ class HintCandidate { // can be record
+ final String version;
+ final POMExtensibilityElement component;
+ HintCandidate(String version, POMExtensibilityElement
component) {
+ this.version = version;
+ this.component = component;
+ }
+ }
+
+ List<HintCandidate> candidates = List.of();
+
+ if (PomModelUtils.isPropertyExpression(comp.getVersion())) {
+ // properties can be set in profiles and the properties
section
+ // this collects all candidates which might need an
annotation, versions are checked later
+ candidates = new ArrayList<>();
+ String propName =
PomModelUtils.getPropertyName(comp.getVersion());
+ Properties props =
comp.getModel().getProject().getProperties();
+ if (props != null) {
+ POMComponent c = PomModelUtils.getFirstChild(props,
propName);
+ if (c instanceof POMExtensibilityElement) {
+ candidates.add(new
HintCandidate(props.getProperty(propName), (POMExtensibilityElement) c));
+ }
+ }
+ // check profile properties for candidates
+ List<Profile> profiles =
comp.getModel().getProject().getProfiles();
+ if (profiles != null) {
+ for (Profile profile : profiles) {
+ Properties profProps = profile.getProperties();
+ if (profProps != null) {
+ POMComponent c =
PomModelUtils.getFirstChild(profProps, propName);
+ if (c instanceof POMExtensibilityElement) {
+ candidates.add(new
HintCandidate(profProps.getProperty(propName), (POMExtensibilityElement) c));
+ }
+ }
+ }
+ }
+ } else {
+ // simple case, were the version is directly set where the
artifact is declared
+ POMComponent c = PomModelUtils.getFirstChild(comp,
"version");
+ if (c instanceof POMExtensibilityElement) {
+ candidates = List.of(new
HintCandidate(comp.getVersion(), (POMExtensibilityElement) c));
+ }
+ }
- boolean property = false;
- String version = comp.getVersion();
- if (PomModelUtils.isPropertyExpression(version)) {
- version = PomModelUtils.getProperty(comp.getModel(),
version);
- property = true;
+ if (candidates.isEmpty()) {
+ continue;
}
- if (version != null) {
+ List<NBVersionInfo> versions =
RepositoryQueries.getVersionsResult(groupId, artifactId, null).getResults();
+
+ for (HintCandidate candidate : candidates) {
// don't upgrade clean numerical versions to timestamps or
non-numerical versions (other way around is allowed)
- boolean allow_qualifier = !isNumerical(version);
- boolean allow_timestamp = !noTimestamp(version);
- String requiredPrefix = noMajorUpgrde ?
getMajorComponentPrefix(version) : "";
+ boolean allow_qualifier = !isNumerical(candidate.version);
+ boolean allow_timestamp = !noTimestamp(candidate.version);
+ String requiredPrefix = noMajorUpgrde ?
getMajorComponentPrefix(candidate.version) : "";
- Optional<ComparableVersion> latest =
RepositoryQueries.getVersionsResult(groupId, artifactId, null)
- .getResults().stream()
+ Optional<ComparableVersion> latest = versions.stream()
.map(NBVersionInfo::getVersion)
.filter((v) -> !v.isEmpty() &&
v.startsWith(requiredPrefix))
.filter((v) -> allow_qualifier ||
!Character.isDigit(v.charAt(0)) || isNumerical(v))
@@ -152,21 +211,10 @@ public class UpdateDependencyHint implements
POMErrorFixProvider {
.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()));
- }
+ if (latest.isPresent() && latest.get().compareTo(new
ComparableVersion(candidate.version)) > 0) {
+ ErrorDescription previous =
hints.get(candidate.component);
+ if (previous == null || compare(((UpdateVersionFix)
previous.getFixes().getFixes().get(0)).version, candidate.version) > 0) {
+ hints.put(candidate.component,
createHintForComponent(candidate.component, latest.get().toString()));
}
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]
For further information about the NetBeans mailing lists, visit:
https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists