Index: CMDIValidator/trunk/cmdi-validator-core/pom.xml
===================================================================
--- CMDIValidator/trunk/cmdi-validator-core/pom.xml	(revision 5384)
+++ CMDIValidator/trunk/cmdi-validator-core/pom.xml	(revision 5387)
@@ -40,5 +40,5 @@
             <groupId>org.apache.httpcomponents</groupId>
             <artifactId>httpclient</artifactId>
-            <version>4.2.5</version>
+            <version>4.3.4</version>
         </dependency>
 
@@ -111,4 +111,5 @@
         </dependency>
 
+        <!--
         <dependency>
             <groupId>org.apache.commons</groupId>
@@ -116,4 +117,5 @@
             <version>3.2</version>
         </dependency>
+        -->
     </dependencies>
 
Index: CMDIValidator/trunk/cmdi-validator-core/src/main/java/eu/clarin/cmdi/validator/CMDISchemaLoader.java
===================================================================
--- CMDIValidator/trunk/cmdi-validator-core/src/main/java/eu/clarin/cmdi/validator/CMDISchemaLoader.java	(revision 5384)
+++ CMDIValidator/trunk/cmdi-validator-core/src/main/java/eu/clarin/cmdi/validator/CMDISchemaLoader.java	(revision 5387)
@@ -22,6 +22,12 @@
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.InterruptedIOException;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.nio.channels.FileLock;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
 import javax.xml.XMLConstants;
 
@@ -30,12 +36,15 @@
 import org.apache.http.HttpStatus;
 import org.apache.http.StatusLine;
-import org.apache.http.client.HttpClient;
+import org.apache.http.client.config.CookieSpecs;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
 import org.apache.http.client.methods.HttpGet;
-import org.apache.http.client.params.ClientPNames;
-import org.apache.http.client.utils.HttpClientUtils;
-import org.apache.http.impl.client.DefaultHttpClient;
-import org.apache.http.params.CoreProtocolPNames;
-import org.apache.http.params.HttpParams;
-import org.apache.http.util.EntityUtils;
+import org.apache.http.config.SocketConfig;
+import org.apache.http.conn.ConnectionKeepAliveStrategy;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.http.protocol.BasicHttpContext;
+import org.apache.http.protocol.HttpContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -45,11 +54,20 @@
     private static final Logger logger =
             LoggerFactory.getLogger(CMDISchemaLoader.class);
+    private static final String USER_AGENT =
+            "CMDI-Validator-SchemaLoader/" + Version.getVersion();
     private static final String XML_XSD_RESSOURCE = "/xml.xsd";
+    private static final String EXTENSION_XSD   = "xsd";
+    private static final String EXTENSION_ERROR = "error";
     private final File cacheDirectory;
     private final long maxCacheAge;
