View Javadoc
1   package org.eclipse.aether.internal.impl;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   * 
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   * 
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.ByteArrayInputStream;
23  import java.io.ByteArrayOutputStream;
24  import java.io.Closeable;
25  import java.io.File;
26  import java.io.FileInputStream;
27  import java.io.IOException;
28  import java.io.RandomAccessFile;
29  import java.nio.channels.FileChannel;
30  import java.nio.channels.FileLock;
31  import java.nio.channels.OverlappingFileLockException;
32  import java.util.Map;
33  import java.util.Properties;
34  
35  import org.eclipse.aether.spi.log.Logger;
36  import org.eclipse.aether.spi.log.NullLoggerFactory;
37  
38  /**
39   * Manages potentially concurrent accesses to a properties file.
40   */
41  class TrackingFileManager
42  {
43  
44      private Logger logger = NullLoggerFactory.LOGGER;
45  
46      public TrackingFileManager setLogger( Logger logger )
47      {
48          this.logger = ( logger != null ) ? logger : NullLoggerFactory.LOGGER;
49          return this;
50      }
51  
52      public Properties read( File file )
53      {
54          synchronized ( getLock( file ) )
55          {
56              FileLock lock = null;
57              FileInputStream stream = null;
58              try
59              {
60                  if ( !file.exists() )
61                  {
62                      return null;
63                  }
64  
65                  stream = new FileInputStream( file );
66  
67                  lock = lock( stream.getChannel(), Math.max( 1, file.length() ), true );
68  
69                  Properties props = new Properties();
70                  props.load( stream );
71  
72                  return props;
73              }
74              catch ( IOException e )
75              {
76                  logger.warn( "Failed to read tracking file " + file, e );
77              }
78              finally
79              {
80                  release( lock, file );
81                  close( stream, file );
82              }
83          }
84  
85          return null;
86      }
87  
88      public Properties update( File file, Map<String, String> updates )
89      {
90          Properties props = new Properties();
91  
92          synchronized ( getLock( file ) )
93          {
94              File directory = file.getParentFile();
95              if ( !directory.mkdirs() && !directory.exists() )
96              {
97                  logger.warn( "Failed to create parent directories for tracking file " + file );
98                  return props;
99              }
100 
101             RandomAccessFile raf = null;
102             FileLock lock = null;
103             try
104             {
105                 raf = new RandomAccessFile( file, "rw" );
106                 lock = lock( raf.getChannel(), Math.max( 1, raf.length() ), false );
107 
108                 if ( file.canRead() )
109                 {
110                     byte[] buffer = new byte[(int) raf.length()];
111 
112                     raf.readFully( buffer );
113 
114                     ByteArrayInputStream stream = new ByteArrayInputStream( buffer );
115 
116                     props.load( stream );
117                 }
118 
119                 for ( Map.Entry<String, String> update : updates.entrySet() )
120                 {
121                     if ( update.getValue() == null )
122                     {
123                         props.remove( update.getKey() );
124                     }
125                     else
126                     {
127                         props.setProperty( update.getKey(), update.getValue() );
128                     }
129                 }
130 
131                 ByteArrayOutputStream stream = new ByteArrayOutputStream( 1024 * 2 );
132 
133                 logger.debug( "Writing tracking file " + file );
134                 props.store( stream, "NOTE: This is a Maven Resolver internal implementation file"
135                     + ", its format can be changed without prior notice." );
136 
137                 raf.seek( 0 );
138                 raf.write( stream.toByteArray() );
139                 raf.setLength( raf.getFilePointer() );
140             }
141             catch ( IOException e )
142             {
143                 logger.warn( "Failed to write tracking file " + file, e );
144             }
145             finally
146             {
147                 release( lock, file );
148                 close( raf, file );
149             }
150         }
151 
152         return props;
153     }
154 
155     private void release( FileLock lock, File file )
156     {
157         if ( lock != null )
158         {
159             try
160             {
161                 lock.release();
162             }
163             catch ( IOException e )
164             {
165                 logger.warn( "Error releasing lock for tracking file " + file, e );
166             }
167         }
168     }
169 
170     private void close( Closeable closeable, File file )
171     {
172         if ( closeable != null )
173         {
174             try
175             {
176                 closeable.close();
177             }
178             catch ( IOException e )
179             {
180                 logger.warn( "Error closing tracking file " + file, e );
181             }
182         }
183     }
184 
185     private Object getLock( File file )
186     {
187         /*
188          * NOTE: Locks held by one JVM must not overlap and using the canonical path is our best bet, still another
189          * piece of code might have locked the same file (unlikely though) or the canonical path fails to capture file
190          * identity sufficiently as is the case with Java 1.6 and symlinks on Windows.
191          */
192         try
193         {
194             return file.getCanonicalPath().intern();
195         }
196         catch ( IOException e )
197         {
198             logger.warn( "Failed to canonicalize path " + file + ": " + e.getMessage() );
199             return file.getAbsolutePath().intern();
200         }
201     }
202 
203     private FileLock lock( FileChannel channel, long size, boolean shared )
204         throws IOException
205     {
206         FileLock lock = null;
207 
208         for ( int attempts = 8; attempts >= 0; attempts-- )
209         {
210             try
211             {
212                 lock = channel.lock( 0, size, shared );
213                 break;
214             }
215             catch ( OverlappingFileLockException e )
216             {
217                 if ( attempts <= 0 )
218                 {
219                     throw (IOException) new IOException().initCause( e );
220                 }
221                 try
222                 {
223                     Thread.sleep( 50L );
224                 }
225                 catch ( InterruptedException e1 )
226                 {
227                     Thread.currentThread().interrupt();
228                 }
229             }
230         }
231 
232         if ( lock == null )
233         {
234             throw new IOException( "Could not lock file" );
235         }
236 
237         return lock;
238     }
239 
240 }