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