001package org.apache.maven.repository.legacy;
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.ByteArrayInputStream;
023import java.io.ByteArrayOutputStream;
024import java.io.File;
025import java.io.FileInputStream;
026import java.io.IOException;
027import java.io.RandomAccessFile;
028import java.nio.ByteBuffer;
029import java.nio.channels.FileChannel;
030import java.nio.channels.FileLock;
031import java.util.Date;
032import java.util.Properties;
033
034import org.apache.maven.artifact.Artifact;
035import org.apache.maven.artifact.repository.ArtifactRepository;
036import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy;
037import org.apache.maven.artifact.repository.Authentication;
038import org.apache.maven.artifact.repository.metadata.RepositoryMetadata;
039import org.apache.maven.repository.Proxy;
040import org.codehaus.plexus.component.annotations.Component;
041import org.codehaus.plexus.logging.AbstractLogEnabled;
042import org.codehaus.plexus.logging.Logger;
043import org.codehaus.plexus.util.IOUtil;
044
045@Component( role = UpdateCheckManager.class )
046public class DefaultUpdateCheckManager
047    extends AbstractLogEnabled
048    implements UpdateCheckManager
049{
050
051    private static final String ERROR_KEY_SUFFIX = ".error";
052
053    public DefaultUpdateCheckManager()
054    {
055
056    }
057
058    public DefaultUpdateCheckManager( Logger logger )
059    {
060        enableLogging( logger );
061    }
062
063    public static final String LAST_UPDATE_TAG = ".lastUpdated";
064
065    private static final String TOUCHFILE_NAME = "resolver-status.properties";
066
067    public boolean isUpdateRequired( Artifact artifact, ArtifactRepository repository )
068    {
069        File file = artifact.getFile();
070
071        ArtifactRepositoryPolicy policy = artifact.isSnapshot() ? repository.getSnapshots() : repository.getReleases();
072
073        if ( !policy.isEnabled() )
074        {
075            if ( getLogger().isDebugEnabled() )
076            {
077                getLogger().debug(
078                                   "Skipping update check for " + artifact + " (" + file + ") from "
079                                       + repository.getId() + " (" + repository.getUrl() + ")" );
080            }
081
082            return false;
083        }
084
085        if ( getLogger().isDebugEnabled() )
086        {
087            getLogger().debug(
088                               "Determining update check for " + artifact + " (" + file + ") from "
089                                   + repository.getId() + " (" + repository.getUrl() + ")" );
090        }
091
092        if ( file == null )
093        {
094            // TODO throw something instead?
095            return true;
096        }
097
098        Date lastCheckDate;
099
100        if ( file.exists() )
101        {
102            lastCheckDate = new Date ( file.lastModified() );
103        }
104        else
105        {
106            File touchfile = getTouchfile( artifact );
107            lastCheckDate = readLastUpdated( touchfile, getRepositoryKey( repository ) );
108        }
109
110        return ( lastCheckDate == null ) || policy.checkOutOfDate( lastCheckDate );
111    }
112
113    public boolean isUpdateRequired( RepositoryMetadata metadata, ArtifactRepository repository, File file )
114    {
115        // Here, we need to determine which policy to use. Release updateInterval will be used when
116        // the metadata refers to a release artifact or meta-version, and snapshot updateInterval will be used when
117        // it refers to a snapshot artifact or meta-version.
118        // NOTE: Release metadata includes version information about artifacts that have been released, to allow
119        // meta-versions like RELEASE and LATEST to resolve, and also to allow retrieval of the range of valid, released
120        // artifacts available.
121        ArtifactRepositoryPolicy policy = metadata.getPolicy( repository );
122
123        if ( !policy.isEnabled() )
124        {
125            if ( getLogger().isDebugEnabled() )
126            {
127                getLogger().debug(
128                                   "Skipping update check for " + metadata.getKey() + " (" + file + ") from "
129                                       + repository.getId() + " (" + repository.getUrl() + ")" );
130            }
131
132            return false;
133        }
134
135        if ( getLogger().isDebugEnabled() )
136        {
137            getLogger().debug(
138                               "Determining update check for " + metadata.getKey() + " (" + file + ") from "
139                                   + repository.getId() + " (" + repository.getUrl() + ")" );
140        }
141
142        if ( file == null )
143        {
144            // TODO throw something instead?
145            return true;
146        }
147
148        Date lastCheckDate = readLastUpdated( metadata, repository, file );
149
150        return ( lastCheckDate == null ) || policy.checkOutOfDate( lastCheckDate );
151    }
152
153    private Date readLastUpdated( RepositoryMetadata metadata, ArtifactRepository repository, File file )
154    {
155        File touchfile = getTouchfile( metadata, file );
156
157        String key = getMetadataKey( repository, file );
158
159        return readLastUpdated( touchfile, key );
160    }
161
162    public String getError( Artifact artifact, ArtifactRepository repository )
163    {
164        File touchFile = getTouchfile( artifact );
165        return getError( touchFile, getRepositoryKey( repository ) );
166    }
167
168    public void touch( Artifact artifact, ArtifactRepository repository, String error )
169    {
170        File file = artifact.getFile();
171
172        File touchfile = getTouchfile( artifact );
173
174        if ( file.exists() )
175        {
176            touchfile.delete();
177        }
178        else
179        {
180            writeLastUpdated( touchfile, getRepositoryKey( repository ), error );
181        }
182    }
183
184    public void touch( RepositoryMetadata metadata, ArtifactRepository repository, File file )
185    {
186        File touchfile = getTouchfile( metadata, file );
187
188        String key = getMetadataKey( repository, file );
189
190        writeLastUpdated( touchfile, key, null );
191    }
192
193    String getMetadataKey( ArtifactRepository repository, File file )
194    {
195        return repository.getId() + '.' + file.getName() + LAST_UPDATE_TAG;
196    }
197
198    String getRepositoryKey( ArtifactRepository repository )
199    {
200        StringBuilder buffer = new StringBuilder( 256 );
201
202        Proxy proxy = repository.getProxy();
203        if ( proxy != null )
204        {
205            if ( proxy.getUserName() != null )
206            {
207                int hash = ( proxy.getUserName() + proxy.getPassword() ).hashCode();
208                buffer.append( hash ).append( '@' );
209            }
210            buffer.append( proxy.getHost() ).append( ':' ).append( proxy.getPort() ).append( '>' );
211        }
212
213        // consider the username&password because a repo manager might block artifacts depending on authorization
214        Authentication auth = repository.getAuthentication();
215        if ( auth != null )
216        {
217            int hash = ( auth.getUsername() + auth.getPassword() ).hashCode();
218            buffer.append( hash ).append( '@' );
219        }
220
221        // consider the URL (instead of the id) as this most closely relates to the contents in the repo
222        buffer.append( repository.getUrl() );
223
224        return buffer.toString();
225    }
226
227    private void writeLastUpdated( File touchfile, String key, String error )
228    {
229        synchronized ( touchfile.getAbsolutePath().intern() )
230        {
231            if ( !touchfile.getParentFile().exists() && !touchfile.getParentFile().mkdirs() )
232            {
233                getLogger().debug( "Failed to create directory: " + touchfile.getParent()
234                                       + " for tracking artifact metadata resolution." );
235                return;
236            }
237
238            FileChannel channel = null;
239            FileLock lock = null;
240            try
241            {
242                Properties props = new Properties();
243
244                channel = new RandomAccessFile( touchfile, "rw" ).getChannel();
245                lock = channel.lock( 0, channel.size(), false );
246
247                if ( touchfile.canRead() )
248                {
249                    getLogger().debug( "Reading resolution-state from: " + touchfile );
250                    ByteBuffer buffer = ByteBuffer.allocate( (int) channel.size() );
251
252                    channel.read( buffer );
253                    buffer.flip();
254
255                    ByteArrayInputStream stream = new ByteArrayInputStream( buffer.array() );
256                    props.load( stream );
257                }
258
259                props.setProperty( key, Long.toString( System.currentTimeMillis() ) );
260
261                if ( error != null )
262                {
263                    props.setProperty( key + ERROR_KEY_SUFFIX, error );
264                }
265                else
266                {
267                    props.remove( key + ERROR_KEY_SUFFIX );
268                }
269
270                ByteArrayOutputStream stream = new ByteArrayOutputStream();
271
272                getLogger().debug( "Writing resolution-state to: " + touchfile );
273                props.store( stream, "Last modified on: " + new Date() );
274
275                byte[] data = stream.toByteArray();
276                ByteBuffer buffer = ByteBuffer.allocate( data.length );
277                buffer.put( data );
278                buffer.flip();
279
280                channel.position( 0 );
281                channel.write( buffer );
282            }
283            catch ( IOException e )
284            {
285                getLogger().debug( "Failed to record lastUpdated information for resolution.\nFile: "
286                                       + touchfile.toString() + "; key: " + key, e );
287            }
288            finally
289            {
290                if ( lock != null )
291                {
292                    try
293                    {
294                        lock.release();
295                    }
296                    catch ( IOException e )
297                    {
298                        getLogger().debug( "Error releasing exclusive lock for resolution tracking file: "
299                                               + touchfile, e );
300                    }
301                }
302
303                if ( channel != null )
304                {
305                    try
306                    {
307                        channel.close();
308                    }
309                    catch ( IOException e )
310                    {
311                        getLogger().debug( "Error closing FileChannel for resolution tracking file: "
312                                               + touchfile, e );
313                    }
314                }
315            }
316        }
317    }
318
319    Date readLastUpdated( File touchfile, String key )
320    {
321        getLogger().debug( "Searching for " + key + " in resolution tracking file." );
322
323        Properties props = read( touchfile );
324        if ( props != null )
325        {
326            String rawVal = props.getProperty( key );
327            if ( rawVal != null )
328            {
329                try
330                {
331                    return new Date( Long.parseLong( rawVal ) );
332                }
333                catch ( NumberFormatException e )
334                {
335                    getLogger().debug( "Cannot parse lastUpdated date: \'" + rawVal + "\'. Ignoring.", e );
336                }
337            }
338        }
339        return null;
340    }
341
342    private String getError( File touchFile, String key )
343    {
344        Properties props = read( touchFile );
345        if ( props != null )
346        {
347            return props.getProperty( key + ERROR_KEY_SUFFIX );
348        }
349        return null;
350    }
351
352    private Properties read( File touchfile )
353    {
354        if ( !touchfile.canRead() )
355        {
356            getLogger().debug( "Skipped unreadable resolution tracking file " + touchfile );
357            return null;
358        }
359
360        synchronized ( touchfile.getAbsolutePath().intern() )
361        {
362            FileLock lock = null;
363            FileChannel channel = null;
364            try
365            {
366                Properties props = new Properties();
367
368                FileInputStream stream = new FileInputStream( touchfile );
369                try
370                {
371                    channel = stream.getChannel();
372                    lock = channel.lock( 0, channel.size(), true );
373
374                    getLogger().debug( "Reading resolution-state from: " + touchfile );
375                    props.load( stream );
376
377                    return props;
378                }
379                finally
380                {
381                    IOUtil.close( stream );
382                }
383            }
384            catch ( IOException e )
385            {
386                getLogger().debug( "Failed to read resolution tracking file " + touchfile, e );
387
388                return null;
389            }
390            finally
391            {
392                if ( lock != null )
393                {
394                    try
395                    {
396                        lock.release();
397                    }
398                    catch ( IOException e )
399                    {
400                        getLogger().debug( "Error releasing shared lock for resolution tracking file: " + touchfile,
401                                           e );
402                    }
403                }
404
405                if ( channel != null )
406                {
407                    try
408                    {
409                        channel.close();
410                    }
411                    catch ( IOException e )
412                    {
413                        getLogger().debug( "Error closing FileChannel for resolution tracking file: " + touchfile, e );
414                    }
415                }
416            }
417        }
418    }
419
420    File getTouchfile( Artifact artifact )
421    {
422        StringBuilder sb = new StringBuilder( 128 );
423        sb.append( artifact.getArtifactId() );
424        sb.append( '-' ).append( artifact.getBaseVersion() );
425        if ( artifact.getClassifier() != null )
426        {
427            sb.append( '-' ).append( artifact.getClassifier() );
428        }
429        sb.append( '.' ).append( artifact.getType() ).append( LAST_UPDATE_TAG );
430        return new File( artifact.getFile().getParentFile(), sb.toString() );
431    }
432
433    File getTouchfile( RepositoryMetadata metadata, File file )
434    {
435        return new File( file.getParent(), TOUCHFILE_NAME );
436    }
437
438}