-    private final HttpClient httpClient;
-
-
-    public CMDISchemaLoader(File cacheDirectory, long maxCacheAge) {
+    private final long maxNegativeCacheAge;
+    private final CloseableHttpClient httpClient;
+    private final Set<String> pending = new HashSet<String>(128);
+    private final Object guard = new Object();
+    private final Object waiter = new Object();
+
+
+    public CMDISchemaLoader(File cacheDirectory, long maxCacheAge,
+            long maxNegativeCacheAge) {
         if (cacheDirectory == null) {
             throw new NullPointerException("cacheDirectory == null");
@@ -58,14 +76,16 @@
             throw new IllegalArgumentException("maxCacheAge < -1");
         }
-        this.cacheDirectory = cacheDirectory;
-        this.maxCacheAge    = maxCacheAge;
-        this.httpClient     = new DefaultHttpClient();
-
-        final HttpParams params = this.httpClient.getParams();
-        params.setParameter(CoreProtocolPNames.USER_AGENT,
-                this.getClass().getPackage().getName() + "/0.0.1");
-        params.setBooleanParameter(ClientPNames.HANDLE_REDIRECTS, Boolean.TRUE);
-        params.setBooleanParameter(ClientPNames.ALLOW_CIRCULAR_REDIRECTS, true);
-        params.setIntParameter(ClientPNames.MAX_REDIRECTS, 16);
+        if (maxNegativeCacheAge < -1) {
+            throw new IllegalArgumentException("maxNegativeCacheAge < -1");
+        }
+        this.cacheDirectory      = cacheDirectory;
+        this.maxCacheAge         = maxCacheAge;
+        this.maxNegativeCacheAge = maxNegativeCacheAge;
+        this.httpClient          = createHttpClient(2500, 5000);
+    }
+
+
+    public CMDISchemaLoader(File cacheDirectory, long maxCacheAge) {
+        this(cacheDirectory, maxCacheAge, TimeUnit.HOURS.toMillis(1));
     }
 
@@ -76,5 +96,5 @@
 
 
-    public synchronized InputStream loadSchemaFile(String targetNamespace,
+    public InputStream loadSchemaFile(String targetNamespace,
             String schemaLocation) throws IOException {
         if (targetNamespace == null) {
@@ -99,50 +119,143 @@
 
         // fall back to file cache ...
-        final File cacheFile = makeCacheFile(schemaLocation);
-        if (cacheFile.exists()) {
-            final long age =
-                    System.currentTimeMillis() - cacheFile.lastModified();
-            if ((maxCacheAge != DISABLE_CACHE_AGING) && (age > maxCacheAge)) {
-                logger.trace("-> cached file '{}' expired", cacheFile);
-                cacheFile.delete();
+        final File cacheDataFile =
+                makeFile(schemaLocation, EXTENSION_XSD);
+        final File cacheErrorFile =
+                makeFile(schemaLocation, EXTENSION_ERROR);
+
+        for (;;) {
+            boolean doDownload = false;
+
+            synchronized (guard) {
+                /*
+                 * check, if an earlier attempt to download the schema failed.
+                 */
+                if (cacheErrorFile.exists()) {
+                    if (isExpired(cacheErrorFile, maxNegativeCacheAge)) {
+                        logger.trace("-> error file '{}' expired",
+                                cacheErrorFile);
+                        cacheErrorFile.delete();
+                    } else {
+                        throw new IOException("cached error condition detected");
+                    }
+                }
+
+                if (cacheDataFile.exists()) {
+                    if (isExpired(cacheDataFile, maxCacheAge)) {
+                        logger.debug("cached entry for '{}' has expired",
+                                schemaLocation);
+                        cacheDataFile.delete();
+                    } else {
+                        logger.trace("-> from file cache");
+                        return new FileInputStream(cacheDataFile);
+                    }
+                }
+
+                synchronized (pending) {
+                    if (!pending.contains(schemaLocation)) {
+                        doDownload = true;
+                        pending.add(schemaLocation);
+                    }
+                } // synchronized (pending)
+            } // synchronized (guard)
+
+            // either download in this thread of wait for pending download
+            if (doDownload) {
+                boolean failed = false;
+                try {
+                    download(cacheDataFile, schemaLocation);
+                    return new FileInputStream(cacheDataFile);
+                } catch (IOException e) {
+                    failed = true;
+                    throw e;
+                } finally {
+                    synchronized (guard) {
+                        if (failed) {
+                            if (cacheErrorFile.exists()) {
+                                cacheErrorFile.setLastModified(
+                                        System.currentTimeMillis());
+                            } else {
+                                cacheErrorFile.createNewFile();
+                            }
+                        }
+                        synchronized (pending) {
+                            pending.remove(schemaLocation);
+                            synchronized (waiter) {
+                                waiter.notifyAll();
+                            } // synchronized (waiter)
+                        }// synchronized (pending)
+                    } // synchronized (guard)
+                }
             } else {
-                logger.trace("-> from file cache");
-                return new FileInputStream(cacheFile);
-            }
-        }
-
+                try {
+                    synchronized (waiter) {
+                        waiter.wait();
+                    } // synchronized (waiter)
+                } catch (InterruptedException e) {
+                    throw new InterruptedIOException(
+                            "interrupted while waiting for download");
+                }
+            }
+        } // for
+    }
+
+
+    private void download(File cacheFile, String schemaLocation)
+            throws IOException {
         try {
             logger.debug("downloading schema from '{}'", schemaLocation);
             final URI uri = new URI(schemaLocation);
-            final HttpResponse response = executeRequest(uri);
-            final HttpEntity entity = response.getEntity();
-            if (entity == null) {
-                throw new IOException("the request returned no message body");
-            }
-
+            final HttpGet request = new HttpGet(uri);
             try {
-                final InputStream in = entity.getContent();
-
-                final FileOutputStream out =
-                        new FileOutputStream(cacheFile);
-                int read;
-                final byte[] buffer = new byte[4096];
-                while ((read = in.read(buffer)) != -1) {
-                    out.write(buffer, 0, read);
-                }
-                out.close();
-
-                return new FileInputStream(cacheFile);
-            } catch (IllegalStateException e) {
-                throw new IOException("error reading response", e);
-            } catch (IOException e) {
-                /* delete broken cache file */
-                if (cacheFile != null) {
-                    cacheFile.delete();
-                }
-                throw e;
+                logger.trace("submitting HTTP request: {}", uri.toString());
+                final CloseableHttpResponse response =
+                        httpClient.execute(request, new BasicHttpContext());
+                try {
+                    final StatusLine status = response.getStatusLine();
+                    if (status.getStatusCode() == HttpStatus.SC_OK) {
+                        final HttpEntity entity = response.getEntity();
+                        if (entity == null) {
+                            throw new IOException(
+                                    "request returned no message body");
+                        }
+
+                        FileOutputStream out = null;
+                        try {
+                            out = new FileOutputStream(cacheFile);
+                            // use exclusive lock
+                            final FileLock lock = out.getChannel().lock();
+                            try {
+                                entity.writeTo(out);
+                                out.flush();
+                                out.getFD().sync();
+                            } finally {
+                                lock.release();
+                            }
+                        } finally {
+                            if (out != null) {
+                                out.close();
+                            }
+                        }
+                    } else {
+                        switch (status.getStatusCode()) {
+                        case HttpStatus.SC_NOT_FOUND:
+                            throw new IOException("not found: " + uri);
+                        default:
+                            throw new IOException("unexpected status: " +
+                                    status.getStatusCode());
+                        } // switch
+                    }
+                } catch (IOException e) {
+                    /* delete broken cache file */
+                    if (cacheFile != null) {
+                        cacheFile.delete();
+                    }
+                    throw e;
+                } finally {
+                    /* make sure to release allocated resources */
+                    response.close();
+                }
             } finally {
-                /* make sure to release allocated resources */
-                HttpClientUtils.closeQuietly(response);
+                request.reset();
             }
         } catch (URISyntaxException e) {
@@ -153,5 +266,5 @@
 
 
-    private File makeCacheFile(String schemaLocation) {
+    private File makeFile(String schemaLocation, String extension) {
         final StringBuilder sb = new StringBuilder();
         for (int i = 0; i < schemaLocation.length(); i++) {
@@ -183,46 +296,64 @@
             }
         } // for
-        sb.append(".xsd");
+        sb.append(".").append(extension);
         return new File(cacheDirectory, sb.toString());
     }
 
 
-    private HttpResponse executeRequest(URI uri) throws IOException {
-        HttpGet request = null;
-        HttpResponse response = null;
-        try {
-            logger.trace("submitting HTTP request: {}", uri.toString());
-            request = new HttpGet(uri);
-            response = httpClient.execute(request);
-            StatusLine status = response.getStatusLine();
-            if (status.getStatusCode() != HttpStatus.SC_OK) {
-                if (status.getStatusCode() == HttpStatus.SC_NOT_FOUND) {
-                    throw new IOException("not found: " + uri);
-                } else {
-                    throw new IOException("unexpected status: " +
-                            status.getStatusCode());
-                }
-            }
-            return response;
-        } catch (IOException e) {
-            /*
-             * if an error occurred, make sure we are freeing up the resources
-             * we've used
-             */
-            if (response != null) {
-                try {
-                    EntityUtils.consume(response.getEntity());
-                } catch (IOException ex) {
-                    /* IGNORE */
-                }
-
-                /* make sure to release allocated resources */
-                HttpClientUtils.closeQuietly(response);
-            }
-            if (request != null) {
-                request.abort();
-            }
-            throw e;
-        }
+    private CloseableHttpClient createHttpClient(int connectTimeout,
+            int socketTimeout) {
+        final PoolingHttpClientConnectionManager manager =
+                new PoolingHttpClientConnectionManager();
+        manager.setDefaultMaxPerRoute(8);
+        manager.setMaxTotal(128);
+
+        final SocketConfig socketConfig = SocketConfig.custom()
+                .setSoReuseAddress(true)
+                .setSoLinger(0)
+                .build();
+
+        final RequestConfig requestConfig = RequestConfig.custom()
+                .setAuthenticationEnabled(false)
+                .setRedirectsEnabled(true)
+                .setMaxRedirects(4)
+                .setCircularRedirectsAllowed(false)
+                .setCookieSpec(CookieSpecs.IGNORE_COOKIES)
+                .setConnectTimeout(connectTimeout)
+                .setSocketTimeout(socketTimeout)
+                .setConnectionRequestTimeout(0) /* infinite */
+                .setStaleConnectionCheckEnabled(false)
+                .build();
+
+        final ConnectionKeepAliveStrategy keepAliveStrategy =
+                new ConnectionKeepAliveStrategy() {
+            @Override
+            public long getKeepAliveDuration(final HttpResponse response,
+                    final HttpContext context) {
+                return 60000;
+            }
+        };
+
+        return HttpClients.custom()
+                .setUserAgent(USER_AGENT)
+                .setConnectionManager(manager)
+                .setDefaultSocketConfig(socketConfig)
+                .setDefaultRequestConfig(requestConfig)
+                .setKeepAliveStrategy(keepAliveStrategy)
+                .build();
+    }
+
+
+    private static boolean isExpired(File file, long maxAge) {
+        if (maxAge != DISABLE_CACHE_AGING) {
+            return (System.currentTimeMillis() - file.lastModified()) >= maxAge;
+        } else {
+            return false;
+        }
+    }
+
+
+    @Override
+    protected void finalize() throws Throwable {
+        httpClient.close();
     }
 
Index: CMDIValidator/trunk/cmdi-validator-core/src/main/java/eu/clarin/cmdi/validator/extensions/CheckHandlesExtension.java
===================================================================
--- CMDIValidator/trunk/cmdi-validator-core/src/main/java/eu/clarin/cmdi/validator/extensions/CheckHandlesExtension.java	(revision 5384)
+++ CMDIValidator/trunk/cmdi-validator-core/src/main/java/eu/clarin/cmdi/validator/extensions/CheckHandlesExtension.java	(revision 5387)
@@ -17,4 +17,5 @@
 package eu.clarin.cmdi.validator.extensions;
 
+import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -46,5 +47,4 @@
     private static final Logger logger =
             LoggerFactory.getLogger(CheckHandlesExtension.class);
-    private final int threads;
     private final boolean resolveHandles;
     private HandleResolver resolver = null;
@@ -52,15 +52,16 @@
 
 
-    public CheckHandlesExtension(int threads, boolean resolveHandles) {
-        if (threads < 1) {
-            throw new IllegalArgumentException("threads < 1");
-        }
-        this.threads        = threads;
+    public CheckHandlesExtension(boolean resolveHandles) {
         this.resolveHandles = resolveHandles;
     }
 
 
-    public CheckHandlesExtension(boolean resolveHandles) {
-        this(Runtime.getRuntime().availableProcessors(), resolveHandles);
+    public boolean isResolvingHandles() {
+        return resolveHandles;
+    }
+
+
+    public HandleResolver.Statistics getStatistics() {
+        return (resolver != null) ? resolver.getStatistics() : null;
     }
 
@@ -69,5 +70,5 @@
     protected void doInitialize() throws CMDIValidatorInitException {
         if (resolveHandles) {
-            this.resolver = new HandleResolver(threads);
+            this.resolver = new HandleResolver();
         }
 
@@ -89,9 +90,16 @@
             selector.setContextItem(document);
             for (XdmItem item : selector) {
-                String handle = item.getStringValue();
-                if (handle != null) {
-                    handle = handle.trim();
+                String handle = null;
+                final String h = item.getStringValue();
+                if (h != null) {
+                    handle = h.trim();
                     if (handle.isEmpty()) {
                         handle = null;
+                    } else {
+                        if (!handle.equals(h)) {
+                            result.reportWarning(-1, -1, "handle '" + h +
+                                    "' contains leading or tailing spaces " +
+                                    "within <ResourceRef> element");
+                        }
                     }
                 }
@@ -124,4 +132,5 @@
                     checkHandleResolves(actionableURI, result);
                 } catch (URISyntaxException e) {
+                    /* should not happen */
                     throw new CMDIValidatorException(
                             "created an invalid URI", e);
@@ -137,17 +146,26 @@
             } else if (HDL_PROXY_HTTP.equalsIgnoreCase(uri.getScheme()) ||
                     HDL_PROXY_HTTPS.equalsIgnoreCase(uri.getScheme())) {
-                if (HDL_PROXY_HOST.equalsIgnoreCase(uri.getHost())) {
+                if (uri.getHost() != null) {
+                    if (!HDL_PROXY_HOST.equalsIgnoreCase(uri.getHost())) {
+                        result.reportError(-1, -1,
+                                "The URI of PID '" + handle +
+                                "' contains an unexpected host part of '" +
+                                uri.getHost() + "'");
+                    }
                     checkHandleResolves(uri, result);
                 } else {
+                    result.reportError(-1, -1, "The URI of PID '" + handle +
+                            "' is missing the host part");
+                }
+            } else {
+                if (uri.getScheme() != null) {
                     result.reportError(-1, -1,
-                            "PID '" + handle +
-                            "' contains an unexpected host part in the URI: " +
-                            uri.getHost());
-                }
-            } else {
-                result.reportError(-1, -1,
-                        "PID '" + handle +
-                        "' contains an unexpected schema part in the URI: " +
-                        uri.getScheme());
+                            "The URI of PID '" + handle +
+                            "' contains an unexpected schema part of '" +
+                            uri.getScheme() + "'");
+                } else {
+                    result.reportError(-1, -1, "The URI of PID '" + handle +
+                            "' is missing a proper schema part");
+                }
             }
         } catch (URISyntaxException e) {
@@ -161,24 +179,45 @@
             CMDIValidatorWriteableResult result) throws CMDIValidatorException {
         if (resolver != null) {
-            int code = resolver.resolve(uri);
-            switch (code) {
-            case HttpStatus.SC_OK:
-                /* no special message in this case */
-                break;
-            case HttpStatus.SC_UNAUTHORIZED:
-                /* FALL-THROUGH */
-            case HttpStatus.SC_FORBIDDEN:
-                result.reportInfo(-1, -1, "PID '" + uri +
-                        "' resolved to an access protected resource (" + code +
-                        ")");
-                break;
-            case HttpStatus.SC_NOT_FOUND:
-                result.reportError(-1, -1, "PID '" + uri +
-                        "' resolved to an non-existing resource (" + code + ")");
-                break;
-            default:
-                result.reportError(-1, -1, "PID '" + uri +
-                        "' resolved with an unexpected result (" + code + ")");
-                break;
+            try {
+                int code = resolver.resolve(uri);
+                switch (code) {
+                case HttpStatus.SC_OK:
+                    /* no special message in this case */
+                    break;
+                case HttpStatus.SC_UNAUTHORIZED:
+                    /* FALL-THROUGH */
+                case HttpStatus.SC_FORBIDDEN:
+                    result.reportInfo(-1, -1, "PID '" + uri +
+                            "' resolved to an access protected resource (" +
+                            code + ")");
+                    break;
+                case HttpStatus.SC_NOT_FOUND:
+                    result.reportError(-1, -1, "PID '" + uri +
+                            "' resolved to an non-existing resource (" + code +
+                            ")");
+                    break;
+                case HandleResolver.TIMEOUT:
+                    result.reportWarning(-1, -1, "Timeout while resolving PID '" +
+                            uri + "'");
+                    break;
+                case HandleResolver.UNKNOWN_HOST:
+                    result.reportWarning(-1, -1, "Unable to resolve host '" +
+                            uri.getHost() + "' while resolving PID '" +
+                            uri + "'");
+                    break;
+                case HandleResolver.ERROR:
+                    result.reportWarning(-1, -1,
+                            "An error occurred while resolving PID '" +
+                            uri + "'");
+                    break;
+                default:
+                    result.reportWarning(-1, -1, "PID '" + uri +
+                            "' resolved with an unexpected result (" + code +
+                            ")");
+                    break;
+                } // switch
+            } catch (IOException e) {
+                throw new CMDIValidatorException(
+                        "error while resolving handle '" + uri + "'", e);
             }
         }
Index: CMDIValidator/trunk/cmdi-validator-core/src/main/java/eu/clarin/cmdi/validator/utils/HandleResolver.java
===================================================================
--- CMDIValidator/trunk/cmdi-validator-core/src/main/java/eu/clarin/cmdi/validator/utils/HandleResolver.java	(revision 5384)
+++ CMDIValidator/trunk/cmdi-validator-core/src/main/java/eu/clarin/cmdi/validator/utils/HandleResolver.java	(revision 5387)
@@ -18,54 +18,151 @@
 
 import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.SocketTimeoutException;
 import java.net.URI;
+import java.net.UnknownHostException;
 import java.util.HashSet;
 import java.util.Set;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
 
 import org.apache.http.HttpResponse;
 import org.apache.http.StatusLine;
-import org.apache.http.client.HttpClient;
+import org.apache.http.client.config.CookieSpecs;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
 import org.apache.http.client.methods.HttpHead;
-import org.apache.http.client.params.ClientPNames;
-import org.apache.http.client.utils.HttpClientUtils;
-import org.apache.http.impl.client.DefaultHttpClient;
-import org.apache.http.params.CoreProtocolPNames;
-import org.apache.http.params.HttpParams;
+import org.apache.http.config.ConnectionConfig;
+import org.apache.http.config.SocketConfig;
+import org.apache.http.conn.ConnectTimeoutException;
+import org.apache.http.conn.ConnectionKeepAliveStrategy;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.http.protocol.BasicHttpContext;
+import org.apache.http.protocol.HttpContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import eu.clarin.cmdi.validator.CMDIValidatorException;
+import eu.clarin.cmdi.validator.Version;
 
 public class HandleResolver {
+    public static final class Statistics {
+        private final long cacheHitCount;
+        private final long cacheMissCount;
+        private final long timeoutCount;
+        private final long unknownHostCount;
+        private final long errorCount;
+        private final long totalRequestsCount;
+        private final int currentRequestsCount;
+        private final int currentCacheSize;
+
+        private Statistics(long cacheHitCount,
+                long cacheMissCount,
+                long timeoutCount,
+                long unknownHostCount,
+                long errorCount,
+                long totalRequestsCount,
+                int currentRequestsCount,
+                int currentCacheSize) {
+            this.cacheHitCount        = cacheHitCount;
+            this.cacheMissCount       = cacheMissCount;
+            this.timeoutCount         = timeoutCount;
+            this.unknownHostCount     = unknownHostCount;
+            this.errorCount           = errorCount;
+            this.totalRequestsCount   = totalRequestsCount;
+            this.currentRequestsCount = currentRequestsCount;
+            this.currentCacheSize     = currentCacheSize;
+        }
+
+
+        public long getCacheHitCount() {
+            return cacheHitCount;
+        }
+
+
+        public long getCacheMissCount() {
+            return cacheMissCount;
+        }
+
+
+        public long getTimeoutCount() {
+            return timeoutCount;
+        }
+
+
+        public long getUnknownHostCount() {
+            return unknownHostCount;
+        }
+
+
+        public long getErrorCount() {
+            return errorCount;
+        }
+
+
+        public long getTotalRequestsCount() {
+            return totalRequestsCount;
+        }
+
+
+        public int getCurrentRequestsCount() {
+            return currentRequestsCount;
+        }
+
+        public int getCurrentCacheSize() {
+            return currentCacheSize;
+        }
+    }
     private static final Logger logger =
             LoggerFactory.getLogger(HandleResolver.class);
-    public final int ERROR   = -1;
+    public static final int TIMEOUT      = -1;
+    public static final int UNKNOWN_HOST = -2;
+    public static final int ERROR        = -3;
+    private static final String USER_AGENT =
+            "CMDI-Validator-HandleResolver/" + Version.getVersion();
+    private static final int DEFAULT_MAX_CONCURRENT_REQUESTS = 8;
+    private static final int DEFAULT_CONNECT_TIMEOUT = 5000;
+    private static final int DEFAULT_SOCKET_TIMEOUT = 10000;
     private final LRUCache<URI, Integer> cache =
             new LRUCache<URI, Integer>(16 * 1024);
     private final Set<URI> pending;
+    private final int maxConcurrentRequestsCount;
+    private final Semaphore maxConcurrentRequests;
+    private final CloseableHttpClient client;
+    private long cacheHitCount                = 0;
+    private long cacheMissCount               = 0;
+    private AtomicLong timeoutCount           = new AtomicLong();
+    private AtomicLong unknownHostCount       = new AtomicLong();
+    private AtomicLong errorCount             = new AtomicLong();
+    private AtomicLong totalRequestsCount     = new AtomicLong();
+    private AtomicInteger currentRequestCount = new AtomicInteger();
     private final Object waiter = new Object();
-    private long cacheHits   = 0;
-    private long cacheMisses = 0;
-
-
-    public HandleResolver(final int threads) {
-        this.pending = new HashSet<URI>(threads * 2);
-    }
-
-
-    public long getCacheHits() {
-        return cacheHits;
-    }
-
-
-    public long getCacheMisses() {
-        return cacheMisses;
-    }
-
-
-    public int resolve(final URI handle) throws CMDIValidatorException {
+
+
+    public HandleResolver(int maxConcurrentRequests) {
+        if (maxConcurrentRequests < 1) {
+            throw new IllegalArgumentException("maxConcurrentRequests < 1");
+        }
+        this.pending = new HashSet<URI>(maxConcurrentRequests * 4);
+        this.client = createHttpClient(DEFAULT_CONNECT_TIMEOUT,
+                DEFAULT_SOCKET_TIMEOUT);
+        this.maxConcurrentRequestsCount = maxConcurrentRequests;
+        this.maxConcurrentRequests = new Semaphore(maxConcurrentRequests, true);
+    }
+
+
+    public HandleResolver() {
+        this(DEFAULT_MAX_CONCURRENT_REQUESTS);
+    }
+
+
+    public int resolve(final URI handle) throws IOException {
         if (handle == null) {
             throw new NullPointerException("handle == null");
         }
         logger.debug("resolving '{}'", handle);
+        totalRequestsCount.incrementAndGet();
         for (;;) {
             boolean doResolve = false;
@@ -75,11 +172,11 @@
                     logger.trace("got cached result for '{}': {}",
                             handle, cached);
-                    cacheHits++;
+                    cacheHitCount++;
                     return cached.intValue();
                 }
+                cacheMissCount++;
 
                 synchronized (pending) {
                     if (!pending.contains(handle)) {
-                        cacheMisses++;
                         doResolve = true;
                         pending.add(handle);
@@ -92,13 +189,5 @@
                 int result = ERROR;
                 try {
-                    final HttpClient httpClient = newHttpClient();
-                    try {
-                        result = doResolve(handle, httpClient);
-                    } finally {
-                        HttpClientUtils.closeQuietly(httpClient);
-                    }
-                } catch (Throwable e) {
-                    throw new CMDIValidatorException(
-                            "error while resolving handle '" + handle + "'", e);
+                    result = doResolve(handle);
                 } finally {
                     // cache result and notify other threads
@@ -114,4 +203,7 @@
                         } // synchronized (pending)
                     } // synchronized (cache)
+                    if (result == ERROR) {
+                        errorCount.incrementAndGet();
+                    }
                 }
                 return result;
@@ -122,4 +214,5 @@
                     } // synchronized (waiter)
                 } catch (InterruptedException e) {
+                    errorCount.incrementAndGet();
                     return ERROR;
                 }
@@ -129,37 +222,131 @@
 
 
-    private int doResolve(final URI handle, final HttpClient httpClient)
-            throws CMDIValidatorException {
+    public Statistics getStatistics() {
+        synchronized (cache) {
+            return new Statistics(cacheHitCount,
+                    cacheMissCount,
+                    timeoutCount.get(),
+                    unknownHostCount.get(),
+                    errorCount.get(),
+                    totalRequestsCount.get(),
+                    currentRequestCount.get(),
+                    cache.size());
+        } // synchronized (cache)
+    }
+
+
+    public void clear() {
+        try {
+            try {
+                // acquire all permits to deny any requests from happening ...
+                maxConcurrentRequests.acquire(maxConcurrentRequestsCount);
+
+                // clear data
+                synchronized (cache) {
+                    cache.clear();
+                    cacheHitCount  = 0;
+                    cacheMissCount = 0;
+                    timeoutCount.set(0);
+                    unknownHostCount.set(0);
+                    errorCount.set(0);
+                    totalRequestsCount.set(0);
+                } // synchronized (cache)
+            } finally {
+                maxConcurrentRequests.release();
+            }
+        } catch (InterruptedException e) {
+            /* IGNORE */
+        }
+    }
+
+
+    private int doResolve(final URI handle) throws IOException {
         logger.trace("performing HTTP request for '{}'", handle);
-        HttpHead request = null;
-        HttpResponse response = null;
+
         try {
-            request = new HttpHead(handle);
-            response = httpClient.execute(request);
-
-            final StatusLine status = response.getStatusLine();
-            return status.getStatusCode();
-        } catch (IOException e) {
-            if (request != null) {
-                request.abort();
+            maxConcurrentRequests.acquire();
+        } catch (InterruptedException e) {
+            throw new InterruptedIOException();
+        }
+
+        currentRequestCount.incrementAndGet();
+        final HttpHead request = new HttpHead(handle);
+        try {
+            final CloseableHttpResponse response =
+                    client.execute(request, new BasicHttpContext());
+            try {
+                final StatusLine status = response.getStatusLine();
+                return status.getStatusCode();
+            } finally {
+                response.close();
             }
-            throw new CMDIValidatorException("error resolving handle '" +
-                    handle + "'", e);
+        } catch (ConnectTimeoutException e) {
+            timeoutCount.incrementAndGet();
+            return TIMEOUT;
+        } catch (SocketTimeoutException e) {
+            timeoutCount.incrementAndGet();
+            return TIMEOUT;
+        } catch (UnknownHostException e) {
+            unknownHostCount.incrementAndGet();
+            return UNKNOWN_HOST;
         } finally {
-            /* make sure to release allocated resources */
-            HttpClientUtils.closeQuietly(response);
-        }
-    }
-
-
-    private HttpClient newHttpClient() {
-        final HttpClient client = new DefaultHttpClient();
-        final HttpParams params = client.getParams();
-        params.setParameter(CoreProtocolPNames.USER_AGENT,
-                getClass().getName() + "/1.0");
-        params.setBooleanParameter(ClientPNames.HANDLE_REDIRECTS, Boolean.TRUE);
-        params.setBooleanParameter(ClientPNames.ALLOW_CIRCULAR_REDIRECTS, true);
-        params.setIntParameter(ClientPNames.MAX_REDIRECTS, 16);
-        return client;
+            request.reset();
+            currentRequestCount.decrementAndGet();
+            maxConcurrentRequests.release();
+        }
+    }
+
+
+    private CloseableHttpClient createHttpClient(int connectTimeout,
+            int socketTimeout) {
+        final PoolingHttpClientConnectionManager manager =
+                new PoolingHttpClientConnectionManager();
+        manager.setDefaultMaxPerRoute(8);
+        manager.setMaxTotal(128);
+
+        final SocketConfig socketConfig = SocketConfig.custom()
+                .setSoReuseAddress(true)
+                .setSoLinger(0)
+                .build();
+
+        final ConnectionConfig connectionConfig = ConnectionConfig.custom()
+                .setBufferSize(1024)
+                .build();
+
+        final RequestConfig requestConfig = RequestConfig.custom()
+                .setAuthenticationEnabled(false)
+                .setRedirectsEnabled(true)
+                .setMaxRedirects(4)
+                .setCircularRedirectsAllowed(false)
+                .setCookieSpec(CookieSpecs.IGNORE_COOKIES)
+                .setConnectTimeout(connectTimeout)
+                .setSocketTimeout(socketTimeout)
+                .setConnectionRequestTimeout(0) /* infinite */
+                .setStaleConnectionCheckEnabled(false)
+                .build();
+
+        final ConnectionKeepAliveStrategy keepAliveStrategy =
+                new ConnectionKeepAliveStrategy() {
+            @Override
+            public long getKeepAliveDuration(final HttpResponse response,
+                    final HttpContext context) {
+                return 15000;
+            }
+        };
+
+        return HttpClients.custom()
+                .setUserAgent(USER_AGENT)
+                .setConnectionManager(manager)
+                .setDefaultSocketConfig(socketConfig)
+                .setDefaultConnectionConfig(connectionConfig)
+                .setDefaultRequestConfig(requestConfig)
+                .setKeepAliveStrategy(keepAliveStrategy)
+                .build();
+    }
+
+
+    @Override
+    protected void finalize() throws Throwable {
+        client.close();
     }
 
Index: CMDIValidator/trunk/cmdi-validator-tool/pom.xml
===================================================================
--- CMDIValidator/trunk/cmdi-validator-tool/pom.xml	(revision 5384)
+++ CMDIValidator/trunk/cmdi-validator-tool/pom.xml	(revision 5387)
@@ -42,4 +42,11 @@
             <artifactId>slf4j-log4j12</artifactId>
         </dependency>
+
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>jcl-over-slf4j</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+
 
         <!-- project specific dependencies -->
Index: CMDIValidator/trunk/cmdi-validator-tool/src/main/java/eu/clarin/cmdi/validator/tool/CMDIValidatorTool.java
===================================================================
--- CMDIValidator/trunk/cmdi-validator-tool/src/main/java/eu/clarin/cmdi/validator/tool/CMDIValidatorTool.java	(revision 5384)
+++ CMDIValidator/trunk/cmdi-validator-tool/src/main/java/eu/clarin/cmdi/validator/tool/CMDIValidatorTool.java	(revision 5387)
@@ -23,4 +23,5 @@
 import java.util.Locale;
 import java.util.concurrent.TimeUnit;
+
 import net.java.truevfs.access.TFile;
 import net.java.truevfs.access.TVFS;
@@ -48,21 +49,22 @@
 import eu.clarin.cmdi.validator.CMDIValidatorResult.Severity;
 import eu.clarin.cmdi.validator.extensions.CheckHandlesExtension;
+import eu.clarin.cmdi.validator.utils.HandleResolver;
 
 
 public class CMDIValidatorTool {
-    private static final String PRG_NAME                = "cmdi-validator";
-    private static final long DEFAULT_PROGRESS_INTERVAL = 15000;
-    private static final Locale LOCALE                  = Locale.ENGLISH;
-    private static final char OPT_DEBUG                 = 'd';
-    private static final char OPT_QUIET                 = 'q';
-    private static final char OPT_VERBOSE               = 'v';
-    private static final char OPT_NO_PROGRESS           = 'P';
-    private static final char OPT_THREAD_COUNT          = 't';
-    private static final char OPT_NO_THREADS            = 'T';
-    private static final char OPT_NO_ESTIMATE           = 'E';
-    private static final char OPT_SCHEMA_CACHE_DIR      = 'c';
-    private static final char OPT_NO_SCHEMATRON         = 'S';
-    private static final char OPT_SCHEMATRON_FILE       = 's';
-    private static final char OPT_CHECK_PIDS            = 'p';
+    private static final String PRG_NAME                 = "cmdi-validator";
+    private static final long DEFAULT_PROGRESS_INTERVAL  = 15000;
+    private static final Locale LOCALE                   = Locale.ENGLISH;
+    private static final char OPT_DEBUG                  = 'd';
+    private static final char OPT_QUIET                  = 'q';
+    private static final char OPT_VERBOSE                = 'v';
+    private static final char OPT_THREAD_COUNT           = 't';
+    private static final char OPT_NO_THREADS             = 'T';
+    private static final char OPT_NO_ESTIMATE            = 'E';
+    private static final char OPT_SCHEMA_CACHE_DIR       = 'c';
+    private static final char OPT_NO_SCHEMATRON          = 'S';
+    private static final char OPT_SCHEMATRON_FILE        = 's';
+    private static final char OPT_CHECK_PIDS             = 'p';
+    private static final char OPT_CHECK_AND_RESOLVE_PIDS = 'P';
     private static final Logger logger =
             LoggerFactory.getLogger(CMDIValidatorTool.class);
@@ -84,4 +86,5 @@
         File schematronFile       = null;
         boolean checkPids         = false;
+        boolean checkAndResolvePids   = false;
 
         /*
@@ -103,6 +106,10 @@
             }
             if (line.hasOption(OPT_NO_SCHEMATRON) && line.hasOption(OPT_SCHEMATRON_FILE)) {
-                throw new ParseException("The -s and -T options are mutually exclusive");
-            }
+                throw new ParseException("The -s and -S options are mutually exclusive");
+            }
+            if (line.hasOption(OPT_CHECK_PIDS) && line.hasOption(OPT_CHECK_AND_RESOLVE_PIDS)) {
+                throw new ParseException("The -p and -P options are mutually exclusive");
+            }
+
             // extract options
             if (line.hasOption(OPT_DEBUG)) {
@@ -115,5 +122,5 @@
                 verbose = true;
             }
-            if (line.hasOption(OPT_NO_PROGRESS) || quiet) {
+            if (quiet) {
                 progressInterval = -1;
             }
@@ -157,4 +164,7 @@
             if (line.hasOption(OPT_CHECK_PIDS)) {
                 checkPids = true;
+            }
+            if (line.hasOption(OPT_CHECK_AND_RESOLVE_PIDS)) {
+                checkAndResolvePids = true;
             }
 
@@ -224,8 +234,15 @@
                         builder.disableSchematron();
                     }
-                    if (checkPids) {
-                        logger.info("performing PID checking");
-                        builder.extension(
-                                new CheckHandlesExtension(threadCount, true));
+
+                    CheckHandlesExtension checkHandleExtension = null;
+                    if (checkPids || checkAndResolvePids) {
+                        if (checkAndResolvePids) {
+                            logger.info("enabling PID validation (syntax and resolving)");
+                        } else {
+                            logger.info("enabling PID validation (syntax only)");
+                        }
+                        checkHandleExtension =
+                                new CheckHandlesExtension(checkAndResolvePids);
+                        builder.extension(checkHandleExtension);
                     }
 
@@ -279,4 +296,17 @@
                                             ((bps != -1) ? Humanize.binaryPrefix(bps, LOCALE) : "N/A MB"));
                                 }
+                                if (logger.isDebugEnabled()) {
+                                    if ((checkHandleExtension != null) &&
+                                            checkHandleExtension.isResolvingHandles()) {
+                                        final HandleResolver.Statistics stats =
+                                                checkHandleExtension.getStatistics();
+                                        logger.debug("[handle resolver stats] total requests: {}, running requests: {}, cache hits: {}, cache misses: {}, current cache size: {}",
+                                                stats.getTotalRequestsCount(),
+                                                stats.getCurrentRequestsCount(),
+                                                stats.getCacheHitCount(),
+                                                stats.getCacheMissCount(),
+                                                stats.getCurrentCacheSize());
+                                    }
+                                }
                             }
                         } // for (;;)
@@ -360,8 +390,4 @@
                 .withLongOpt("verbose")
                 .create(OPT_VERBOSE));
-        options.addOption(OptionBuilder
-                .withDescription("no progress reporting")
-                .withLongOpt("no-progress")
-                .create(OPT_NO_PROGRESS));
         OptionGroup g2 = new OptionGroup();
         g2.addOption(OptionBuilder
@@ -398,8 +424,14 @@
                 .create(OPT_SCHEMATRON_FILE));
         options.addOptionGroup(g3);
-        options.addOption(OptionBuilder
-                .withDescription("check if persistent identifiers resolve correctly")
+        OptionGroup g4 = new OptionGroup();
+        g4.addOption(OptionBuilder
+                .withDescription("check persistent identifiers syntax")
                 .withLongOpt("check-pids")
                 .create(OPT_CHECK_PIDS));
+        g4.addOption(OptionBuilder
+                .withDescription("check persistent identifiers syntax and if they resolve properly")
+                .withLongOpt("check-and-resolve-pids")
+                .create(OPT_CHECK_AND_RESOLVE_PIDS));
+        options.addOptionGroup(g4);
         return options;
     }
Index: CMDIValidator/trunk/pom.xml
===================================================================
--- CMDIValidator/trunk/pom.xml	(revision 5384)
+++ CMDIValidator/trunk/pom.xml	(revision 5387)
@@ -50,4 +50,10 @@
                 <version>${slf4j.version}</version>
             </dependency>
+
+            <dependency>
+            	<groupId>org.slf4j</groupId>
+            	<artifactId>jcl-over-slf4j</artifactId>
+            	<version>${slf4j.version}</version>
+            </dependency>
         </dependencies>
     </dependencyManagement>
