org.rssowl.core.internal.connection.DefaultProtocolHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.rssowl.core.internal.connection.DefaultProtocolHandler.java

Source

/*   **********************************************************************  **
 **   Copyright notice                                                       **
 **                                                                          **
 **   (c) 2005-2009 RSSOwl Development Team                                  **
 **   http://www.rssowl.org/                                                 **
 **                                                                          **
 **   All rights reserved                                                    **
 **                                                                          **
 **   This program and the accompanying materials are made available under   **
 **   the terms of the Eclipse Public License v1.0 which accompanies this    **
 **   distribution, and is available at:                                     **
 **   http://www.rssowl.org/legal/epl-v10.html                               **
 **                                                                          **
 **   A copy is found in the file epl-v10.html and important notices to the  **
 **   license from the team is found in the textfile LICENSE.txt distributed **
 **   in this package.                                                       **
 **                                                                          **
 **   This copyright notice MUST APPEAR in all copies of the file!           **
 **                                                                          **
 **   Contributors:                                                          **
 **     RSSOwl Development Team - initial API and implementation             **
 **                                                                          **
 **  **********************************************************************  */

package org.rssowl.core.internal.connection;

import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethodBase;
import org.apache.commons.httpclient.NTCredentials;
import org.apache.commons.httpclient.SimpleHttpConnectionManager;
import org.apache.commons.httpclient.URIException;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.auth.AuthState;
import org.apache.commons.httpclient.cookie.CookiePolicy;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.protocol.DefaultProtocolSocketFactory;
import org.apache.commons.httpclient.protocol.Protocol;
import org.apache.commons.httpclient.protocol.ProtocolSocketFactory;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.osgi.util.NLS;
import org.osgi.service.url.URLStreamHandlerService;
import org.rssowl.core.Owl;
import org.rssowl.core.connection.AuthenticationRequiredException;
import org.rssowl.core.connection.ConnectionException;
import org.rssowl.core.connection.CredentialsException;
import org.rssowl.core.connection.HttpConnectionInputStream;
import org.rssowl.core.connection.IAbortable;
import org.rssowl.core.connection.IConditionalGetCompatible;
import org.rssowl.core.connection.IConnectionPropertyConstants;
import org.rssowl.core.connection.ICredentials;
import org.rssowl.core.connection.ICredentialsProvider;
import org.rssowl.core.connection.IProtocolHandler;
import org.rssowl.core.connection.IProxyCredentials;
import org.rssowl.core.connection.MonitorCanceledException;
import org.rssowl.core.connection.NotModifiedException;
import org.rssowl.core.connection.ProxyAuthenticationRequiredException;
import org.rssowl.core.connection.SyncConnectionException;
import org.rssowl.core.internal.Activator;
import org.rssowl.core.interpreter.EncodingException;
import org.rssowl.core.persist.IConditionalGet;
import org.rssowl.core.persist.IFeed;
import org.rssowl.core.persist.IModelFactory;
import org.rssowl.core.util.CoreUtils;
import org.rssowl.core.util.StringUtils;
import org.rssowl.core.util.SyncUtils;
import org.rssowl.core.util.Triple;
import org.rssowl.core.util.URIUtils;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;

/**
 * The <code>DefaultFeedHandler</code> is an implementation of
 * <code>IProtocolHandler</code> that works on HTTP, HTTPS and the FILE
 * Protocol. After loading the Inputstream of the given URL, the stream is
 * passed to the Interpreter-Component to interpret it as one of the supported
 * XML-Formats for Newsfeeds.
 *
 * @author bpasero
 */
public class DefaultProtocolHandler implements IProtocolHandler {

    /* Http Status Codes */
    private static final int HTTP_ERRORS = 400;
    private static final int HTTP_STATUS_NOT_MODIFIED = 304;
    private static final int HTTP_ERROR_AUTH_REQUIRED = 401;
    private static final int HTTP_ERROR_FORBIDDEN = 403;
    private static final int HTTP_ERROR_PROXY_AUTH_REQUIRED = 407;

    /* Header Constants */
    private static final String HEADER_REQUEST_COOKIE = "Cookie"; //$NON-NLS-1$
    private static final String HEADER_REQUEST_ACCEPT_LANGUAGE = "Accept-Language"; //$NON-NLS-1$
    private static final String HEADER_REQUEST_USER_AGENT = "User-Agent"; //$NON-NLS-1$
    private static final String HEADER_REQUEST_ACCEPT_ENCODING = "Accept-Encoding"; //$NON-NLS-1$
    private static final String HEADER_RESPONSE_IF_NONE_MATCH = "If-None-Match"; //$NON-NLS-1$
    private static final String HEADER_RESPONSE_IF_MODIFIED_SINCE = "If-Modified-Since"; //$NON-NLS-1$
    private static final String HEADER_RESPONSE_CONTENT_ENCODING = "Content-Encoding"; //$NON-NLS-1$

