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.eclipse.aether.connector.basic;
20  
21  import java.io.IOException;
22  import java.io.UncheckedIOException;
23  import java.net.URI;
24  import java.nio.file.Path;
25  import java.util.ArrayList;
26  import java.util.Collection;
27  import java.util.Collections;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.concurrent.ConcurrentHashMap;
31  import java.util.concurrent.atomic.AtomicBoolean;
32  
33  import org.eclipse.aether.RepositorySystemSession;
34  import org.eclipse.aether.metadata.Metadata;
35  import org.eclipse.aether.repository.RemoteRepository;
36  import org.eclipse.aether.spi.checksums.ProvidedChecksumsSource;
37  import org.eclipse.aether.spi.connector.ArtifactDownload;
38  import org.eclipse.aether.spi.connector.ArtifactTransfer;
39  import org.eclipse.aether.spi.connector.ArtifactUpload;
40  import org.eclipse.aether.spi.connector.MetadataDownload;
41  import org.eclipse.aether.spi.connector.MetadataTransfer;
42  import org.eclipse.aether.spi.connector.MetadataUpload;
43  import org.eclipse.aether.spi.connector.RepositoryConnector;
44  import org.eclipse.aether.spi.connector.Transfer;
45  import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
46  import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmHelper;
47  import org.eclipse.aether.spi.connector.checksum.ChecksumPolicy;
48  import org.eclipse.aether.spi.connector.checksum.ChecksumPolicyProvider;
49  import org.eclipse.aether.spi.connector.layout.RepositoryLayout;
50  import org.eclipse.aether.spi.connector.layout.RepositoryLayoutProvider;
51  import org.eclipse.aether.spi.connector.transport.GetTask;
52  import org.eclipse.aether.spi.connector.transport.PeekTask;
53  import org.eclipse.aether.spi.connector.transport.PutTask;
54  import org.eclipse.aether.spi.connector.transport.Transporter;
55  import org.eclipse.aether.spi.connector.transport.TransporterProvider;
56  import org.eclipse.aether.spi.io.ChecksumProcessor;
57  import org.eclipse.aether.spi.io.PathProcessor;
58  import org.eclipse.aether.transfer.ChecksumFailureException;
59  import org.eclipse.aether.transfer.NoRepositoryConnectorException;
60  import org.eclipse.aether.transfer.NoRepositoryLayoutException;
61  import org.eclipse.aether.transfer.TransferEvent;
62  import org.eclipse.aether.transfer.TransferResource;
63  import org.eclipse.aether.util.ConfigUtils;
64  import org.eclipse.aether.util.concurrency.RunnableErrorForwarder;
65  import org.eclipse.aether.util.concurrency.SmartExecutor;
66  import org.eclipse.aether.util.concurrency.SmartExecutorUtils;
67  import org.slf4j.Logger;
68  import org.slf4j.LoggerFactory;
69  
70  import static java.util.Objects.requireNonNull;
71  import static org.eclipse.aether.connector.basic.BasicRepositoryConnectorConfigurationKeys.CONFIG_PROP_DOWNSTREAM_THREADS;
72  import static org.eclipse.aether.connector.basic.BasicRepositoryConnectorConfigurationKeys.CONFIG_PROP_FAIL_ON_CHECKSUM_UPLOAD_FAILURE;
73  import static org.eclipse.aether.connector.basic.BasicRepositoryConnectorConfigurationKeys.CONFIG_PROP_INCLUDED_CHECKSUMS;
74  import static org.eclipse.aether.connector.basic.BasicRepositoryConnectorConfigurationKeys.CONFIG_PROP_PARALLEL_PUT;
75  import static org.eclipse.aether.connector.basic.BasicRepositoryConnectorConfigurationKeys.CONFIG_PROP_PERSISTED_CHECKSUMS;
76  import static org.eclipse.aether.connector.basic.BasicRepositoryConnectorConfigurationKeys.CONFIG_PROP_THREADS;
77  import static org.eclipse.aether.connector.basic.BasicRepositoryConnectorConfigurationKeys.CONFIG_PROP_UPSTREAM_THREADS;
78  import static org.eclipse.aether.connector.basic.BasicRepositoryConnectorConfigurationKeys.DEFAULT_FAIL_ON_CHECKSUM_UPLOAD_FAILURE;
79  import static org.eclipse.aether.connector.basic.BasicRepositoryConnectorConfigurationKeys.DEFAULT_INCLUDED_CHECKSUMS;
80  import static org.eclipse.aether.connector.basic.BasicRepositoryConnectorConfigurationKeys.DEFAULT_PARALLEL_PUT;
81  import static org.eclipse.aether.connector.basic.BasicRepositoryConnectorConfigurationKeys.DEFAULT_PERSISTED_CHECKSUMS;
82  import static org.eclipse.aether.connector.basic.BasicRepositoryConnectorConfigurationKeys.DEFAULT_THREADS;
83  
84  /**
85   *
86   */
87  final class BasicRepositoryConnector implements RepositoryConnector {
88      private static final Logger LOGGER = LoggerFactory.getLogger(BasicRepositoryConnector.class);
89  
90      private final Map<String, ProvidedChecksumsSource> providedChecksumsSources;
91  
92      private final PathProcessor pathProcessor;
93  
94      private final ChecksumProcessor checksumProcessor;
95  
96      private final RemoteRepository repository;
97  
98      private final RepositorySystemSession session;
99  
100     private final Transporter transporter;
101 
102     private final RepositoryLayout layout;
103 
104     private final ChecksumPolicyProvider checksumPolicyProvider;
105 
106     private final int maxDownstreamThreads;
107 
108     private final int maxUpstreamThreads;
109 
110     private final boolean includedChecksums;
111 
112     private final boolean parallelPut;
113 
114     private final boolean persistedChecksums;
115 
116     private final boolean failOnChecksumUploadFailure;
117 
118     private final ConcurrentHashMap<Boolean, SmartExecutor> executors;
119 
120     private final AtomicBoolean closed;
121 
122     @SuppressWarnings("checkstyle:parameternumber")
123     BasicRepositoryConnector(
124             RepositorySystemSession session,
125             RemoteRepository repository,
126             TransporterProvider transporterProvider,
127             RepositoryLayoutProvider layoutProvider,
128             ChecksumPolicyProvider checksumPolicyProvider,
129             PathProcessor pathProcessor,
130             ChecksumProcessor checksumProcessor,
131             Map<String, ProvidedChecksumsSource> providedChecksumsSources)
132             throws NoRepositoryConnectorException {
133         try {
134             layout = layoutProvider.newRepositoryLayout(session, repository);
135         } catch (NoRepositoryLayoutException e) {
136             throw new NoRepositoryConnectorException(repository, e.getMessage(), e);
137         }
138         try {
139             transporter = transporterProvider.newTransporter(session, repository);
140         } catch (RuntimeException e) {
141             throw new NoRepositoryConnectorException(
142                     repository, "Transporter configuration issue: " + e.getMessage(), e);
143         } catch (Exception e) {
144             throw new NoRepositoryConnectorException(repository, e.getMessage(), e);
145         }
146         this.checksumPolicyProvider = checksumPolicyProvider;
147 
148         this.session = session;
149         this.repository = repository;
150         this.checksumProcessor = checksumProcessor;
151         this.pathProcessor = pathProcessor;
152         this.providedChecksumsSources = providedChecksumsSources;
153         this.executors = new ConcurrentHashMap<>();
154         this.closed = new AtomicBoolean(false);
155 
156         maxUpstreamThreads = ConfigUtils.getInteger(
157                 session,
158                 DEFAULT_THREADS,
159                 CONFIG_PROP_UPSTREAM_THREADS + "." + repository.getId(),
160                 CONFIG_PROP_UPSTREAM_THREADS,
161                 CONFIG_PROP_THREADS);
162         maxDownstreamThreads = ConfigUtils.getInteger(
163                 session,
164                 DEFAULT_THREADS,
165                 CONFIG_PROP_DOWNSTREAM_THREADS + "." + repository.getId(),
166                 CONFIG_PROP_DOWNSTREAM_THREADS,
167                 CONFIG_PROP_THREADS);
168         includedChecksums = ConfigUtils.getBoolean(
169                 session, DEFAULT_INCLUDED_CHECKSUMS, CONFIG_PROP_INCLUDED_CHECKSUMS, "aether.connector.smartChecksums");
170         parallelPut = ConfigUtils.getBoolean(
171                 session,
172                 DEFAULT_PARALLEL_PUT,
173                 CONFIG_PROP_PARALLEL_PUT + "." + repository.getId(),
174                 CONFIG_PROP_PARALLEL_PUT);
175         persistedChecksums =
176                 ConfigUtils.getBoolean(session, DEFAULT_PERSISTED_CHECKSUMS, CONFIG_PROP_PERSISTED_CHECKSUMS);
177         failOnChecksumUploadFailure = ConfigUtils.getBoolean(
178                 session,
179                 DEFAULT_FAIL_ON_CHECKSUM_UPLOAD_FAILURE,
180                 CONFIG_PROP_FAIL_ON_CHECKSUM_UPLOAD_FAILURE + "." + repository.getId(),
181                 CONFIG_PROP_FAIL_ON_CHECKSUM_UPLOAD_FAILURE);
182     }
183 
184     /**
185      * Returns {@link SmartExecutor} to execute tasks with or {@code null} if "direct execution" is more appropriate.
186      */
187     private SmartExecutor getExecutor(boolean downstream, int tasks) {
188         int maxThreads = downstream ? maxDownstreamThreads : maxUpstreamThreads;
189         if (maxThreads <= 1 || tasks <= 1) {
190             // direct and do not cache it
191             return null;
192         }
193         // we intentionally ignore tasks here as deployer may invoke several times connector with different count of
194         // payloads; so maximize it
195         return executors.computeIfAbsent(
196                 downstream,
197                 k -> SmartExecutorUtils.smartExecutor(
198                         session, null, maxThreads, getClass().getSimpleName() + '-' + repository.getHost() + '-'));
199     }
200 
201     @Override
202     public void close() {
203         if (closed.compareAndSet(false, true)) {
204             for (SmartExecutor executor : executors.values()) {
205                 executor.close();
206             }
207             transporter.close();
208         }
209     }
210 
211     private void failIfClosed() {
212         if (closed.get()) {
213             throw new IllegalStateException("connector already closed");
214         }
215     }
216 
217     @Override
218     public void get(
219             Collection<? extends ArtifactDownload> artifactDownloads,
220             Collection<? extends MetadataDownload> metadataDownloads) {
221         failIfClosed();
222 
223         Collection<? extends ArtifactDownload> safeArtifactDownloads = safe(artifactDownloads);
224         Collection<? extends MetadataDownload> safeMetadataDownloads = safe(metadataDownloads);
225 
226         SmartExecutor executor = getExecutor(true, safeArtifactDownloads.size() + safeMetadataDownloads.size());
227         RunnableErrorForwarder errorForwarder = new RunnableErrorForwarder();
228         List<ChecksumAlgorithmFactory> checksumAlgorithmFactories = layout.getChecksumAlgorithmFactories(false);
229 
230         boolean first = true;
231 
232         for (MetadataDownload transfer : safeMetadataDownloads) {
233             Map<String, String> providedChecksums = Collections.emptyMap();
234             for (ProvidedChecksumsSource providedChecksumsSource : providedChecksumsSources.values()) {
235                 Map<String, String> provided = providedChecksumsSource.getProvidedMetadataChecksums(
236                         session, transfer, repository, checksumAlgorithmFactories);
237 
238                 if (provided != null) {
239                     providedChecksums = provided;
240                     break;
241                 }
242             }
243 
244             URI location = layout.getLocation(transfer.getMetadata(), false);
245 
246             TransferResource resource = newTransferResource(location, transfer);
247             TransferEvent.Builder builder = newEventBuilder(resource, false, false);
248             MetadataTransportListener listener = new MetadataTransportListener(transfer, repository, builder);
249 
250             ChecksumPolicy checksumPolicy = newChecksumPolicy(transfer.getChecksumPolicy(), resource);
251             List<RepositoryLayout.ChecksumLocation> checksumLocations = null;
252             if (checksumPolicy != null) {
253                 checksumLocations = layout.getChecksumLocations(transfer.getMetadata(), false, location);
254             }
255 
256             Runnable task = new GetTaskRunner(
257                     location,
258                     transfer.getPath(),
259                     checksumPolicy,
260                     checksumAlgorithmFactories,
261                     checksumLocations,
262                     providedChecksums,
263                     listener);
264             if (executor == null || first) {
265                 task.run();
266                 first = false;
267             } else {
268                 executor.submit(errorForwarder.wrap(task));
269             }
270         }
271 
272         for (ArtifactDownload transfer : safeArtifactDownloads) {
273             Map<String, String> providedChecksums = Collections.emptyMap();
274             for (ProvidedChecksumsSource providedChecksumsSource : providedChecksumsSources.values()) {
275                 Map<String, String> provided = providedChecksumsSource.getProvidedArtifactChecksums(
276                         session, transfer, repository, checksumAlgorithmFactories);
277 
278                 if (provided != null) {
279                     providedChecksums = provided;
280                     break;
281                 }
282             }
283 
284             URI location = layout.getLocation(transfer.getArtifact(), false);
285 
286             TransferResource resource = newTransferResource(location, transfer);
287             TransferEvent.Builder builder = newEventBuilder(resource, false, transfer.isExistenceCheck());
288             ArtifactTransportListener listener = new ArtifactTransportListener(transfer, repository, builder);
289 
290             Runnable task;
291             if (transfer.isExistenceCheck()) {
292                 task = new PeekTaskRunner(location, listener);
293             } else {
294                 ChecksumPolicy checksumPolicy = newChecksumPolicy(transfer.getChecksumPolicy(), resource);
295                 List<RepositoryLayout.ChecksumLocation> checksumLocations = null;
296                 if (checksumPolicy != null) {
297                     checksumLocations = layout.getChecksumLocations(transfer.getArtifact(), false, location);
298                 }
299 
300                 task = new GetTaskRunner(
301                         location,
302                         transfer.getPath(),
303                         checksumPolicy,
304                         checksumAlgorithmFactories,
305                         checksumLocations,
306                         providedChecksums,
307                         listener);
308             }
309             if (executor == null || first) {
310                 task.run();
311                 first = false;
312             } else {
313                 executor.submit(errorForwarder.wrap(task));
314             }
315         }
316 
317         errorForwarder.await();
318     }
319 
320     @Override
321     public void put(
322             Collection<? extends ArtifactUpload> artifactUploads,
323             Collection<? extends MetadataUpload> metadataUploads) {
324         failIfClosed();
325 
326         Collection<? extends ArtifactUpload> safeArtifactUploads = safe(artifactUploads);
327         Collection<? extends MetadataUpload> safeMetadataUploads = safe(metadataUploads);
328 
329         SmartExecutor executor =
330                 getExecutor(false, parallelPut ? safeArtifactUploads.size() + safeMetadataUploads.size() : 1);
331         RunnableErrorForwarder errorForwarder = new RunnableErrorForwarder();
332 
333         boolean first = true;
334 
335         for (ArtifactUpload transfer : safeArtifactUploads) {
336             URI location = layout.getLocation(transfer.getArtifact(), true);
337 
338             TransferResource resource = newTransferResource(location, transfer);
339             TransferEvent.Builder builder = newEventBuilder(resource, true, false);
340             ArtifactTransportListener listener = new ArtifactTransportListener(transfer, repository, builder);
341 
342             List<RepositoryLayout.ChecksumLocation> checksumLocations =
343                     layout.getChecksumLocations(transfer.getArtifact(), true, location);
344 
345             Runnable task = new PutTaskRunner(location, transfer.getPath(), checksumLocations, listener);
346             if (executor == null || first) {
347                 task.run();
348                 first = false;
349             } else {
350                 executor.submit(errorForwarder.wrap(task));
351             }
352         }
353 
354         errorForwarder.await(); // make sure all artifacts are PUT before we go with Metadata
355 
356         for (List<? extends MetadataUpload> transferGroup : groupUploads(safeMetadataUploads)) {
357             for (MetadataUpload transfer : transferGroup) {
358                 URI location = layout.getLocation(transfer.getMetadata(), true);
359 
360                 TransferResource resource = newTransferResource(location, transfer);
361                 TransferEvent.Builder builder = newEventBuilder(resource, true, false);
362                 MetadataTransportListener listener = new MetadataTransportListener(transfer, repository, builder);
363 
364                 List<RepositoryLayout.ChecksumLocation> checksumLocations =
365                         layout.getChecksumLocations(transfer.getMetadata(), true, location);
366 
367                 Runnable task = new PutTaskRunner(location, transfer.getPath(), checksumLocations, listener);
368                 if (executor == null || first) {
369                     task.run();
370                     first = false;
371                 } else {
372                     executor.submit(errorForwarder.wrap(task));
373                 }
374             }
375 
376             errorForwarder.await(); // make sure each group is done before starting next group
377         }
378     }
379 
380     /**
381      * This method "groups" the Metadata to be uploaded by their level (version, artifact, group and root). This is MUST
382      * as clients consume metadata in opposite order (root, group, artifact, version), and hence, we must deploy and
383      * ensure (in case of parallel deploy) that all V level metadata is deployed before we start deploying A level, etc.
384      */
385     private static List<List<MetadataUpload>> groupUploads(Collection<? extends MetadataUpload> metadataUploads) {
386         ArrayList<MetadataUpload> v = new ArrayList<>();
387         ArrayList<MetadataUpload> a = new ArrayList<>();
388         ArrayList<MetadataUpload> g = new ArrayList<>();
389         ArrayList<MetadataUpload> r = new ArrayList<>();
390 
391         for (MetadataUpload transfer : metadataUploads) {
392             Metadata metadata = transfer.getMetadata();
393             if (!"".equals(metadata.getVersion())) {
394                 v.add(transfer);
395             } else if (!"".equals(metadata.getArtifactId())) {
396                 a.add(transfer);
397             } else if (!"".equals(metadata.getGroupId())) {
398                 g.add(transfer);
399             } else {
400                 r.add(transfer);
401             }
402         }
403 
404         List<List<MetadataUpload>> result = new ArrayList<>(4);
405         if (!v.isEmpty()) {
406             result.add(v);
407         }
408         if (!a.isEmpty()) {
409             result.add(a);
410         }
411         if (!g.isEmpty()) {
412             result.add(g);
413         }
414         if (!r.isEmpty()) {
415             result.add(r);
416         }
417         return result;
418     }
419 
420     private static <T> Collection<T> safe(Collection<T> items) {
421         return (items != null) ? items : Collections.emptyList();
422     }
423 
424     private TransferResource newTransferResource(URI path, Transfer transfer) {
425         if (transfer instanceof ArtifactTransfer) {
426             ArtifactTransfer artifactTransfer = (ArtifactTransfer) transfer;
427             return new TransferResource(
428                     repository.getId(),
429                     repository.getUrl(),
430                     path.toString(),
431                     artifactTransfer.getPath(),
432                     artifactTransfer.getArtifact(),
433                     artifactTransfer.getTrace());
434         } else if (transfer instanceof MetadataTransfer) {
435             MetadataTransfer metadataTransfer = (MetadataTransfer) transfer;
436             return new TransferResource(
437                     repository.getId(),
438                     repository.getUrl(),
439                     path.toString(),
440                     metadataTransfer.getPath(),
441                     metadataTransfer.getMetadata(),
442                     metadataTransfer.getTrace());
443         } else {
444             throw new IllegalArgumentException("Accepting only artifact or metadata transfers");
445         }
446     }
447 
448     private TransferEvent.Builder newEventBuilder(TransferResource resource, boolean upload, boolean peek) {
449         TransferEvent.Builder builder = new TransferEvent.Builder(session, resource);
450         if (upload) {
451             builder.setRequestType(TransferEvent.RequestType.PUT);
452         } else if (!peek) {
453             builder.setRequestType(TransferEvent.RequestType.GET);
454         } else {
455             builder.setRequestType(TransferEvent.RequestType.GET_EXISTENCE);
456         }
457         return builder;
458     }
459 
460     private ChecksumPolicy newChecksumPolicy(String policy, TransferResource resource) {
461         return checksumPolicyProvider.newChecksumPolicy(session, repository, resource, policy);
462     }
463 
464     @Override
465     public String toString() {
466         return BasicRepositoryConnectorFactory.NAME + "( " + repository + " )";
467     }
468 
469     abstract class TaskRunner implements Runnable {
470 
471         protected final URI path;
472 
473         protected final TransferTransportListener<?> listener;
474 
475         TaskRunner(URI path, TransferTransportListener<?> listener) {
476             this.path = path;
477             this.listener = listener;
478         }
479 
480         @Override
481         public void run() {
482             try {
483                 listener.transferInitiated();
484                 runTask();
485                 listener.transferSucceeded();
486             } catch (Exception e) {
487                 listener.transferFailed(e, transporter.classify(e));
488             }
489         }
490 
491         protected abstract void runTask() throws Exception;
492     }
493 
494     class PeekTaskRunner extends TaskRunner {
495 
496         PeekTaskRunner(URI path, TransferTransportListener<?> listener) {
497             super(path, listener);
498         }
499 
500         @Override
501         protected void runTask() throws Exception {
502             transporter.peek(new PeekTask(path));
503         }
504     }
505 
506     class GetTaskRunner extends TaskRunner implements ChecksumValidator.ChecksumFetcher {
507 
508         private final Path file;
509 
510         private final ChecksumValidator checksumValidator;
511 
512         GetTaskRunner(
513                 URI path,
514                 Path file,
515                 ChecksumPolicy checksumPolicy,
516                 List<ChecksumAlgorithmFactory> checksumAlgorithmFactories,
517                 List<RepositoryLayout.ChecksumLocation> checksumLocations,
518                 Map<String, String> providedChecksums,
519                 TransferTransportListener<?> listener) {
520             super(path, listener);
521             this.file = requireNonNull(file, "destination file cannot be null");
522             checksumValidator = new ChecksumValidator(
523                     file,
524                     checksumAlgorithmFactories,
525                     pathProcessor,
526                     checksumProcessor,
527                     this,
528                     checksumPolicy,
529                     providedChecksums,
530                     safe(checksumLocations));
531         }
532 
533         @Override
534         public boolean fetchChecksum(URI remote, Path local) throws Exception {
535             try {
536                 transporter.get(new GetTask(remote).setDataPath(local));
537             } catch (Exception e) {
538                 if (transporter.classify(e) == Transporter.ERROR_NOT_FOUND) {
539                     return false;
540                 }
541                 throw e;
542             }
543             return true;
544         }
545 
546         @Override
547         protected void runTask() throws Exception {
548             try (PathProcessor.CollocatedTempFile tempFile = pathProcessor.newTempFile(file)) {
549                 final Path tmp = tempFile.getPath();
550                 listener.setChecksumCalculator(checksumValidator.newChecksumCalculator(tmp));
551                 for (int firstTrial = 0, lastTrial = 1, trial = firstTrial; ; trial++) {
552                     GetTask task = new GetTask(path).setDataPath(tmp, false).setListener(listener);
553                     transporter.get(task);
554                     try {
555                         checksumValidator.validate(
556                                 listener.getChecksums(), includedChecksums ? task.getChecksums() : null);
557                         break;
558                     } catch (ChecksumFailureException e) {
559                         boolean retry = trial < lastTrial && e.isRetryWorthy();
560                         if (!retry && !checksumValidator.handle(e)) {
561                             throw e;
562                         }
563                         listener.transferCorrupted(e);
564                         if (retry) {
565                             checksumValidator.retry();
566                         } else {
567                             break;
568                         }
569                     }
570                 }
571                 tempFile.move();
572                 if (persistedChecksums) {
573                     checksumValidator.commit();
574                 }
575             }
576         }
577     }
578 
579     class PutTaskRunner extends TaskRunner {
580 
581         private final Path file;
582 
583         private final Collection<RepositoryLayout.ChecksumLocation> checksumLocations;
584 
585         PutTaskRunner(
586                 URI path,
587                 Path file,
588                 List<RepositoryLayout.ChecksumLocation> checksumLocations,
589                 TransferTransportListener<?> listener) {
590             super(path, listener);
591             this.file = requireNonNull(file, "source file cannot be null");
592             this.checksumLocations = safe(checksumLocations);
593         }
594 
595         @SuppressWarnings("checkstyle:innerassignment")
596         @Override
597         protected void runTask() throws Exception {
598             transporter.put(new PutTask(path).setDataPath(file).setListener(listener));
599             uploadChecksums(file, null);
600         }
601 
602         /**
603          * @param path  source
604          * @param bytes transformed data from file or {@code null}
605          */
606         private void uploadChecksums(Path path, byte[] bytes) throws Exception {
607             if (checksumLocations.isEmpty()) {
608                 return;
609             }
610             Map<String, String> sumsByAlgo;
611             try {
612                 ArrayList<ChecksumAlgorithmFactory> algorithms = new ArrayList<>();
613                 for (RepositoryLayout.ChecksumLocation checksumLocation : checksumLocations) {
614                     algorithms.add(checksumLocation.getChecksumAlgorithmFactory());
615                 }
616 
617                 if (bytes != null) {
618                     sumsByAlgo = ChecksumAlgorithmHelper.calculate(bytes, algorithms);
619                 } else {
620                     sumsByAlgo = ChecksumAlgorithmHelper.calculate(path, algorithms);
621                 }
622             } catch (IOException e) {
623                 LOGGER.warn("Failed to upload checksums for {}", file, e);
624                 throw new UncheckedIOException(e);
625             }
626 
627             for (RepositoryLayout.ChecksumLocation checksumLocation : checksumLocations) {
628                 uploadChecksum(
629                         checksumLocation.getLocation(),
630                         sumsByAlgo.get(
631                                 checksumLocation.getChecksumAlgorithmFactory().getName()));
632             }
633         }
634 
635         private void uploadChecksum(URI location, Object checksum) throws Exception {
636             try {
637                 if (checksum instanceof Exception) {
638                     throw (Exception) checksum;
639                 }
640                 transporter.put(new PutTask(location).setDataString((String) checksum));
641             } catch (Exception e) {
642                 if (failOnChecksumUploadFailure) {
643                     // Fail-closed (default): a deploy must be atomic across artifact bytes and integrity
644                     // metadata. Swallowing this failure would report a successful deploy that published the
645                     // artifact without checksums for consumers to verify.
646                     LOGGER.error("Failed to upload checksum to {}", location, e);
647                     throw e;
648                 }
649                 LOGGER.warn("Failed to upload checksum to {}", location, e);
650             }
651         }
652     }
653 }