HTTP clients in/for Java

Russell Bateman
January 2018
last update:

This is hastily excerpted code from a project and is incomplete. I use it only for reference and comparison. (See also here.)

We're going to write code for three different APIs:

  1. HttpURLConnection, the ancient JDK client
  2. HttpClient, the Apache client, said to be several times faster
  3. OkHttpClient, a much more modern and understandable API

Let's pretend we need a client that always posts one kind of data and queries that kind of data from a server via fairly static URLs. We wrote the code for these three, different clients in a way to be able to switch between them for benchmarking.

HttpClientOperations.java:
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;

public interface HttpClientOperations
{
  Integer QUERY_TIMEOUT  = 1000;                // milliseconds
  String  ACCEPT_CHARSET = "Accept-Charset";
  String  UTF8           = "UTF-8";

  String      postRecord( final String record ) throws IOException;
  InputStream getQuery  ( final String query )  throws IOException;
}

HttpURLConnection

HttpUrlOperations.java:
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;

import org.slf4j.Logger;

public class HttpUrlOperations implements HttpClientOperations
{
  private HttpURLConnection connection;

  private static Logger logger;
  private static String postUrlString;  // something like "http://localhost:30000/cache/addrecords?store=0"
  private static URL    postURL;

  public static void injectPostUrl( final String url ) { postUrlString = url; }
  public static void injectLogger( Logger aLogger )    { logger = aLogger; }

  private static class OperationsHolder
  {
    private static final HttpUrlOperations INSTANCE = new HttpUrlOperations( logger);
  }

  public static HttpUrlOperations getInstance()
  {
    return OperationsHolder.INSTANCE;
  }

  /**
   * The URL is for POST operations. Note that
   * connection.setRequestProperty("Connection", "Keep-Alive");
   * ...is, in fact, the default so we don't need to make it explicit.
   *
   * @param logger if known.
   */
  private HttpUrlOperations( Logger logger )
  {
    HttpUrlOperations.logger = logger;
    try
    {
      postURL = new URL( postUrlString );
    }
    catch( IOException e )
    {
      postURL = null;
    }

    logInfo( "Using OkHttpClient client" );
  }

  /**
   * This posts data being written to the server.
   */
  public String postRecord( final String record ) throws IOException
  {
    if( postURL == null )
      throw new IOException( "URL to post to was botched at initialization" );

    connection = ( HttpURLConnection ) ( postURL.openConnection() );

    connection.setRequestMethod( "POST" );
    connection.setUseCaches( false );
    connection.setDoOutput( true );
    connection.setRequestProperty( "Accept", "application/xml,application/json" );
    connection.addRequestProperty( "Content-type", "application/xml" );

    try
    {
      DataOutputStream writer = new DataOutputStream( connection.getOutputStream() );
      writer.writeBytes( record );
      writer.close();
    }
    catch( IOException e )
    {
      Throwable t = e.getCause();
      String message = e.getMessage() + " occurred posting record (" + t.getMessage() + ')';
      throw new IOException( message );
    }

    // This is where the request is actually sent. No matter what,
    // get the response either to return or drain it.
    InputStream inputStream = connection.getInputStream();

    if( inputStream != null )
    {
      try
      {
        return StreamUtilities.fromStream( inputStream );
      }
      catch( IOException e )
      {
        Throwable t = e.getCause();
        String message = e.getMessage() + " occurred copying response (" + t.getMessage() + ')';
        throw new IOException( message );
      }
      finally
      {
        inputStream.close();
      }
    }

    InputStream errorStream = connection.getErrorStream();

    try
    {
      // the stream must be read through, so we do that.
      if( inputStream != null )
      {
        StreamUtilities.drainStream( inputStream, logger );
        inputStream.close();
      }

      // do similarly for the error stream.
      if( errorStream != null )
      {
        StreamUtilities.drainStream( errorStream, logger );
        errorStream.close();
      }
    }
    catch( IOException e )
    {
      Throwable t = e.getCause();
      String message = e.getMessage() + " occurred draining response and/or error (" + t.getMessage() + ')';
      throw new IOException( message );
    }
    finally
    {
      if( inputStream != null )
        inputStream.close();
      if( errorStream != null )
        errorStream.close();
    }

    return null;
  }