    /* Google Error Response Codes */
    private static final String HEADER_RESPONSE_ERROR = "Error"; //$NON-NLS-1$
    private static final String HEADER_RESPONSE_URL = "Url"; //$NON-NLS-1$
    private static final String ERROR_BAD_AUTH = "BadAuthentication"; //$NON-NLS-1$
    private static final String ERROR_NOT_VERIFIED = "NotVerified"; //$NON-NLS-1$
    private static final String ERROR_NO_TERMS = "TermsNotAgreed"; //$NON-NLS-1$
    private static final String ERROR_CAPTCHA_REQUIRED = "CaptchaRequired"; //$NON-NLS-1$
    private static final String ERROR_UNKNOWN = "Unknown"; //$NON-NLS-1$
    private static final String ERROR_ACCOUNT_DELETED = "AccountDeleted"; //$NON-NLS-1$
    private static final String ERROR_ACCOUNT_DISABLED = "AccountDisabled"; //$NON-NLS-1$
    private static final String ERROR_SERVICE_DISABLED = "ServiceDisabled"; //$NON-NLS-1$
    private static final String ERROR_SERVICE_UNAVAILABLE = "ServiceUnavailable"; //$NON-NLS-1$

    /** Property to tell the XML parser to use platform encoding */
    public static final String USE_PLATFORM_ENCODING = "org.rssowl.core.internal.connection.DefaultProtocolHandler.UsePlatformEncoding"; //$NON-NLS-1$

    /* The Default Connection Timeout */
    private static final int DEFAULT_CON_TIMEOUT = 30000;

    /* Timeout for loading a Feed or Label for a Feed */
    private static final int FEED_LABEL_CON_TIMEOUT = 10000;

    /* Timeout for loading a Favicon */
    private static final int FAVICON_CON_TIMEOUT = 5000;

    /* Set a limit for titles that are looked up from feeds */
    private static final int MAX_DETECTED_TITLE_LENGTH = 1024;

    private static final String USER_AGENT = CoreUtils.getUserAgent();
    private static boolean fgSSLInitialized;
    private static boolean fgFeedProtocolInitialized;

    /*
     * @see org.rssowl.core.connection.IProtocolHandler#reload(java.net.URI,
     * org.eclipse.core.runtime.IProgressMonitor, java.util.Map)
     */
    public Triple<IFeed, IConditionalGet, URI> reload(URI link, IProgressMonitor monitor,
            Map<Object, Object> properties) throws CoreException {
        IModelFactory typesFactory = Owl.getModelFactory();

        /* Create a new empty feed from the existing one */
        IFeed feed = typesFactory.createFeed(null, link);

        /* Add Monitor to support early cancelation */
        if (properties == null)
            properties = new HashMap<Object, Object>();
        properties.put(IConnectionPropertyConstants.PROGRESS_MONITOR, monitor);

        /* Retrieve the InputStream out of the Feed's Link */
        InputStream inS = openStream(link, properties);

        /* Retrieve Conditional Get if present */
        IConditionalGet conditionalGet = getConditionalGet(link, inS);

        /* Return on Cancelation or Shutdown */
        if (monitor.isCanceled()) {
            closeStream(inS, true);
            return null;
        }

        /* Pass the Stream to the Interpreter */
        try {
            Owl.getInterpreter().interpret(inS, feed, null);
        } catch (EncodingException e) {

            /* Return on Cancelation or Shutdown */
            if (monitor.isCanceled()) {
                closeStream(inS, true);
                return null;
            }

            /* Re-retrieve InputStream from the Feed's Link */
            inS = openStream(link, properties);

            /* Re-retrieve Conditional Get if present */
            conditionalGet = getConditionalGet(link, inS);

            /* Return on Cancelation or Shutdown */
            if (monitor.isCanceled()) {
                closeStream(inS, true);
                return null;
            }

            /* Second try: Use platform encoding */
            Owl.getInterpreter().interpret(inS, feed,
                    Collections.singletonMap((Object) USE_PLATFORM_ENCODING, (Object) Boolean.TRUE));
        }

        /* Return actual URI that was connected to (supporting redirects) */
        if (inS instanceof HttpConnectionInputStream)
            return Triple.create(feed, conditionalGet, ((HttpConnectionInputStream) inS).getLink());

        /* Otherwise just use input URI */
        return Triple.create(feed, conditionalGet, link);
    }

    protected IConditionalGet getConditionalGet(URI link, InputStream inS) {
        IModelFactory typesFactory = Owl.getModelFactory();

        if (inS instanceof IConditionalGetCompatible) {
            String ifModifiedSince = ((IConditionalGetCompatible) inS).getIfModifiedSince();
            String ifNoneMatch = ((IConditionalGetCompatible) inS).getIfNoneMatch();

            if (ifModifiedSince != null || ifNoneMatch != null)
                return typesFactory.createConditionalGet(ifModifiedSince, link, ifNoneMatch);
        }

        return null;
    }

    protected void closeStream(InputStream inS, boolean abort) {
        try {
            if (abort && inS instanceof IAbortable)
                ((IAbortable) inS).abort();
            else if (inS != null)
                inS.close();
        } catch (IOException ex) {
            /* Ignore */
        }
    }

