package org.apache.maven.wagon.http;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import org.apache.maven.wagon.FileTestUtils;
import org.apache.maven.wagon.ResourceDoesNotExistException;
import org.apache.maven.wagon.StreamingWagon;
import org.apache.maven.wagon.StreamingWagonTestCase;
import org.apache.maven.wagon.TransferFailedException;
import org.apache.maven.wagon.Wagon;
import org.apache.maven.wagon.WagonException;
import org.apache.maven.wagon.authentication.AuthenticationInfo;
import org.apache.maven.wagon.authorization.AuthorizationException;
import org.apache.maven.wagon.proxy.ProxyInfo;
import org.apache.maven.wagon.proxy.ProxyInfoProvider;
import org.apache.maven.wagon.repository.Repository;
import org.apache.maven.wagon.resource.Resource;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.StringUtils;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.security.HashLoginService;
import org.eclipse.jetty.security.SecurityHandler;
import org.eclipse.jetty.security.authentication.BasicAuthenticator;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.security.Password;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPOutputStream;

/**
 *
 */
public abstract class HttpWagonTestCase
    extends StreamingWagonTestCase
{
    public static final int SC_TOO_MANY_REQUESTS = 429;

    private Server server;
    private ServerConnector connector;

    protected int getLocalPort( Server server )
    {
        Connector connector = server.getConnectors()[0];
        return ( ( ServerConnector ) connector ).getLocalPort();
    }

    protected void setupWagonTestingFixtures()
        throws Exception
    {
        // File round trip testing

        File file = FileTestUtils.createUniqueFile( "local-repository", "test-resource" );

        file.delete();

        file.getParentFile().mkdirs();

        File repositoryDirectory = getRepositoryDirectory();
        FileUtils.deleteDirectory( repositoryDirectory );
        repositoryDirectory.mkdirs();

        server = new Server( );
        //connector = new ServerConnector( server, new HttpConnectionFactory( new HttpConfiguration() ) );
        //server.addConnector( connector );
        connector = addConnector( server );

        PutHandler putHandler = new PutHandler( repositoryDirectory );

        ServletContextHandler context = createContext( server, repositoryDirectory );
        HandlerCollection handlers = new HandlerCollection();
        handlers.addHandler( putHandler );
        handlers.addHandler( context );
        server.setHandler( handlers );

        server.start();
    }

    protected final int getTestRepositoryPort()
    {
        if ( server == null )
        {
            return 0;
        }
        return connector.getLocalPort();
    }

    protected ServletContextHandler createContext( Server server, File repositoryDirectory )
        throws IOException
    {
        ServletContextHandler root = new ServletContextHandler( ServletContextHandler.SESSIONS );
        root.setResourceBase( repositoryDirectory.getAbsolutePath() );
        ServletHolder servletHolder = new ServletHolder( new DefaultServlet() );
        root.addServlet( servletHolder, "/*" );
        return root;
    }

    protected void tearDownWagonTestingFixtures()
        throws Exception
    {
        server.stop();
    }

    public void testHttpHeaders()
        throws Exception
    {
        Properties headers = new Properties();
        headers.setProperty( "User-Agent", "Maven-Wagon/1.0" );

        StreamingWagon wagon = (StreamingWagon) getWagon();

        setHttpConfiguration( wagon, headers, new Properties() );

        Server server = new Server(  );
        TestHeaderHandler handler = new TestHeaderHandler();
        server.setHandler( handler );
        ServerConnector serverConnector = addConnector( server );
        server.start();

        wagon.connect(
            new Repository( "id", getProtocol() + "://localhost:" + serverConnector.getLocalPort() ) );

        wagon.getToStream( "resource", new ByteArrayOutputStream() );

        wagon.disconnect();

        server.stop();

        assertEquals( "Maven-Wagon/1.0", handler.headers.get( "User-Agent" ) );
    }

    /**
     * test set of User-Agent as it's done by aether wagon connector with using setHttpHeaders
     */
    public void testHttpHeadersWithCommonMethods()
        throws Exception
    {
        Properties properties = new Properties();
        properties.setProperty( "User-Agent", "Maven-Wagon/1.0" );

        StreamingWagon wagon = (StreamingWagon) getWagon();

        Method setHttpHeaders = wagon.getClass().getMethod( "setHttpHeaders", Properties.class );
        setHttpHeaders.invoke( wagon, properties );

        Server server = new Server( );
        ServerConnector serverConnector = addConnector( server );
        TestHeaderHandler handler = new TestHeaderHandler();
        server.setHandler( handler );
        addConnector( server );
        server.start();

        wagon.connect(
            new Repository( "id", getProtocol() + "://localhost:" + serverConnector.getLocalPort() ) );

        wagon.getToStream( "resource", new ByteArrayOutputStream() );

        wagon.disconnect();

        server.stop();

        assertEquals( "Maven-Wagon/1.0", handler.headers.get( "User-Agent" ) );
    }

    public void testUserAgentHeaderIsPresentByDefault()
        throws Exception
    {
        StreamingWagon wagon = (StreamingWagon) getWagon();
        Server server = new Server(  );
        TestHeaderHandler handler = new TestHeaderHandler();
        server.setHandler( handler );
        addConnector( server );
        server.start();
        wagon.connect( new Repository( "id", getProtocol() + "://localhost:" + getLocalPort( server ) ) );
        wagon.getToStream( "resource", new ByteArrayOutputStream() );
        wagon.disconnect();
        server.stop();

        assertNotNull( "default User-Agent header of wagon provider should be present",
                       handler.headers.get( "User-Agent" ) );
    }

    public void testUserAgentHeaderIsPresentOnlyOnceIfSetMultipleTimes()
        throws Exception
    {
        StreamingWagon wagon = (StreamingWagon) getWagon();

        // 1. set User-Agent header via HttpConfiguration
        Properties headers1 = new Properties();
        headers1.setProperty( "User-Agent", "test-user-agent" );
        setHttpConfiguration( wagon, headers1, new Properties() );

        // 2. redundantly set User-Agent header via setHttpHeaders()
        Properties headers2 = new Properties();
        headers2.setProperty( "User-Agent", "test-user-agent" );
        Method setHttpHeaders = wagon.getClass().getMethod( "setHttpHeaders", Properties.class );
        setHttpHeaders.invoke( wagon, headers2 );

        Server server = new Server(  );
        TestHeaderHandler handler = new TestHeaderHandler();
        server.setHandler( handler );
        addConnector( server );
        server.start();
        wagon.connect( new Repository( "id", getProtocol() + "://localhost:" + getLocalPort( server ) ) );
        wagon.getToStream( "resource", new ByteArrayOutputStream() );
        wagon.disconnect();
        server.stop();

        assertEquals( "test-user-agent", handler.headers.get( "User-Agent" ) );

    }

    protected abstract void setHttpConfiguration( StreamingWagon wagon, Properties headers, Properties params );

    protected ServerConnector addConnector( Server server )
    {
        ServerConnector serverConnector =
            new ServerConnector( server, new HttpConnectionFactory( new HttpConfiguration() ) );
        server.addConnector( serverConnector );
        return serverConnector;
    }

    protected String getRepositoryUrl( Server server )
    {
        int localPort = getLocalPort( server );
        return getProtocol() + "://localhost:" + localPort;
    }

    public void testGetForbidden()
        throws Exception
    {
        try
        {
            runTestGet( HttpServletResponse.SC_FORBIDDEN );
            fail();
        }
        catch ( AuthorizationException e )
        {
            assertTrue( true );
        }
    }

    public void testGet404()
        throws Exception
    {
        try
        {
            runTestGet( HttpServletResponse.SC_NOT_FOUND );
            fail();
        }
        catch ( ResourceDoesNotExistException e )
        {
            assertTrue( true );
        }
    }

    public void testGet500()
        throws Exception
    {
        try
        {
            runTestGet( HttpServletResponse.SC_INTERNAL_SERVER_ERROR );
            fail();
        }
        catch ( TransferFailedException e )
        {
            assertTrue( true );
        }
    }

    private void runTestGet( int status )
        throws Exception
    {
        StreamingWagon wagon = (StreamingWagon) getWagon();

        Server server = createStatusServer( status );
        server.start();

        String baseUrl = getRepositoryUrl( server );
        String resourceName = "resource";
        String serverReasonPhrase = HttpStatus.getCode( status ).getMessage();

        wagon.connect( new Repository( "id", baseUrl ) );

        try
        {
            wagon.getToStream( "resource", new ByteArrayOutputStream() );
            fail();
        }
        catch ( Exception e )
        {
            verifyWagonExceptionMessage( e, status, baseUrl + "/" + resourceName, serverReasonPhrase );
            throw e;
        }
        finally
        {
            wagon.disconnect();

            server.stop();
        }
    }

    public void testResourceExistsForbidden()
        throws Exception
    {
        try
        {
            runTestResourceExists( HttpServletResponse.SC_FORBIDDEN );
            fail();
        }
        catch ( AuthorizationException e )
        {
            assertTrue( true );
        }
    }

    public void testResourceExists404()
        throws Exception
    {
        try
        {
            assertFalse( runTestResourceExists( HttpServletResponse.SC_NOT_FOUND ) );
        }
        catch ( ResourceDoesNotExistException e )
        {
            assertTrue( true );
        }
    }

    public void testResourceExists500()
        throws Exception
    {
        try
        {
            runTestResourceExists( HttpServletResponse.SC_INTERNAL_SERVER_ERROR );
            fail();
        }
        catch ( TransferFailedException e )
        {
            assertTrue( true );
        }
    }

    public void testResourceExists429()
        throws Exception
    {
        try
        {

            final AtomicBoolean called = new AtomicBoolean();

            AbstractHandler handler = new AbstractHandler()
            {
                public void handle( String target, Request baseRequest, HttpServletRequest request,
                    HttpServletResponse response ) throws IOException, ServletException
                {
                    if ( called.get() )
                    {
                        response.setStatus( HttpServletResponse.SC_INTERNAL_SERVER_ERROR );
                        baseRequest.setHandled( true );
                    }
                    else
                    {
                        called.set( true );
                        response.setStatus( SC_TOO_MANY_REQUESTS );
                        baseRequest.setHandled( true );
                    }
                }
            };

            StreamingWagon wagon = (StreamingWagon) getWagon();
            Server server = new Server(  );
            server.setHandler( handler );
            addConnector( server );
            server.start();
            wagon.connect( new Repository( "id", getRepositoryUrl( server ) ) );

            try
            {
                wagon.resourceExists( "resource" );
            }
            finally
            {
                wagon.disconnect();

                server.stop();
            }

            fail();
        }
        catch ( TransferFailedException e )
        {
            assertTrue( true );
        }
    }


    private boolean runTestResourceExists( int status )
        throws Exception
    {
        StreamingWagon wagon = (StreamingWagon) getWagon();

        Server server = createStatusServer( status );
        server.start();

        wagon.connect( new Repository( "id", getRepositoryUrl( server ) ) );

        try
        {
            return wagon.resourceExists( "resource" );
        }
        finally
        {
            wagon.disconnect();

            server.stop();
        }
    }

    protected long getExpectedLastModifiedOnGet( Repository repository, Resource resource )
    {
        File file = new File( getRepositoryDirectory(), resource.getName() );
        return ( file.lastModified() / 1000 ) * 1000;
    }

    protected File getRepositoryDirectory()
    {
        return getTestFile( "target/test-output/http-repository" );
    }

    public void testGzipGet()
        throws Exception
    {
        Server server = new Server( );

        String localRepositoryPath = FileTestUtils.getTestOutputDir().toString();
        ServletContextHandler root = new ServletContextHandler( ServletContextHandler.SESSIONS );
        root.setResourceBase( localRepositoryPath );
        ServletHolder servletHolder = new ServletHolder( new DefaultServlet() );
        servletHolder.setInitParameter( "gzip", "true" );
        root.addServlet( servletHolder, "/*" );
        addConnector( server );
        server.setHandler( root );
        server.start();

        try
        {
            Wagon wagon = getWagon();

            Repository testRepository = new Repository( "id", getRepositoryUrl( server ) );

            File sourceFile = new File( localRepositoryPath + "/gzip" );

            sourceFile.deleteOnExit();

            String resName = "gzip-res.txt";
            String sourceContent = writeTestFile( sourceFile, resName, "gzip" );

            wagon.connect( testRepository );

            File destFile = FileTestUtils.createUniqueFile( getName(), getName() );

            destFile.deleteOnExit();

            wagon.get( "gzip/" + resName, destFile );

            wagon.disconnect();

            String destContent = FileUtils.fileRead( destFile );

            assertEquals( sourceContent, destContent );
        }
        finally
        {
            server.stop();
        }
    }

    /* This test cannot be enabled because we cannot tell GzipFilter to compress with deflate only
    public void testDeflateGet()
            throws Exception
        {
            Server server = new Server( );

            String localRepositoryPath = FileTestUtils.getTestOutputDir().toString();
            ServletContextHandler root = new ServletContextHandler( ServletContextHandler.SESSIONS );
            root.setResourceBase( localRepositoryPath );
            ServletHolder servletHolder = new ServletHolder( new DefaultServlet() );
            root.addServlet( servletHolder, "/*" );
            FilterHolder filterHolder = new FilterHolder( new GzipFilter() );
            root.addFilter( filterHolder, "/deflate/*", EnumSet.of( DispatcherType.REQUEST ) );
            addConnector( server );
            server.setHandler( root );
            server.start();

            try
            {
                Wagon wagon = getWagon();

                Repository testRepository = new Repository( "id", getRepositoryUrl( server ) );

                File sourceFile = new File( localRepositoryPath + "/deflate" );

                sourceFile.deleteOnExit();

                String resName = "deflate-res.txt";
                String sourceContent = writeTestFile( sourceFile, resName, null );

                wagon.connect( testRepository );

                File destFile = FileTestUtils.createUniqueFile( getName(), getName() );

                destFile.deleteOnExit();

                wagon.get( "deflate/" + resName, destFile );

                wagon.disconnect();

                String destContent = FileUtils.fileRead( destFile );

                assertEquals( sourceContent, destContent );
            }
            finally
            {
                server.stop();
            }
        }*/

    public void testProxiedRequest()
        throws Exception
    {
        ProxyInfo proxyInfo = createProxyInfo();
        TestHeaderHandler handler = new TestHeaderHandler();

        runTestProxiedRequest( proxyInfo, handler );
    }

    public void testProxiedRequestWithAuthentication()
        throws Exception
    {
        ProxyInfo proxyInfo = createProxyInfo();
        proxyInfo.setUserName( "user" );
        proxyInfo.setPassword( "secret" );
        AuthorizingProxyHandler handler = new AuthorizingProxyHandler();

        runTestProxiedRequest( proxyInfo, handler );

        assertTrue( handler.headers.containsKey( "Proxy-Authorization" ) );

        if ( supportProxyPreemptiveAuthentication() )
        {
            assertEquals( HttpServletResponse.SC_OK, handler.handlerRequestResponses.get( 0 ).responseCode );
        }
        else
        {
            assertEquals( HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED,
                          handler.handlerRequestResponses.get( 0 ).responseCode );
            assertEquals( HttpServletResponse.SC_OK, handler.handlerRequestResponses.get( 1 ).responseCode );
        }

    }

    public void testProxiedRequestWithAuthenticationWithProvider()
        throws Exception
    {
        final ProxyInfo proxyInfo = createProxyInfo();
        proxyInfo.setUserName( "user" );
        proxyInfo.setPassword( "secret" );
        AuthorizingProxyHandler handler = new AuthorizingProxyHandler();

        ProxyInfoProvider proxyInfoProvider = new ProxyInfoProvider()
        {
            public ProxyInfo getProxyInfo( String protocol )
            {
                return proxyInfo;
            }
        };
        runTestProxiedRequestWithProvider( proxyInfoProvider, handler );

        assertTrue( handler.headers.containsKey( "Proxy-Authorization" ) );

        if ( supportProxyPreemptiveAuthentication() )
        {
            assertEquals( HttpServletResponse.SC_OK, handler.handlerRequestResponses.get( 0 ).responseCode );
        }
        else
        {
            assertEquals( HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED,
                          handler.handlerRequestResponses.get( 0 ).responseCode );
            assertEquals( HttpServletResponse.SC_OK, handler.handlerRequestResponses.get( 1 ).responseCode );
        }

    }

    public void testRedirectGetToStream()
        throws Exception
    {
        StreamingWagon wagon = (StreamingWagon) getWagon();

        Server realServer = new Server(  );
        TestHeaderHandler handler = new TestHeaderHandler();

        realServer.setHandler( handler );
        addConnector( realServer );
        realServer.start();

        Server redirectServer = new Server(  );

        addConnector( redirectServer );

        String protocol = getProtocol();

        // protocol is wagon protocol but in fact dav is http(s)
        if ( protocol.equals( "dav" ) )
        {
            protocol = "http";
        }

        if ( protocol.equals( "davs" ) )
        {
            protocol = "https";
        }

        String redirectUrl = protocol + "://localhost:" + getLocalPort( realServer );

        RedirectHandler redirectHandler =
            new RedirectHandler( "See Other", HttpServletResponse.SC_SEE_OTHER, redirectUrl, null );

        redirectServer.setHandler( redirectHandler );

        redirectServer.start();

        wagon.connect( new Repository( "id", getRepositoryUrl( redirectServer ) ) );

        File tmpResult = File.createTempFile( "foo", "get" );

        try ( FileOutputStream fileOutputStream = new FileOutputStream( tmpResult ) )
        {
            wagon.getToStream( "resource", fileOutputStream );
            fileOutputStream.flush();
            fileOutputStream.close();
            String found = FileUtils.fileRead( tmpResult );
            assertEquals( "found:'" + found + "'", "Hello, World!", found );

            checkHandlerResult( redirectHandler.handlerRequestResponses, HttpServletResponse.SC_SEE_OTHER );
            checkHandlerResult( handler.handlerRequestResponses, HttpServletResponse.SC_OK );
        }
        finally
        {
            wagon.disconnect();

            redirectServer.stop();
            realServer.stop();

            tmpResult.delete();
        }
    }

    public void testRedirectGet()
        throws Exception
    {
        StreamingWagon wagon = (StreamingWagon) getWagon();

        Server realServer = new Server( );
        TestHeaderHandler handler = new TestHeaderHandler();

        realServer.setHandler( handler );
        addConnector( realServer );
        realServer.start();

        Server redirectServer = new Server( );

        addConnector( redirectServer );

        String protocol = getProtocol();

        // protocol is wagon protocol but in fact dav is http(s)
        if ( protocol.equals( "dav" ) )
        {
            protocol = "http";
        }

        if ( protocol.equals( "davs" ) )
        {
            protocol = "https";
        }

        String redirectUrl = protocol + "://localhost:" + getLocalPort( realServer );

        RedirectHandler redirectHandler =
            new RedirectHandler( "See Other", HttpServletResponse.SC_SEE_OTHER, redirectUrl, null );

        redirectServer.setHandler( redirectHandler );

        redirectServer.start();

        wagon.connect( new Repository( "id", getRepositoryUrl( redirectServer ) ) );

        File tmpResult = File.createTempFile( "foo", "get" );

        try
        {
            wagon.get( "resource", tmpResult );
            String found = FileUtils.fileRead( tmpResult );
            assertEquals( "found:'" + found + "'", "Hello, World!", found );

            checkHandlerResult( redirectHandler.handlerRequestResponses, HttpServletResponse.SC_SEE_OTHER );
            checkHandlerResult( handler.handlerRequestResponses, HttpServletResponse.SC_OK );
        }
        finally
        {
            wagon.disconnect();

            redirectServer.stop();
            realServer.stop();

            tmpResult.delete();
        }
    }


    public void testRedirectPutFromStreamWithFullUrl()
        throws Exception
    {
        Server realServer = new Server( );

        addConnector( realServer );

        File repositoryDirectory = getRepositoryDirectory();
        FileUtils.deleteDirectory( repositoryDirectory );
        repositoryDirectory.mkdirs();

        PutHandler putHandler = new PutHandler( repositoryDirectory );

        realServer.setHandler( putHandler );

        realServer.start();

        Server redirectServer = new Server( );

        addConnector( redirectServer );

        String protocol = getProtocol();

        // protocol is wagon protocol but in fact dav is http(s)
        if ( protocol.equals( "dav" ) )
        {
            protocol = "http";
        }

        if ( protocol.equals( "davs" ) )
        {
            protocol = "https";
        }

        String redirectUrl = protocol + "://localhost:" + getLocalPort( realServer );

        RedirectHandler redirectHandler =
            new RedirectHandler( "See Other", HttpServletResponse.SC_SEE_OTHER, redirectUrl, repositoryDirectory );

        redirectServer.setHandler( redirectHandler );

        redirectServer.start();

        try
        {
            StreamingWagon wagon = (StreamingWagon) getWagon();
            Repository repository = new Repository( "foo", getRepositoryUrl( redirectServer ) );
            wagon.connect( repository );

            File sourceFile = new File( repositoryDirectory, "test-secured-put-resource" );
            sourceFile.delete();
            assertFalse( sourceFile.exists() );

            File tempFile = File.createTempFile( "wagon", "tmp" );
            tempFile.deleteOnExit();
            String content = "put top secret";
            FileUtils.fileWrite( tempFile.getAbsolutePath(), content );

            try ( FileInputStream fileInputStream = new FileInputStream( tempFile ) )
            {
                wagon.putFromStream( fileInputStream, "test-secured-put-resource", content.length(), -1 );
                assertEquals( content, FileUtils.fileRead( sourceFile.getAbsolutePath() ) );

                checkRequestResponseForRedirectPutWithFullUrl( redirectHandler, putHandler );
            }
            finally
            {
                wagon.disconnect();
                tempFile.delete();
            }

        }
        finally
        {
            realServer.stop();
            redirectServer.stop();
        }
    }

    protected void checkRequestResponseForRedirectPutWithFullUrl( RedirectHandler redirectHandler,
                                                                  PutHandler putHandler )
    {
        checkHandlerResult( redirectHandler.handlerRequestResponses, HttpServletResponse.SC_SEE_OTHER );
        checkHandlerResult( putHandler.handlerRequestResponses, HttpServletResponse.SC_CREATED );
    }

    public void testRedirectPutFromStreamRelativeUrl()
        throws Exception
    {
        Server realServer = new Server( );
        addConnector( realServer );
        File repositoryDirectory = getRepositoryDirectory();
        FileUtils.deleteDirectory( repositoryDirectory );
        repositoryDirectory.mkdirs();

        PutHandler putHandler = new PutHandler( repositoryDirectory );

        realServer.setHandler( putHandler );

        realServer.start();

        Server redirectServer = new Server( );

        addConnector( redirectServer );

        RedirectHandler redirectHandler =
            new RedirectHandler( "See Other", HttpServletResponse.SC_SEE_OTHER, "/redirectRequest/foo",
                                 repositoryDirectory );

        redirectServer.setHandler( redirectHandler );

        redirectServer.start();

        try
        {
            StreamingWagon wagon = (StreamingWagon) getWagon();
            Repository repository = new Repository( "foo", getRepositoryUrl( redirectServer ) );
            wagon.connect( repository );

            File sourceFile = new File( repositoryDirectory, "/redirectRequest/foo/test-secured-put-resource" );
            sourceFile.delete();
            assertFalse( sourceFile.exists() );

            File tempFile = File.createTempFile( "wagon", "tmp" );
            tempFile.deleteOnExit();
            String content = "put top secret";
            FileUtils.fileWrite( tempFile.getAbsolutePath(), content );

            try ( FileInputStream fileInputStream = new FileInputStream( tempFile ) )
            {
                wagon.putFromStream( fileInputStream, "test-secured-put-resource", content.length(), -1 );
                assertEquals( content, FileUtils.fileRead( sourceFile.getAbsolutePath() ) );

                checkRequestResponseForRedirectPutWithRelativeUrl( redirectHandler, putHandler );
            }
            finally
            {
                wagon.disconnect();
                tempFile.delete();
            }

        }
        finally
        {
            realServer.stop();
            redirectServer.stop();
        }
    }

    protected void checkRequestResponseForRedirectPutWithRelativeUrl( RedirectHandler redirectHandler,
                                                                      PutHandler putHandler )
    {
        checkHandlerResult( redirectHandler.handlerRequestResponses, HttpServletResponse.SC_SEE_OTHER,
                            HttpServletResponse.SC_CREATED );
        checkHandlerResult( putHandler.handlerRequestResponses );
    }

    protected void checkHandlerResult( List<HandlerRequestResponse> handlerRequestResponses,
                                       int... expectedResponseCodes )
    {
        boolean success = true;
        if ( handlerRequestResponses.size() == expectedResponseCodes.length )
        {
            for ( int i = 0; i < expectedResponseCodes.length; i++ )
            {
                success &= ( expectedResponseCodes[i] == handlerRequestResponses.get( i ).responseCode );
            }
        }

        if ( !success )
        {
            fail( "expected " + expectedResponseCodes + ", got " + handlerRequestResponses );
        }
    }

    public void testRedirectPutFileWithFullUrl()
        throws Exception
    {
        Server realServer = new Server( );

        addConnector( realServer );

        File repositoryDirectory = getRepositoryDirectory();
        FileUtils.deleteDirectory( repositoryDirectory );
        repositoryDirectory.mkdirs();

        PutHandler putHandler = new PutHandler( repositoryDirectory );

        realServer.setHandler( putHandler );

        realServer.start();

        Server redirectServer = new Server( );

        addConnector( redirectServer );

        String protocol = getProtocol();

        // protocol is wagon protocol but in fact dav is http(s)
        if ( protocol.equals( "dav" ) )
        {
            protocol = "http";
        }

        if ( protocol.equals( "davs" ) )
        {
            protocol = "https";
        }

        String redirectUrl = protocol + "://localhost:" + getLocalPort( realServer );

        RedirectHandler redirectHandler =
            new RedirectHandler( "See Other", HttpServletResponse.SC_SEE_OTHER, redirectUrl, repositoryDirectory );

        redirectServer.setHandler( redirectHandler );

        redirectServer.start();

        try
        {
            StreamingWagon wagon = (StreamingWagon) getWagon();
            Repository repository = new Repository( "foo", getRepositoryUrl( redirectServer ) );
            wagon.connect( repository );

            File sourceFile = new File( repositoryDirectory, "test-secured-put-resource" );
            sourceFile.delete();
            assertFalse( sourceFile.exists() );

            File tempFile = File.createTempFile( "wagon", "tmp" );
            tempFile.deleteOnExit();
            String content = "put top secret";
            FileUtils.fileWrite( tempFile.getAbsolutePath(), content );

            try
            {
                wagon.put( tempFile, "test-secured-put-resource" );
                assertEquals( content, FileUtils.fileRead( sourceFile.getAbsolutePath() ) );

                checkRequestResponseForRedirectPutWithFullUrl( redirectHandler, putHandler );
            }
            finally
            {
                wagon.disconnect();
                tempFile.delete();
            }

        }
        finally
        {
            realServer.stop();
            redirectServer.stop();
        }
    }


    public void testRedirectPutFileRelativeUrl()
        throws Exception
    {
        Server realServer = new Server( );
        addConnector( realServer );
        File repositoryDirectory = getRepositoryDirectory();
        FileUtils.deleteDirectory( repositoryDirectory );
        repositoryDirectory.mkdirs();

        PutHandler putHandler = new PutHandler( repositoryDirectory );

        realServer.setHandler( putHandler );

        realServer.start();

        Server redirectServer = new Server( );

        addConnector( redirectServer );

        RedirectHandler redirectHandler =
            new RedirectHandler( "See Other", HttpServletResponse.SC_SEE_OTHER, "/redirectRequest/foo",
                                 repositoryDirectory );

        redirectServer.setHandler( redirectHandler );

        redirectServer.start();

        try
        {
            StreamingWagon wagon = (StreamingWagon) getWagon();
            Repository repository = new Repository( "foo", getRepositoryUrl( redirectServer ) );
            wagon.connect( repository );

            File sourceFile = new File( repositoryDirectory, "/redirectRequest/foo/test-secured-put-resource" );
            sourceFile.delete();
            assertFalse( sourceFile.exists() );

            File tempFile = File.createTempFile( "wagon", "tmp" );
            tempFile.deleteOnExit();
            String content = "put top secret";
            FileUtils.fileWrite( tempFile.getAbsolutePath(), content );

            try
            {
                wagon.put( tempFile, "test-secured-put-resource" );
                assertEquals( content, FileUtils.fileRead( sourceFile.getAbsolutePath() ) );

                checkRequestResponseForRedirectPutWithRelativeUrl( redirectHandler, putHandler );
            }
            finally
            {
                wagon.disconnect();
                tempFile.delete();
            }

        }
        finally
        {
            realServer.stop();
            redirectServer.stop();
        }
    }

    public void testRedirectPutFailureNonRepeatableStream()
            throws Exception
        {
            File repositoryDirectory = getRepositoryDirectory();
            FileUtils.deleteDirectory( repositoryDirectory );
            repositoryDirectory.mkdirs();

            Server redirectServer = new Server( );

            addConnector( redirectServer );

            RedirectHandler redirectHandler =
                new RedirectHandler( "See Other", HttpServletResponse.SC_SEE_OTHER, "/redirectRequest/foo",
                                     null );

            redirectServer.setHandler( redirectHandler );

            redirectServer.start();

            try
            {
                StreamingWagon wagon = (StreamingWagon) getWagon();

                Properties params = new Properties();
                params.put( "http.protocol.expect-continue", "%b,false" );
                setHttpConfiguration( wagon, new Properties(), params );
                Repository repository = new Repository( "foo", getRepositoryUrl( redirectServer ) );
                wagon.connect( repository );

                File sourceFile = new File( repositoryDirectory, "/redirectRequest/foo/test-secured-put-resource" );
                sourceFile.delete();
                assertFalse( sourceFile.exists() );

                File tempFile = File.createTempFile( "wagon", "tmp" );
                tempFile.deleteOnExit();
                String content = "put top secret";
                FileUtils.fileWrite( tempFile.getAbsolutePath(), content );

                try ( FileInputStream fileInputStream = new FileInputStream( tempFile ) )
                {
                    wagon.putFromStream( fileInputStream, "test-secured-put-resource", content.length(), -1 );
                    // This does not behave as expected because LightweightWagon does buffering by default
                    if ( wagon.getClass().getName().contains( "Lightweight" ) )
                    {
                        assertTrue( true );
                    }
                    else
                    {
                        fail();
                    }
                }
                catch ( TransferFailedException e )
                {
                    assertTrue( true );
                }
                finally
                {
                    wagon.disconnect();
                    tempFile.delete();
                }

            }
            finally
            {
                redirectServer.stop();
            }
        }

    /**
     *
     */
    @SuppressWarnings( "checkstyle:visibilitymodifier" )
    public static class RedirectHandler
        extends AbstractHandler
    {
        String reason;

        int retCode;

        String redirectUrl;

        File repositoryDirectory;

        public List<HandlerRequestResponse> handlerRequestResponses = new ArrayList<HandlerRequestResponse>();

        RedirectHandler( String reason, int retCode, String redirectUrl, File repositoryDirectory )
        {
            this.reason = reason;
            this.retCode = retCode;
            this.redirectUrl = redirectUrl;
            this.repositoryDirectory = repositoryDirectory;
        }

        public void handle( String target, Request baseRequest, HttpServletRequest request,
            HttpServletResponse response ) throws IOException, ServletException
        {
            if ( request.getRequestURI().contains( "redirectRequest" ) )
            {
                PutHandler putHandler = new PutHandler( this.repositoryDirectory );
                putHandler.handle( target, baseRequest, request, response );
                handlerRequestResponses.add(
                    new HandlerRequestResponse( request.getMethod(), response.getStatus(),
                                                request.getRequestURI() ) );
                return;
            }
            response.setStatus( this.retCode );
            response.setHeader( "Location", this.redirectUrl + request.getRequestURI() );
            baseRequest.setHandled( true );

            handlerRequestResponses.add(
                new HandlerRequestResponse( request.getMethod(), response.getStatus(),
                                            request.getRequestURI() ) );
        }


    }


    private void runTestProxiedRequest( ProxyInfo proxyInfo, TestHeaderHandler handler )
        throws Exception
    {
        // what an UGLY hack!
        // but apparently jetty needs some time to free up resources
        // <5s: broken test :(
        // CHECKSTYLE_OFF: MagicNumber
        Thread.sleep( 5001L );
        // CHECKSTYLE_ON: MagicNumber

        Server proxyServer = new Server( );
        ServerConnector serverConnector =
            new ServerConnector( proxyServer, new HttpConnectionFactory( new HttpConfiguration() ) );
        proxyServer.addConnector( serverConnector );
        proxyServer.setHandler( handler );

        proxyServer.start();

        proxyInfo.setPort( getLocalPort( proxyServer ) );

        System.out.println(
            "start proxy on host/port " + proxyInfo.getHost() + "/" + proxyInfo.getPort() + " with non proxyHosts "
                + proxyInfo.getNonProxyHosts() );

        while ( !proxyServer.isRunning() || !proxyServer.isStarted() )
        {
            Thread.sleep( 10 );
        }

        try
        {
            StreamingWagon wagon = (StreamingWagon) getWagon();

            Repository testRepository = new Repository( "id", "http://www.example.com/" );

            String localRepositoryPath = FileTestUtils.getTestOutputDir().toString();
            File sourceFile = new File( localRepositoryPath, "test-proxied-resource" );
            FileUtils.fileWrite( sourceFile.getAbsolutePath(), "content" );

            wagon.connect( testRepository, proxyInfo );

            try
            {
                wagon.getToStream( "test-proxied-resource", new ByteArrayOutputStream() );

                assertTrue( handler.headers.containsKey( "Proxy-Connection" ) );
            }
            finally
            {
                System.setProperty( "http.proxyHost", "" );
                System.setProperty( "http.proxyPort", "" );
                wagon.disconnect();
            }
        }
        finally
        {
            proxyServer.stop();
        }
    }

    private void runTestProxiedRequestWithProvider( ProxyInfoProvider proxyInfoProvider, TestHeaderHandler handler )
        throws Exception
    {
        // what an UGLY hack!
        // but apparently jetty needs some time to free up resources
        // <5s: broken test :(
        // CHECKSTYLE_OFF: MagicNumber
        Thread.sleep( 5001L );
        // CHECKSTYLE_ON: MagicNumber

        Server proxyServer = new Server( );
        ServerConnector serverConnector =
            new ServerConnector( proxyServer, new HttpConnectionFactory( new HttpConfiguration() ) );
        proxyServer.addConnector( serverConnector );

        proxyServer.setHandler( handler );

        proxyServer.start();

        proxyInfoProvider.getProxyInfo( null ).setPort( getLocalPort( proxyServer ) );

        System.out.println( "start proxy on host/port " + proxyInfoProvider.getProxyInfo( null ).getHost() + "/"
                                + proxyInfoProvider.getProxyInfo( null ).getPort() + " with non proxyHosts "
                                + proxyInfoProvider.getProxyInfo( null ).getNonProxyHosts() );

        while ( !proxyServer.isRunning() || !proxyServer.isStarted() )
        {
            Thread.sleep( 10 );
        }

        try
        {
            StreamingWagon wagon = (StreamingWagon) getWagon();

            Repository testRepository = new Repository( "id", "http://www.example.com/" );

            String localRepositoryPath = FileTestUtils.getTestOutputDir().toString();
            File sourceFile = new File( localRepositoryPath, "test-proxied-resource" );
            FileUtils.fileWrite( sourceFile.getAbsolutePath(), "content" );

            wagon.connect( testRepository, proxyInfoProvider );

            try
            {
                wagon.getToStream( "test-proxied-resource", new ByteArrayOutputStream() );

                assertTrue( handler.headers.containsKey( "Proxy-Connection" ) );
            }
            finally
            {
                System.setProperty( "http.proxyHost", "" );
                System.setProperty( "http.proxyPort", "" );
                wagon.disconnect();
            }
        }
        finally
        {
            proxyServer.stop();
        }
    }

    private ProxyInfo createProxyInfo()
    {
        ProxyInfo proxyInfo = new ProxyInfo();
        proxyInfo.setHost( "localhost" );
        proxyInfo.setNonProxyHosts( null );
        proxyInfo.setType( "http" );
        return proxyInfo;
    }

    public void testSecuredGetUnauthorized()
        throws Exception
    {
        try
        {
            runTestSecuredGet( null );
            fail();
        }
        catch ( AuthorizationException e )
        {
            assertTrue( true );
        }
    }

    public void testSecuredGetWrongPassword()
        throws Exception
    {
        try
        {
            AuthenticationInfo authInfo = new AuthenticationInfo();
            authInfo.setUserName( "user" );
            authInfo.setPassword( "admin" );
            runTestSecuredGet( authInfo );
            fail();
        }
        catch ( AuthorizationException e )
        {
            assertTrue( true );
        }
    }

    public void testSecuredGet()
        throws Exception
    {
        AuthenticationInfo authInfo = new AuthenticationInfo();
        authInfo.setUserName( "user" );
        authInfo.setPassword( "secret" );
        runTestSecuredGet( authInfo );
    }


    public void runTestSecuredGet( AuthenticationInfo authInfo )
        throws Exception
    {
        String localRepositoryPath = FileTestUtils.getTestOutputDir().toString();
        Server server = createSecurityServer( localRepositoryPath );

        server.start();

        try
        {
            StreamingWagon wagon = (StreamingWagon) getWagon();

            Repository testRepository = new Repository( "id", getRepositoryUrl( server ) );

            File sourceFile = new File( localRepositoryPath, "test-secured-resource" );
            FileUtils.fileWrite( sourceFile.getAbsolutePath(), "top secret" );

            wagon.connect( testRepository, authInfo );

            File file = File.createTempFile( "wagon-test", "txt" );

            try
            {
                wagon.get( "test-secured-resource", file );
            }
            finally
            {
                wagon.disconnect();
            }

            FileInputStream in = new FileInputStream( file );

            assertEquals( "top secret", IOUtil.toString( in ) );

            /*
             * We need to wait a bit for all Jetty workers/threads to complete their work. Otherwise
             * we may suffer from race conditions where handlerRequestResponses list is not completely
             * populated and its premature iteration in testPreemptiveAuthenticationGet will lead to
             * a test failure.
             */
            // CHECKSTYLE_OFF: MagicNumber
            Thread.sleep ( 2000L );
            // CHECKSTYLE_ON: MagicNumber

            TestSecurityHandler securityHandler = server.getChildHandlerByClass( TestSecurityHandler.class );
            testPreemptiveAuthenticationGet( securityHandler, supportPreemptiveAuthenticationGet() );

        }
        finally
        {
            server.stop();
        }
    }


    public void testSecuredGetToStream()
        throws Exception
    {
        AuthenticationInfo authInfo = new AuthenticationInfo();
        authInfo.setUserName( "user" );
        authInfo.setPassword( "secret" );
        runTestSecuredGetToStream( authInfo );
    }

    public void runTestSecuredGetToStream( AuthenticationInfo authInfo )
        throws Exception
    {
        String localRepositoryPath = FileTestUtils.getTestOutputDir().toString();
        Server server = createSecurityServer( localRepositoryPath );

        server.start();

        try
        {
            StreamingWagon wagon = (StreamingWagon) getWagon();

            Repository testRepository = new Repository( "id", getRepositoryUrl( server ) );

            File sourceFile = new File( localRepositoryPath, "test-secured-resource" );
            FileUtils.fileWrite( sourceFile.getAbsolutePath(), "top secret" );

            wagon.connect( testRepository, authInfo );

            ByteArrayOutputStream out = new ByteArrayOutputStream();
            try
            {
                wagon.getToStream( "test-secured-resource", out );
            }
            finally
            {
                wagon.disconnect();
            }

            assertEquals( "top secret", out.toString( "US-ASCII" ) );

            /*
             * We need to wait a bit for all Jetty workers/threads to complete their work. Otherwise
             * we may suffer from race conditions where handlerRequestResponses list is not completely
             * populated and its premature iteration in testPreemptiveAuthenticationGet will lead to
             * a test failure.
             */
            // CHECKSTYLE_OFF: MagicNumber
            Thread.sleep ( 2000L );
            // CHECKSTYLE_ON: MagicNumber

            TestSecurityHandler securityHandler = server.getChildHandlerByClass( TestSecurityHandler.class );
            testPreemptiveAuthenticationGet( securityHandler, supportPreemptiveAuthenticationGet() );
        }
        finally
        {
            server.stop();
        }
    }

    public void testSecuredResourceExistsUnauthorized()
        throws Exception
    {
        try
        {
            runTestSecuredResourceExists( null );
            fail();
        }
        catch ( AuthorizationException e )
        {
            assertTrue( true );
        }
    }

    public void testSecuredResourceExistsWrongPassword()
        throws Exception
    {
        try
        {
            AuthenticationInfo authInfo = new AuthenticationInfo();
            authInfo.setUserName( "user" );
            authInfo.setPassword( "admin" );
            runTestSecuredResourceExists( authInfo );
        }
        catch ( AuthorizationException e )
        {
            assertTrue( true );
        }
    }

    public void testSecuredResourceExists()
        throws Exception
    {
        AuthenticationInfo authInfo = new AuthenticationInfo();
        authInfo.setUserName( "user" );
        authInfo.setPassword( "secret" );
        runTestSecuredResourceExists( authInfo );
    }

    public void runTestSecuredResourceExists( AuthenticationInfo authInfo )
        throws Exception
    {
        String localRepositoryPath = FileTestUtils.getTestOutputDir().toString();
        Server server = createSecurityServer( localRepositoryPath );

        server.start();

        try
        {
            StreamingWagon wagon = (StreamingWagon) getWagon();

            Repository testRepository = new Repository( "id", getRepositoryUrl( server ) );

            File sourceFile = new File( localRepositoryPath, "test-secured-resource-exists" );
            FileUtils.fileWrite( sourceFile.getAbsolutePath(), "top secret" );

            wagon.connect( testRepository, authInfo );

            try
            {
                assertTrue( wagon.resourceExists( "test-secured-resource-exists" ) );

                assertFalse( wagon.resourceExists( "test-secured-resource-not-exists" ) );
            }
            finally
            {
                wagon.disconnect();
            }
        }
        finally
        {
            server.stop();
        }
    }

    private Server createSecurityServer( String localRepositoryPath )
    {
        Server server = new Server( );

        SecurityHandler sh = createSecurityHandler();

        ServletContextHandler root = new ServletContextHandler( ServletContextHandler.SESSIONS
            | ServletContextHandler.SECURITY );
        root.setResourceBase( localRepositoryPath );
        root.setSecurityHandler( sh );
        ServletHolder servletHolder = new ServletHolder( new DefaultServlet() );
        root.addServlet( servletHolder, "/*" );

        server.setHandler( root );
        addConnector( server );
        return server;
    }

    private Server createStatusServer( int status )
    {
        Server server = new Server( );
        StatusHandler handler = new StatusHandler();
        handler.setStatusToReturn( status );
        server.setHandler( handler );
        addConnector( server );
        return server;
    }


    private String writeTestFile( File parent, String child, String compressionType )
        throws IOException
    {
        File file = new File( parent, child );
        file.getParentFile().mkdirs();
        file.deleteOnExit();
        OutputStream out = new FileOutputStream( file );
        try
        {
            out.write( child.getBytes() );
        }
        finally
        {
            out.close();
        }

        String ext = "";
        if ( "gzip".equals( compressionType ) )
        {
            ext = ".gz";
        }
        if ( "deflate".equals( compressionType ) )
        {
            ext = ".deflate";
        }

        file = new File( parent, child + ext );
        file.deleteOnExit();
        String content;
        out = new FileOutputStream( file );
        if ( "gzip".equals( compressionType ) )
        {
            out = new GZIPOutputStream( out );
        }
        if ( "deflate".equals( compressionType ) )
        {
            out = new DeflaterOutputStream( out );
        }
        try
        {
            // write out different data than non-compressed file, so we can
            // assert the compressed version was returned
            content = file.getAbsolutePath();
            out.write( content.getBytes() );
        }
        finally
        {
            out.close();
        }

        return content;
    }

    public void testPutForbidden()
        throws Exception
    {
        try
        {
            runTestPut( HttpServletResponse.SC_FORBIDDEN );
            fail();
        }
        catch ( AuthorizationException e )
        {
            assertTrue( true );
        }
    }

    public void testPut404()
        throws Exception
    {
        try
        {
            runTestPut( HttpServletResponse.SC_NOT_FOUND );
            fail();
        }
        catch ( ResourceDoesNotExistException e )
        {
            assertTrue( true );
        }
    }

    public void testPut500()
        throws Exception
    {
        try
        {
            runTestPut( HttpServletResponse.SC_INTERNAL_SERVER_ERROR );
            fail();
        }
        catch ( TransferFailedException e )
        {
            assertTrue( true );
        }
    }

    public void testPut429()
        throws Exception
    {

        try
        {

            StreamingWagon wagon = (StreamingWagon) getWagon();
            Server server = new Server( );
            final AtomicBoolean called = new AtomicBoolean();

            AbstractHandler handler = new AbstractHandler()
            {
                public void handle( String target, Request baseRequest, HttpServletRequest request,
                    HttpServletResponse response ) throws IOException, ServletException
                {
                    if ( called.get() )
                    {
                        response.setStatus( HttpServletResponse.SC_INTERNAL_SERVER_ERROR );
                        baseRequest.setHandled( true );
                    }
                    else
                    {
                        called.set( true );
                        response.setStatus( SC_TOO_MANY_REQUESTS );
                        baseRequest.setHandled( true );
                    }
                }
            };

            server.setHandler( handler );
            addConnector( server );
            server.start();

            wagon.connect( new Repository( "id", getRepositoryUrl( server ) ) );

            File tempFile = File.createTempFile( "wagon", "tmp" );
            tempFile.deleteOnExit();
            FileUtils.fileWrite( tempFile.getAbsolutePath(), "content" );

            try
            {
                wagon.put( tempFile, "resource" );
                fail();
            }
            finally
            {
                wagon.disconnect();

                server.stop();

                tempFile.delete();
            }

        }
        catch ( TransferFailedException e )
        {
            assertTrue( true );
        }
    }


    private void runTestPut( int status )
        throws Exception
    {
        StreamingWagon wagon = (StreamingWagon) getWagon();

        Server server = createStatusServer( status );
        server.start();

        wagon.connect( new Repository( "id", getRepositoryUrl( server ) ) );

        File tempFile = File.createTempFile( "wagon", "tmp" );
        tempFile.deleteOnExit();
        FileUtils.fileWrite( tempFile.getAbsolutePath(), "content" );

        String baseUrl = getRepositoryUrl( server );
        String resourceName = "resource";
        String serverReasonPhrase = HttpStatus.getCode( status ).getMessage();

        try
        {
            wagon.put( tempFile, resourceName );
            fail();
        }
        catch ( Exception e )
        {
            verifyWagonExceptionMessage( e, status, baseUrl + "/" + resourceName, serverReasonPhrase );
            throw e;
        }
        finally
        {
            wagon.disconnect();

            server.stop();

            tempFile.delete();
        }
    }

    public void testSecuredPutUnauthorized()
        throws Exception
    {
        try
        {
            runTestSecuredPut( null );
            fail();
        }
        catch ( AuthorizationException e )
        {
            assertTrue( true );
        }
    }

    public void testSecuredPutWrongPassword()
        throws Exception
    {
        try
        {
            AuthenticationInfo authInfo = new AuthenticationInfo();
            authInfo.setUserName( "user" );
            authInfo.setPassword( "admin" );
            runTestSecuredPut( authInfo );
            fail();
        }
        catch ( AuthorizationException e )
        {
            assertTrue( true );
        }
    }

    public void testSecuredPut()
        throws Exception
    {
        AuthenticationInfo authInfo = new AuthenticationInfo();
        authInfo.setUserName( "user" );
        authInfo.setPassword( "secret" );
        runTestSecuredPut( authInfo );
    }

    public void runTestSecuredPut( AuthenticationInfo authInfo )
        throws Exception
    {
        runTestSecuredPut( authInfo, 1 );
    }

    public void runTestSecuredPut( AuthenticationInfo authInfo, int putNumber )
        throws Exception
    {
        String localRepositoryPath = FileTestUtils.getTestOutputDir().toString();
        Server server = new Server( );

        TestSecurityHandler sh = createSecurityHandler();

        PutHandler putHandler = new PutHandler( new File( localRepositoryPath ) );

        sh.setHandler( putHandler );
        server.setHandler( sh );
        addConnector( server );
        server.start();

        StreamingWagon wagon = (StreamingWagon) getWagon();
        Repository testRepository = new Repository( "id", getRepositoryUrl( server ) );
        wagon.connect( testRepository, authInfo );
        try
        {
            for ( int i = 0; i < putNumber; i++ )
            {
                File sourceFile = new File( localRepositoryPath, "test-secured-put-resource" );
                sourceFile.delete();
                assertFalse( sourceFile.exists() );

                File tempFile = File.createTempFile( "wagon", "tmp" );
                tempFile.deleteOnExit();
                FileUtils.fileWrite( tempFile.getAbsolutePath(), "put top secret" );

                try
                {
                    wagon.put( tempFile, "test-secured-put-resource" );
                }
                finally
                {
                    tempFile.delete();
                }

                assertEquals( "put top secret", FileUtils.fileRead( sourceFile.getAbsolutePath() ) );
            }
        }
        finally
        {
            wagon.disconnect();
            server.stop();
        }
        assertEquals( putNumber, putHandler.putCallNumber );
        testPreemptiveAuthenticationPut( sh, supportPreemptiveAuthenticationPut() );
    }

    public void testNonSecuredPutFromStream()
        throws Exception
    {
        AuthenticationInfo authInfo = new AuthenticationInfo();
        authInfo.setUserName( "user" );
        authInfo.setPassword( "secret" );
        runTestSecuredPutFromStream( authInfo, 1, false );
    }

    public void testSecuredPutFromStream()
        throws Exception
    {
        AuthenticationInfo authInfo = new AuthenticationInfo();
        authInfo.setUserName( "user" );
        authInfo.setPassword( "secret" );
        runTestSecuredPutFromStream( authInfo, 1, true );
    }

    public void runTestSecuredPutFromStream( AuthenticationInfo authInfo, int putNumber, boolean addSecurityHandler )
        throws Exception
    {
        String localRepositoryPath = FileTestUtils.getTestOutputDir().toString();
        Server server = new Server( );

        TestSecurityHandler sh = createSecurityHandler();

        PutHandler putHandler = new PutHandler( new File( localRepositoryPath ) );

        if ( addSecurityHandler )
        {
            sh.setHandler( putHandler );
            server.setHandler( sh );
        }
        else
        {
            server.setHandler( putHandler );
        }
        addConnector( server );
        server.start();

        StreamingWagon wagon = (StreamingWagon) getWagon();
        Repository testRepository = new Repository( "id", getRepositoryUrl( server ) );
        if ( addSecurityHandler )
        {
            wagon.connect( testRepository, authInfo );
        }
        else
        {
            wagon.connect( testRepository );
        }
        try
        {
            for ( int i = 0; i < putNumber; i++ )
            {
                File sourceFile = new File( localRepositoryPath, "test-secured-put-resource" );
                sourceFile.delete();
                assertFalse( sourceFile.exists() );

                File tempFile = File.createTempFile( "wagon", "tmp" );
                tempFile.deleteOnExit();
                String content = "put top secret";
                FileUtils.fileWrite( tempFile.getAbsolutePath(), content );

                try ( FileInputStream fileInputStream = new FileInputStream( tempFile ) )
                {
                    wagon.putFromStream( fileInputStream, "test-secured-put-resource", content.length(), -1 );
                }
                finally
                {
                    tempFile.delete();
                }

                assertEquals( content, FileUtils.fileRead( sourceFile.getAbsolutePath() ) );
            }
        }
        finally
        {
            wagon.disconnect();
            server.stop();
        }
        assertEquals( putNumber, putHandler.putCallNumber );
        if ( addSecurityHandler )
        {
            testPreemptiveAuthenticationPut( sh, supportPreemptiveAuthenticationPut() );
        }

        // ensure we didn't use chunked transfer which doesn't work on ngnix
        for ( DeployedResource deployedResource : putHandler.deployedResources )
        {
            if ( StringUtils.equalsIgnoreCase( "chunked", deployedResource.transferEncoding ) )
            {
                fail( "deployedResource use chunked: " + deployedResource );
            }
        }
    }


    protected abstract boolean supportPreemptiveAuthenticationPut();

    protected abstract boolean supportPreemptiveAuthenticationGet();

    protected abstract boolean supportProxyPreemptiveAuthentication();

    protected void testPreemptiveAuthenticationGet( TestSecurityHandler sh, boolean preemptive )
    {
        testPreemptiveAuthentication( sh, preemptive, HttpServletResponse.SC_OK );
    }

    protected void testPreemptiveAuthenticationPut( TestSecurityHandler sh, boolean preemptive )
    {
        testPreemptiveAuthentication( sh, preemptive, HttpServletResponse.SC_CREATED );
    }

    protected void testPreemptiveAuthentication( TestSecurityHandler sh, boolean preemptive, int statusCode )
    {

        if ( preemptive )
        {
            assertEquals( "not 1 security handler use " + sh.handlerRequestResponses, 1,
                          sh.handlerRequestResponses.size() );
            assertEquals( statusCode, sh.handlerRequestResponses.get( 0 ).responseCode );
        }
        else
        {
            assertEquals( "not 2 security handler use " + sh.handlerRequestResponses, 2,
                          sh.handlerRequestResponses.size() );
            assertEquals( HttpServletResponse.SC_UNAUTHORIZED, sh.handlerRequestResponses.get( 0 ).responseCode );
            assertEquals( statusCode, sh.handlerRequestResponses.get( 1 ).responseCode );

        }
    }

    static class StatusHandler
        extends AbstractHandler
    {
        private int status;

        public void setStatusToReturn( int status )
        {
            this.status = status;
        }

        public void handle( String target, Request baseRequest, HttpServletRequest request,
            HttpServletResponse response ) throws IOException, ServletException
        {
            if ( status != 0 )
            {
                response.setStatus( status );
                baseRequest.setHandled( true );
            }
        }
    }

    static class DeployedResource
    {
        String httpMethod;

        String requestUri;

        String contentLength;

        String transferEncoding;

        DeployedResource()
        {
            // no op
        }

        @Override
        public String toString()
        {
            final StringBuilder sb = new StringBuilder();
            sb.append( "DeployedResource" );
            sb.append( "{httpMethod='" ).append( httpMethod ).append( '\'' );
            sb.append( ", requestUri='" ).append( requestUri ).append( '\'' );
            sb.append( ", contentLength='" ).append( contentLength ).append( '\'' );
            sb.append( ", transferEncoding='" ).append( transferEncoding ).append( '\'' );
            sb.append( '}' );
            return sb.toString();
        }
    }

    /**
     *
     */
    @SuppressWarnings( "checkstyle:visibilitymodifier" )
    public static class PutHandler
        extends AbstractHandler
    {
        private final File resourceBase;

        public List<DeployedResource> deployedResources = new ArrayList<DeployedResource>();

        public int putCallNumber = 0;

        public List<HandlerRequestResponse> handlerRequestResponses = new ArrayList<HandlerRequestResponse>();

        public PutHandler( File repositoryDirectory )
        {
            this.resourceBase = repositoryDirectory;
        }

        public void handle( String target, Request baseRequest, HttpServletRequest request,
            HttpServletResponse response ) throws IOException, ServletException
        {
            if ( baseRequest.isHandled() || !"PUT".equals( baseRequest.getMethod() ) )
            {
                return;
            }

            baseRequest.setHandled( true );

            File file = new File( resourceBase, URLDecoder.decode( request.getPathInfo() ) );
            file.getParentFile().mkdirs();
            OutputStream out = null;
            InputStream in = null;
            try
            {
                in = request.getInputStream();
                out = new FileOutputStream( file );
                IOUtil.copy( in, out );
                out.close();
                out = null;
                in.close();
                in = null;
            }
            finally
            {
                IOUtil.close( in );
                IOUtil.close( out );
            }
            putCallNumber++;
            DeployedResource deployedResource = new DeployedResource();

            deployedResource.httpMethod = request.getMethod();
            deployedResource.requestUri = request.getRequestURI();
            deployedResource.transferEncoding = request.getHeader( "Transfer-Encoding" );
            deployedResource.contentLength = request.getHeader( "Content-Length" );
            deployedResources.add( deployedResource );

            response.setStatus( HttpServletResponse.SC_CREATED );

            handlerRequestResponses.add(
                new HandlerRequestResponse( request.getMethod(), ( (Response) response ).getStatus(),
                                            request.getRequestURI() ) );
        }
    }

    private static class AuthorizingProxyHandler
        extends TestHeaderHandler
    {

        List<HandlerRequestResponse> handlerRequestResponses = new ArrayList<HandlerRequestResponse>();

        public void handle( String target, Request baseRequest, HttpServletRequest request,
            HttpServletResponse response ) throws IOException, ServletException
        {
            System.out.println( " handle proxy request" );
            if ( request.getHeader( "Proxy-Authorization" ) == null )
            {
                handlerRequestResponses.add(
                    new HandlerRequestResponse( request.getMethod(),
                                                HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED,
                                                request.getRequestURI() ) );
                response.setStatus( HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED );
                response.addHeader( "Proxy-Authenticate", "Basic realm=\"Squid proxy-caching web server\"" );

                baseRequest.setHandled( true );
                return;
            }
            handlerRequestResponses.add(
                new HandlerRequestResponse( request.getMethod(), HttpServletResponse.SC_OK, request.getRequestURI() ) );
            super.handle( target, baseRequest, request, response );
        }
    }

    /**
     *
     */
    @SuppressWarnings( "checkstyle:visibilitymodifier" )
    private static class TestHeaderHandler
        extends AbstractHandler
    {
        public Map<String, String> headers = Collections.emptyMap();

        public List<HandlerRequestResponse> handlerRequestResponses = new ArrayList<HandlerRequestResponse>();

        TestHeaderHandler()
        {
        }

        public void handle( String target, Request baseRrequest, HttpServletRequest request,
            HttpServletResponse response ) throws IOException, ServletException
        {
            headers = new HashMap<String, String>();
            for ( Enumeration<String> e = baseRrequest.getHeaderNames(); e.hasMoreElements(); )
            {
                String name = e.nextElement();
                Enumeration headerValues = baseRrequest.getHeaders( name );
                // as per HTTP spec http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html
                // multiple values for the same header key are concatenated separated by comma
                // otherwise we wouldn't notice headers with same key added multiple times
                StringBuffer combinedHeaderValue = new StringBuffer();
                for ( int i = 0; headerValues.hasMoreElements(); i++ )
                {
                    if ( i > 0 )
                    {
                        combinedHeaderValue.append( "," );
                    }
                    combinedHeaderValue.append( headerValues.nextElement() );
                }
                headers.put( name, combinedHeaderValue.toString() );
            }

            response.setContentType( "text/plain" );
            response.setStatus( HttpServletResponse.SC_OK );
            response.getWriter().print( "Hello, World!" );

            handlerRequestResponses.add(
                new HandlerRequestResponse( baseRrequest.getMethod(), ( (Response) response ).getStatus(),
                                            baseRrequest.getRequestURI() ) );

            baseRrequest.setHandled( true );
        }

    }

    protected TestSecurityHandler createSecurityHandler()
    {
        Constraint constraint = new Constraint();
        constraint.setName( Constraint.__BASIC_AUTH );
        constraint.setRoles( new String[]{ "admin" } );
        constraint.setAuthenticate( true );

        ConstraintMapping cm = new ConstraintMapping();
        cm.setConstraint( constraint );
        cm.setPathSpec( "/*" );

        TestSecurityHandler sh = new TestSecurityHandler();
        HashLoginService hashLoginService = new HashLoginService( "MyRealm" );
        hashLoginService.putUser( "user", new Password( "secret" ), new String[] { "admin" } );
        sh.setLoginService( hashLoginService );
        sh.setConstraintMappings( new ConstraintMapping[]{ cm } );
        sh.setAuthenticator ( new BasicAuthenticator() );
        return sh;
    }

    /**
     *
     */
    @SuppressWarnings( "checkstyle:visibilitymodifier" )
    public static class TestSecurityHandler
        extends ConstraintSecurityHandler
    {

        public List<HandlerRequestResponse> handlerRequestResponses = new ArrayList<HandlerRequestResponse>();

        @Override
        public void handle( String target, Request baseRequest, HttpServletRequest request,
            HttpServletResponse response ) throws IOException, ServletException
        {
            String method = request.getMethod();
            super.handle( target, baseRequest, request, response );

            handlerRequestResponses.add(
                new HandlerRequestResponse( method, ( (Response) response ).getStatus(), request.getRequestURI() ) );
        }
    }

    /**
     *
     */
    @SuppressWarnings( "checkstyle:visibilitymodifier" )
    public static class HandlerRequestResponse
    {
        public String method;

        public int responseCode;

        public String requestUri;

        private HandlerRequestResponse( String method, int responseCode, String requestUri )
        {
            this.method = method;
            this.responseCode = responseCode;
            this.requestUri = requestUri;
        }

        @Override
        public String toString()
        {
            final StringBuilder sb = new StringBuilder();
            sb.append( "HandlerRequestResponse" );
            sb.append( "{method='" ).append( method ).append( '\'' );
            sb.append( ", responseCode=" ).append( responseCode );
            sb.append( ", requestUri='" ).append( requestUri ).append( '\'' );
            sb.append( '}' );
            return sb.toString();
        }
    }

    /**
     * Verify a WagonException message contains required format and context based on the status code we expected to
     * trigger it in the first place.
     * <p>
     * This implementation represents the most desired assertions, but HttpWagonTestCase sub-classes could override
     * this method if a specific wagon representation makes it impossible to meet these assertions.
     *
     * @param e               an instance of {@link WagonException}
     * @param forStatusCode   the response status code that triggered the exception
     * @param forUrl          the url that triggered the exception
     * @param forReasonPhrase the optional status line reason phrase the server returned
     */
    protected void verifyWagonExceptionMessage( Exception e, int forStatusCode, String forUrl, String forReasonPhrase )
    {
        // TODO: handle AuthenticationException for Wagon.connect() calls
        assertNotNull( e );
        try
        {
            assertTrue( "only verify instances of WagonException", e instanceof WagonException );

            String reasonPhrase;
            String assertMessageForBadMessage = "exception message not described properly";
            switch ( forStatusCode )
            {
                case HttpServletResponse.SC_NOT_FOUND:
                    // TODO: add test for 410: Gone?
                    assertTrue( "404 not found response should throw ResourceDoesNotExistException",
                            e instanceof ResourceDoesNotExistException );
                    reasonPhrase = StringUtils.isEmpty( forReasonPhrase ) ? " Not Found" : ( " " + forReasonPhrase );
                    assertEquals( assertMessageForBadMessage, "resource missing at " + forUrl + ", status: 404"
                            + reasonPhrase, e.getMessage() );
                    break;

                case HttpServletResponse.SC_UNAUTHORIZED:
                    // FIXME assumes Wagon.get()/put() returning 401 instead of Wagon.connect()
                    assertTrue( "401 Unauthorized should throw AuthorizationException since "
                                    + " AuthenticationException is not explicitly declared as thrown from wagon "
                                    + "methods",
                            e instanceof AuthorizationException );
                    reasonPhrase = StringUtils.isEmpty( forReasonPhrase ) ? " Unauthorized" : ( " " + forReasonPhrase );
                    assertEquals( assertMessageForBadMessage, "authentication failed for " + forUrl + ", status: 401"
                            + reasonPhrase, e.getMessage() );
                    break;

                case HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED:
                    assertTrue( "407 Proxy authentication required should throw AuthorizationException",
                            e instanceof AuthorizationException );
                    reasonPhrase = StringUtils.isEmpty( forReasonPhrase ) ? " Proxy Authentication Required"
                            : ( " " + forReasonPhrase );
                    assertEquals( assertMessageForBadMessage, "proxy authentication failed for "
                            + forUrl + ", status: 407" + reasonPhrase, e.getMessage() );
                    break;

                case HttpServletResponse.SC_FORBIDDEN:
                    assertTrue( "403 Forbidden should throw AuthorizationException",
                            e instanceof AuthorizationException );
                    reasonPhrase = StringUtils.isEmpty( forReasonPhrase ) ? " Forbidden" : ( " " + forReasonPhrase );
                    assertEquals( assertMessageForBadMessage, "authorization failed for " + forUrl + ", status: 403"
                            + reasonPhrase, e.getMessage() );
                    break;

                default:
                    assertTrue( "transfer failures should at least be wrapped in a TransferFailedException", e
                                    instanceof TransferFailedException );
                    assertTrue( "expected status code for transfer failures should be >= 400",
                            forStatusCode >= HttpServletResponse.SC_BAD_REQUEST );
                    reasonPhrase = forReasonPhrase == null ? "" : " " + forReasonPhrase;
                    assertEquals( assertMessageForBadMessage, "transfer failed for " + forUrl + ", status: "
                            + forStatusCode + reasonPhrase, e.getMessage() );
                    break;
            }
        }
        catch ( AssertionError assertionError )
        {
            logger.error( "Exception which failed assertions: ", e );
            throw assertionError;
        }

    }

}