  /**
   * This gets data from the server.
   * @param query something like "http://localhost:30000/query?q=" plus the query
   * @return the server's response as an InputStream.
   * @throws IOException upon failure.
   */
  public InputStream getQuery( final String query ) throws IOException
  {
    connection = ( HttpURLConnection ) ( query.openConnection() );

    connection.setRequestProperty( ACCEPT_CHARSET, UTF8 );
    connection.setReadTimeout( QUERY_TIMEOUT );
    connection.setRequestMethod( "GET" );
    connection.setUseCaches( false );
    connection.setDoOutput( true );
    connection.setRequestProperty( "Accept", "application/xml,application/json" );

    try
    {
      return connection.getInputStream();
    }
    catch( IOException e )
    {
      Throwable t = e.getCause();
      String message = e.getMessage() + " occurred during query (" + t.getMessage() + ')';
      throw new IOException( message );
    }
  }

  public void injectLogging( Logger logger ) { HttpUrlOperations.logger = logger; }

  private void logInfo( final String info )
  {
    if( logger != null )
      logger.info( info );
  }
}

Apache HttpClient

ApacheClientOperations.java:
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;

import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

public class ApacheClientOperations implements HttpClientOperations
{
  private static Logger              logger;
  private static CloseableHttpClient client;
  private static String              postURL; // something like "http://localhost:30000/cache/addrecords?store=0"

  public static void injectPostUrl( final String url ) { postURL = url; }
  public static void injectLogger( Logger aLogger )    { logger = aLogger; }

  private static class OperationsHolder
  {
    private static final ApacheClientOperations INSTANCE = new ApacheClientOperations( logger);
  }

  public static ApacheClientOperations getInstance()
  {
    return OperationsHolder.INSTANCE;
  }

  /**
   * The URL is for POST operations.
   *
   * @param logger if known.
   */
  private ApacheClientOperations( Logger logger )
  {
    ApacheClientOperations.logger = logger;
    ApacheClientOperations.client = HttpClients.createDefault();
    logInfo( "Using Apache HTTP client" );
  }

  /**
   * This posts data being written to the server.
   */
  @Override
  public String postRecord( String record ) throws IOException
  {
    HttpPost post = new HttpPost( postURL );

    List< NameValuePair > properties = new ArrayList<>();
    properties.add( new BasicNameValuePair( "Accept",         "application/xml,application/json" ) );
    properties.add( new BasicNameValuePair( "Accept-Charset", "charset=utf-8" ) );
    properties.add( new BasicNameValuePair( "Content-type",   "application/xml" ) );
    post.setEntity( new UrlEncodedFormEntity( properties ) );

    CloseableHttpResponse response = client.execute( post );
    HttpEntity            entity   = response.getEntity();

    try
    {
      EntityUtils.consume( entity );
      return null;
    }
    catch( IOException e )
    {
      Throwable t = e.getCause();
      String message = e.getMessage() + " occurred discarding response (" + t.getMessage() + ')';
      throw new IOException( message );
    }
    finally
    {
      response.close();
    }
  }

  /**
   * This gets data from the server. The query is built as a URL before calling.
   * @param query something like "http://localhost:30000/query?q=" plus the query
   * @return the server's response as an InputStream.
   * @throws IOException upon failure.
   */
  @Override
  public InputStream getQuery( String query ) throws IOException
  {
    HttpGet get = new HttpGet( query );

    get.setHeader( ACCEPT_CHARSET, UTF8 );
    get.setHeader( "Accept", "application/xml,application/json" );

    CloseableHttpResponse response = client.execute( get );
    HttpEntity            entity   = response.getEntity();

    try
    {
      return entity.getContent();
    }
    catch( IOException e )
    {
      Throwable t = e.getCause();
      String message = e.getMessage() + " occurred getting query response (" + t.getMessage() + ')';
      throw new IOException( message );
    }
    finally
    {
      // TODO: Ha! this renders the InputStream we want to return invalid! Whatever shall we do?
      //response.close();
    }
  }