    /*
     * @see org.rssowl.core.connection.IProtocolHandler#getFeedIcon(java.net.URI,
     * org.eclipse.core.runtime.IProgressMonitor)
     */
    public byte[] getFeedIcon(URI link, IProgressMonitor monitor) {

        /* Try to load the Favicon directly from the supplied Link */
        byte[] favicon = loadFavicon(link, false, false, monitor);

        /* Fallback: Scan the Homepage of the Link for a Favicon entry */
        if (favicon == null || favicon.length == 0) {
            try {
                URI topLevelUri = URIUtils.toTopLevel(link);
                if (topLevelUri != null) {
                    URI faviconUri = getFavicon(topLevelUri, monitor);
                    if (faviconUri != null && faviconUri.isAbsolute())
                        return loadFavicon(faviconUri, true, false, monitor);
                }
            } catch (ConnectionException e) {
            } catch (URISyntaxException e) {
            } catch (Throwable t) {
                Activator.getDefault().logError(t.getMessage(), t);
            }
        }

        return favicon;
    }

    /* Load a possible Favicon from the given Feed */
    byte[] loadFavicon(URI link, boolean isFavicon, boolean rewriteHost, IProgressMonitor monitor) {
        InputStream inS = null;
        boolean isError = false;
        try {

            /* Define Properties for Connection */
            Map<Object, Object> properties = new HashMap<Object, Object>();
            properties.put(IConnectionPropertyConstants.CON_TIMEOUT, FAVICON_CON_TIMEOUT);
            properties.put(IConnectionPropertyConstants.PROGRESS_MONITOR, monitor);

            /* Load Favicon */
            URI faviconLink = isFavicon ? link : URIUtils.toFaviconUrl(link, rewriteHost);
            if (faviconLink == null)
                return null;

            inS = openStream(faviconLink, properties);

            ByteArrayOutputStream fos = new ByteArrayOutputStream();
            byte buffer[] = new byte[0xffff];
            int nbytes;

            while ((nbytes = inS.read(buffer)) != -1)
                fos.write(buffer, 0, nbytes);

            return fos.toByteArray();
        } catch (URISyntaxException e) {
            /* Ignore */
        } catch (ConnectionException e) {
            isError = true;

            /* Try rewriting the Host to obtain the Favicon */
            if (!rewriteHost && !isFavicon) {
                String exceptionName = e.getClass().getName();

                /* Only retry in case this is a generic ConnectionException */
                if (ConnectionException.class.getName().equals(exceptionName))
                    return loadFavicon(link, false, true, monitor);
            }
        } catch (IOException e) {
            /* Ignore */
        } finally {
            closeStream(inS, isError);
        }

        return null;
    }

    private URI getFavicon(URI link, IProgressMonitor monitor) throws ConnectionException {

        /* Define Properties for Connection */
        Map<Object, Object> properties = new HashMap<Object, Object>();
        properties.put(IConnectionPropertyConstants.PROGRESS_MONITOR, monitor);
        properties.put(IConnectionPropertyConstants.CON_TIMEOUT, FAVICON_CON_TIMEOUT);

        /* Open Stream */
        InputStream inS = openStream(link, properties);
        BufferedInputStream bufIns = new BufferedInputStream(inS);
        BufferedReader reader = new BufferedReader(new InputStreamReader(bufIns));
        try {

            /* Use real Base if possible */
            if (inS instanceof HttpConnectionInputStream)
                return CoreUtils.findFavicon(reader, ((HttpConnectionInputStream) inS).getLink(), monitor);

            /* Otherwise use request URI */
            return CoreUtils.findFavicon(reader, link, monitor);
        }

        /* Finally close the Stream */
        finally {
            closeStream(inS, true); //Abort the stream to avoid downloading the full content
        }
    }

    /**
     * Do not override default URLStreamHandler of HTTP/HTTPS and therefor return
     * NULL.
     *
     * @see org.rssowl.core.connection.IProtocolHandler#getURLStreamHandler()
     */
    public URLStreamHandlerService getURLStreamHandler() {
        return null;
    }

    /*
     * @see org.rssowl.core.connection.IProtocolHandler#openStream(java.net.URI,
     * org.eclipse.core.runtime.IProgressMonitor, java.util.Map)
     */
    public InputStream openStream(URI link, IProgressMonitor monitor, Map<Object, Object> properties)
            throws ConnectionException {

        /* Add Monitor to support early cancelation */
        if (monitor != null) {
            if (properties == null)
                properties = new HashMap<Object, Object>();
            properties.put(IConnectionPropertyConstants.PROGRESS_MONITOR, monitor);
        }

        return openStream(link, properties);
    }

    /**
     * Load the Contents of the given URL by connecting to it. The additional
     * properties may be used in conjunction with the
     * <code>IConnectionPropertyConstants</code> to define connection related
     * properties..
     *
     * @param link The URL to load.
     * @param properties Connection related properties as defined in
     * <code>IConnectionPropertyConstants</code> for example, or <code>NULL</code>
     * if none.
     * @return The Content of the URL as InputStream.
     * @throws ConnectionException Checked Exception to be used in case of any
     * Exception.
     * @see AuthenticationRequiredException
     * @see NotModifiedException
     */
    protected InputStream openStream(URI link, Map<Object, Object> properties) throws ConnectionException {

        /* Retrieve the InputStream out of the Link */
        try {
            return internalOpenStream(link, link, null, properties);
        }

        /* Handle Authentication Required */
        catch (AuthenticationRequiredException e) {

            /* Realm required from here on */
            if (e.getRealm() == null)
                throw e;

            /* Try to load credentials using Host / Port / Realm */
            URI normalizedUri = URIUtils.normalizeUri(link, true);
            ICredentials authCredentials = Owl.getConnectionService().getAuthCredentials(normalizedUri,
                    e.getRealm());

            /* Credentials based on Host / Port / Realm provided */
            if (authCredentials != null) {

                /* Store for plain URI too */
                ICredentialsProvider credProvider = Owl.getConnectionService().getCredentialsProvider(link);
                if (credProvider.getPersistedAuthCredentials(normalizedUri, e.getRealm()) != null)
                    credProvider.setAuthCredentials(authCredentials, link, null);
                else
                    credProvider.setInMemoryAuthCredentials(authCredentials, link, null);

                /* Reopen Stream */
                try {
                    return internalOpenStream(link, normalizedUri, e.getRealm(), properties);
                } catch (AuthenticationRequiredException ex) {
                    Owl.getConnectionService().getCredentialsProvider(normalizedUri)
                            .deleteAuthCredentials(normalizedUri, e.getRealm());
                    throw ex;
                }
            }

            /* Otherwise throw exception to callee */
            throw e;
        }
    }

