001package org.eclipse.aether.connector.basic;
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 static org.junit.Assert.*;
023import static org.junit.Assume.*;
024
025import java.io.Closeable;
026import java.io.File;
027import java.io.FileOutputStream;
028import java.io.IOException;
029import java.io.OutputStream;
030import java.io.RandomAccessFile;
031import java.nio.channels.FileLock;
032import java.util.ArrayList;
033import java.util.List;
034import java.util.Locale;
035import java.util.concurrent.CountDownLatch;
036
037import org.eclipse.aether.internal.test.util.TestFileUtils;
038import org.junit.After;
039import org.junit.Before;
040import org.junit.Test;
041
042public class PartialFileTest
043{
044
045    private static class StubRemoteAccessChecker
046        implements PartialFile.RemoteAccessChecker
047    {
048
049        Exception exception;
050
051        int invocations;
052
053        public void checkRemoteAccess()
054            throws Exception
055        {
056            invocations++;
057            if ( exception != null )
058            {
059                throw exception;
060            }
061        }
062
063    }
064
065    private static class ConcurrentWriter
066        extends Thread
067    {
068
069        private final File dstFile;
070
071        private final File partFile;
072
073        private final File lockFile;
074
075        private final CountDownLatch locked;
076
077        private final int sleep;
078
079        volatile int length;
080
081        Exception error;
082
083        public ConcurrentWriter( File dstFile, int sleep, int length )
084            throws InterruptedException
085        {
086            super( "ConcurrentWriter-" + dstFile.getAbsolutePath() );
087            this.dstFile = dstFile;
088            partFile = new File( dstFile.getPath() + PartialFile.EXT_PART );
089            lockFile = new File( partFile.getPath() + PartialFile.EXT_LOCK );
090            this.sleep = sleep;
091            this.length = length;
092            locked = new CountDownLatch( 1 );
093            start();
094            locked.await();
095        }
096
097        @Override
098        public void run()
099        {
100            RandomAccessFile raf = null;
101            FileLock lock = null;
102            OutputStream out = null;
103            try
104            {
105                raf = new RandomAccessFile( lockFile, "rw" );
106                lock = raf.getChannel().lock( 0, 1, false );
107                locked.countDown();
108                out = new FileOutputStream( partFile );
109                for ( int i = 0, n = Math.abs( length ); i < n; i++ )
110                {
111                    for ( long start = System.currentTimeMillis(); System.currentTimeMillis() - start < sleep; )
112                    {
113                        Thread.sleep( 10 );
114                    }
115                    out.write( 65 );
116                    out.flush();
117                    System.out.println( "  " + System.currentTimeMillis() + " Wrote byte " + ( i + 1 ) + "/"
118                                            + n );
119                }
120                if ( length >= 0 && !dstFile.setLastModified( System.currentTimeMillis() ) )
121                {
122                    throw new IOException( "Could not update destination file" );
123                }
124
125                out.close();
126                out = null;
127                lock.release();
128                lock = null;
129                raf.close();
130                raf = null;
131            }
132            catch ( Exception e )
133            {
134                error = e;
135            }
136            finally
137            {
138                try
139                {
140                    if ( out != null )
141                    {
142                        out.close();
143                    }
144                }
145                catch ( final IOException e )
146                {
147                    // Suppressed due to an exception already thrown in the try block.
148                }
149                finally
150                {
151                    try
152                    {
153                        if ( lock != null )
154                        {
155                            lock.release();
156                        }
157                    }
158                    catch ( final IOException e )
159                    {
160                        // Suppressed due to an exception already thrown in the try block.
161                    }
162                    finally
163                    {
164                        try
165                        {
166                            if ( raf != null )
167                            {
168                                raf.close();
169                            }
170                        }
171                        catch ( final IOException e )
172                        {
173                            // Suppressed due to an exception already thrown in the try block.
174                        }
175                        finally
176                        {
177                            if ( !lockFile.delete() )
178                            {
179                                lockFile.deleteOnExit();
180                            }
181                        }
182                    }
183                }
184            }
185        }
186
187    }
188
189    private static final boolean PROPER_LOCK_SUPPORT;
190
191    static
192    {
193        String javaVersion = System.getProperty( "java.version" ).trim();
194        boolean notJava5 = !javaVersion.startsWith( "1.5." );
195        String osName = System.getProperty( "os.name" ).toLowerCase( Locale.ENGLISH );
196        boolean windows = osName.contains( "windows" );
197        PROPER_LOCK_SUPPORT = notJava5 || windows;
198    }
199
200    private StubRemoteAccessChecker remoteAccessChecker;
201
202    private File dstFile;
203
204    private File partFile;
205
206    private File lockFile;
207
208    private List<Closeable> closeables;
209
210    private PartialFile newPartialFile( long resumeThreshold, int requestTimeout )
211        throws Exception
212    {
213        PartialFile.Factory factory =
214            new PartialFile.Factory( resumeThreshold >= 0L, resumeThreshold, requestTimeout );
215        PartialFile partFile = factory.newInstance( dstFile, remoteAccessChecker );
216        if ( partFile != null )
217        {
218            closeables.add( partFile );
219        }
220        return partFile;
221    }
222
223    @Before
224    public void init()
225        throws Exception
226    {
227        closeables = new ArrayList<>();
228        remoteAccessChecker = new StubRemoteAccessChecker();
229        dstFile = TestFileUtils.createTempFile( "Hello World!" );
230        partFile = new File( dstFile.getPath() + PartialFile.EXT_PART );
231        lockFile = new File( partFile.getPath() + PartialFile.EXT_LOCK );
232    }
233
234    @After
235    public void exit()
236    {
237        for ( Closeable closeable : closeables )
238        {
239            try
240            {
241                closeable.close();
242            }
243            catch ( Exception e )
244            {
245                e.printStackTrace();
246            }
247        }
248    }
249
250    @Test
251    public void testCloseNonResumableFile()
252        throws Exception
253    {
254        PartialFile partialFile = newPartialFile( -1, 100 );
255        assertNotNull( partialFile );
256        assertNotNull( partialFile.getFile() );
257        assertTrue( partialFile.getFile().getAbsolutePath(), partialFile.getFile().isFile() );
258        partialFile.close();
259        assertFalse( partialFile.getFile().getAbsolutePath(), partialFile.getFile().exists() );
260    }
261
262    @Test
263    public void testCloseResumableFile()
264        throws Exception
265    {
266        PartialFile partialFile = newPartialFile( 0, 100 );
267        assertNotNull( partialFile );
268        assertNotNull( partialFile.getFile() );
269        assertTrue( partialFile.getFile().getAbsolutePath(), partialFile.getFile().isFile() );
270        assertEquals( partFile, partialFile.getFile() );
271        assertTrue( lockFile.getAbsolutePath(), lockFile.isFile() );
272        partialFile.close();
273        assertTrue( partialFile.getFile().getAbsolutePath(), partialFile.getFile().isFile() );
274        assertFalse( lockFile.getAbsolutePath(), lockFile.exists() );
275    }
276
277    @Test
278    public void testResumableFileCreationError()
279        throws Exception
280    {
281        assertTrue( partFile.getAbsolutePath(), partFile.mkdirs() );
282        PartialFile partialFile = newPartialFile( 0, 100 );
283        assertNotNull( partialFile );
284        assertFalse( partialFile.isResume() );
285        assertFalse( lockFile.getAbsolutePath(), lockFile.exists() );
286    }
287
288    @Test
289    public void testResumeThreshold()
290        throws Exception
291    {
292        PartialFile partialFile = newPartialFile( 0, 100 );
293        assertNotNull( partialFile );
294        assertTrue( partialFile.isResume() );
295        partialFile.close();
296        partialFile = newPartialFile( 1, 100 );
297        assertNotNull( partialFile );
298        assertFalse( partialFile.isResume() );
299        partialFile.close();
300    }
301
302    @Test( timeout = 10000L )
303    public void testResumeConcurrently_RequestTimeout()
304        throws Exception
305    {
306        assumeTrue( PROPER_LOCK_SUPPORT );
307        ConcurrentWriter writer = new ConcurrentWriter( dstFile, 5 * 1000, 1 );
308        try
309        {
310            newPartialFile( 0, 1000 );
311            fail( "expected exception" );
312        }
313        catch ( Exception e )
314        {
315            assertTrue( e.getMessage().contains( "Timeout" ) );
316        }
317        writer.interrupt();
318        writer.join();
319    }
320
321    @Test( timeout = 10000L )
322    public void testResumeConcurrently_AwaitCompletion_ConcurrentWriterSucceeds()
323        throws Exception
324    {
325        assumeTrue( PROPER_LOCK_SUPPORT );
326        assertTrue( dstFile.setLastModified( System.currentTimeMillis() - 60L * 1000L ) );
327        ConcurrentWriter writer = new ConcurrentWriter( dstFile, 100, 10 );
328        assertNull( newPartialFile( 0, 500 ) );
329        writer.join();
330        assertNull( writer.error );
331        assertEquals( 1, remoteAccessChecker.invocations );
332    }
333
334    @Test( timeout = 10000L )
335    public void testResumeConcurrently_AwaitCompletion_ConcurrentWriterFails()
336        throws Exception
337    {
338        assumeTrue( PROPER_LOCK_SUPPORT );
339        assertTrue( dstFile.setLastModified( System.currentTimeMillis() - 60L * 1000L ) );
340        ConcurrentWriter writer = new ConcurrentWriter( dstFile, 100, -10 );
341        PartialFile partialFile = newPartialFile( 0, 500 );
342        assertNotNull( partialFile );
343        assertTrue( partialFile.isResume() );
344        writer.join();
345        assertNull( writer.error );
346        assertEquals( 1, remoteAccessChecker.invocations );
347    }
348
349    @Test( timeout = 10000L )
350    public void testResumeConcurrently_CheckRemoteAccess()
351        throws Exception
352    {
353        assumeTrue( PROPER_LOCK_SUPPORT );
354        remoteAccessChecker.exception = new IOException( "missing" );
355        ConcurrentWriter writer = new ConcurrentWriter( dstFile, 1000, 1 );
356        try
357        {
358            newPartialFile( 0, 1000 );
359            fail( "expected exception" );
360        }
361        catch ( Exception e )
362        {
363            assertSame( remoteAccessChecker.exception, e );
364        }
365        writer.interrupt();
366        writer.join();
367    }
368
369}