[WAGON-567] Provide request retry strategy on transient client and server side errors

This closes #57
diff --git a/wagon-providers/wagon-http-shared/src/main/java/org/apache/maven/wagon/shared/http/AbstractHttpClientWagon.java b/wagon-providers/wagon-http-shared/src/main/java/org/apache/maven/wagon/shared/http/AbstractHttpClientWagon.java
index 7fcbc7f..3bf4291 100755
--- a/wagon-providers/wagon-http-shared/src/main/java/org/apache/maven/wagon/shared/http/AbstractHttpClientWagon.java
+++ b/wagon-providers/wagon-http-shared/src/main/java/org/apache/maven/wagon/shared/http/AbstractHttpClientWagon.java
@@ -34,6 +34,7 @@
 import org.apache.http.client.AuthCache;
 import org.apache.http.client.CredentialsProvider;
 import org.apache.http.client.HttpRequestRetryHandler;
+import org.apache.http.client.ServiceUnavailableRetryStrategy;
 import org.apache.http.client.config.AuthSchemes;
 import org.apache.http.client.config.CookieSpecs;
 import org.apache.http.client.config.RequestConfig;
@@ -61,6 +62,7 @@
 import org.apache.http.impl.client.BasicCredentialsProvider;
 import org.apache.http.impl.client.CloseableHttpClient;
 import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
+import org.apache.http.impl.client.DefaultServiceUnavailableRetryStrategy;
 import org.apache.http.impl.client.HttpClientBuilder;
 import org.apache.http.impl.client.StandardHttpRequestRetryHandler;
 import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
@@ -456,6 +458,55 @@
         }
     }
 
+    /**
+     * The type of the serviceUnavailableRetryStrategy, defaults to {@code none}.
+     * Values can be {@link default DefaultServiceUnavailableRetryStrategy},
+     * or {@link standard StandardServiceUnavailableRetryStrategy}, or
+     * a fully qualified name class with a no-arg or none to not use a ServiceUnavailableRetryStrategy.
+     */
+    private static final String SERVICE_UNAVAILABLE_RETRY_STRATEGY_CLASS =
+            System.getProperty( "maven.wagon.http.serviceUnavailableRetryStrategy.class", "none" );
+
+    /**
+     * Interval in milliseconds between retries when using a serviceUnavailableRetryStrategy.
+     * <b>1000 by default</b>
+     */
+    private static final int SERVICE_UNAVAILABLE_RETRY_STRATEGY_RETRY_INTERVAL =
+        Integer.getInteger( "maven.wagon.http.serviceUnavailableRetryStrategy.retryInterval", 1000 );
+
+    /**
+     * Maximum number of retries when using a serviceUnavailableRetryStrategy.
+     * <b>5 by default</b>
+     */
+    private static final int SERVICE_UNAVAILABLE_RETRY_STRATEGY_MAX_RETRIES =
+        Integer.getInteger( "maven.wagon.http.serviceUnavailableRetryStrategy.maxRetries", 5 );
+
+    private static ServiceUnavailableRetryStrategy createServiceUnavailableRetryStrategy()
+    {
+        switch ( SERVICE_UNAVAILABLE_RETRY_STRATEGY_CLASS )
+        {
+            case "none": return null;
+            case "default":
+                return new DefaultServiceUnavailableRetryStrategy(
+                    SERVICE_UNAVAILABLE_RETRY_STRATEGY_MAX_RETRIES, SERVICE_UNAVAILABLE_RETRY_STRATEGY_RETRY_INTERVAL );
+            case "standard":
+                return new StandardServiceUnavailableRetryStrategy(
+                    SERVICE_UNAVAILABLE_RETRY_STRATEGY_MAX_RETRIES, SERVICE_UNAVAILABLE_RETRY_STRATEGY_RETRY_INTERVAL );
+            default:
+                try
+                {
+                    final ClassLoader classLoader = AbstractHttpClientWagon.class.getClassLoader();
+                    return ServiceUnavailableRetryStrategy.class.cast(
+                            classLoader.loadClass( SERVICE_UNAVAILABLE_RETRY_STRATEGY_CLASS )
+                                                                          .getConstructor().newInstance() );
+                }
+                catch ( final Exception e )
+                {
+                    throw new IllegalArgumentException( e );
+                }
+        }
+    }
+
     private static Registry<AuthSchemeProvider> createAuthSchemeRegistry()
     {
         return RegistryBuilder.<AuthSchemeProvider>create()
@@ -492,6 +543,7 @@
             .disableConnectionState() //
             .setConnectionManager( httpClientConnectionManager ) //
             .setRetryHandler( createRetryHandler() )
+            .setServiceUnavailableRetryStrategy( createServiceUnavailableRetryStrategy() )
             .setDefaultAuthSchemeRegistry( createAuthSchemeRegistry() )
             .build();
     }