    private InputStream internalOpenStream(URI link, URI authLink, String authRealm, Map<Object, Object> properties)
            throws ConnectionException {

        /* Handle File Protocol at first */
        if (URIUtils.FILE_SCHEME.equals(link.getScheme()))
            return loadFileProtocol(link);

        /* SSL Support */
        if (URIUtils.HTTPS_SCHEME.equals(link.getScheme()))
            initSSLProtocol();

        /* Feed Support */
        if (URIUtils.FEED_SCHEME.equals(link.getScheme()))
            initFeedProtocol();

        /* Init Client */
        HttpClient client = initClient(properties);

        /* Init the connection */
        HttpMethodBase method = null;
        InputStream inS = null;
        try {

            /* Create Method (GET or POST) */
            method = initConnection(link, properties);

            /* Proxy if required */
            setupProxy(link, client);

            /* Authentication if required */
            setupAuthentication(authLink, authRealm, client, method);

            /* Open the connection */
            inS = openConnection(client, method);

            /* Try to pipe the resulting stream into a GZipInputStream */
            if (inS != null)
                inS = pipeStream(inS, method);
        } catch (IOException e) {
            abortAndRelease(method);
            throw new ConnectionException(Activator.getDefault().createErrorStatus(e.getMessage(), e));
        }

        /* In case authentication required */
        int statusCode = method.getStatusCode();
        if (statusCode == HTTP_ERROR_AUTH_REQUIRED) {
            AuthState hostAuthState = method.getHostAuthState();
            String realm = hostAuthState != null ? hostAuthState.getRealm() : null;
            abortAndRelease(method);

            throw new AuthenticationRequiredException(realm, Activator.getDefault()
                    .createErrorStatus(Messages.DefaultProtocolHandler_ERROR_AUTHENTICATION_REQUIRED, null));
        }

        /* In case sync authentication failed (Forbidden) */
        else if (isSyncAuthenticationIssue(method, authLink)) {
            abortAndRelease(method);

            throw new AuthenticationRequiredException(null, Activator.getDefault()
                    .createErrorStatus(Messages.DefaultProtocolHandler_GR_ERROR_BAD_AUTH, null));
        }

        /* In case of Forbidden Status with Error Code (Google Reader) */
        else if (statusCode == HTTP_ERROR_FORBIDDEN && method.getResponseHeader(HEADER_RESPONSE_ERROR) != null)
            handleForbidden(method);

        /* In case proxy-authentication required / failed */
        else if (statusCode == HTTP_ERROR_PROXY_AUTH_REQUIRED) {
            abortAndRelease(method);

            throw new ProxyAuthenticationRequiredException(Activator.getDefault()
                    .createErrorStatus(Messages.DefaultProtocolHandler_ERROR_PROXY_AUTHENTICATION_REQUIRED, null));
        }

        /* If status code is 4xx, throw an IOException with the status code included */
        else if (statusCode >= HTTP_ERRORS) {
            String error = getError(statusCode);
            abortAndRelease(method);

            if (error != null)
                throw new ConnectionException(Activator.getDefault()
                        .createErrorStatus(NLS.bind(Messages.DefaultProtocolHandler_ERROR_HTTP_STATUS_MSG,
                                String.valueOf(statusCode), error), null));

            throw new ConnectionException(Activator.getDefault().createErrorStatus(
                    NLS.bind(Messages.DefaultProtocolHandler_ERROR_HTTP_STATUS, String.valueOf(statusCode)), null));
        }

        /* In case the Feed has not been modified since */
        else if (statusCode == HTTP_STATUS_NOT_MODIFIED) {
            abortAndRelease(method);

            throw new NotModifiedException(Activator.getDefault()
                    .createInfoStatus(Messages.DefaultProtocolHandler_INFO_NOT_MODIFIED_SINCE, null));
        }

        /* In case response body is not available */
        if (inS == null) {
            abortAndRelease(method);

            throw new ConnectionException(Activator.getDefault()
                    .createErrorStatus(Messages.DefaultProtocolHandler_ERROR_STREAM_UNAVAILABLE, null));
        }

        /* Check wether a Progress Monitor is provided to support early cancelation */
        IProgressMonitor monitor = null;
        if (properties != null && properties.containsKey(IConnectionPropertyConstants.PROGRESS_MONITOR))
            monitor = (IProgressMonitor) properties.get(IConnectionPropertyConstants.PROGRESS_MONITOR);

        /* Return a Stream that releases the connection once closed */
        return new HttpConnectionInputStream(link, method, monitor, inS);
    }