  public void injectLogging( Logger logger ) { ApacheClientOperations.logger = logger; }

  private void logInfo( final String info )
  {
    if( logger != null )
      logger.info( info );
  }
}

OkHttpClient

OkHttpOperations.java:
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;

import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;

public class OkHttpOperations implements HttpClientOperations
{
  private static Logger       logger;
  private static OkHttpClient okHttpClient;
  private static String       postURL;        // something like "http://localhost:30000/cache/addrecords?store=0"

  public static void injectPostUrl( final String url ) { postURL = url; }
  public static void injectLogger( Logger aLogger )    { logger = aLogger; }

  private static class OperationsHolder
  {
    private static final OkHttpOperations INSTANCE = new OkHttpOperations( logger);
  }

  public static OkHttpOperations getInstance()
  {
    return OperationsHolder.INSTANCE;
  }

  /**
   * The URL is for POST operations.
   *
   * @param logger if known.
   */
  private OkHttpOperations( Logger logger )
  {
    OkHttpOperations.logger = logger;
    okHttpClient = new OkHttpClient.Builder()
                                   .addInterceptor( new DefaultContentTypeInterceptor( "application/xml" ) )
                                   .readTimeout( QUERY_TIMEOUT, TimeUnit.MILLISECONDS )
                                   .build();
    logInfo( "Using OkHttpClient client" );
  }

  private class DefaultContentTypeInterceptor implements Interceptor
  {
    private String contentType;

    public DefaultContentTypeInterceptor( final String contentType ) { this.contentType = contentType; }

    @Override
    public Response intercept( Interceptor.Chain chain ) throws IOException
    {
      Request originalRequest      = chain.request();
      Request requestWithUserAgent = originalRequest.newBuilder()
                                                    .header("Content-Type", contentType )
                                                    .build();

      return chain.proceed( requestWithUserAgent );
    }
  }

  /**
   * This posts data being written to the server.
   */
  public String postRecord( final String record ) throws IOException
  {
    Request request = new Request.Builder()
                                 .url( postURL )
                                 .post( RequestBody.create(
                                           MediaType.parse( "application/xml; charset=utf-8" ), record ) )
                                 .build();
    try
    {
      Response     response = okHttpClient.newCall( request ).execute();
      ResponseBody body     = response.body();

      return( body != null ) ? body.string() : null;
    }
    catch( IOException e )
    {
      Throwable t = e.getCause();
      String message = e.getMessage() + " occurred posting record (" + t.getMessage() + ')';
      throw new IOException( message );
    }
  }

  /**
   * This gets data from the server. The query is built as a URL before calling.
   * @param query something like "http://localhost:30000/query?q=" plus the query
   * @return the server's response as an InputStream.
   * @throws IOException upon failure.
   */
  public InputStream getQuery( final String query ) throws IOException
  {
    try
    {
      Request      request  = new Request.Builder().url( query ).get().build();
      Response     response = okHttpClient.newCall( request ).execute();
      ResponseBody body     = response.body();

      if( body == null )      // TODO: learn a lot more about OkHttpClient failures!
        throw new IOException( "No response or other failure" );

      return body.byteStream();
    }
    catch( IOException e )
    {
      Throwable t = e.getCause();
      String message = e.getMessage() + " occurred during query (" + t.getMessage() + ')';
      throw new IOException( message );
    }
  }

  public void injectLogging( Logger logger ) { OkHttpOperations.logger = logger; }

  private void logInfo( final String info )
  {
    if( logger != null )
      logger.info( info );
  }
}
StreamUtilities.java:

Implemenation of String-from-Stream and drainStream().

import org.slf4j.Logger;

