001    package 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    
022    import java.io.ByteArrayInputStream;
023    import java.io.ByteArrayOutputStream;
024    import java.io.File;
025    import java.io.FileInputStream;
026    import java.io.IOException;
027    import java.io.RandomAccessFile;
028    import java.nio.ByteBuffer;
029    import java.nio.channels.FileChannel;
030    import java.nio.channels.FileLock;
031    import java.util.Date;
032    import java.util.Properties;
033    
034    import org.apache.maven.artifact.Artifact;
035    import org.apache.maven.artifact.repository.ArtifactRepository;
036    import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy;
037    import org.apache.maven.artifact.repository.Authentication;
038    import org.apache.maven.artifact.repository.metadata.RepositoryMetadata;
039    import org.apache.maven.repository.Proxy;
040    import org.codehaus.plexus.component.annotations.Component;
041    import org.codehaus.plexus.logging.AbstractLogEnabled;
042    import org.codehaus.plexus.logging.Logger;
043    
044    @Component( role = UpdateCheckManager.class )
045    public 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    }