    private void abortAndRelease(HttpMethodBase method) {
        if (method != null) {
            method.abort();
            method.releaseConnection();
        }
    }

    private boolean isSyncAuthenticationIssue(HttpMethodBase method, URI link) {

        /* Handle Google Error Response "Forbidden" for synced connections */
        if (method.getStatusCode() == HTTP_ERROR_FORBIDDEN && SyncUtils.fromGoogle(link.toString())) {
            Header errorHeader = method.getResponseHeader(HEADER_RESPONSE_ERROR);
            if (errorHeader == null || ERROR_BAD_AUTH.equals(errorHeader.getValue()))
                return true;
        }

        return false;
    }

    protected void handleForbidden(HttpMethodBase method) throws ConnectionException {
        String errorMsg = null;
        String errorUrl = null;

        /* Lookup Google Error if present */
        Header errorHeader = method.getResponseHeader(HEADER_RESPONSE_ERROR);
        if (errorHeader != null && StringUtils.isSet(errorHeader.getValue())) {
            String errorCode = errorHeader.getValue();
            if (ERROR_BAD_AUTH.equals(errorCode))
                errorMsg = Messages.DefaultProtocolHandler_GR_ERROR_BAD_AUTH;
            else if (ERROR_NOT_VERIFIED.equals(errorCode))
                errorMsg = Messages.DefaultProtocolHandler_GR_ERROR_NOT_VERIFIED;
            else if (ERROR_NO_TERMS.equals(errorCode))
                errorMsg = Messages.DefaultProtocolHandler_GR_ERROR_NO_TERMS;
            else if (ERROR_UNKNOWN.equals(errorCode))
                errorMsg = Messages.DefaultProtocolHandler_GR_ERROR_UNKNOWN;
            else if (ERROR_ACCOUNT_DELETED.equals(errorCode))
                errorMsg = Messages.DefaultProtocolHandler_GR_ERROR_ACCOUNT_DELETED;
            else if (ERROR_ACCOUNT_DISABLED.equals(errorCode))
                errorMsg = Messages.DefaultProtocolHandler_GR_ERROR_ACCOUNT_DISABLED;
            else if (ERROR_SERVICE_DISABLED.equals(errorCode))
                errorMsg = Messages.DefaultProtocolHandler_GR_ERROR_SERVICE_DISABLED;
            else if (ERROR_SERVICE_UNAVAILABLE.equals(errorCode))
                errorMsg = Messages.DefaultProtocolHandler_GR_ERROR_SERVICE_UNAVAILABLE;
            else if (ERROR_CAPTCHA_REQUIRED.equals(errorCode)) {
                errorMsg = Messages.DefaultProtocolHandler_GR_ERROR_CAPTCHA_REQUIRED;
                errorUrl = SyncUtils.CAPTCHA_UNLOCK_URL;
            }

            /* Also look up specified Error URL as necessary */
            if (errorUrl == null) {
                Header urlHeader = method.getResponseHeader(HEADER_RESPONSE_URL);
                if (urlHeader != null && StringUtils.isSet(urlHeader.getValue()))
                    errorUrl = urlHeader.getValue();
            }
        }

        /* Otherwise throw generic Forbidden Exception */
        if (errorMsg == null)
            errorMsg = Messages.DefaultProtocolHandler_ERROR_FORBIDDEN;

        abortAndRelease(method);

        if (errorUrl != null)
            throw new SyncConnectionException(errorUrl, Activator.getDefault().createErrorStatus(errorMsg, null));

        throw new ConnectionException(Activator.getDefault().createErrorStatus(errorMsg, null));
    }

    /* Some HTTP Error Messages */
    private String getError(int errorCode) {
        switch (errorCode) {
        case 400:
            return "Bad Request"; //$NON-NLS-1$
        case 403:
            return "Forbidden"; //$NON-NLS-1$
        case 404:
            return "Not Found"; //$NON-NLS-1$
        case 408:
            return "Request Timeout"; //$NON-NLS-1$
        case 500:
            return "Internal Server Error"; //$NON-NLS-1$
        case 502:
            return "Bad Gateway"; //$NON-NLS-1$
        case 503:
            return "Service Unavailable"; //$NON-NLS-1$
        }

        return null;
    }

    private InputStream loadFileProtocol(URI link) throws ConnectionException {
        try {
            File file = new File(link);
            return new BufferedInputStream(new FileInputStream(file));
        } catch (FileNotFoundException e) {
            throw new ConnectionException(Activator.getDefault().createErrorStatus(e.getMessage(), e));
        }
    }

    private void setupAuthentication(URI link, String realm, HttpClient client, HttpMethodBase method)
            throws URIException, CredentialsException {
        ICredentials authCredentials = Owl.getConnectionService().getAuthCredentials(link, realm);
        if (authCredentials != null) {
            client.getParams().setAuthenticationPreemptive(true);

            /* Require Host */
            String host = method.getURI().getHost();

            /* Create the UsernamePasswordCredentials */
            NTCredentials userPwCreds = new NTCredentials(authCredentials.getUsername(),
                    authCredentials.getPassword(), host,
                    (authCredentials.getDomain() != null) ? authCredentials.getDomain() : ""); //$NON-NLS-1$

            /* Authenticate to the Server */
            client.getState().setCredentials(AuthScope.ANY, userPwCreds);
            method.setDoAuthentication(true);
        }
    }

