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