import java.io.IOException;
import java.io.InputStream;

public class StreamUtilities
{
  public static String fromStream( InputStream inputStream ) throws IOException
  {
    int           ch;
    StringBuilder sb = new StringBuilder();

    while( ( ch = inputStream.read() ) != -1 )
      sb.append( ( char ) ch );

    return sb.toString();
  }

  private static final Integer DRAINBUFFERSIZE = 65536;

  private static byte[] drainBuffer = new byte[ DRAINBUFFERSIZE ];

  /**
   * Per https://stackoverflow.com/questions/4767553/safe-use-of-httpurlconnection
   * and https://stackoverflow.com/questions/9943351/httpsurlconnection-and-keep-alive,
   * in order for the HttpURLConnection to remain open for continued use,
   * the response must be completely drained otherwise the connection will have to
   * close.
   *
   * We figure that the underlying HttpURLConnection layer knows when to stop
   * reading and won't attempt to read too much.
   *
   * TODO: how to optimize this? I considered available() and skip(), but I hear
   * bad things about them.
   *
   * @param inputStream with crap in it we don't care about, but must drain away.
   */
  public static void drainStream( InputStream inputStream, Logger logger )
  {
    int  thisRead;
    long bytesRead = 0;
    try
    {
      while( ( thisRead = inputStream.read( drainBuffer ) ) != -1 )
        bytesRead += thisRead;
    }
    catch( IOException e )
    {
      if( logger != null )
        logger.warn( "Failed while draining stream after reading " + bytesRead + " bytes" );
    }
  }
}

Java Servlet HTTP Notes

Some more useful code.

HttpServletRequestImpl.java:

Useful in JUnit-testing web service (especially Jersey) end-points.

package com.windofkeltia.utilities;

import java.io.BufferedReader;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.servlet.AsyncContext;
import javax.servlet.DispatcherType;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpUpgradeHandler;
import javax.servlet.http.Part;

import static java.util.Objects.nonNull;

/**
 * This was created as a testing aid. It uses a builder interface
 * added to (a work in progress) as needs arise.
 * @author Russell Bateman
 */
public class HttpServletRequestImpl implements HttpServletRequest
{
  private final Map< String, ? >      headers;
  private       Enumeration< String > headerNames;
  private final String                method;
  private final String                url;          // protocol :// hostname:port / extra path ? query parameters
  private final String                protocol;
  private final String                hostname;
  private final String                portNumber;
  private final String                extraPath;
  private final String                queryString;
  private       Map< String, String > queryParameters;
  private       List< String >        queryParameterNames;

  protected HttpServletRequestImpl( Builder builder )
  {
    if( nonNull( headers = builder.headers ) )
      headerNames = Collections.enumeration( headers.keySet() );

    method = builder.method;
    url    = builder.url;
    String work  = url;
    int    colon = work.indexOf( ':' );
    protocol     = work.substring( 0, colon );
    work         = work.substring( colon+3 );
    colon    = work.indexOf( ':' );
    hostname = work.substring( 0, colon );
    work     = work.substring( colon+1 );
    int slash  = work.indexOf( '/' );
    portNumber = work.substring( 0, slash );
    work       = work.substring( portNumber.length() );
    int question = work.indexOf( '?' );
    extraPath    = work.substring( 0, question );
    queryString  = work.substring( question+1 );

    String[] parametersArray = queryString.split( "&" );
    if( parametersArray.length > 0 )
    {
      queryParameters = new HashMap<>();
      for( String parameter : parametersArray )
      {
        int equals = parameter.indexOf( '=' );

        if( equals == -1 )
        {
          queryParameters.put( parameter, null );
          continue;
        }
        queryParameters.put( parameter.substring( 0, equals ), parameter.substring( equals+1 ) );
      }
      if( queryParameters.size() > 0 )
      {
        queryParameterNames = new ArrayList<>();
        queryParameterNames.addAll( queryParameters.keySet() );
      }
    }
  }