diff --git a/wagon-providers/wagon-http-shared/src/main/java/org/apache/maven/wagon/shared/http/StandardServiceUnavailableRetryStrategy.java b/wagon-providers/wagon-http-shared/src/main/java/org/apache/maven/wagon/shared/http/StandardServiceUnavailableRetryStrategy.java
new file mode 100644
index 0000000..7f4d485
--- /dev/null
+++ b/wagon-providers/wagon-http-shared/src/main/java/org/apache/maven/wagon/shared/http/StandardServiceUnavailableRetryStrategy.java
@@ -0,0 +1,79 @@
+package org.apache.maven.wagon.shared.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.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.annotation.Contract;
+import org.apache.http.annotation.ThreadingBehavior;
+import org.apache.http.client.ServiceUnavailableRetryStrategy;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.util.Args;
+
+/**
+ * An implementation of the {@link ServiceUnavailableRetryStrategy} interface.
+ * that retries {@code 408} (Request Timeout), {@code 429} (Too Many Requests),
+ * and {@code 500} (Server side error) responses for a fixed number of times at a fixed interval.
+ */
+@Contract( threading = ThreadingBehavior.IMMUTABLE )
+public class StandardServiceUnavailableRetryStrategy implements ServiceUnavailableRetryStrategy
+{
+    /**
+     * Maximum number of allowed retries if the server responds with a HTTP code
+     * in our retry code list.
+     */
+    private final int maxRetries;
+
+    /**
+     * Retry interval between subsequent requests, in milliseconds.
+     */
+    private final long retryInterval;
+
+    public StandardServiceUnavailableRetryStrategy( final int maxRetries, final int retryInterval )
+    {
+        super();
+        Args.positive( maxRetries, "Max retries" );
+        Args.positive( retryInterval, "Retry interval" );
+        this.maxRetries = maxRetries;
+        this.retryInterval = retryInterval;
+    }
+
+    @Override
+    public boolean retryRequest( final HttpResponse response, final int executionCount, final HttpContext context )
+    {
+        int statusCode = response.getStatusLine().getStatusCode();
+        boolean retryableStatusCode = statusCode == HttpStatus.SC_REQUEST_TIMEOUT
+                // Too Many Requests ("standard" rate-limiting)
+                || statusCode == AbstractHttpClientWagon.SC_TOO_MANY_REQUESTS
+                // Assume server errors are momentary hiccups
+                || statusCode == HttpStatus.SC_INTERNAL_SERVER_ERROR
+                || statusCode == HttpStatus.SC_BAD_GATEWAY
+                || statusCode == HttpStatus.SC_SERVICE_UNAVAILABLE
+                || statusCode == HttpStatus.SC_GATEWAY_TIMEOUT;
+        return executionCount <= maxRetries && retryableStatusCode;
+    }
+
+    @Override
+    public long getRetryInterval()
+    {
+        return retryInterval;
+    }
+
+}