Skip to content

Commit e995ba6

Browse files
authored
[MNG-8150] Handle absent source/target files in transfer listener (#1575)
The PR address two issues observed in the `SimplexTransferListener` and `ConsoleMavenTransferListener`: 1. [TransferResource#getFile()](https://github.com/apache/maven-resolver/blob/master/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/TransferResource.java#L170) can be null. The current `SimplexTransferListener` will break with an NPE if the `file` is not set on the resource. 2. `TransferResource` is not immutable and does not implement `equals` or `hashCode,` making its usage in collections brittle. Listener consumers are not guaranteed to reuse the same instance across listener invocations. I suggest wrapping it in an immutable identifier. Resolves https://issues.apache.org/jira/browse/MNG-8150 - [x] I hereby declare this contribution to be licenced under the [Apache License Version 2.0, January 2004](http://www.apache.org/licenses/LICENSE-2.0) --- https://issues.apache.org/jira/browse/MNG-8150
1 parent 2a43242 commit e995ba6

File tree

4 files changed

+96
-29
lines changed

4 files changed

+96
-29
lines changed

maven-embedder/src/main/java/org/apache/maven/cli/transfer/ConsoleMavenTransferListener.java

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,16 @@
3131

3232
/**
3333
* Console download progress meter.
34-
*
34+
* <p>
35+
* This listener is not thread-safe and should be wrapped in the {@link SimplexTransferListener} in a multi-threaded scenario.
3536
*/
3637
public class ConsoleMavenTransferListener extends AbstractMavenTransferListener {
3738

38-
private Map<TransferResource, Long> transfers = new LinkedHashMap<>();
39-
private FileSizeFormat format = new FileSizeFormat(Locale.ENGLISH); // use in a synchronized fashion
40-
private StringBuilder buffer = new StringBuilder(128); // use in a synchronized fashion
39+
private final Map<TransferResourceIdentifier, TransferResourceAndSize> transfers = new LinkedHashMap<>();
40+
private final FileSizeFormat format = new FileSizeFormat(Locale.ENGLISH); // use in a synchronized fashion
41+
private final StringBuilder buffer = new StringBuilder(128); // use in a synchronized fashion
4142

42-
private boolean printResourceNames;
43+
private final boolean printResourceNames;
4344
private int lastLength;
4445

4546
public ConsoleMavenTransferListener(
@@ -65,18 +66,19 @@ public void transferCorrupted(TransferEvent event) throws TransferCancelledExcep
6566
@Override
6667
public void transferProgressed(TransferEvent event) throws TransferCancelledException {
6768
TransferResource resource = event.getResource();
68-
transfers.put(resource, event.getTransferredBytes());
69+
transfers.put(
70+
new TransferResourceIdentifier(resource),
71+
new TransferResourceAndSize(resource, event.getTransferredBytes()));
6972

7073
buffer.append("Progress (").append(transfers.size()).append("): ");
7174

72-
Iterator<Map.Entry<TransferResource, Long>> entries =
73-
transfers.entrySet().iterator();
75+
Iterator<TransferResourceAndSize> entries = transfers.values().iterator();
7476
while (entries.hasNext()) {
75-
Map.Entry<TransferResource, Long> entry = entries.next();
76-
long total = entry.getKey().getContentLength();
77-
Long complete = entry.getValue();
77+
TransferResourceAndSize entry = entries.next();
78+
long total = entry.resource.getContentLength();
79+
Long complete = entry.transferredBytes;
7880

79-
String resourceName = entry.getKey().getResourceName();
81+
String resourceName = entry.resource.getResourceName();
8082

8183
if (printResourceNames) {
8284
int idx = resourceName.lastIndexOf('/');
@@ -120,15 +122,15 @@ private void pad(StringBuilder buffer, int spaces) {
120122

121123
@Override
122124
public void transferSucceeded(TransferEvent event) {
123-
transfers.remove(event.getResource());
125+
transfers.remove(new TransferResourceIdentifier(event.getResource()));
124126
overridePreviousTransfer(event);
125127

126128
super.transferSucceeded(event);
127129
}
128130

129131
@Override
130132
public void transferFailed(TransferEvent event) {
131-
transfers.remove(event.getResource());
133+
transfers.remove(new TransferResourceIdentifier(event.getResource()));
132134
overridePreviousTransfer(event);
133135

134136
super.transferFailed(event);
@@ -144,4 +146,6 @@ private void overridePreviousTransfer(TransferEvent event) {
144146
buffer.setLength(0);
145147
}
146148
}
149+
150+
private record TransferResourceAndSize(TransferResource resource, long transferredBytes) {}
147151
}

maven-embedder/src/main/java/org/apache/maven/cli/transfer/SimplexTransferListener.java

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
*/
1919
package org.apache.maven.cli.transfer;
2020

21-
import java.io.File;
2221
import java.util.ArrayList;
2322
import java.util.List;
2423
import java.util.concurrent.ArrayBlockingQueue;
@@ -129,7 +128,7 @@ private void demux(List<Exchange> exchanges) {
129128
LOGGER.warn("Invalid TransferEvent.EventType={}; ignoring it", type);
130129
}
131130
} catch (TransferCancelledException e) {
132-
ongoing.put(transferEvent.getResource().getFile(), Boolean.FALSE);
131+
ongoing.put(new TransferResourceIdentifier(transferEvent.getResource()), Boolean.FALSE);
133132
}
134133
});
135134
}
@@ -150,47 +149,47 @@ private void put(TransferEvent event, boolean last) {
150149
}
151150
}
152151

153-
private final ConcurrentHashMap<File, Boolean> ongoing = new ConcurrentHashMap<>();
152+
private final ConcurrentHashMap<TransferResourceIdentifier, Boolean> ongoing = new ConcurrentHashMap<>();
154153

155154
@Override
156155
public void transferInitiated(TransferEvent event) {
157-
ongoing.putIfAbsent(event.getResource().getFile(), Boolean.TRUE);
156+
ongoing.putIfAbsent(new TransferResourceIdentifier(event.getResource()), Boolean.TRUE);
158157
put(event, false);
159158
}
160159

161160
@Override
162161
public void transferStarted(TransferEvent event) throws TransferCancelledException {
163-
if (ongoing.get(event.getResource().getFile()) == Boolean.FALSE) {
162+
if (ongoing.get(new TransferResourceIdentifier(event.getResource())) == Boolean.FALSE) {
164163
throw new TransferCancelledException();
165164
}
166165
put(event, false);
167166
}
168167

169168
@Override
170169
public void transferProgressed(TransferEvent event) throws TransferCancelledException {
171-
if (ongoing.get(event.getResource().getFile()) == Boolean.FALSE) {
170+
if (ongoing.get(new TransferResourceIdentifier(event.getResource())) == Boolean.FALSE) {
172171
throw new TransferCancelledException();
173172
}
174173
put(event, false);
175174
}
176175

177176
@Override
178177
public void transferCorrupted(TransferEvent event) throws TransferCancelledException {
179-
if (ongoing.get(event.getResource().getFile()) == Boolean.FALSE) {
178+
if (ongoing.get(new TransferResourceIdentifier(event.getResource())) == Boolean.FALSE) {
180179
throw new TransferCancelledException();
181180
}
182181
put(event, false);
183182
}
184183

185184
@Override
186185
public void transferSucceeded(TransferEvent event) {
187-
ongoing.remove(event.getResource().getFile());
186+
ongoing.remove(new TransferResourceIdentifier(event.getResource()));
188187
put(event, ongoing.isEmpty());
189188
}
190189

191190
@Override
192191
public void transferFailed(TransferEvent event) {
193-
ongoing.remove(event.getResource().getFile());
192+
ongoing.remove(new TransferResourceIdentifier(event.getResource()));
194193
put(event, ongoing.isEmpty());
195194
}
196195

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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.cli.transfer;
20+
21+
import java.io.File;
22+
23+
import org.apache.maven.api.annotations.Nullable;
24+
import org.eclipse.aether.transfer.TransferResource;
25+
26+
/**
27+
* Immutable identifier of a {@link TransferResource}.
28+
* The {@link TransferResource} is not immutable and does not implement {@code Objects#equals} and {@code Objects#hashCode} methods,
29+
* making it not very suitable for usage in collections.
30+
*/
31+
record TransferResourceIdentifier(String repositoryId, String repositoryUrl, String resourceName, @Nullable File file) {
32+
TransferResourceIdentifier(TransferResource resource) {
33+
this(resource.getRepositoryId(), resource.getRepositoryUrl(), resource.getResourceName(), resource.getFile());
34+
}
35+
}

maven-embedder/src/test/java/org/apache/maven/cli/transfer/SimplexTransferListenerTest.java

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@
2121
import java.io.File;
2222

2323
import org.eclipse.aether.DefaultRepositorySystemSession;
24+
import org.eclipse.aether.RepositorySystemSession;
2425
import org.eclipse.aether.transfer.TransferCancelledException;
2526
import org.eclipse.aether.transfer.TransferEvent;
2627
import org.eclipse.aether.transfer.TransferListener;
2728
import org.eclipse.aether.transfer.TransferResource;
2829
import org.junit.jupiter.api.Test;
30+
import org.mockito.Mockito;
2931

3032
import static org.junit.jupiter.api.Assertions.assertThrows;
3133

@@ -67,17 +69,44 @@ public void transferFailed(TransferEvent event) {}
6769
DefaultRepositorySystemSession session = new DefaultRepositorySystemSession(h -> false); // no close handle
6870

6971
// for technical reasons we cannot throw here, even if delegate does cancel transfer
70-
listener.transferInitiated(new TransferEvent.Builder(session, resource)
71-
.setType(TransferEvent.EventType.INITIATED)
72-
.build());
72+
listener.transferInitiated(event(session, resource, TransferEvent.EventType.INITIATED));
7373

7474
Thread.sleep(500); // to make sure queue is processed, cancellation applied
7575

7676
// subsequent call will cancel
7777
assertThrows(
7878
TransferCancelledException.class,
79-
() -> listener.transferStarted(new TransferEvent.Builder(session, resource)
80-
.resetType(TransferEvent.EventType.STARTED)
81-
.build()));
79+
() -> listener.transferStarted(event(session, resource, TransferEvent.EventType.STARTED)));
80+
}
81+
82+
@Test
83+
void handlesAbsentTransferSource() throws InterruptedException, TransferCancelledException {
84+
TransferResource resource = new TransferResource(null, null, "http://maven.org/test/test-resource", null, null);
85+
86+
RepositorySystemSession session = Mockito.mock(RepositorySystemSession.class);
87+
TransferListener delegate = Mockito.mock(TransferListener.class);
88+
SimplexTransferListener listener = new SimplexTransferListener(delegate);
89+
90+
TransferEvent transferInitiatedEvent = event(session, resource, TransferEvent.EventType.INITIATED);
91+
TransferEvent transferStartedEvent = event(session, resource, TransferEvent.EventType.STARTED);
92+
TransferEvent transferProgressedEvent = event(session, resource, TransferEvent.EventType.PROGRESSED);
93+
TransferEvent transferSucceededEvent = event(session, resource, TransferEvent.EventType.SUCCEEDED);
94+
95+
listener.transferInitiated(transferInitiatedEvent);
96+
listener.transferStarted(transferStartedEvent);
97+
listener.transferProgressed(transferProgressedEvent);
98+
listener.transferSucceeded(transferSucceededEvent);
99+
100+
Thread.sleep(500); // to make sure queue is processed, cancellation applied
101+
102+
Mockito.verify(delegate).transferInitiated(transferInitiatedEvent);
103+
Mockito.verify(delegate).transferStarted(transferStartedEvent);
104+
Mockito.verify(delegate).transferProgressed(transferProgressedEvent);
105+
Mockito.verify(delegate).transferSucceeded(transferSucceededEvent);
106+
}
107+
108+
private static TransferEvent event(
109+
RepositorySystemSession session, TransferResource resource, TransferEvent.EventType type) {
110+
return new TransferEvent.Builder(session, resource).setType(type).build();
82111
}
83112
}

0 commit comments

Comments
 (0)