    private void setupProxy(URI link, HttpClient client) throws CredentialsException {
        IProxyCredentials creds = Owl.getConnectionService().getProxyCredentials(link);
        if (creds != null) {

            /* Apply Proxy Config to HTTPClient */
            client.getParams().setAuthenticationPreemptive(true);
            client.getHostConfiguration().setProxy(creds.getHost(), creds.getPort());

            /* Authenticate if required */
            if (creds.getUsername() != null || creds.getPassword() != null) {
                String user = StringUtils.isSet(creds.getUsername()) ? creds.getUsername() : ""; //$NON-NLS-1$
                String pw = StringUtils.isSet(creds.getPassword()) ? creds.getPassword() : ""; //$NON-NLS-1$

                AuthScope proxyAuthScope = new AuthScope(creds.getHost(), creds.getPort());

                /* Use NTLM Credentials if Domain is set */
                if (creds.getDomain() != null)
                    client.getState().setProxyCredentials(proxyAuthScope,
                            new NTCredentials(user, pw, creds.getHost(), creds.getDomain()));

                /* Use normal Credentials if Domain is not set */
                else
                    client.getState().setProxyCredentials(proxyAuthScope,
                            new UsernamePasswordCredentials(user, pw));
            }
        }
    }

    private synchronized void initSSLProtocol() {
        if (fgSSLInitialized)
            return;

        /* Register Easy Protocol Socket Factory with HTTPS */
        Protocol easyHttpsProtocol = new Protocol(URIUtils.HTTPS_SCHEME,
                (ProtocolSocketFactory) Owl.getConnectionService().getSecureProtocolSocketFactory(), 443);
        Protocol.registerProtocol(URIUtils.HTTPS_SCHEME, easyHttpsProtocol);

        fgSSLInitialized = true;
    }

    private synchronized void initFeedProtocol() {
        if (fgFeedProtocolInitialized)
            return;

        Protocol feed = new Protocol(URIUtils.FEED_SCHEME, new DefaultProtocolSocketFactory(), 80);
        Protocol.registerProtocol(URIUtils.FEED_SCHEME, feed);

        fgFeedProtocolInitialized = true;
    }

    private void setHeaders(Map<Object, Object> properties, HttpMethodBase method) {
        method.setRequestHeader(HEADER_REQUEST_ACCEPT_ENCODING, "gzip"); //$NON-NLS-1$
        method.setRequestHeader(HEADER_REQUEST_USER_AGENT, USER_AGENT);

        /* Add Conditional GET Headers if present */
        if (properties != null) {
            String ifModifiedSince = (String) properties.get(IConnectionPropertyConstants.IF_MODIFIED_SINCE);
            String ifNoneMatch = (String) properties.get(IConnectionPropertyConstants.IF_NONE_MATCH);

            if (ifModifiedSince != null)
                method.setRequestHeader(HEADER_RESPONSE_IF_MODIFIED_SINCE, ifModifiedSince);

            if (ifNoneMatch != null)
                method.setRequestHeader(HEADER_RESPONSE_IF_NONE_MATCH, ifNoneMatch);
        }

        /* Add Accept-Language Header if present */
        if (properties != null && properties.containsKey(IConnectionPropertyConstants.ACCEPT_LANGUAGE))
            method.setRequestHeader(HEADER_REQUEST_ACCEPT_LANGUAGE,
                    (String) properties.get(IConnectionPropertyConstants.ACCEPT_LANGUAGE));

        /* Add Cookie Header if present */
        if (properties != null && properties.containsKey(IConnectionPropertyConstants.COOKIE))
            method.setRequestHeader(HEADER_REQUEST_COOKIE,
                    (String) properties.get(IConnectionPropertyConstants.COOKIE));

        /* Add more Headers */
        if (properties != null && properties.containsKey(IConnectionPropertyConstants.HEADERS)) {
            Map<?, ?> headers = (Map<?, ?>) properties.get(IConnectionPropertyConstants.HEADERS);
            Set<?> entries = headers.entrySet();
            for (Object obj : entries) {
                Entry<?, ?> entry = (Entry<?, ?>) obj;
                method.setRequestHeader((String) entry.getKey(), (String) entry.getValue());
            }
        }
    }

    private HttpClient initClient(Map<Object, Object> properties) {

        /* Retrieve Connection Timeout from Properties if set */
        int conTimeout = DEFAULT_CON_TIMEOUT;
        if (properties != null && properties.containsKey(IConnectionPropertyConstants.CON_TIMEOUT))
            conTimeout = (Integer) properties.get(IConnectionPropertyConstants.CON_TIMEOUT);

        /* Create a new HttpClient */
        HttpClient client = new HttpClient(new SimpleHttpConnectionManager(true));

        /* Socket Timeout - Max. time to wait for an answer */
        client.getHttpConnectionManager().getParams().setSoTimeout(conTimeout);

        /* Connection Timeout - Max. time to wait for a connection */
        client.getHttpConnectionManager().getParams().setConnectionTimeout(conTimeout);

        return client;
    }