  public static class Builder
  {
    private Map< String, ? > headers = null;
    private String           method;
    private String           url;

    public Builder headers   ( Map< String, ? > headers ) { this.headers    = headers;    return this; }
    public Builder method    ( final String method )      { this.method     = method;     return this; }
    public Builder url       ( final String url )         { this.url        = url;        return this; }
    public HttpServletRequestImpl build()                 { return new HttpServletRequestImpl( this ); }
  }

  /** Returns the value of the specified request parameter, a String. */
  @Override public String getParameter( String s ) { return queryParameters.get( s ); }

  @Override public Enumeration< String > getParameterNames() { return Collections.enumeration( queryParameterNames ); }

  @Override public String[] getParameterValues( String s )
  {
    // TODO: when needed; we don't understand a parameter value as an array yet...
    return new String[ 0 ];
  }

  @Override public Map< String, String[] > getParameterMap() { return null; }

  /** Returns the value of the specified request header as a String. */
  @Override public String getHeader( String s ) { return headers.get( s ).toString(); }
  /** Returns the value of the specified request header as an int. */
  @Override public int getIntHeader( String s ) { String value = headers.get( s ).toString(); return Integer.parseInt( value ); }
  /** Returns the value of the specified request header as a long value that represents a Date object. */
  @Override public long getDateHeader( String s )
  {
//    String value = headers.get( s ).toString();
//    Date   date  = new Date( value );
    return 0L; /* TODO: finish as soon as really needed... */
  }

  /** Returns an enumeration of all the header names this request contains. */
   @Override public Enumeration< String > getHeaderNames() { return headerNames; }

  /** Returns all the values of the specified request header as an Enumeration of String objects. */
  @Override public Enumeration< String > getHeaders( String s )
  {
    List< String > values = new ArrayList<>( headers.size() );
    for( Map.Entry< String, ? > element : headers.entrySet() )
    {
      Object value = element.getValue();
      if( value instanceof String )
      {
        values.add( ( String ) value );
        continue;
      }

      // TODO: this may need some investigation when 'value' isn't a String...
      values.add( value.toString() );
    }
    return Collections.enumeration( values );
  }

