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.artifact.repository.metadata;
20  
21  import java.io.File;
22  import java.io.FileNotFoundException;
23  import java.io.IOException;
24  import java.io.Reader;
25  import java.io.Writer;
26  import java.util.Date;
27  import java.util.HashMap;
28  import java.util.List;
29  import java.util.Map;
30  
31  import org.apache.maven.artifact.metadata.ArtifactMetadata;
32  import org.apache.maven.artifact.repository.ArtifactRepository;
33  import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy;
34  import org.apache.maven.artifact.repository.DefaultRepositoryRequest;
35  import org.apache.maven.artifact.repository.RepositoryRequest;
36  import org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Writer;
37  import org.apache.maven.repository.internal.metadata.ValidatingMetadataXpp3Reader;
38  import org.apache.maven.repository.legacy.ChecksumFailedException;
39  import org.apache.maven.repository.legacy.UpdateCheckManager;
40  import org.apache.maven.repository.legacy.WagonManager;
41  import org.apache.maven.wagon.ResourceDoesNotExistException;
42  import org.apache.maven.wagon.TransferFailedException;
43  import org.codehaus.plexus.component.annotations.Component;
44  import org.codehaus.plexus.component.annotations.Requirement;
45  import org.codehaus.plexus.logging.AbstractLogEnabled;
46  import org.codehaus.plexus.util.ReaderFactory;
47  import org.codehaus.plexus.util.WriterFactory;
48  import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
49  
50  /**
51   * @author Jason van Zyl
52   */
53  @Component(role = RepositoryMetadataManager.class)
54  public class DefaultRepositoryMetadataManager extends AbstractLogEnabled implements RepositoryMetadataManager {
55      @Requirement
56      private WagonManager wagonManager;
57  
58      @Requirement
59      private UpdateCheckManager updateCheckManager;
60  
61      public void resolve(
62              RepositoryMetadata metadata,
63              List<ArtifactRepository> remoteRepositories,
64              ArtifactRepository localRepository)
65              throws RepositoryMetadataResolutionException {
66          RepositoryRequest request = new DefaultRepositoryRequest();
67          request.setLocalRepository(localRepository);
68          request.setRemoteRepositories(remoteRepositories);
69          resolve(metadata, request);
70      }
71  
72      public void resolve(RepositoryMetadata metadata, RepositoryRequest request)
73              throws RepositoryMetadataResolutionException {
74          ArtifactRepository localRepo = request.getLocalRepository();
75          List<ArtifactRepository> remoteRepositories = request.getRemoteRepositories();
76  
77          if (!request.isOffline()) {
78              Date localCopyLastModified = null;
79              if (metadata.getBaseVersion() != null) {
80                  localCopyLastModified = getLocalCopyLastModified(localRepo, metadata);
81              }
82  
83              for (ArtifactRepository repository : remoteRepositories) {
84                  ArtifactRepositoryPolicy policy = metadata.getPolicy(repository);
85  
86                  File file =
87                          new File(localRepo.getBasedir(), localRepo.pathOfLocalRepositoryMetadata(metadata, repository));
88                  boolean update;
89  
90                  if (!policy.isEnabled()) {
91                      update = false;
92  
93                      if (getLogger().isDebugEnabled()) {
94                          getLogger()
95                                  .debug("Skipping update check for " + metadata.getKey() + " (" + file
96                                          + ") from disabled repository " + repository.getId() + " ("
97                                          + repository.getUrl() + ")");
98                      }
99                  } else if (request.isForceUpdate()) {
100                     update = true;
101                 } else if (localCopyLastModified != null && !policy.checkOutOfDate(localCopyLastModified)) {
102                     update = false;
103 
104                     if (getLogger().isDebugEnabled()) {
105                         getLogger()
106                                 .debug("Skipping update check for " + metadata.getKey() + " (" + file
107                                         + ") from repository " + repository.getId() + " (" + repository.getUrl()
108                                         + ") in favor of local copy");
109                     }
110                 } else {
111                     update = updateCheckManager.isUpdateRequired(metadata, repository, file);
112                 }
113 
114                 if (update) {
115                     getLogger().info(metadata.getKey() + ": checking for updates from " + repository.getId());
116                     try {
117                         wagonManager.getArtifactMetadata(metadata, repository, file, policy.getChecksumPolicy());
118                         updateCheckManager.touch(metadata, repository, file);
119                     } catch (ChecksumFailedException e) {
120                         // ChecksumFailedException is only thrown by the wagon manager under
121                         // CHECKSUM_POLICY_FAIL: honor the strict policy by failing metadata resolution
122                         // instead of downgrading the integrity failure to a warning. The update
123                         // tracking file is deliberately not touched, so the next build retries
124                         // immediately instead of trusting stale metadata for a full update interval.
125                         throw new RepositoryMetadataResolutionException(
126                                 metadata + " failed checksum verification against repository: " + repository.getId()
127                                         + " due to an error: " + e.getMessage(),
128                                 e);
129                     } catch (ResourceDoesNotExistException e) {
130                         getLogger().debug(metadata + " could not be found on repository: " + repository.getId());
131 
132                         // delete the local copy so the old details aren't used.
133                         if (file.exists()) {
134                             if (!file.delete()) {
135                                 // sleep for 10ms just in case this is windows holding a file lock
136                                 try {
137                                     Thread.sleep(10);
138                                 } catch (InterruptedException ie) {
139                                     // ignore
140                                 }
141                                 file.delete(); // if this fails, forget about it
142                             }
143                         }
144                         updateCheckManager.touch(metadata, repository, file);
145                     } catch (TransferFailedException e) {
146                         getLogger()
147                                 .warn(metadata + " could not be retrieved from repository: " + repository.getId()
148                                         + " due to an error: " + e.getMessage());
149                         getLogger().debug("Exception", e);
150                         updateCheckManager.touch(metadata, repository, file);
151                     }
152                 }
153 
154                 // TODO should this be inside the above check?
155                 // touch file so that this is not checked again until interval has passed
156                 if (file.exists()) {
157                     file.setLastModified(System.currentTimeMillis());
158                 }
159             }
160         }
161 
162         try {
163             mergeMetadata(metadata, remoteRepositories, localRepo);
164         } catch (RepositoryMetadataStoreException e) {
165             throw new RepositoryMetadataResolutionException(
166                     "Unable to store local copy of metadata: " + e.getMessage(), e);
167         }
168     }
169 
170     private Date getLocalCopyLastModified(ArtifactRepository localRepository, RepositoryMetadata metadata) {
171         String metadataPath = localRepository.pathOfLocalRepositoryMetadata(metadata, localRepository);
172         File metadataFile = new File(localRepository.getBasedir(), metadataPath);
173         return metadataFile.isFile() ? new Date(metadataFile.lastModified()) : null;
174     }
175 
176     private void mergeMetadata(
177             RepositoryMetadata metadata,
178             List<ArtifactRepository> remoteRepositories,
179             ArtifactRepository localRepository)
180             throws RepositoryMetadataStoreException {
181         // TODO currently this is first wins, but really we should take the latest by comparing either the
182         // snapshot timestamp, or some other timestamp later encoded into the metadata.
183         // TODO this needs to be repeated here so the merging doesn't interfere with the written metadata
184         //  - we'd be much better having a pristine input, and an ongoing metadata for merging instead
185 
186         Map<ArtifactRepository, Metadata> previousMetadata = new HashMap<>();
187         ArtifactRepository selected = null;
188         for (ArtifactRepository repository : remoteRepositories) {
189             ArtifactRepositoryPolicy policy = metadata.getPolicy(repository);
190 
191             if (policy.isEnabled() && loadMetadata(metadata, repository, localRepository, previousMetadata)) {
192                 metadata.setRepository(repository);
193                 selected = repository;
194             }
195         }
196         if (loadMetadata(metadata, localRepository, localRepository, previousMetadata)) {
197             metadata.setRepository(null);
198             selected = localRepository;
199         }
200 
201         updateSnapshotMetadata(metadata, previousMetadata, selected, localRepository);
202     }
203 
204     private void updateSnapshotMetadata(
205             RepositoryMetadata metadata,
206             Map<ArtifactRepository, Metadata> previousMetadata,
207             ArtifactRepository selected,
208             ArtifactRepository localRepository)
209             throws RepositoryMetadataStoreException {
210         // TODO this could be a lot nicer... should really be in the snapshot transformation?
211         if (metadata.isSnapshot()) {
212             Metadata prevMetadata = metadata.getMetadata();
213 
214             for (ArtifactRepository repository : previousMetadata.keySet()) {
215                 Metadata m = previousMetadata.get(repository);
216                 if (repository.equals(selected)) {
217                     if (m.getVersioning() == null) {
218                         m.setVersioning(new Versioning());
219                     }
220 
221                     if (m.getVersioning().getSnapshot() == null) {
222                         m.getVersioning().setSnapshot(new Snapshot());
223                     }
224                 } else {
225                     if ((m.getVersioning() != null)
226                             && (m.getVersioning().getSnapshot() != null)
227                             && m.getVersioning().getSnapshot().isLocalCopy()) {
228                         m.getVersioning().getSnapshot().setLocalCopy(false);
229                         metadata.setMetadata(m);
230                         metadata.storeInLocalRepository(localRepository, repository);
231                     }
232                 }
233             }
234 
235             metadata.setMetadata(prevMetadata);
236         }
237     }
238 
239     private boolean loadMetadata(
240             RepositoryMetadata repoMetadata,
241             ArtifactRepository remoteRepository,
242             ArtifactRepository localRepository,
243             Map<ArtifactRepository, Metadata> previousMetadata) {
244         boolean setRepository = false;
245 
246         File metadataFile = new File(
247                 localRepository.getBasedir(),
248                 localRepository.pathOfLocalRepositoryMetadata(repoMetadata, remoteRepository));
249 
250         if (metadataFile.exists()) {
251             Metadata metadata;
252 
253             try {
254                 metadata = readMetadata(metadataFile);
255             } catch (RepositoryMetadataReadException e) {
256                 if (getLogger().isDebugEnabled()) {
257                     getLogger().warn(e.getMessage(), e);
258                 } else {
259                     getLogger().warn(e.getMessage());
260                 }
261                 return setRepository;
262             }
263 
264             if (repoMetadata.isSnapshot() && (previousMetadata != null)) {
265                 previousMetadata.put(remoteRepository, metadata);
266             }
267 
268             if (repoMetadata.getMetadata() != null) {
269                 setRepository = repoMetadata.getMetadata().merge(metadata);
270             } else {
271                 repoMetadata.setMetadata(metadata);
272                 setRepository = true;
273             }
274         }
275         return setRepository;
276     }
277 
278     /**
279      * TODO share with DefaultPluginMappingManager.
280      */
281     protected Metadata readMetadata(File mappingFile) throws RepositoryMetadataReadException {
282         Metadata result;
283 
284         try (Reader reader = ReaderFactory.newXmlReader(mappingFile)) {
285             ValidatingMetadataXpp3Reader mappingReader = new ValidatingMetadataXpp3Reader();
286 
287             result = mappingReader.read(reader, false);
288         } catch (FileNotFoundException e) {
289             throw new RepositoryMetadataReadException("Cannot read metadata from '" + mappingFile + "'", e);
290         } catch (IOException | XmlPullParserException e) {
291             throw new RepositoryMetadataReadException(
292                     "Cannot read metadata from '" + mappingFile + "': " + e.getMessage(), e);
293         }
294 
295         return result;
296     }
297 
298     /**
299      * Ensures the last updated timestamp of the specified metadata does not refer to the future and fixes the local
300      * metadata if necessary to allow proper merging/updating of metadata during deployment.
301      */
302     private void fixTimestamp(File metadataFile, Metadata metadata, Metadata reference) {
303         boolean changed = false;
304 
305         if (metadata != null && reference != null) {
306             Versioning versioning = metadata.getVersioning();
307             Versioning versioningRef = reference.getVersioning();
308             if (versioning != null && versioningRef != null) {
309                 String lastUpdated = versioning.getLastUpdated();
310                 String now = versioningRef.getLastUpdated();
311                 if (lastUpdated != null && now != null && now.compareTo(lastUpdated) < 0) {
312                     getLogger()
313                             .warn("The last updated timestamp in " + metadataFile + " refers to the future (now = "
314                                     + now
315                                     + ", lastUpdated = " + lastUpdated + "). Please verify that the clocks of all"
316                                     + " deploying machines are reasonably synchronized.");
317                     versioning.setLastUpdated(now);
318                     changed = true;
319                 }
320             }
321         }
322 
323         if (changed) {
324             getLogger().debug("Repairing metadata in " + metadataFile);
325 
326             try (Writer writer = WriterFactory.newXmlWriter(metadataFile)) {
327                 new MetadataXpp3Writer().write(writer, metadata);
328             } catch (IOException e) {
329                 String msg = "Could not write fixed metadata to " + metadataFile + ": " + e.getMessage();
330                 if (getLogger().isDebugEnabled()) {
331                     getLogger().warn(msg, e);
332                 } else {
333                     getLogger().warn(msg);
334                 }
335             }
336         }
337     }
338 
339     public void resolveAlways(
340             RepositoryMetadata metadata, ArtifactRepository localRepository, ArtifactRepository remoteRepository)
341             throws RepositoryMetadataResolutionException {
342         File file;
343         try {
344             file = getArtifactMetadataFromDeploymentRepository(metadata, localRepository, remoteRepository);
345         } catch (TransferFailedException e) {
346             throw new RepositoryMetadataResolutionException(
347                     metadata + " could not be retrieved from repository: " + remoteRepository.getId()
348                             + " due to an error: " + e.getMessage(),
349                     e);
350         }
351 
352         try {
353             if (file.exists()) {
354                 Metadata prevMetadata = readMetadata(file);
355                 metadata.setMetadata(prevMetadata);
356             }
357         } catch (RepositoryMetadataReadException e) {
358             throw new RepositoryMetadataResolutionException(e.getMessage(), e);
359         }
360     }
361 
362     private File getArtifactMetadataFromDeploymentRepository(
363             ArtifactMetadata metadata, ArtifactRepository localRepo, ArtifactRepository remoteRepository)
364             throws TransferFailedException {
365         File file =
366                 new File(localRepo.getBasedir(), localRepo.pathOfLocalRepositoryMetadata(metadata, remoteRepository));
367 
368         try {
369             wagonManager.getArtifactMetadataFromDeploymentRepository(
370                     metadata, remoteRepository, file, getChecksumPolicy(metadata, remoteRepository));
371         } catch (ResourceDoesNotExistException e) {
372             getLogger()
373                     .info(metadata + " could not be found on repository: " + remoteRepository.getId()
374                             + ", so will be created");
375 
376             // delete the local copy so the old details aren't used.
377             if (file.exists()) {
378                 if (!file.delete()) {
379                     // sleep for 10ms just in case this is windows holding a file lock
380                     try {
381                         Thread.sleep(10);
382                     } catch (InterruptedException ie) {
383                         // ignore
384                     }
385                     file.delete(); // if this fails, forget about it
386                 }
387             }
388         } finally {
389             if (metadata instanceof RepositoryMetadata) {
390                 updateCheckManager.touch((RepositoryMetadata) metadata, remoteRepository, file);
391             }
392         }
393         return file;
394     }
395 
396     /**
397      * Determines the effective checksum policy for a transfer from the given repository. The
398      * operator-configured policy (e.g. {@code fail} via {@code -C}/{@code --strict-checksums} or a
399      * per-repository {@code checksumPolicy}) must govern every remote transfer, so it must not be
400      * hardcoded at the call sites.
401      */
402     private String getChecksumPolicy(ArtifactMetadata metadata, ArtifactRepository repository) {
403         if (metadata instanceof RepositoryMetadata) {
404             ArtifactRepositoryPolicy policy = ((RepositoryMetadata) metadata).getPolicy(repository);
405             if (policy != null && policy.getChecksumPolicy() != null) {
406                 return policy.getChecksumPolicy();
407             }
408         }
409         return ArtifactRepositoryPolicy.CHECKSUM_POLICY_WARN;
410     }
411 
412     public void deploy(
413             ArtifactMetadata metadata, ArtifactRepository localRepository, ArtifactRepository deploymentRepository)
414             throws RepositoryMetadataDeploymentException {
415         File file;
416         if (metadata instanceof RepositoryMetadata) {
417             getLogger().info("Retrieving previous metadata from " + deploymentRepository.getId());
418             try {
419                 file = getArtifactMetadataFromDeploymentRepository(metadata, localRepository, deploymentRepository);
420             } catch (TransferFailedException e) {
421                 throw new RepositoryMetadataDeploymentException(
422                         metadata + " could not be retrieved from repository: " + deploymentRepository.getId()
423                                 + " due to an error: " + e.getMessage(),
424                         e);
425             }
426 
427             if (file.isFile()) {
428                 try {
429                     fixTimestamp(file, readMetadata(file), ((RepositoryMetadata) metadata).getMetadata());
430                 } catch (RepositoryMetadataReadException e) {
431                     // will be reported via storeInlocalRepository
432                 }
433             }
434         } else {
435             // It's a POM - we don't need to retrieve it first
436             file = new File(
437                     localRepository.getBasedir(),
438                     localRepository.pathOfLocalRepositoryMetadata(metadata, deploymentRepository));
439         }
440 
441         try {
442             metadata.storeInLocalRepository(localRepository, deploymentRepository);
443         } catch (RepositoryMetadataStoreException e) {
444             throw new RepositoryMetadataDeploymentException("Error installing metadata: " + e.getMessage(), e);
445         }
446 
447         try {
448             wagonManager.putArtifactMetadata(file, metadata, deploymentRepository);
449         } catch (TransferFailedException e) {
450             throw new RepositoryMetadataDeploymentException("Error while deploying metadata: " + e.getMessage(), e);
451         }
452     }
453 
454     public void install(ArtifactMetadata metadata, ArtifactRepository localRepository)
455             throws RepositoryMetadataInstallationException {
456         try {
457             metadata.storeInLocalRepository(localRepository, localRepository);
458         } catch (RepositoryMetadataStoreException e) {
459             throw new RepositoryMetadataInstallationException("Error installing metadata: " + e.getMessage(), e);
460         }
461     }
462 }