001package org.apache.maven.artifact.repository.metadata;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *  http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import org.apache.maven.artifact.metadata.ArtifactMetadata;
023import org.apache.maven.artifact.repository.ArtifactRepository;
024import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy;
025import org.apache.maven.artifact.repository.DefaultRepositoryRequest;
026import org.apache.maven.artifact.repository.RepositoryRequest;
027import org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Reader;
028import org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Writer;
029import org.apache.maven.repository.legacy.UpdateCheckManager;
030import org.apache.maven.repository.legacy.WagonManager;
031import org.apache.maven.wagon.ResourceDoesNotExistException;
032import org.apache.maven.wagon.TransferFailedException;
033import org.codehaus.plexus.component.annotations.Component;
034import org.codehaus.plexus.component.annotations.Requirement;
035import org.codehaus.plexus.logging.AbstractLogEnabled;
036import org.codehaus.plexus.util.ReaderFactory;
037import org.codehaus.plexus.util.WriterFactory;
038import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
039
040import java.io.File;
041import java.io.FileNotFoundException;
042import java.io.IOException;
043import java.io.Reader;
044import java.io.Writer;
045import java.util.Date;
046import java.util.HashMap;
047import java.util.List;
048import java.util.Map;
049
050/**
051 * @author Jason van Zyl
052 */
053@Component( role = RepositoryMetadataManager.class )
054public class DefaultRepositoryMetadataManager
055    extends AbstractLogEnabled
056    implements RepositoryMetadataManager
057{
058    @Requirement
059    private WagonManager wagonManager;
060
061    @Requirement
062    private UpdateCheckManager updateCheckManager;
063
064    public void resolve( RepositoryMetadata metadata, List<ArtifactRepository> remoteRepositories,
065                         ArtifactRepository localRepository )
066        throws RepositoryMetadataResolutionException
067    {
068        RepositoryRequest request = new DefaultRepositoryRequest();
069        request.setLocalRepository( localRepository );
070        request.setRemoteRepositories( remoteRepositories );
071        resolve( metadata, request );
072    }
073
074    public void resolve( RepositoryMetadata metadata, RepositoryRequest request )
075        throws RepositoryMetadataResolutionException
076    {
077        ArtifactRepository localRepo = request.getLocalRepository();
078        List<ArtifactRepository> remoteRepositories = request.getRemoteRepositories();
079
080        if ( !request.isOffline() )
081        {
082            Date localCopyLastModified = null;
083            if ( metadata.getBaseVersion() != null )
084            {
085                localCopyLastModified = getLocalCopyLastModified( localRepo, metadata );
086            }
087
088            for ( ArtifactRepository repository : remoteRepositories )
089            {
090                ArtifactRepositoryPolicy policy = metadata.getPolicy( repository );
091
092                File file =
093                    new File( localRepo.getBasedir(), localRepo.pathOfLocalRepositoryMetadata( metadata, repository ) );
094                boolean update;
095
096                if ( !policy.isEnabled() )
097                {
098                    update = false;
099
100                    if ( getLogger().isDebugEnabled() )
101                    {
102                        getLogger().debug( "Skipping update check for " + metadata.getKey() + " (" + file
103                                               + ") from disabled repository " + repository.getId() + " ("
104                                               + repository.getUrl() + ")" );
105                    }
106                }
107                else if ( request.isForceUpdate() )
108                {
109                    update = true;
110                }
111                else if ( localCopyLastModified != null && !policy.checkOutOfDate( localCopyLastModified ) )
112                {
113                    update = false;
114
115                    if ( getLogger().isDebugEnabled() )
116                    {
117                        getLogger().debug(
118                            "Skipping update check for " + metadata.getKey() + " (" + file + ") from repository "
119                                + repository.getId() + " (" + repository.getUrl() + ") in favor of local copy" );
120                    }
121                }
122                else
123                {
124                    update = updateCheckManager.isUpdateRequired( metadata, repository, file );
125                }
126
127                if ( update )
128                {
129                    getLogger().info( metadata.getKey() + ": checking for updates from " + repository.getId() );
130                    try
131                    {
132                        wagonManager.getArtifactMetadata( metadata, repository, file, policy.getChecksumPolicy() );
133                    }
134                    catch ( ResourceDoesNotExistException e )
135                    {
136                        getLogger().debug( metadata + " could not be found on repository: " + repository.getId() );
137
138                        // delete the local copy so the old details aren't used.
139                        if ( file.exists() )
140                        {
141                            if ( !file.delete() )
142                            {
143                                // sleep for 10ms just in case this is windows holding a file lock
144                                try
145                                {
146                                    Thread.sleep( 10 );
147                                }
148                                catch ( InterruptedException ie )
149                                {
150                                    // ignore
151                                }
152                                file.delete(); // if this fails, forget about it
153                            }
154                        }
155                    }
156                    catch ( TransferFailedException e )
157                    {
158                        getLogger().warn( metadata + " could not be retrieved from repository: " + repository.getId()
159                                              + " due to an error: " + e.getMessage() );
160                        getLogger().debug( "Exception", e );
161                    }
162                    finally
163                    {
164                        updateCheckManager.touch( metadata, repository, file );
165                    }
166                }
167
168                // TODO: should this be inside the above check?
169                // touch file so that this is not checked again until interval has passed
170                if ( file.exists() )
171                {
172                    file.setLastModified( System.currentTimeMillis() );
173                }
174            }
175        }
176
177        try
178        {
179            mergeMetadata( metadata, remoteRepositories, localRepo );
180        }
181        catch ( RepositoryMetadataStoreException e )
182        {
183            throw new RepositoryMetadataResolutionException(
184                "Unable to store local copy of metadata: " + e.getMessage(), e );
185        }
186    }
187
188    private Date getLocalCopyLastModified( ArtifactRepository localRepository, RepositoryMetadata metadata )
189    {
190        String metadataPath = localRepository.pathOfLocalRepositoryMetadata( metadata, localRepository );
191        File metadataFile = new File( localRepository.getBasedir(), metadataPath );
192        return metadataFile.isFile() ? new Date( metadataFile.lastModified() ) : null;
193    }
194
195    private void mergeMetadata( RepositoryMetadata metadata, List<ArtifactRepository> remoteRepositories,
196                                ArtifactRepository localRepository )
197        throws RepositoryMetadataStoreException
198    {
199        // TODO: currently this is first wins, but really we should take the latest by comparing either the
200        // snapshot timestamp, or some other timestamp later encoded into the metadata.
201        // TODO: this needs to be repeated here so the merging doesn't interfere with the written metadata
202        //  - we'd be much better having a pristine input, and an ongoing metadata for merging instead
203
204        Map<ArtifactRepository, Metadata> previousMetadata = new HashMap<>();
205        ArtifactRepository selected = null;
206        for ( ArtifactRepository repository : remoteRepositories )
207        {
208            ArtifactRepositoryPolicy policy = metadata.getPolicy( repository );
209
210            if ( policy.isEnabled() && loadMetadata( metadata, repository, localRepository, previousMetadata ) )
211            {
212                metadata.setRepository( repository );
213                selected = repository;
214            }
215        }
216        if ( loadMetadata( metadata, localRepository, localRepository, previousMetadata ) )
217        {
218            metadata.setRepository( null );
219            selected = localRepository;
220        }
221
222        updateSnapshotMetadata( metadata, previousMetadata, selected, localRepository );
223    }
224
225    private void updateSnapshotMetadata( RepositoryMetadata metadata,
226                                         Map<ArtifactRepository, Metadata> previousMetadata,
227                                         ArtifactRepository selected, ArtifactRepository localRepository )
228        throws RepositoryMetadataStoreException
229    {
230        // TODO: this could be a lot nicer... should really be in the snapshot transformation?
231        if ( metadata.isSnapshot() )
232        {
233            Metadata prevMetadata = metadata.getMetadata();
234
235            for ( ArtifactRepository repository : previousMetadata.keySet() )
236            {
237                Metadata m = previousMetadata.get( repository );
238                if ( repository.equals( selected ) )
239                {
240                    if ( m.getVersioning() == null )
241                    {
242                        m.setVersioning( new Versioning() );
243                    }
244
245                    if ( m.getVersioning().getSnapshot() == null )
246                    {
247                        m.getVersioning().setSnapshot( new Snapshot() );
248                    }
249                }
250                else
251                {
252                    if ( ( m.getVersioning() != null ) && ( m.getVersioning().getSnapshot() != null )
253                        && m.getVersioning().getSnapshot().isLocalCopy() )
254                    {
255                        m.getVersioning().getSnapshot().setLocalCopy( false );
256                        metadata.setMetadata( m );
257                        metadata.storeInLocalRepository( localRepository, repository );
258                    }
259                }
260            }
261
262            metadata.setMetadata( prevMetadata );
263        }
264    }
265
266    private boolean loadMetadata( RepositoryMetadata repoMetadata, ArtifactRepository remoteRepository,
267                                  ArtifactRepository localRepository,
268                                  Map<ArtifactRepository, Metadata> previousMetadata )
269    {
270        boolean setRepository = false;
271
272        File metadataFile = new File( localRepository.getBasedir(),
273                                      localRepository.pathOfLocalRepositoryMetadata( repoMetadata, remoteRepository ) );
274
275        if ( metadataFile.exists() )
276        {
277            Metadata metadata;
278
279            try
280            {
281                metadata = readMetadata( metadataFile );
282            }
283            catch ( RepositoryMetadataReadException e )
284            {
285                if ( getLogger().isDebugEnabled() )
286                {
287                    getLogger().warn( e.getMessage(), e );
288                }
289                else
290                {
291                    getLogger().warn( e.getMessage() );
292                }
293                return setRepository;
294            }
295
296            if ( repoMetadata.isSnapshot() && ( previousMetadata != null ) )
297            {
298                previousMetadata.put( remoteRepository, metadata );
299            }
300
301            if ( repoMetadata.getMetadata() != null )
302            {
303                setRepository = repoMetadata.getMetadata().merge( metadata );
304            }
305            else
306            {
307                repoMetadata.setMetadata( metadata );
308                setRepository = true;
309            }
310        }
311        return setRepository;
312    }
313
314    /**
315     * @todo share with DefaultPluginMappingManager.
316     */
317    protected Metadata readMetadata( File mappingFile )
318        throws RepositoryMetadataReadException
319    {
320        Metadata result;
321
322        try ( Reader reader = ReaderFactory.newXmlReader( mappingFile ) )
323        {
324            MetadataXpp3Reader mappingReader = new MetadataXpp3Reader();
325
326            result = mappingReader.read( reader, false );
327        }
328        catch ( FileNotFoundException e )
329        {
330            throw new RepositoryMetadataReadException( "Cannot read metadata from '" + mappingFile + "'", e );
331        }
332        catch ( IOException | XmlPullParserException e )
333        {
334            throw new RepositoryMetadataReadException(
335                "Cannot read metadata from '" + mappingFile + "': " + e.getMessage(), e );
336        }
337        return result;
338    }
339
340    /**
341     * Ensures the last updated timestamp of the specified metadata does not refer to the future and fixes the local
342     * metadata if necessary to allow proper merging/updating of metadata during deployment.
343     */
344    private void fixTimestamp( File metadataFile, Metadata metadata, Metadata reference )
345    {
346        boolean changed = false;
347
348        if ( metadata != null && reference != null )
349        {
350            Versioning versioning = metadata.getVersioning();
351            Versioning versioningRef = reference.getVersioning();
352            if ( versioning != null && versioningRef != null )
353            {
354                String lastUpdated = versioning.getLastUpdated();
355                String now = versioningRef.getLastUpdated();
356                if ( lastUpdated != null && now != null && now.compareTo( lastUpdated ) < 0 )
357                {
358                    getLogger().warn(
359                        "The last updated timestamp in " + metadataFile + " refers to the future (now = " + now
360                            + ", lastUpdated = " + lastUpdated + "). Please verify that the clocks of all"
361                            + " deploying machines are reasonably synchronized." );
362                    versioning.setLastUpdated( now );
363                    changed = true;
364                }
365            }
366        }
367
368        if ( changed )
369        {
370            getLogger().debug( "Repairing metadata in " + metadataFile );
371
372            try ( Writer writer = WriterFactory.newXmlWriter( metadataFile ) )
373            {
374                new MetadataXpp3Writer().write( writer, metadata );
375            }
376            catch ( IOException e )
377            {
378                String msg = "Could not write fixed metadata to " + metadataFile + ": " + e.getMessage();
379                if ( getLogger().isDebugEnabled() )
380                {
381                    getLogger().warn( msg, e );
382                }
383                else
384                {
385                    getLogger().warn( msg );
386                }
387            }
388        }
389    }
390
391    public void resolveAlways( RepositoryMetadata metadata, ArtifactRepository localRepository,
392                               ArtifactRepository remoteRepository )
393        throws RepositoryMetadataResolutionException
394    {
395        File file;
396        try
397        {
398            file = getArtifactMetadataFromDeploymentRepository( metadata, localRepository, remoteRepository );
399        }
400        catch ( TransferFailedException e )
401        {
402            throw new RepositoryMetadataResolutionException(
403                metadata + " could not be retrieved from repository: " + remoteRepository.getId() + " due to an error: "
404                    + e.getMessage(), e );
405        }
406
407        try
408        {
409            if ( file.exists() )
410            {
411                Metadata prevMetadata = readMetadata( file );
412                metadata.setMetadata( prevMetadata );
413            }
414        }
415        catch ( RepositoryMetadataReadException e )
416        {
417            throw new RepositoryMetadataResolutionException( e.getMessage(), e );
418        }
419    }
420
421    private File getArtifactMetadataFromDeploymentRepository( ArtifactMetadata metadata, ArtifactRepository localRepo,
422                                                              ArtifactRepository remoteRepository )
423        throws TransferFailedException
424    {
425        File file =
426            new File( localRepo.getBasedir(), localRepo.pathOfLocalRepositoryMetadata( metadata, remoteRepository ) );
427
428        try
429        {
430            wagonManager.getArtifactMetadataFromDeploymentRepository( metadata, remoteRepository, file,
431                                                                      ArtifactRepositoryPolicy.CHECKSUM_POLICY_WARN );
432        }
433        catch ( ResourceDoesNotExistException e )
434        {
435            getLogger().info(
436                metadata + " could not be found on repository: " + remoteRepository.getId() + ", so will be created" );
437
438            // delete the local copy so the old details aren't used.
439            if ( file.exists() )
440            {
441                if ( !file.delete() )
442                {
443                    // sleep for 10ms just in case this is windows holding a file lock
444                    try
445                    {
446                        Thread.sleep( 10 );
447                    }
448                    catch ( InterruptedException ie )
449                    {
450                        // ignore
451                    }
452                    file.delete(); // if this fails, forget about it
453                }
454            }
455        }
456        finally
457        {
458            if ( metadata instanceof RepositoryMetadata )
459            {
460                updateCheckManager.touch( (RepositoryMetadata) metadata, remoteRepository, file );
461            }
462        }
463        return file;
464    }
465
466    public void deploy( ArtifactMetadata metadata, ArtifactRepository localRepository,
467                        ArtifactRepository deploymentRepository )
468        throws RepositoryMetadataDeploymentException
469    {
470        File file;
471        if ( metadata instanceof RepositoryMetadata )
472        {
473            getLogger().info( "Retrieving previous metadata from " + deploymentRepository.getId() );
474            try
475            {
476                file = getArtifactMetadataFromDeploymentRepository( metadata, localRepository, deploymentRepository );
477            }
478            catch ( TransferFailedException e )
479            {
480                throw new RepositoryMetadataDeploymentException(
481                    metadata + " could not be retrieved from repository: " + deploymentRepository.getId()
482                        + " due to an error: " + e.getMessage(), e );
483            }
484
485            if ( file.isFile() )
486            {
487                try
488                {
489                    fixTimestamp( file, readMetadata( file ), ( (RepositoryMetadata) metadata ).getMetadata() );
490                }
491                catch ( RepositoryMetadataReadException e )
492                {
493                    // will be reported via storeInlocalRepository
494                }
495            }
496        }
497        else
498        {
499            // It's a POM - we don't need to retrieve it first
500            file = new File( localRepository.getBasedir(),
501                             localRepository.pathOfLocalRepositoryMetadata( metadata, deploymentRepository ) );
502        }
503
504        try
505        {
506            metadata.storeInLocalRepository( localRepository, deploymentRepository );
507        }
508        catch ( RepositoryMetadataStoreException e )
509        {
510            throw new RepositoryMetadataDeploymentException( "Error installing metadata: " + e.getMessage(), e );
511        }
512
513        try
514        {
515            wagonManager.putArtifactMetadata( file, metadata, deploymentRepository );
516        }
517        catch ( TransferFailedException e )
518        {
519            throw new RepositoryMetadataDeploymentException( "Error while deploying metadata: " + e.getMessage(), e );
520        }
521    }
522
523    public void install( ArtifactMetadata metadata, ArtifactRepository localRepository )
524        throws RepositoryMetadataInstallationException
525    {
526        try
527        {
528            metadata.storeInLocalRepository( localRepository, localRepository );
529        }
530        catch ( RepositoryMetadataStoreException e )
531        {
532            throw new RepositoryMetadataInstallationException( "Error installing metadata: " + e.getMessage(), e );
533        }
534    }
535
536}