  @Override public String getAuthType() { return null; /* if not authenticated */ }
  @Override public Cookie[] getCookies() { return new Cookie[ 0 ]; }
  @Override public String getMethod() { return method; }
  @Override public String getPathInfo() { return extraPath; }
  @Override public String getPathTranslated() { return extraPath; }
  @Override public String getContextPath()
  {
    int slash = extraPath.indexOf( '/', 1 );
    return ( slash == -1 ) ? extraPath : extraPath.substring( 0, slash );
  }
  @Override public String getQueryString() { return queryString; }
  @Override public String getRemoteUser() { return null; }
  @Override public boolean isUserInRole( String s ) { return false; }
  @Override public Principal getUserPrincipal() { return null; }
  @Override public String getRequestedSessionId() { return null; }
  @Override public String getRequestURI() { return url; }
  @Override public StringBuffer getRequestURL() { return null; }
  @Override public String getServletPath() { return null; }
  @Override public HttpSession getSession( boolean b ) { return null; }
  @Override public HttpSession getSession() { return null; }
  @Override public String changeSessionId() { return null; }
  @Override public boolean isRequestedSessionIdValid() { return false; }
  @Override public boolean isRequestedSessionIdFromCookie() { return false; }
  @Override public boolean isRequestedSessionIdFromURL() { return false; }
  @Override public boolean isRequestedSessionIdFromUrl() { return false; }
  @Override public boolean authenticate( HttpServletResponse httpServletResponse ) { return false; }
  @Override public void login( String s, String s1 ) { }
  @Override public void logout() { }
  @Override public Collection<Part> getParts() { return null; }
  @Override public Part getPart( String s ) { return null; }
  @Override public <T extends HttpUpgradeHandler> T upgrade( Class<T> aClass ) { return null; }
  @Override public Object getAttribute( String s ) { return null; }
  @Override public Enumeration<String> getAttributeNames() { return null; }
  @Override public String getCharacterEncoding() { return null; }
  @Override public void setCharacterEncoding( String s ) { }
  @Override public int getContentLength() { return 0; }
  @Override public long getContentLengthLong() { return 0; }
  @Override public String getContentType() { return null; }
  @Override public ServletInputStream getInputStream() { return null; }
  @Override public String getProtocol() { return protocol; }
  @Override public String getScheme() { return null; }
  @Override public String getServerName() { return hostname; }
  @Override public int getServerPort() { return Integer.parseInt( portNumber ); }
  @Override public BufferedReader getReader() { return null; }
  @Override public String getRemoteAddr() { return null; }
  @Override public String getRemoteHost() { return null; }
  @Override public void setAttribute( String s, Object o ) { }
  @Override public void removeAttribute( String s ) { }
  @Override public Locale getLocale() { return null; }
  @Override public Enumeration<Locale> getLocales() { return null; }
  @Override public boolean isSecure() { return false; }
  @Override public RequestDispatcher getRequestDispatcher( String s ) { return null; }
  @Override public String getRealPath( String s ) { return null; }
  @Override public int getRemotePort() { return 0; }
  @Override public String getLocalName() { return null; }
  @Override public String getLocalAddr() { return null; }
  @Override public int getLocalPort() { return 0; }
  @Override public ServletContext getServletContext() { return null; }
  @Override public AsyncContext startAsync()
      throws IllegalStateException { return null; }
  @Override public AsyncContext startAsync( ServletRequest servletRequest, ServletResponse servletResponse )
      throws IllegalStateException { return null; }
  @Override public boolean isAsyncStarted() { return false; }
  @Override public boolean isAsyncSupported() { return false; }
  @Override public AsyncContext getAsyncContext() { return null; }
  @Override public DispatcherType getDispatcherType() { return null; }
}
HttpServletRequestImplTest.java:
package com.windofkeltia.trials;

import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;

import static java.util.Objects.isNull;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import com.windofkeltia.utilities.HttpServletRequestImpl;
import com.windofkeltia.utilities.StringUtilities;
import com.windofkeltia.utilities.TestUtilities;

/**
 * Vets the class above.
 * @author Russell Bateman
 */
public class HttpServletRequestImplTest
{
  @Rule public TestName name = new TestName();
  @After public void tearDown() { }
  @Before public void setUp()
  {
    TestUtilities.setUp( name );
    Map< String, String > headerMappings  = new HashMap<>( 3 );
    headerMappings.put( "ContentType", "application/xml" );
    headerMappings.put( "Accept",      "application/json" );
    request = new HttpServletRequestImpl.Builder()
                         .url( "http://localhost:8080/extra/path?one=this&two=that&three=the%20other%20thing" )
                         .headers( headerMappings )
                         .build();
  }

  private static final boolean VERBOSE = TestUtilities.VERBOSE;

  private HttpServletRequest request;

