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.transport.wagon;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.OutputStream;
25  import java.lang.reflect.InvocationTargetException;
26  import java.lang.reflect.Method;
27  import java.nio.file.Files;
28  import java.nio.file.StandardCopyOption;
29  import java.util.Locale;
30  import java.util.Map;
31  import java.util.Properties;
32  import java.util.Queue;
33  import java.util.concurrent.ConcurrentLinkedQueue;
34  import java.util.concurrent.atomic.AtomicBoolean;
35  
36  import org.apache.maven.wagon.ConnectionException;
37  import org.apache.maven.wagon.ResourceDoesNotExistException;
38  import org.apache.maven.wagon.StreamingWagon;
39  import org.apache.maven.wagon.Wagon;
40  import org.apache.maven.wagon.WagonException;
41  import org.apache.maven.wagon.authentication.AuthenticationInfo;
42  import org.apache.maven.wagon.proxy.ProxyInfo;
43  import org.apache.maven.wagon.proxy.ProxyInfoProvider;
44  import org.apache.maven.wagon.repository.Repository;
45  import org.apache.maven.wagon.repository.RepositoryPermissions;
46  import org.eclipse.aether.ConfigurationProperties;
47  import org.eclipse.aether.RepositorySystemSession;
48  import org.eclipse.aether.repository.AuthenticationContext;
49  import org.eclipse.aether.repository.Proxy;
50  import org.eclipse.aether.repository.RemoteRepository;
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.TransportTask;
55  import org.eclipse.aether.spi.connector.transport.Transporter;
56  import org.eclipse.aether.transfer.NoTransporterException;
57  import org.eclipse.aether.util.ConfigUtils;
58  import org.eclipse.aether.util.FileUtils;
59  import org.slf4j.Logger;
60  import org.slf4j.LoggerFactory;
61  
62  import static java.util.Objects.requireNonNull;
63  import static org.eclipse.aether.transport.wagon.WagonTransporterConfigurationKeys.CONFIG_PROP_CONFIG;
64  import static org.eclipse.aether.transport.wagon.WagonTransporterConfigurationKeys.CONFIG_PROP_DIR_MODE;
65  import static org.eclipse.aether.transport.wagon.WagonTransporterConfigurationKeys.CONFIG_PROP_FILE_MODE;
66  import static org.eclipse.aether.transport.wagon.WagonTransporterConfigurationKeys.CONFIG_PROP_GROUP;
67  
68  /**
69   * A transporter using Maven Wagon.
70   */
71  final class WagonTransporter implements Transporter {
72      private static final Logger LOGGER = LoggerFactory.getLogger(WagonTransporter.class);
73  
74      private final RemoteRepository repository;
75  
76      private final RepositorySystemSession session;
77  
78      private final AuthenticationContext repoAuthContext;
79  
80      private final AuthenticationContext proxyAuthContext;
81  
82      private final WagonProvider wagonProvider;
83  
84      private final WagonConfigurator wagonConfigurator;
85  
86      private final String wagonHint;
87  
88      private final Repository wagonRepo;
89  
90      private final AuthenticationInfo wagonAuth;
91  
92      private final ProxyInfoProvider wagonProxy;
93  
94      private final Properties headers;
95  
96      private final Queue<Wagon> wagons = new ConcurrentLinkedQueue<>();
97  
98      private final AtomicBoolean closed = new AtomicBoolean();
99  
100     WagonTransporter(
101             WagonProvider wagonProvider,
102             WagonConfigurator wagonConfigurator,
103             RemoteRepository repository,
104             RepositorySystemSession session)
105             throws NoTransporterException {
106         this.wagonProvider = wagonProvider;
107         this.wagonConfigurator = wagonConfigurator;
108         this.repository = repository;
109         this.session = session;
110 
111         wagonRepo = new Repository(repository.getId(), repository.getUrl());
112         wagonRepo.setPermissions(getPermissions(repository.getId(), session));
113 
114         wagonHint = wagonRepo.getProtocol().toLowerCase(Locale.ENGLISH);
115         if (wagonHint.isEmpty()) {
116             throw new NoTransporterException(repository);
117         }
118 
119         try {
120             wagons.add(lookupWagon());
121         } catch (Exception e) {
122             LOGGER.debug("No transport", e);
123             throw new NoTransporterException(repository, e);
124         }
125 
126         repoAuthContext = AuthenticationContext.forRepository(session, repository);
127         proxyAuthContext = AuthenticationContext.forProxy(session, repository);
128 
129         wagonAuth = getAuthenticationInfo(repoAuthContext);
130         wagonProxy = getProxy(repository, proxyAuthContext);
131 
132         headers = new Properties();
133         headers.put(
134                 "User-Agent",
135                 ConfigUtils.getString(
136                         session, ConfigurationProperties.DEFAULT_USER_AGENT, ConfigurationProperties.USER_AGENT));
137         Map<?, ?> headers = ConfigUtils.getMap(
138                 session,
139                 null,
140                 ConfigurationProperties.HTTP_HEADERS + "." + repository.getId(),
141                 ConfigurationProperties.HTTP_HEADERS);
142         if (headers != null) {
143             this.headers.putAll(headers);
144         }
145     }
146 
147     private static RepositoryPermissions getPermissions(String repoId, RepositorySystemSession session) {
148         RepositoryPermissions result = null;
149 
150         RepositoryPermissions perms = new RepositoryPermissions();
151 
152         String suffix = '.' + repoId;
153 
154         String fileMode = ConfigUtils.getString(session, null, CONFIG_PROP_FILE_MODE + suffix);
155         if (fileMode != null) {
156             perms.setFileMode(fileMode);
157             result = perms;
158         }
159 
160         String dirMode = ConfigUtils.getString(session, null, CONFIG_PROP_DIR_MODE + suffix);
161         if (dirMode != null) {
162             perms.setDirectoryMode(dirMode);
163             result = perms;
164         }
165 
166         String group = ConfigUtils.getString(session, null, CONFIG_PROP_GROUP + suffix);
167         if (group != null) {
168             perms.setGroup(group);
169             result = perms;
170         }
171 
172         return result;
173     }
174 
175     private AuthenticationInfo getAuthenticationInfo(final AuthenticationContext authContext) {
176         AuthenticationInfo auth = null;
177 
178         if (authContext != null) {
179             auth = new AuthenticationInfo() {
180                 @Override
181                 public String getUserName() {
182                     return authContext.get(AuthenticationContext.USERNAME);
183                 }
184 
185                 @Override
186                 public String getPassword() {
187                     return authContext.get(AuthenticationContext.PASSWORD);
188                 }
189 
190                 @Override
191                 public String getPrivateKey() {
192                     return authContext.get(AuthenticationContext.PRIVATE_KEY_PATH);
193                 }
194 
195                 @Override
196                 public String getPassphrase() {
197                     return authContext.get(AuthenticationContext.PRIVATE_KEY_PASSPHRASE);
198                 }
199             };
200         }
201 
202         return auth;
203     }
204 
205     private ProxyInfoProvider getProxy(RemoteRepository repository, final AuthenticationContext authContext) {
206         ProxyInfoProvider proxy = null;
207 
208         Proxy p = repository.getProxy();
209         if (p != null) {
210             final ProxyInfo prox;
211             if (authContext != null) {
212                 prox = new ProxyInfo() {
213                     @Override
214                     public String getUserName() {
215                         return authContext.get(AuthenticationContext.USERNAME);
216                     }
217 
218                     @Override
219                     public String getPassword() {
220                         return authContext.get(AuthenticationContext.PASSWORD);
221                     }
222 
223                     @Override
224                     public String getNtlmDomain() {
225                         return authContext.get(AuthenticationContext.NTLM_DOMAIN);
226                     }
227 
228                     @Override
229                     public String getNtlmHost() {
230                         return authContext.get(AuthenticationContext.NTLM_WORKSTATION);
231                     }
232                 };
233             } else {
234                 prox = new ProxyInfo();
235             }
236             prox.setType(p.getType());
237             prox.setHost(p.getHost());
238             prox.setPort(p.getPort());
239 
240             proxy = protocol -> prox;
241         }
242 
243         return proxy;
244     }
245 
246     private Wagon lookupWagon() throws Exception {
247         return wagonProvider.lookup(wagonHint);
248     }
249 
250     private void releaseWagon(Wagon wagon) {
251         wagonProvider.release(wagon);
252     }
253 
254     private void connectWagon(Wagon wagon) throws WagonException {
255         if (!headers.isEmpty()) {
256             try {
257                 Method setHttpHeaders = wagon.getClass().getMethod("setHttpHeaders", Properties.class);
258                 setHttpHeaders.invoke(wagon, headers);
259             } catch (NoSuchMethodException e) {
260                 // normal for non-http wagons
261             } catch (InvocationTargetException | IllegalAccessException | RuntimeException e) {
262                 LOGGER.debug(
263                         "Could not set user agent for Wagon {}",
264                         wagon.getClass().getName(),
265                         e);
266             }
267         }
268 
269         int connectTimeout = ConfigUtils.getInteger(
270                 session, ConfigurationProperties.DEFAULT_CONNECT_TIMEOUT, ConfigurationProperties.CONNECT_TIMEOUT);
271         int requestTimeout = ConfigUtils.getInteger(
272                 session, ConfigurationProperties.DEFAULT_REQUEST_TIMEOUT, ConfigurationProperties.REQUEST_TIMEOUT);
273 
274         wagon.setTimeout(Math.max(Math.max(connectTimeout, requestTimeout), 0));
275 
276         wagon.setInteractive(ConfigUtils.getBoolean(
277                 session, ConfigurationProperties.DEFAULT_INTERACTIVE, ConfigurationProperties.INTERACTIVE));
278 
279         Object configuration = ConfigUtils.getObject(session, null, CONFIG_PROP_CONFIG + "." + repository.getId());
280         if (configuration != null && wagonConfigurator != null) {
281             try {
282                 wagonConfigurator.configure(wagon, configuration);
283             } catch (Exception e) {
284                 LOGGER.warn(
285                         "Could not apply configuration for {} to Wagon {}",
286                         repository.getId(),
287                         wagon.getClass().getName(),
288                         e);
289             }
290         }
291 
292         wagon.connect(wagonRepo, wagonAuth, wagonProxy);
293     }
294 
295     private void disconnectWagon(Wagon wagon) {
296         try {
297             if (wagon != null) {
298                 wagon.disconnect();
299             }
300         } catch (ConnectionException e) {
301             LOGGER.debug("Could not disconnect Wagon {}", wagon, e);
302         }
303     }
304 
305     private Wagon pollWagon() throws Exception {
306         Wagon wagon = wagons.poll();
307 
308         if (wagon == null) {
309             try {
310                 wagon = lookupWagon();
311                 connectWagon(wagon);
312             } catch (Exception e) {
313                 releaseWagon(wagon);
314                 throw e;
315             }
316         } else if (wagon.getRepository() == null) {
317             try {
318                 connectWagon(wagon);
319             } catch (Exception e) {
320                 wagons.add(wagon);
321                 throw e;
322             }
323         }
324 
325         return wagon;
326     }
327 
328     @Override
329     public int classify(Throwable error) {
330         if (error instanceof ResourceDoesNotExistException) {
331             return ERROR_NOT_FOUND;
332         }
333         return ERROR_OTHER;
334     }
335 
336     @Override
337     public void peek(PeekTask task) throws Exception {
338         execute(task, new PeekTaskRunner(task));
339     }
340 
341     @Override
342     public void get(GetTask task) throws Exception {
343         execute(task, new GetTaskRunner(task));
344     }
345 
346     @Override
347     public void put(PutTask task) throws Exception {
348         execute(task, new PutTaskRunner(task));
349     }
350 
351     private void execute(TransportTask task, TaskRunner runner) throws Exception {
352         requireNonNull(task, "task cannot be null");
353 
354         if (closed.get()) {
355             throw new IllegalStateException("transporter closed, cannot execute task " + task);
356         }
357         try {
358             WagonTransferListener listener = new WagonTransferListener(task.getListener());
359             Wagon wagon = pollWagon();
360             try {
361                 wagon.addTransferListener(listener);
362                 runner.run(wagon);
363             } finally {
364                 wagon.removeTransferListener(listener);
365                 wagons.add(wagon);
366             }
367         } catch (RuntimeException e) {
368             throw WagonCancelledException.unwrap(e);
369         }
370     }
371 
372     @Override
373     public void close() {
374         if (closed.compareAndSet(false, true)) {
375             AuthenticationContext.close(repoAuthContext);
376             AuthenticationContext.close(proxyAuthContext);
377 
378             for (Wagon wagon = wagons.poll(); wagon != null; wagon = wagons.poll()) {
379                 disconnectWagon(wagon);
380                 releaseWagon(wagon);
381             }
382         }
383     }
384 
385     private interface TaskRunner {
386 
387         void run(Wagon wagon) throws IOException, WagonException;
388     }
389 
390     private static class PeekTaskRunner implements TaskRunner {
391 
392         private final PeekTask task;
393 
394         PeekTaskRunner(PeekTask task) {
395             this.task = task;
396         }
397 
398         @Override
399         public void run(Wagon wagon) throws WagonException {
400             String src = task.getLocation().toString();
401             if (!wagon.resourceExists(src)) {
402                 throw new ResourceDoesNotExistException(
403                         "Could not find " + src + " in " + wagon.getRepository().getUrl());
404             }
405         }
406     }
407 
408     private static class GetTaskRunner implements TaskRunner {
409 
410         private final GetTask task;
411 
412         GetTaskRunner(GetTask task) {
413             this.task = task;
414         }
415 
416         @Override
417         public void run(Wagon wagon) throws IOException, WagonException {
418             final String src = task.getLocation().toString();
419             final File file = task.getDataFile();
420             if (file == null && wagon instanceof StreamingWagon) {
421                 try (OutputStream dst = task.newOutputStream()) {
422                     ((StreamingWagon) wagon).getToStream(src, dst);
423                 }
424             } else {
425                 // if file == null -> $TMP used, otherwise we place tmp file next to file
426                 try (FileUtils.TempFile tempFile =
427                         file == null ? FileUtils.newTempFile() : FileUtils.newTempFile(file.toPath())) {
428                     File dst = tempFile.getPath().toFile();
429                     wagon.get(src, dst);
430                     /*
431                      * NOTE: Wagon (1.0-beta-6) doesn't create the destination file when transferring a 0-byte
432                      * resource. So if the resource we asked for didn't cause any exception but doesn't show up in
433                      * the dst file either, Wagon tells us in its weird way the file is empty.
434                      */
435                     if (!dst.exists() && !dst.createNewFile()) {
436                         throw new IOException(String.format("Failure creating file '%s'.", dst.getAbsolutePath()));
437                     }
438 
439                     if (file != null) {
440                         ((FileUtils.CollocatedTempFile) tempFile).move();
441                     } else {
442                         try (OutputStream outputStream = task.newOutputStream()) {
443                             Files.copy(dst.toPath(), outputStream);
444                         }
445                     }
446                 }
447             }
448         }
449     }
450 
451     private static class PutTaskRunner implements TaskRunner {
452 
453         private final PutTask task;
454 
455         PutTaskRunner(PutTask task) {
456             this.task = task;
457         }
458 
459         @Override
460         public void run(Wagon wagon) throws WagonException, IOException {
461             final String dst = task.getLocation().toString();
462             final File file = task.getDataFile();
463             if (file == null && wagon instanceof StreamingWagon) {
464                 try (InputStream src = task.newInputStream()) {
465                     // StreamingWagon uses an internal buffer on src input stream.
466                     ((StreamingWagon) wagon).putFromStream(src, dst, task.getDataLength(), -1);
467                 }
468             } else if (file == null) {
469                 try (FileUtils.TempFile tempFile = FileUtils.newTempFile()) {
470                     try (InputStream inputStream = task.newInputStream()) {
471                         Files.copy(inputStream, tempFile.getPath(), StandardCopyOption.REPLACE_EXISTING);
472                     }
473                     wagon.put(tempFile.getPath().toFile(), dst);
474                 }
475             } else {
476                 wagon.put(file, dst);
477             }
478         }
479     }
480 }