    private HttpMethodBase initConnection(URI link, Map<Object, Object> properties) throws IOException {

        /* Create the Method. Wrap any RuntimeException into an IOException */
        HttpMethodBase method = null;
        try {
            if (properties != null && properties.containsKey(IConnectionPropertyConstants.POST))
                method = new PostMethod(link.toString());
            else
                method = new GetMethod(link.toString());
        } catch (RuntimeException e) {
            throw new IOException(e.getMessage());
        }

        /* Ignore Cookies */
        if (method instanceof GetMethod)
            method.getParams().setCookiePolicy(CookiePolicy.IGNORE_COOKIES);

        /* Set Headers */
        setHeaders(properties, method);

        /* Set Parameters (POST only) */
        if (method instanceof PostMethod && properties != null
                && properties.containsKey(IConnectionPropertyConstants.PARAMETERS)) {
            Map<?, ?> parameters = (Map<?, ?>) properties.get(IConnectionPropertyConstants.PARAMETERS);
            Set<?> entries = parameters.entrySet();
            for (Object obj : entries) {
                Entry<?, ?> entry = (Entry<?, ?>) obj;
                String key = (String) entry.getKey();
                if (entry.getValue() instanceof String)
                    ((PostMethod) method).addParameter(key, (String) entry.getValue());
                else if (entry.getValue() instanceof String[]) {
                    String[] parameterValues = (String[]) entry.getValue();
                    for (String value : parameterValues) {
                        ((PostMethod) method).addParameter(key, value);
                    }
                }
            }
        }

        /* Follow Redirects */
        if (method instanceof GetMethod)
            method.setFollowRedirects(true);

        return method;
    }

    private InputStream openConnection(HttpClient client, HttpMethodBase method) throws HttpException, IOException {

        /* Execute the Method */
        client.executeMethod(method);

        /* Finally retrieve the InputStream from the respond body */
        return method.getResponseBodyAsStream();
    }

    private InputStream pipeStream(InputStream inputStream, HttpMethodBase method) throws IOException {
        Assert.isNotNull(inputStream);

        /* Retrieve the Content Encoding */
        String contentEncoding = method.getResponseHeader(HEADER_RESPONSE_CONTENT_ENCODING) != null
                ? method.getResponseHeader(HEADER_RESPONSE_CONTENT_ENCODING).getValue()
                : null;
        boolean isGzipStream = false;

        /*
         * Return in case the Content Encoding is not given and the InputStream does
         * not support mark() and reset()
         */
        if ((contentEncoding == null || !contentEncoding.equals("gzip")) && !inputStream.markSupported()) //$NON-NLS-1$
            return inputStream;

        /* Content Encoding is set to gzip, so use the GZipInputStream */
        if (contentEncoding != null && contentEncoding.equals("gzip")) { //$NON-NLS-1$
            isGzipStream = true;
        }

        /* Detect if the Stream is gzip encoded */
        else if (inputStream.markSupported()) {
            inputStream.mark(2);
            int id1 = inputStream.read();
            int id2 = inputStream.read();
            inputStream.reset();

            /* Check for GZip Magic Numbers (See RFC 1952) */
            if (id1 == 0x1F && id2 == 0x8B)
                isGzipStream = true;
        }

        /* Create the GZipInputStream then */
        if (isGzipStream) {
            try {
                return new GZIPInputStream(inputStream);
            } catch (IOException e) {
                return inputStream;
            }
        }
        return inputStream;
    }

    /*
     * @see org.rssowl.core.connection.IProtocolHandler#getLabel(java.net.URI,
     * org.eclipse.core.runtime.IProgressMonitor)
     */
    public String getLabel(URI link, IProgressMonitor monitor) throws ConnectionException {
        String title = ""; //$NON-NLS-1$

        /* Define Properties for Connection */
        Map<Object, Object> properties = new HashMap<Object, Object>();
        properties.put(IConnectionPropertyConstants.PROGRESS_MONITOR, monitor);
        properties.put(IConnectionPropertyConstants.CON_TIMEOUT, FEED_LABEL_CON_TIMEOUT);

        /* Open Stream */
        InputStream inS = openStream(link, properties);
        try {

            /* Return on Cancelation or Shutdown */
            if (monitor.isCanceled())
                return null;

            /* Buffered Stream to support mark and reset */
            BufferedInputStream bufIns = new BufferedInputStream(inS);
            bufIns.mark(8192);

            /* Try to read Encoding out of XML Document */
            String encoding = getEncodingFromXML(new InputStreamReader(bufIns), monitor);

            /* Avoid lowercase UTF-8 notation */
            if ("utf-8".equalsIgnoreCase(encoding)) //$NON-NLS-1$
                encoding = "UTF-8"; //$NON-NLS-1$

            /* Reset the Stream to its beginning */
            bufIns.reset();

            /* Grab Title using supplied Encoding */
            if (StringUtils.isSet(encoding) && Charset.isSupported(encoding))
                title = getTitleFromFeed(new BufferedReader(new InputStreamReader(bufIns, encoding)), monitor);

            /* Grab Title using Default Encoding */
            else
                title = getTitleFromFeed(new BufferedReader(new InputStreamReader(bufIns)), monitor);

            /* Remove the title tags (also delete attributes in title tag) */
            title = title.replaceAll("<title[^>]*>", ""); //$NON-NLS-1$ //$NON-NLS-2$
            title = title.replaceAll("</title>", ""); //$NON-NLS-1$ //$NON-NLS-2$

            /* Remove potential CDATA Tags */
            title = title.replaceAll(Pattern.quote("<![CDATA["), ""); //$NON-NLS-1$ //$NON-NLS-2$
            title = title.replaceAll(Pattern.quote("]]>"), ""); //$NON-NLS-1$ //$NON-NLS-2$
        } catch (IOException e) {
            if (!(e instanceof MonitorCanceledException))
                Activator.safeLogError(e.getMessage(), e);
        }

        /* Finally close the Stream */
        finally {
            closeStream(inS, true); //Abort the stream to avoid downloading the full content
        }

        // Have an upper maximum of title length to protect against issues
        String result = StringUtils.stripTags(title.trim(), true);
        if (result.length() > MAX_DETECTED_TITLE_LENGTH)
            result = result.substring(0, MAX_DETECTED_TITLE_LENGTH);

        return result;
    }

