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 }