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                            file.delete();
145                        }
146                    }
147                    catch ( TransferFailedException e )
148                    {
149                        getLogger().warn( metadata + " could not be retrieved from repository: " + repository.getId()
150                                              + " due to an error: " + e.getMessage() );
151                        getLogger().debug( "Exception", e );
152                    }
153                    finally
154                    {
155                        updateCheckManager.touch( metadata, repository, file );
156                    }
157                }
158
159                // TODO: should this be inside the above check?
160                // touch file so that this is not checked again until interval has passed
161                if ( file.exists() )
162                {
163                    file.setLastModified( System.currentTimeMillis() );
164                }
165            }
166        }
167
168        try
169        {
170            mergeMetadata( metadata, remoteRepositories, localRepo );
171        }
172        catch ( RepositoryMetadataStoreException e )
173        {
174            throw new RepositoryMetadataResolutionException( "Unable to store local copy of metadata: "
175                + e.getMessage(), e );
176        }
177    }
178
179    private Date getLocalCopyLastModified( ArtifactRepository localRepository, RepositoryMetadata metadata )
180    {
181        String metadataPath = localRepository.pathOfLocalRepositoryMetadata( metadata, localRepository );
182        File metadataFile = new File( localRepository.getBasedir(), metadataPath );
183        return metadataFile.isFile() ? new Date( metadataFile.lastModified() ) : null;
184    }
185
186    private void mergeMetadata( RepositoryMetadata metadata, List<ArtifactRepository> remoteRepositories,
187                                ArtifactRepository localRepository )
188        throws RepositoryMetadataStoreException
189    {
190        // TODO: currently this is first wins, but really we should take the latest by comparing either the
191        // snapshot timestamp, or some other timestamp later encoded into the metadata.
192        // TODO: this needs to be repeated here so the merging doesn't interfere with the written metadata
193        //  - we'd be much better having a pristine input, and an ongoing metadata for merging instead
194
195        Map<ArtifactRepository, Metadata> previousMetadata = new HashMap<ArtifactRepository, Metadata>();
196        ArtifactRepository selected = null;
197        for ( ArtifactRepository repository : remoteRepositories )
198        {
199            ArtifactRepositoryPolicy policy = metadata.getPolicy( repository );
200
201            if ( policy.isEnabled() && loadMetadata( metadata, repository, localRepository, previousMetadata ) )
202            {
203                metadata.setRepository( repository );
204                selected = repository;
205            }
206        }
207        if ( loadMetadata( metadata, localRepository, localRepository, previousMetadata ) )
208        {
209            metadata.setRepository( null );
210            selected = localRepository;
211        }
212
213        updateSnapshotMetadata( metadata, previousMetadata, selected, localRepository );
214    }
215
216    private void updateSnapshotMetadata( RepositoryMetadata metadata,
217                                         Map<ArtifactRepository, Metadata> previousMetadata,
218                                         ArtifactRepository selected, ArtifactRepository localRepository )
219        throws RepositoryMetadataStoreException
220    {
221        // TODO: this could be a lot nicer... should really be in the snapshot transformation?
222        if ( metadata.isSnapshot() )
223        {
224            Metadata prevMetadata = metadata.getMetadata();
225
226            for ( ArtifactRepository repository : previousMetadata.keySet() )
227            {
228                Metadata m = previousMetadata.get( repository );
229                if ( repository.equals( selected ) )
230                {
231                    if ( m.getVersioning() == null )
232                    {
233                        m.setVersioning( new Versioning() );
234                    }
235
236                    if ( m.getVersioning().getSnapshot() == null )
237                    {
238                        m.getVersioning().setSnapshot( new Snapshot() );
239                    }
240                }
241                else
242                {
243                    if ( ( m.getVersioning() != null ) && ( m.getVersioning().getSnapshot() != null )
244                        && m.getVersioning().getSnapshot().isLocalCopy() )
245                    {
246                        m.getVersioning().getSnapshot().setLocalCopy( false );
247                        metadata.setMetadata( m );
248                        metadata.storeInLocalRepository( localRepository, repository );
249                    }
250                }
251            }
252
253            metadata.setMetadata( prevMetadata );
254        }
255    }
256
257    private boolean loadMetadata( RepositoryMetadata repoMetadata, ArtifactRepository remoteRepository,
258                                  ArtifactRepository localRepository, Map<ArtifactRepository,
259                                  Metadata> previousMetadata )
260    {
261        boolean setRepository = false;
262
263        File metadataFile =
264            new File( localRepository.getBasedir(), localRepository.pathOfLocalRepositoryMetadata( repoMetadata,
265                                                                                                   remoteRepository ) );
266
267        if ( metadataFile.exists() )
268        {
269            Metadata metadata;
270
271            try
272            {
273                metadata = readMetadata( metadataFile );
274            }
275            catch ( RepositoryMetadataReadException e )
276            {
277                if ( getLogger().isDebugEnabled() )
278                {
279                    getLogger().warn( e.getMessage(), e );
280                }
281                else
282                {
283                    getLogger().warn( e.getMessage() );
284                }
285                return setRepository;
286            }
287
288            if ( repoMetadata.isSnapshot() && ( previousMetadata != null ) )
289            {
290                previousMetadata.put( remoteRepository, metadata );
291            }
292
293            if ( repoMetadata.getMetadata() != null )
294            {
295                setRepository = repoMetadata.getMetadata().merge( metadata );
296            }
297            else
298            {
299                repoMetadata.setMetadata( metadata );
300                setRepository = true;
301            }
302        }
303        return setRepository;
304    }
305
306    /** @todo share with DefaultPluginMappingManager. */
307    protected Metadata readMetadata( File mappingFile )
308        throws RepositoryMetadataReadException
309    {
310        Metadata result;
311
312        Reader reader = null;
313        try
314        {
315            reader = ReaderFactory.newXmlReader( mappingFile );
316
317            MetadataXpp3Reader mappingReader = new MetadataXpp3Reader();
318
319            result = mappingReader.read( reader, false );
320        }
321        catch ( FileNotFoundException e )
322        {
323            throw new RepositoryMetadataReadException( "Cannot read metadata from '" + mappingFile + "'", e );
324        }
325        catch ( IOException e )
326        {
327            throw new RepositoryMetadataReadException( "Cannot read metadata from '" + mappingFile + "': "
328                + e.getMessage(), e );
329        }
330        catch ( XmlPullParserException e )
331        {
332            throw new RepositoryMetadataReadException( "Cannot read metadata from '" + mappingFile + "': "
333                + e.getMessage(), e );
334        }
335        finally
336        {
337            IOUtil.close( reader );
338        }
339
340        return result;
341    }
342
343    /**
344     * Ensures the last updated timestamp of the specified metadata does not refer to the future and fixes the local
345     * metadata if necessary to allow proper merging/updating of metadata during deployment.
346     */
347    private void fixTimestamp( File metadataFile, Metadata metadata, Metadata reference )
348    {
349        boolean changed = false;
350
351        if ( metadata != null && reference != null )
352        {
353            Versioning versioning = metadata.getVersioning();
354            Versioning versioningRef = reference.getVersioning();
355            if ( versioning != null && versioningRef != null )
356            {
357                String lastUpdated = versioning.getLastUpdated();
358                String now = versioningRef.getLastUpdated();
359                if ( lastUpdated != null && now != null && now.compareTo( lastUpdated ) < 0 )
360                {
361                    getLogger().warn(
362                                      "The last updated timestamp in " + metadataFile + " refers to the future (now = "
363                                          + now + ", lastUpdated = " + lastUpdated
364                                          + "). Please verify that the clocks of all"
365                                          + " deploying machines are reasonably synchronized." );
366                    versioning.setLastUpdated( now );
367                    changed = true;
368                }
369            }
370        }
371
372        if ( changed )
373        {
374            getLogger().debug( "Repairing metadata in " + metadataFile );
375
376            Writer writer = null;
377            try
378            {
379                writer = WriterFactory.newXmlWriter( metadataFile );
380                new MetadataXpp3Writer().write( writer, metadata );
381            }
382            catch ( IOException e )
383            {
384                String msg = "Could not write fixed metadata to " + metadataFile + ": " + e.getMessage();
385                if ( getLogger().isDebugEnabled() )
386                {
387                    getLogger().warn( msg, e );
388                }
389                else
390                {
391                    getLogger().warn( msg );
392                }
393            }
394            finally
395            {
396                IOUtil.close( writer );
397            }
398        }
399    }
400
401    public void resolveAlways( RepositoryMetadata metadata, ArtifactRepository localRepository,
402                               ArtifactRepository remoteRepository )
403        throws RepositoryMetadataResolutionException
404    {
405        File file;
406        try
407        {
408            file = getArtifactMetadataFromDeploymentRepository( metadata, localRepository, remoteRepository );
409        }
410        catch ( TransferFailedException e )
411        {
412            throw new RepositoryMetadataResolutionException( metadata + " could not be retrieved from repository: "
413                + remoteRepository.getId() + " due to an error: " + e.getMessage(), e );
414        }
415
416        try
417        {
418            if ( file.exists() )
419            {
420                Metadata prevMetadata = readMetadata( file );
421                metadata.setMetadata( prevMetadata );
422            }
423        }
424        catch ( RepositoryMetadataReadException e )
425        {
426            throw new RepositoryMetadataResolutionException( e.getMessage(), e );
427        }
428    }
429
430    private File getArtifactMetadataFromDeploymentRepository( ArtifactMetadata metadata,
431                                                              ArtifactRepository localRepo,
432                                                              ArtifactRepository remoteRepository )
433        throws TransferFailedException
434    {
435        File file =
436            new File( localRepo.getBasedir(), localRepo.pathOfLocalRepositoryMetadata( metadata, remoteRepository ) );
437
438        try
439        {
440            wagonManager.getArtifactMetadataFromDeploymentRepository( metadata, remoteRepository, file,
441                                                                      ArtifactRepositoryPolicy.CHECKSUM_POLICY_WARN );
442        }
443        catch ( ResourceDoesNotExistException e )
444        {
445            getLogger().info( metadata + " could not be found on repository: " + remoteRepository.getId()
446                                  + ", so will be created" );
447
448            // delete the local copy so the old details aren't used.
449            if ( file.exists() )
450            {
451                file.delete();
452            }
453        }
454        finally
455        {
456            if ( metadata instanceof RepositoryMetadata )
457            {
458                updateCheckManager.touch( (RepositoryMetadata) metadata, remoteRepository, file );
459            }
460        }
461        return file;
462    }
463
464    public void deploy( ArtifactMetadata metadata, ArtifactRepository localRepository,
465                        ArtifactRepository deploymentRepository )
466        throws RepositoryMetadataDeploymentException
467    {
468        File file;
469        if ( metadata instanceof RepositoryMetadata )
470        {
471            getLogger().info( "Retrieving previous metadata from " + deploymentRepository.getId() );
472            try
473            {
474                file = getArtifactMetadataFromDeploymentRepository( metadata, localRepository, deploymentRepository );
475            }
476            catch ( TransferFailedException e )
477            {
478                throw new RepositoryMetadataDeploymentException( metadata + " could not be retrieved from repository: "
479                    + deploymentRepository.getId() + " due to an error: " + e.getMessage(), e );
480            }
481
482            if ( file.isFile() )
483            {
484                try
485                {
486                    fixTimestamp( file, readMetadata( file ), ( (RepositoryMetadata) metadata ).getMetadata() );
487                }
488                catch ( RepositoryMetadataReadException e )
489                {
490                    // will be reported via storeInlocalRepository
491                }
492            }
493        }
494        else
495        {
496            // It's a POM - we don't need to retrieve it first
497            file =
498                new File( localRepository.getBasedir(),
499                          localRepository.pathOfLocalRepositoryMetadata( metadata, deploymentRepository ) );
500        }
501
502        try
503        {
504            metadata.storeInLocalRepository( localRepository, deploymentRepository );
505        }
506        catch ( RepositoryMetadataStoreException e )
507        {
508            throw new RepositoryMetadataDeploymentException( "Error installing metadata: " + e.getMessage(), e );
509        }
510
511        try
512        {
513            wagonManager.putArtifactMetadata( file, metadata, deploymentRepository );
514        }
515        catch ( TransferFailedException e )
516        {
517            throw new RepositoryMetadataDeploymentException( "Error while deploying metadata: " + e.getMessage(), e );
518        }
519    }
520
521    public void install( ArtifactMetadata metadata, ArtifactRepository localRepository )
522        throws RepositoryMetadataInstallationException
523    {
524        try
525        {
526            metadata.storeInLocalRepository( localRepository, localRepository );
527        }
528        catch ( RepositoryMetadataStoreException e )
529        {
530            throw new RepositoryMetadataInstallationException( "Error installing metadata: " + e.getMessage(), e );
531        }
532    }
533
534}