View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.resolver.internal.ant.tasks;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.util.ArrayList;
24  import java.util.Collection;
25  import java.util.Collections;
26  import java.util.HashMap;
27  import java.util.HashSet;
28  import java.util.LinkedList;
29  import java.util.List;
30  import java.util.Map;
31  
32  import org.apache.maven.resolver.internal.ant.AntRepoSys;
33  import org.apache.maven.resolver.internal.ant.Names;
34  import org.apache.maven.resolver.internal.ant.types.Dependencies;
35  import org.apache.maven.resolver.internal.ant.types.Pom;
36  import org.apache.tools.ant.BuildException;
37  import org.apache.tools.ant.Project;
38  import org.apache.tools.ant.ProjectComponent;
39  import org.apache.tools.ant.types.FileSet;
40  import org.apache.tools.ant.types.Reference;
41  import org.apache.tools.ant.types.resources.FileResource;
42  import org.apache.tools.ant.types.resources.Resources;
43  import org.apache.tools.ant.util.FileUtils;
44  import org.eclipse.aether.RepositorySystem;
45  import org.eclipse.aether.RepositorySystemSession;
46  import org.eclipse.aether.artifact.Artifact;
47  import org.eclipse.aether.graph.DependencyFilter;
48  import org.eclipse.aether.graph.DependencyNode;
49  import org.eclipse.aether.resolution.ArtifactRequest;
50  import org.eclipse.aether.resolution.ArtifactResolutionException;
51  import org.eclipse.aether.resolution.ArtifactResult;
52  import org.eclipse.aether.util.artifact.SubArtifact;
53  import org.eclipse.aether.util.filter.ScopeDependencyFilter;
54  
55  /**
56   * Ant task to resolve dependencies using Maven Resolver.
57   * <p>
58   * This task reads dependency and repository definitions (either inline or via references)
59   * and resolves them according to the specified scopes and remote repositories. Resolved artifacts
60   * can be stored in Ant references or used for further processing (e.g., setting up classpaths).
61   * </p>
62   *
63   * <h2>Usage Example:</h2>
64   * <pre>{@code
65   * <resolve>
66   *   <dependencies>
67   *     <dependency groupId="org.apache.commons" artifactId="commons-lang3" version="3.18.0"/>
68   *   </dependencies>
69   *   <repositories>
70   *     <repository id="central" url="https://repo.maven.apache.org/maven2"/>
71   *   </repositories>
72   *   <path id="my.classpath"/>
73   * </resolve>
74   * }</pre>
75   *
76   * <h2>Attributes:</h2>
77   * <ul>
78   *   <li><strong>failOnMissingDescriptor</strong> — whether to fail if a POM file cannot be resolved (default: false)</li>
79   *   <li><strong>offline</strong> — whether to operate in offline mode (default: false)</li>
80   * </ul>
81   *
82   * <h2>Nested Elements:</h2>
83   * <ul>
84   *   <li>{@code <dependencies>} — defines one or more dependencies to resolve</li>
85   *   <li>{@code <repositories>} — a container for one or more {@code <repository>} elements</li>
86   *   <li>{@code <repository>} — specifies a remote Maven repository</li>
87   *   <li>{@code <path>} — optionally defines an Ant path to which resolved artifacts are added</li>
88   * </ul>
89   *
90   * <h2>Ant References Created:</h2>
91   * <ul>
92   *   <li>May register resolved artifacts under a path reference if {@code <path>} is used</li>
93   * </ul>
94   *
95   * <h2>Typical Use Cases:</h2>
96   * <ul>
97   *   <li>Resolving Maven artifacts for use in compilation, testing, or runtime</li>
98   *   <li>Dynamically constructing classpaths using Maven coordinates</li>
99   * </ul>
100  *
101  * @see org.apache.maven.resolver.internal.ant.types.Dependencies
102  * @see org.apache.maven.resolver.internal.ant.types.RemoteRepository
103  * @see org.apache.tools.ant.types.Path
104  */
105 public class Resolve extends AbstractResolvingTask {
106 
107     private final List<ArtifactConsumer> consumers = new ArrayList<>();
108 
109     private boolean failOnMissingAttachments;
110 
111     /**
112      * Default constructor used by Ant to create a {@code Resolve} task instance.
113      */
114     public Resolve() {
115         // Default constructor for Ant task
116     }
117 
118     /**
119      * Sets whether the build should fail if an expected attachment (e.g., sources or javadoc)
120      * cannot be resolved.
121      * <p>
122      * This flag only affects artifact consumers that request classified artifacts,
123      * such as sources or javadoc JARs via the {@code attachments} attribute.
124      * </p>
125      * <p>
126      * If {@code false} (default), missing attachments are logged at verbose level and ignored.
127      * If {@code true}, any unresolved attachment will cause the build to fail.
128      * </p>
129      *
130      * @param failOnMissingAttachments {@code true} to fail the build on missing attachments;
131      *                                  {@code false} to ignore them
132      */
133     public void setFailOnMissingAttachments(boolean failOnMissingAttachments) {
134         this.failOnMissingAttachments = failOnMissingAttachments;
135     }
136 
137     /**
138      * Creates a {@link Path} consumer that collects resolved artifact files into an Ant {@code <path>} reference.
139      * <p>
140      * This is useful for dynamically constructing classpaths from Maven artifacts. The reference ID must be
141      * set via {@link Path#setRefId(String)} so that the resulting path can be used elsewhere in the build.
142      * </p>
143      *
144      * <p>Each resolved artifact is added to the path as a {@link org.apache.tools.ant.types.resources.FileResource}.</p>
145      *
146      * @return a new {@link Path} consumer instance
147      *
148      * @see Path#setRefId(String)
149      */
150     public Path createPath() {
151         Path path = new Path();
152         consumers.add(path);
153         return path;
154     }
155 
156     /**
157      * Creates a {@link Files} consumer to collect resolved artifacts into a directory or resource collection.
158      * <p>
159      * This element allows resolved artifacts to be copied to a specified directory with an optional layout,
160      * or referenced as a {@link org.apache.tools.ant.types.FileSet} or {@link org.apache.tools.ant.types.resources.Resources}
161      * depending on the configuration.
162      * </p>
163      *
164      * @return a new {@link Files} consumer instance
165      *
166      * @see Files#setDir(File)
167      * @see Files#setLayout(String)
168      * @see Files#setRefId(String)
169      */
170     public Files createFiles() {
171         Files files = new Files();
172         consumers.add(files);
173         return files;
174     }
175 
176     /**
177      * Creates a {@link Props} consumer that maps resolved artifacts to Ant project properties.
178      * <p>
179      * Each resolved artifact will result in a property assignment using a key composed of the
180      * artifact's Maven coordinates, optionally prefixed. The value will be the absolute path to
181      * the artifact file.
182      * </p>
183      *
184      * <p>Example property name format:</p>
185      * <pre>{@code
186      *   prefix.groupId:artifactId:extension[:classifier]
187      * }</pre>
188      *
189      * @return a new {@link Props} consumer instance
190      *
191      * @see Props#setPrefix(String)
192      * @see Props#setAttachments(String)
193      */
194     public Props createProperties() {
195         Props props = new Props();
196         consumers.add(props);
197         return props;
198     }
199 
200     private void validate() {
201         for (ArtifactConsumer consumer : consumers) {
202             consumer.validate();
203         }
204 
205         Pom pom = AntRepoSys.getInstance(getProject()).getDefaultPom();
206         if (dependencies == null && pom != null) {
207             log("Using default pom for dependency resolution (" + pom.toString() + ")", Project.MSG_INFO);
208             dependencies = new Dependencies();
209             dependencies.setProject(getProject());
210             getProject().addReference(Names.ID_DEFAULT_POM, pom);
211             dependencies.setPomRef(new Reference(getProject(), Names.ID_DEFAULT_POM));
212         }
213 
214         if (dependencies != null) {
215             dependencies.validate(this);
216         } else {
217             throw new BuildException("No <dependencies> set for resolution");
218         }
219     }
220 
221     @Override
222     public void execute() throws BuildException {
223         validate();
224 
225         AntRepoSys sys = AntRepoSys.getInstance(getProject());
226 
227         RepositorySystemSession session = sys.getSession(this, localRepository);
228         RepositorySystem system = sys.getSystem();
229         log("Using local repository " + session.getLocalRepository(), Project.MSG_VERBOSE);
230 
231         DependencyNode root = collectDependencies().getRoot();
232         root.accept(new DependencyGraphLogger(this));
233 
234         Map<String, Group> groups = new HashMap<>();
235         for (ArtifactConsumer consumer : consumers) {
236             String classifier = consumer.getClassifier();
237             Group group = groups.get(classifier);
238             if (group == null) {
239                 group = new Group(classifier);
240                 groups.put(classifier, group);
241             }
242             group.add(consumer);
243         }
244 
245         for (Group group : groups.values()) {
246             group.createRequests(root);
247         }
248 
249         log("Resolving artifacts", Project.MSG_INFO);
250 
251         for (Group group : groups.values()) {
252             List<ArtifactResult> results;
253             try {
254                 results = system.resolveArtifacts(session, group.getRequests());
255             } catch (ArtifactResolutionException e) {
256                 if (!group.isAttachments() || failOnMissingAttachments) {
257                     throw new BuildException("Could not resolve artifacts: " + e.getMessage(), e);
258                 }
259                 results = e.getResults();
260                 for (ArtifactResult result : results) {
261                     if (result.isMissing()) {
262                         log("Ignoring missing attachment " + result.getRequest().getArtifact(), Project.MSG_VERBOSE);
263                     } else if (!result.isResolved()) {
264                         throw new BuildException("Could not resolve artifacts: " + e.getMessage(), e);
265                     }
266                 }
267             }
268 
269             group.processResults(results, session);
270         }
271     }
272 
273     /**
274      * Abstract base class for consumers of resolved artifacts in the {@link Resolve} task.
275      * <p>
276      * Subclasses of this class define how resolved artifacts are handled, such as copying them to a directory,
277      * adding them to a path, or storing their locations in properties. Each consumer may filter artifacts by
278      * Maven scope or classpath profile.
279      * </p>
280      *
281      * <p>
282      * ArtifactConsumers are registered by the enclosing {@link Resolve} task and invoked after artifacts are
283      * resolved from the dependency graph.
284      * </p>
285      *
286      * <p>
287      * Common subclasses include:
288      * <ul>
289      *   <li>{@link Resolve.Path} – Adds artifacts to an Ant {@code <path>} reference</li>
290      *   <li>{@link Resolve.Files} – Copies artifacts to a directory and exposes a fileset or resource collection</li>
291      *   <li>{@link Resolve.Props} – Stores artifact file paths as Ant properties</li>
292      * </ul>
293      *
294      * @see Resolve
295      */
296     public abstract static class ArtifactConsumer extends ProjectComponent {
297 
298         private DependencyFilter filter;
299 
300         /**
301          * Default constructor for Ant task instantiation.
302          * <p>
303          * This constructor is used by Ant to create instances of {@link ArtifactConsumer} subclasses.
304          * </p>
305          */
306         public ArtifactConsumer() {
307             // Default constructor for Ant task
308         }
309 
310         /**
311          * Determines whether the given dependency node should be accepted by this consumer
312          * based on its configured dependency filter (e.g., scope or classpath).
313          *
314          * @param node the dependency node to evaluate
315          * @param parents the list of parent nodes leading to this node in the dependency graph
316          * @return {@code true} if the node passes the filter (or no filter is set); {@code false} otherwise
317          */
318         public boolean accept(org.eclipse.aether.graph.DependencyNode node, List<DependencyNode> parents) {
319             return filter == null || filter.accept(node, parents);
320         }
321 
322         /**
323          * Returns the classifier this consumer is interested in, if any.
324          * <p>
325          * This is typically used to distinguish between main artifacts and attachments
326          * like {@code sources} or {@code javadoc} jars.
327          * </p>
328          *
329          * @return the classifier string (e.g., {@code "*-sources"}), or {@code null} if none
330          */
331         public String getClassifier() {
332             return null;
333         }
334 
335         /**
336          * Validates the configuration of this {@link ArtifactConsumer}.
337          * <p>
338          * This default implementation does nothing. Subclasses may override this method
339          * to enforce that required attributes (e.g., {@code refid}) or configurations are present
340          * before artifact resolution begins.
341          * </p>
342          *
343          * @throws BuildException if the consumer configuration is invalid
344          */
345         public void validate() {}
346 
347         /**
348          * Processes a resolved artifact after dependency resolution has completed.
349          * <p>
350          * This method is invoked for each artifact that has been accepted by the consumer's filter.
351          * Implementations may use this hook to copy files, register references, build paths, or store metadata.
352          * </p>
353          *
354          * @param artifact the resolved {@link Artifact}, including a file location
355          * @param session the {@link RepositorySystemSession} used during resolution, useful for repository information
356          */
357         public abstract void process(Artifact artifact, RepositorySystemSession session);
358 
359         /**
360          * Specifies the scopes of dependencies to include or exclude during resolution.
361          * <p>
362          * The input string can contain a comma- or space-separated list of scopes.
363          * Scopes prefixed with {@code -} or {@code !} will be excluded.
364          * For example, {@code compile, -test} will include only compile-scope dependencies,
365          * excluding those with scope {@code test}.
366          * </p>
367          *
368          * @param scopes a string defining scopes to include/exclude, e.g., {@code "compile, -test"}
369          * @throws BuildException if a scope filter was already set (e.g., via {@link #setClasspath(String)})
370          */
371         public void setScopes(String scopes) {
372             if (filter != null) {
373                 throw new BuildException("You must not specify both 'scopes' and 'classpath'");
374             }
375 
376             Collection<String> included = new HashSet<>();
377             Collection<String> excluded = new HashSet<>();
378 
379             String[] split = scopes.split("[, ]");
380             for (String scope : split) {
381                 scope = scope.trim();
382                 Collection<String> dst;
383                 if (scope.startsWith("-") || scope.startsWith("!")) {
384                     dst = excluded;
385                     scope = scope.substring(1);
386                 } else {
387                     dst = included;
388                 }
389                 if (!scope.isEmpty()) {
390                     dst.add(scope);
391                 }
392             }
393 
394             filter = new ScopeDependencyFilter(included, excluded);
395         }
396 
397         /**
398          * Sets a predefined classpath scope configuration using a shorthand string.
399          * <p>Accepted values are:</p>
400          * <ul>
401          *   <li>{@code compile} — includes {@code provided}, {@code system}, and {@code compile} scopes</li>
402          *   <li>{@code runtime} — includes {@code compile} and {@code runtime} scopes</li>
403          *   <li>{@code test} — includes {@code provided}, {@code system}, {@code compile}, {@code runtime}, and {@code test} scopes</li>
404          * </ul>
405          * <p>
406          * Internally, this method delegates to {@link #setScopes(String)} with an appropriate scope string.
407          * </p>
408          *
409          * @param classpath the classpath type to use ({@code compile}, {@code runtime}, or {@code test})
410          * @throws BuildException if the given classpath is not one of the allowed values
411          */
412         public void setClasspath(String classpath) {
413             if ("compile".equals(classpath)) {
414                 setScopes("provided,system,compile");
415             } else if ("runtime".equals(classpath)) {
416                 setScopes("compile,runtime");
417             } else if ("test".equals(classpath)) {
418                 setScopes("provided,system,compile,runtime,test");
419             } else {
420                 throw new BuildException("The classpath '" + classpath + "' is not defined"
421                         + ", must be one of 'compile', 'runtime' or 'test'");
422             }
423         }
424     }
425 
426     /**
427      * Artifact consumer that adds resolved artifacts to an Ant {@link org.apache.tools.ant.types.Path}.
428      * <p>
429      * This is useful for dynamically constructing classpaths from resolved Maven dependencies.
430      * Each resolved artifact is wrapped in a {@link org.apache.tools.ant.types.resources.FileResource}
431      * and added to a path registered under the specified Ant reference ID.
432      * </p>
433      *
434      * <h2>Usage Example:</h2>
435      * <pre>{@code
436      * <resolve>
437      *   <dependencies>
438      *     <dependency groupId="org.example" artifactId="lib" version="1.0"/>
439      *   </dependencies>
440      *   <path refid="my.classpath"/>
441      * </resolve>
442      * }</pre>
443      *
444      * @see org.apache.tools.ant.types.Path
445      * @see ArtifactConsumer
446      */
447     public static class Path extends ArtifactConsumer {
448 
449         private String refid;
450 
451         private org.apache.tools.ant.types.Path path;
452 
453         /**
454          * This default constructor is used by Ant to create instances of the {@link Path} consumer.
455          */
456         public Path() {
457             // Default constructor for Ant task
458         }
459 
460         /**
461          * Sets the reference ID for the Ant path to which resolved artifacts will be added.
462          *
463          * @param refId the Ant reference ID
464          */
465         public void setRefId(String refId) {
466             this.refid = refId;
467         }
468 
469         /**
470          * Validates that the required {@code refid} has been set.
471          *
472          * @throws BuildException if {@code refid} is not provided
473          */
474         @Override
475         public void validate() {
476             if (refid == null) {
477                 throw new BuildException("You must specify the 'refid' for the path");
478             }
479         }
480 
481         /**
482          * Adds the given artifact file to the configured Ant path.
483          *
484          * @param artifact the resolved artifact
485          * @param session the active repository system session
486          */
487         @Override
488         public void process(Artifact artifact, RepositorySystemSession session) {
489             if (path == null) {
490                 path = new org.apache.tools.ant.types.Path(getProject());
491                 getProject().addReference(refid, path);
492             }
493             File file = artifact.getFile();
494             path.add(new FileResource(file.getParentFile(), file.getName()));
495         }
496     }
497 
498     /**
499      * Artifact consumer that copies resolved artifacts to a local directory and optionally registers
500      * them as an Ant {@link org.apache.tools.ant.types.FileSet} or {@link org.apache.tools.ant.types.resources.Resources}.
501      * <p>
502      * This is useful for collecting artifacts into a structured directory or exporting them
503      * as a resource collection for further processing in the build.
504      * </p>
505      *
506      * <h2>Usage Examples:</h2>
507      * <pre>{@code
508      * <resolve>
509      *   <dependencies>
510      *     <dependency groupId="org.example" artifactId="lib" version="1.0"/>
511      *   </dependencies>
512      *   <files refid="resolved.files" dir="libs"/>
513      * </resolve>
514      * }</pre>
515      *
516      * @see ArtifactConsumer
517      * @see org.apache.tools.ant.types.FileSet
518      * @see org.apache.tools.ant.types.resources.Resources
519      */
520     public class Files extends ArtifactConsumer {
521 
522         private static final String DEFAULT_LAYOUT = Layout.GID_DIRS + "/" + Layout.AID + "/" + Layout.BVER + "/"
523                 + Layout.AID + "-" + Layout.VER + "-" + Layout.CLS + "." + Layout.EXT;
524 
525         private String refid;
526 
527         private String classifier;
528 
529         private File dir;
530 
531         private Layout layout;
532 
533         private FileSet fileset;
534 
535         private Resources resources;
536 
537         /**
538          * Default constructor for Ant task instantiation.
539          * <p>
540          * This constructor is used by Ant to create instances of the {@link Files} consumer.
541          * </p>
542          */
543         public Files() {
544             // Default constructor for Ant task
545         }
546 
547         /**
548          * Sets the Ant reference ID under which the collected fileset or resources will be registered.
549          *
550          * @param refId the reference ID to assign
551          */
552         public void setRefId(String refId) {
553             this.refid = refId;
554         }
555 
556         /**
557          * Returns the classifier pattern used to match specific artifact attachments, such as sources or javadoc.
558          *
559          * @return the classifier pattern, or {@code null} if not set
560          */
561         @Override
562         public String getClassifier() {
563             return classifier;
564         }
565 
566         /**
567          * Specifies which type of attachment to resolve. Valid values are:
568          * <ul>
569          *   <li>{@code sources}</li>
570          *   <li>{@code javadoc}</li>
571          * </ul>
572          * Internally, this sets a classifier pattern for filtering resolved artifacts.
573          *
574          * @param attachments the attachment type
575          * @throws BuildException if an invalid type is provided
576          */
577         public void setAttachments(String attachments) {
578             if ("sources".equals(attachments)) {
579                 classifier = "*-sources";
580             } else if ("javadoc".equals(attachments)) {
581                 classifier = "*-javadoc";
582             } else {
583                 throw new BuildException("The attachment type '" + attachments
584                         + "' is not defined, must be one of 'sources' or 'javadoc'");
585             }
586         }
587 
588         /**
589          * Sets the output directory to which resolved artifacts will be copied.
590          * If this is specified without an explicit layout, a default layout will be used.
591          *
592          * @param dir the destination directory
593          */
594         public void setDir(File dir) {
595             this.dir = dir;
596             if (dir != null && layout == null) {
597                 layout = new Layout(DEFAULT_LAYOUT);
598             }
599         }
600 
601         /**
602          * Sets the layout template used to determine the relative path of each artifact
603          * when copying files to the target directory.
604          * <p>
605          * The layout is a string pattern using variables such as {@code ${gid}}, {@code ${aid}},
606          * {@code ${ver}}, {@code ${cls}}, and {@code ${ext}} to define where artifacts
607          * should be placed under the specified {@code dir}.
608          * </p>
609          * <p>
610          * This method is only meaningful if a {@code dir} is specified. If used without a directory,
611          * it will result in a {@link org.apache.tools.ant.BuildException} during validation.
612          * </p>
613          *
614          * @param layout the path layout pattern to apply for copied artifacts
615          *
616          * @see #setDir(File)
617          */
618         public void setLayout(String layout) {
619             this.layout = new Layout(layout);
620         }
621 
622         /**
623          * Validates that either a destination directory or a reference ID is set.
624          *
625          * @throws BuildException if the configuration is invalid
626          */
627         @Override
628         public void validate() {
629             if (refid == null && dir == null) {
630                 throw new BuildException("You must either specify the 'refid' for the resource collection"
631                         + " or a 'dir' to copy the files to");
632             }
633             if (dir == null && layout != null) {
634                 throw new BuildException("You must not specify a 'layout' unless 'dir' is also specified");
635             }
636         }
637 
638         /**
639          * Processes a resolved artifact by copying it to the destination directory or
640          * registering it as a resource in the Ant project.
641          *
642          * @param artifact the resolved artifact
643          * @param session  the current repository session
644          */
645         @Override
646         public void process(Artifact artifact, RepositorySystemSession session) {
647             if (dir != null) {
648                 if (refid != null && fileset == null) {
649                     fileset = new FileSet();
650                     fileset.setProject(getProject());
651                     fileset.setDir(dir);
652                     getProject().addReference(refid, fileset);
653                 }
654 
655                 String path = layout.getPath(artifact);
656 
657                 if (fileset != null) {
658                     fileset.createInclude().setName(path);
659                 }
660 
661                 File src = artifact.getFile();
662                 File dst = new File(dir, path);
663 
664                 if (src.lastModified() != dst.lastModified() || src.length() != dst.length()) {
665                     try {
666                         Resolve.this.log("Copy " + src + " to " + dst, Project.MSG_VERBOSE);
667                         FileUtils.getFileUtils().copyFile(src, dst, null, true, true);
668                     } catch (IOException e) {
669                         throw new BuildException(
670                                 "Failed to copy artifact file " + src + " to " + dst + ": " + e.getMessage(), e);
671                     }
672                 } else {
673                     Resolve.this.log("Omit to copy " + src + " to " + dst + ", seems unchanged", Project.MSG_VERBOSE);
674                 }
675             } else {
676                 if (resources == null) {
677                     resources = new Resources();
678                     resources.setProject(getProject());
679                     getProject().addReference(refid, resources);
680                 }
681 
682                 FileResource resource = new FileResource(artifact.getFile());
683                 resource.setBaseDir(session.getLocalRepository().getBasedir());
684                 resource.setProject(getProject());
685                 resources.add(resource);
686             }
687         }
688     }
689 
690     /**
691      * Artifact consumer that maps resolved artifacts to Ant project properties.
692      * <p>
693      * Each resolved artifact is converted to a property key-value pair, where the key is
694      * based on the artifact coordinates and an optional prefix, and the value is the absolute
695      * path to the resolved artifact file.
696      * </p>
697      *
698      * <p>
699      * This is useful for referencing artifacts in other tasks or scripts, especially when
700      * integration with tools that expect file paths as properties is needed.
701      * </p>
702      *
703      * <p>
704      * Example output property format:
705      * {@code prefix.groupId:artifactId:extension[:classifier] = /path/to/artifact.jar}
706      * </p>
707      *
708      * <p>Example usage:</p>
709      * <pre>{@code
710      * <resolve>
711      *   <dependencies>
712      *     <dependency groupId="org.example" artifactId="lib" version="1.0"/>
713      *   </dependencies>
714      *   <properties prefix="mydeps"/>
715      * </resolve>
716      * }</pre>
717      *
718      * @see ArtifactConsumer
719      */
720     public static class Props extends ArtifactConsumer {
721 
722         private String prefix;
723 
724         private String classifier;
725 
726         /**
727          * This default constructor is used by Ant to create instances of the {@link Props} consumer.
728          */
729         public Props() {
730             // Default constructor for Ant task
731         }
732 
733         /**
734          * Sets a prefix for the generated Ant property keys.
735          * For example, if prefix is {@code resolved}, properties will be named like:
736          * {@code resolved.groupId:artifactId:type[:classifier]}.
737          *
738          * @param prefix the property key prefix
739          */
740         public void setPrefix(String prefix) {
741             this.prefix = prefix;
742         }
743 
744         /**
745          * Returns the classifier pattern used to match specific artifact attachments.
746          *
747          * @return the classifier pattern, or {@code null} if not set
748          */
749         @Override
750         public String getClassifier() {
751             return classifier;
752         }
753 
754         /**
755          * Specifies which type of attachment to resolve. Valid values are:
756          * <ul>
757          *   <li>{@code sources}</li>
758          *   <li>{@code javadoc}</li>
759          * </ul>
760          * Internally, this sets a classifier pattern for filtering resolved artifacts.
761          *
762          * @param attachments the attachment type
763          * @throws BuildException if an invalid type is provided
764          */
765         public void setAttachments(String attachments) {
766             if ("sources".equals(attachments)) {
767                 classifier = "*-sources";
768             } else if ("javadoc".equals(attachments)) {
769                 classifier = "*-javadoc";
770             } else {
771                 throw new BuildException("The attachment type '" + attachments
772                         + "' is not defined, must be one of 'sources' or 'javadoc'");
773             }
774         }
775 
776         /**
777          * Processes a resolved artifact by registering its absolute path as an Ant property.
778          * The property name is derived from the artifact coordinates and the optional prefix.
779          *
780          * @param artifact the resolved artifact
781          * @param session  the current repository session
782          */
783         @Override
784         public void process(Artifact artifact, RepositorySystemSession session) {
785             StringBuilder buffer = new StringBuilder(256);
786             if (prefix != null && !prefix.isEmpty()) {
787                 buffer.append(prefix);
788                 if (!prefix.endsWith(".")) {
789                     buffer.append('.');
790                 }
791             }
792             buffer.append(artifact.getGroupId());
793             buffer.append(':');
794             buffer.append(artifact.getArtifactId());
795             buffer.append(':');
796             buffer.append(artifact.getExtension());
797             if (!artifact.getClassifier().isEmpty()) {
798                 buffer.append(':');
799                 buffer.append(artifact.getClassifier());
800             }
801 
802             String path = artifact.getFile().getAbsolutePath();
803 
804             getProject().setProperty(buffer.toString(), path);
805         }
806     }
807 
808     private static class Group {
809 
810         private final String classifier;
811 
812         private final List<ArtifactConsumer> consumers = new ArrayList<>();
813 
814         private final List<ArtifactRequest> requests = new ArrayList<>();
815 
816         Group(String classifier) {
817             this.classifier = classifier;
818         }
819 
820         public boolean isAttachments() {
821             return classifier != null;
822         }
823 
824         public void add(ArtifactConsumer consumer) {
825             consumers.add(consumer);
826         }
827 
828         public void createRequests(DependencyNode node) {
829             createRequests(node, new LinkedList<>());
830         }
831 
832         private void createRequests(DependencyNode node, LinkedList<DependencyNode> parents) {
833             if (node.getDependency() != null) {
834                 for (ArtifactConsumer consumer : consumers) {
835                     if (consumer.accept(node, parents)) {
836                         ArtifactRequest request = new ArtifactRequest(node);
837                         if (classifier != null) {
838                             request.setArtifact(new SubArtifact(request.getArtifact(), classifier, "jar"));
839                         }
840                         requests.add(request);
841                         break;
842                     }
843                 }
844             }
845 
846             parents.addFirst(node);
847 
848             for (DependencyNode child : node.getChildren()) {
849                 createRequests(child, parents);
850             }
851 
852             parents.removeFirst();
853         }
854 
855         public List<ArtifactRequest> getRequests() {
856             return requests;
857         }
858 
859         public void processResults(List<ArtifactResult> results, RepositorySystemSession session) {
860             for (ArtifactResult result : results) {
861                 if (!result.isResolved()) {
862                     continue;
863                 }
864                 for (ArtifactConsumer consumer : consumers) {
865                     if (consumer.accept(
866                             result.getRequest().getDependencyNode(), Collections.<DependencyNode>emptyList())) {
867                         consumer.process(result.getArtifact(), session);
868                     }
869                 }
870             }
871         }
872     }
873 }