    /* Tries to read the encoding information from the given InputReader */
    private String getEncodingFromXML(InputStreamReader inputReader, IProgressMonitor monitor) throws IOException {
        String encoding = null;

        /* Read the first line or until the Tag is closed */
        StringBuilder strBuf = new StringBuilder();
        int c;
        while (!monitor.isCanceled() && (c = inputReader.read()) != -1) {
            char character = (char) c;

            /* Append all Characters, except for closing Tag or CR */
            if (character != '>' && character != '\n' && character != '\r')
                strBuf.append(character);

            /* Closing Tag is the last one to append */
            else if (character == '>') {
                strBuf.append(character);
                break;
            }

            /* End of Line or Tag reached */
            else
                break;
        }

        /* Save the first Line */
        String firstLine = strBuf.toString();

        /* Look if Encoding is supplied */
        if (firstLine.indexOf("encoding") >= 0) { //$NON-NLS-1$

            /* Extract the Encoding Value */
            String regEx = "<\\?.*encoding=[\"']([^\\s]*)[\"'].*\\?>"; //$NON-NLS-1$
            Pattern pattern = Pattern.compile(regEx);
            Matcher match = pattern.matcher(firstLine);

            /* Get first matching String */
            if (match.find())
                return match.group(1);
        }

        return encoding;
    }

    /* Tries to find the title information from the given Reader */
    private String getTitleFromFeed(BufferedReader inputReader, IProgressMonitor monitor) throws IOException {
        String title = ""; //$NON-NLS-1$
        String firstLine;
        boolean titleFound = false;

        /* Read the file until the Title is found or EOF is reached */
        while (true) {

            /* Return on Cancelation or Shutdown */
            if (monitor.isCanceled())
                return null;

            /* Will throw an IOException on EOF reached */
            firstLine = inputReader.readLine();

            /* EOF reached */
            if (firstLine == null)
                break;

            /* If the line contains the title, break loop */
            if (firstLine.indexOf("<title") >= 0 && firstLine.indexOf("</title>") >= 0) { //$NON-NLS-1$ //$NON-NLS-2$
                title = firstLine.trim();
                titleFound = true;
                break;
            }
        }

        /* Return if no title was found */
        if (!titleFound)
            return title;

        /* Extract the title String */
        String regEx = "<title[^>]*>[^<]*</title>"; //$NON-NLS-1$
        Pattern pattern = Pattern.compile(regEx);
        Matcher match = pattern.matcher(title);

        /* Get first matching String */
        if (match.find())
            title = match.group();

        return title;
    }

    /*
     * @see org.rssowl.core.connection.IProtocolHandler#getFeed(java.net.URI,
     * org.eclipse.core.runtime.IProgressMonitor)
     */
    public URI getFeed(final URI website, IProgressMonitor monitor) throws ConnectionException {

        /* Define Properties for Connection */
        Map<Object, Object> properties = new HashMap<Object, Object>();
        properties.put(IConnectionPropertyConstants.PROGRESS_MONITOR, monitor);
        properties.put(IConnectionPropertyConstants.CON_TIMEOUT, FEED_LABEL_CON_TIMEOUT);

        /* Open Stream */
        InputStream inS = openStream(website, properties);
        BufferedInputStream bufIns = new BufferedInputStream(inS);
        BufferedReader reader = new BufferedReader(new InputStreamReader(bufIns));
        try {

            /* Our HttpConnectionInputStream */
            if (inS instanceof HttpConnectionInputStream) {

                /* Check the content type and return early if already a feed */
                String contentType = ((HttpConnectionInputStream) inS).getContentType();
                if (contentType != null) {
                    for (String feedContentType : CoreUtils.FEED_MIME_TYPES) {
                        if (contentType.toLowerCase().contains(feedContentType))
                            return website;
                    }
                }

                /* Use real Base if possible */
                return CoreUtils.findFeed(reader, ((HttpConnectionInputStream) inS).getLink(), monitor);
            }

            /* Normal Stream (use request URI) */
            return CoreUtils.findFeed(reader, website, monitor);
        }

        /* Finally close the Stream */
        finally {
            closeStream(inS, true); //Abort the stream to avoid downloading the full content
        }
    }
}