View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.buildcache;
20  
21  import javax.annotation.Nonnull;
22  import javax.inject.Inject;
23  import javax.inject.Named;
24  
25  import java.io.Closeable;
26  import java.io.File;
27  import java.io.IOException;
28  import java.lang.reflect.Method;
29  import java.net.URI;
30  import java.nio.file.Files;
31  import java.nio.file.Path;
32  import java.util.List;
33  import java.util.Optional;
34  import java.util.concurrent.atomic.AtomicReference;
35  
36  import org.apache.http.HttpStatus;
37  import org.apache.http.client.HttpResponseException;
38  import org.apache.maven.SessionScoped;
39  import org.apache.maven.buildcache.checksum.MavenProjectInput;
40  import org.apache.maven.buildcache.xml.Build;
41  import org.apache.maven.buildcache.xml.CacheConfig;
42  import org.apache.maven.buildcache.xml.CacheSource;
43  import org.apache.maven.buildcache.xml.XmlService;
44  import org.apache.maven.buildcache.xml.build.Artifact;
45  import org.apache.maven.buildcache.xml.report.CacheReport;
46  import org.apache.maven.buildcache.xml.report.ProjectReport;
47  import org.apache.maven.execution.MavenSession;
48  import org.apache.maven.project.MavenProject;
49  import org.apache.maven.wagon.ResourceDoesNotExistException;
50  import org.eclipse.aether.RepositorySystemSession;
51  import org.eclipse.aether.repository.Authentication;
52  import org.eclipse.aether.repository.Proxy;
53  import org.eclipse.aether.repository.RemoteRepository;
54  import org.eclipse.aether.spi.connector.transport.GetTask;
55  import org.eclipse.aether.spi.connector.transport.PutTask;
56  import org.eclipse.aether.spi.connector.transport.Transporter;
57  import org.eclipse.aether.spi.connector.transport.TransporterProvider;
58  import org.slf4j.Logger;
59  import org.slf4j.LoggerFactory;
60  
61  /**
62   * Remote cache repository implementation.
63   */
64  @SessionScoped
65  @Named("resolver")
66  public class RemoteCacheRepositoryImpl implements RemoteCacheRepository, Closeable {
67  
68      private static final Logger LOGGER = LoggerFactory.getLogger(RemoteCacheRepositoryImpl.class);
69  
70      private final XmlService xmlService;
71      private final CacheConfig cacheConfig;
72      private final Transporter transporter;
73  
74      @Inject
75      public RemoteCacheRepositoryImpl(
76              XmlService xmlService,
77              CacheConfig cacheConfig,
78              MavenSession mavenSession,
79              TransporterProvider transporterProvider)
80              throws Exception {
81          this.xmlService = xmlService;
82          this.cacheConfig = cacheConfig;
83          if (cacheConfig.isRemoteCacheEnabled()) {
84              RepositorySystemSession session = mavenSession.getRepositorySession();
85              RemoteRepository repo =
86                      new RemoteRepository.Builder(cacheConfig.getId(), "cache", cacheConfig.getUrl()).build();
87              RemoteRepository mirror = session.getMirrorSelector().getMirror(repo);
88              RemoteRepository repoOrMirror = mirror != null ? mirror : repo;
89              Proxy proxy = session.getProxySelector().getProxy(repoOrMirror);
90              Authentication auth = session.getAuthenticationSelector().getAuthentication(repoOrMirror);
91              RemoteRepository repository = new RemoteRepository.Builder(repoOrMirror)
92                      .setProxy(proxy)
93                      .setAuthentication(auth)
94                      .build();
95              this.transporter = transporterProvider.newTransporter(session, repository);
96          } else {
97              this.transporter = null;
98          }
99      }
100 
101     @Override
102     public void close() throws IOException {
103         if (transporter != null) {
104             transporter.close();
105         }
106     }
107 
108     @Nonnull
109     @Override
110     public Optional<Build> findBuild(CacheContext context) throws IOException {
111         final String resourceUrl = getResourceUrl(context, BUILDINFO_XML);
112         return getResourceContent(resourceUrl)
113                 .map(content -> new Build(xmlService.loadBuild(content), CacheSource.REMOTE));
114     }
115 
116     @Override
117     public boolean getArtifactContent(CacheContext context, Artifact artifact, Path target) {
118         return getResourceContent(getResourceUrl(context, artifact.getFileName()), target);
119     }
120 
121     @Override
122     public void saveBuildInfo(CacheResult cacheResult, Build build) throws IOException {
123         final String resourceUrl = getResourceUrl(cacheResult.getContext(), BUILDINFO_XML);
124         putToRemoteCache(xmlService.toBytes(build.getDto()), resourceUrl);
125     }
126 
127     @Override
128     public void saveCacheReport(String buildId, MavenSession session, CacheReport cacheReport) throws IOException {
129         MavenProject rootProject = session.getTopLevelProject();
130         final String resourceUrl = MavenProjectInput.CACHE_IMPLEMENTATION_VERSION
131                 + "/" + rootProject.getGroupId()
132                 + "/" + rootProject.getArtifactId()
133                 + "/" + buildId
134                 + "/" + CACHE_REPORT_XML;
135         putToRemoteCache(xmlService.toBytes(cacheReport), resourceUrl);
136     }
137 
138     @Override
139     public void saveArtifactFile(CacheResult cacheResult, org.apache.maven.artifact.Artifact artifact)
140             throws IOException {
141         final String resourceUrl = getResourceUrl(cacheResult.getContext(), CacheUtils.normalizedName(artifact));
142         putToRemoteCache(artifact.getFile(), resourceUrl);
143     }
144 
145     /**
146      * Downloads content of the resource
147      *
148      * @return null or content
149      */
150     @Nonnull
151     public Optional<byte[]> getResourceContent(String url) {
152         String fullUrl = getFullUrl(url);
153         try {
154             LOGGER.info("Downloading {}", fullUrl);
155             GetTask task = new GetTask(new URI(url));
156             transporter.get(task);
157             return Optional.of(task.getDataBytes());
158         } catch (ResourceDoesNotExistException e) {
159             logNotFound(fullUrl, e);
160             return Optional.empty();
161         } catch (Exception e) {
162             // this can be wagon used so the exception may be different
163             // we want wagon users not flooded with logs when not found
164             if ((e instanceof HttpResponseException
165                             || e.getClass().getName().equals(HttpResponseException.class.getName()))
166                     && getStatusCode(e) == HttpStatus.SC_NOT_FOUND) {
167                 logNotFound(fullUrl, e);
168                 return Optional.empty();
169             }
170             if (cacheConfig.isFailFast()) {
171                 LOGGER.error("Error downloading cache item: {}", fullUrl, e);
172                 throw new RuntimeException("Error downloading cache item: " + fullUrl, e);
173             } else {
174                 LOGGER.error("Error downloading cache item: {}", fullUrl);
175                 return Optional.empty();
176             }
177         }
178     }
179 
180     private int getStatusCode(Exception ex) {
181         // just to avoid this when using wagon provide
182         // java.lang.ClassCastException: class org.apache.http.client.HttpResponseException cannot be cast to class
183         // org.apache.http.client.HttpResponseException
184         // (org.apache.http.client.HttpResponseException is in unnamed module of loader
185         // org.codehaus.plexus.classworlds.realm.ClassRealm @23cd4ff2;
186         //
187         try {
188             Method method = ex.getClass().getMethod("getStatusCode");
189             return (int) method.invoke(ex);
190         } catch (Throwable t) {
191             LOGGER.debug(t.getMessage(), t);
192             return 0;
193         }
194     }
195 
196     private void logNotFound(String fullUrl, Exception e) {
197         if (LOGGER.isDebugEnabled()) {
198             LOGGER.info("Cache item not found: {}", fullUrl, e);
199         } else {
200             LOGGER.info("Cache item not found: {}", fullUrl);
201         }
202     }
203 
204     public boolean getResourceContent(String url, Path target) {
205         try {
206             LOGGER.info("Downloading {}", getFullUrl(url));
207             GetTask task = new GetTask(new URI(url)).setDataFile(target.toFile());
208             transporter.get(task);
209             return true;
210         } catch (Exception e) {
211             LOGGER.info("Cannot download {}: {}", getFullUrl(url), e.toString());
212             return false;
213         }
214     }
215 
216     @Nonnull
217     @Override
218     public String getResourceUrl(CacheContext context, String filename) {
219         return getResourceUrl(
220                 filename,
221                 context.getProject().getGroupId(),
222                 context.getProject().getArtifactId(),
223                 context.getInputInfo().getChecksum());
224     }
225 
226     private String getResourceUrl(String filename, String groupId, String artifactId, String checksum) {
227         return MavenProjectInput.CACHE_IMPLEMENTATION_VERSION + "/" + groupId + "/" + artifactId + "/" + checksum + "/"
228                 + filename;
229     }
230 
231     private void putToRemoteCache(byte[] bytes, String url) throws IOException {
232         Path tmp = Files.createTempFile("mbce-", ".tmp");
233         try {
234             Files.write(tmp, bytes);
235             PutTask put = new PutTask(new URI(url));
236             put.setDataFile(tmp.toFile());
237             transporter.put(put);
238             LOGGER.info("Saved to remote cache {}", getFullUrl(url));
239         } catch (Exception e) {
240             LOGGER.info("Unable to save to remote cache {}", getFullUrl(url), e);
241         } finally {
242             Files.deleteIfExists(tmp);
243         }
244     }
245 
246     private void putToRemoteCache(File file, String url) throws IOException {
247         try {
248             PutTask put = new PutTask(new URI(url));
249             put.setDataFile(file);
250             transporter.put(put);
251             LOGGER.info("Saved to remote cache {}", getFullUrl(url));
252         } catch (Exception e) {
253             LOGGER.info("Unable to save to remote cache {}", getFullUrl(url), e);
254         }
255     }
256 
257     private final AtomicReference<CacheReport> cacheReportSupplier = new AtomicReference<>();
258 
259     @Nonnull
260     @Override
261     public Optional<Build> findBaselineBuild(MavenProject project) {
262         Optional<List<ProjectReport>> cachedProjectsHolder = findCacheInfo().map(CacheReport::getProjects);
263 
264         if (!cachedProjectsHolder.isPresent()) {
265             return Optional.empty();
266         }
267 
268         final List<ProjectReport> projects = cachedProjectsHolder.get();
269         final Optional<ProjectReport> projectReportHolder = projects.stream()
270                 .filter(p -> project.getArtifactId().equals(p.getArtifactId())
271                         && project.getGroupId().equals(p.getGroupId()))
272                 .findFirst();
273 
274         if (!projectReportHolder.isPresent()) {
275             return Optional.empty();
276         }
277 
278         final ProjectReport projectReport = projectReportHolder.get();
279 
280         String url;
281         if (projectReport.getUrl() != null) {
282             url = projectReport.getUrl();
283             LOGGER.info("Retrieving baseline buildinfo: {}", url);
284         } else {
285             url = getResourceUrl(
286                     BUILDINFO_XML, project.getGroupId(), project.getArtifactId(), projectReport.getChecksum());
287             LOGGER.info("Baseline project record doesn't have url, trying default location {}", url);
288         }
289 
290         try {
291             return getResourceContent(url).map(content -> new Build(xmlService.loadBuild(content), CacheSource.REMOTE));
292         } catch (Exception e) {
293             LOGGER.warn("Error restoring baseline build at url: {}, skipping diff", url, e);
294             return Optional.empty();
295         }
296     }
297 
298     private Optional<CacheReport> findCacheInfo() {
299         Optional<CacheReport> report = Optional.ofNullable(cacheReportSupplier.get());
300         if (!report.isPresent()) {
301             try {
302                 LOGGER.info("Downloading baseline cache report from: {}", cacheConfig.getBaselineCacheUrl());
303                 report = getResourceContent(cacheConfig.getBaselineCacheUrl()).map(xmlService::loadCacheReport);
304             } catch (Exception e) {
305                 LOGGER.error(
306                         "Error downloading baseline report from: {}, skipping diff.",
307                         cacheConfig.getBaselineCacheUrl(),
308                         e);
309                 report = Optional.empty();
310             }
311             cacheReportSupplier.compareAndSet(null, report.orElse(null));
312         }
313         return report;
314     }
315 
316     private String getFullUrl(String url) {
317         return cacheConfig.getUrl() + "/" + url;
318     }
319 }