  @Test
  public void testBasics()
  {
    String protocol = request.getProtocol();
    String hostname = request.getServerName();
    int    port     = request.getServerPort();
    if( VERBOSE )
    {
      System.out.println( "Protocol:<n  " + protocol );
      System.out.println( "Hostname:<n  " + hostname );
      System.out.println( "Port:<n  " + port );
    }
    assertEquals( protocol, "http" );
    assertEquals( hostname, "localhost" );
    assertEquals( port,     8080 );

    String pathInfo       = request.getPathInfo();
    String contextPath    = request.getContextPath();
    String pathTranslated = request.getPathTranslated();
    if( VERBOSE )
    {
      System.out.println( "Path info:<n  " + pathInfo );
      System.out.println( "Context path:<n  " + contextPath );
      System.out.println( "Path translated:<n  " + pathTranslated );
    }
    assertEquals( pathInfo,       "/extra/path" );
    assertEquals( contextPath,    "/extra" );
    assertEquals( pathTranslated, "/extra/path" );

    Enumeration< String > parameterNames = request.getParameterNames();
    Enumeration< String > headerNames    = request.getHeaderNames();
    if( VERBOSE )
    {
      System.out.println( "Parameter names:" );
      while( parameterNames.hasMoreElements() )
        System.out.println( "  " + parameterNames.nextElement() );
      System.out.println( "Header names:" );
      while( headerNames.hasMoreElements() )
        System.out.println( "  " + headerNames.nextElement() );
    }
    String accept = request.getHeader( "Accept" );  if( VERBOSE )
		System.out.println( "Accept:<n  <"" + accept + '<"' );
    assertEquals( accept, "application/json" );
    String queryString = request.getQueryString();    if( VERBOSE )
		System.out.println( "Query parameters:<n  <"" + queryString + '<"' );
    assertEquals( queryString, "one=this&two=that&three=the%20other%20thing" );
    String three = request.getParameter( "three" ); if( VERBOSE )
		System.out.println( "Parameter <"three<":<n  <"" + three +
		'<"' );
    assertEquals( three, "the%20other%20thing" );
  }

  /**
   * Demonstrates proper way to dig through headers. Along with getHeader(),
   * there exists also <tt>getIntHeader()</tt> and getDateHeader() for use
   * when you know the value will be <tt>int</tt>> or Date.
   */
  @Test
  public void testHeaders()
  {
    Enumeration< String > requestHeaderNames = request.getHeaderNames();
    if( isNull( requestHeaderNames ) )
      fail( "There was nothing in the request headers" );

    int maxNameWidth = 0;

    // note that once you exhaust the enumeration, you're done--no way around this, so copy it first...
    List< String > headerNames = new ArrayList<>();

    while( requestHeaderNames.hasMoreElements() )
      headerNames.add( requestHeaderNames.nextElement() );

    // now we can make sensible and reusable use of the list of header names...
    for( String name : headerNames )
      maxNameWidth = Math.max( maxNameWidth, name.length() );

    for( String name : headerNames )
    {
      String headerValue = request.getHeader( name );
      if( VERBOSE )
        System.out.println( " " + StringUtilities.padStringLeft( name, maxNameWidth ) + " = " + headerValue );
    }
  }

  @Test
  public void testParameters()
  {
    Enumeration< String > requestParameterNames = request.getParameterNames();
    if( isNull( requestParameterNames ) )
      fail( "There were no request parameters" );

    int maxNameWidth = 0;

    // note that once you exhaust the enumeration, you're done--no way around this, so copy it first...
    List< String > parameterNames = new ArrayList<>();

    while( requestParameterNames.hasMoreElements() )
      parameterNames.add( requestParameterNames.nextElement() );

    // now we can make sensible and reusable use of the list of header names...
    for( String name : parameterNames )
      maxNameWidth = Math.max( maxNameWidth, name.length() );

    for( String name : parameterNames )
    {
      String value = request.getParameter( name );
      if( VERBOSE )
        System.out.println( " " + StringUtilities.padStringLeft( name, maxNameWidth ) + " = " + value );
    }
  }
}
EnumerationTest.java:

—in case you've never messed with Enumerations collection before.

package com.windofkeltia.trials;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.NoSuchElementException;

import org.junit.Test;

/**
 * Enumerations are used in HttpServletRequest. To see how they work, examine
 * {@link com.windofkeltia.http.HttpServletRequestImplTest}.
 * @author Russell Bateman
 */
public class EnumerationTest
{
  @Test
  public void test()
  {
    List< String > headerNames = new ArrayList<>();
    headerNames.add( "ContentType" );
    headerNames.add( "Accept" );

    Enumeration< String > headers = Collections.enumeration( headerNames );

    try
    {
      while( headers.hasMoreElements() )
      {
        System.out.println( headers.nextElement() );
      }
    }
    catch( IllegalArgumentException e )
    {
      e.printStackTrace();
    }
    catch( NoSuchElementException e )
    {
      e.printStackTrace();
    }
  }
}