net.www_eee.portal.channels.ProxyChannel.java Source code

Java tutorial

Introduction

Here is the source code for net.www_eee.portal.channels.ProxyChannel.java

Source

/*
 * Copyright 2007-2015 by Chris Hubick. All Rights Reserved.
 * 
 * This work is licensed under the terms of the "GNU AFFERO GENERAL PUBLIC LICENSE" version 3, as published by the Free
 * Software Foundation <http://www.gnu.org/licenses/>, a copy of which you should have received in the file LICENSE.txt.
 */

package net.www_eee.portal.channels;

import java.io.*;
import java.net.*;
import java.nio.charset.*;
import java.time.*;
import java.time.format.*;
import java.util.*;
import java.util.function.*;
import java.util.logging.*;
import java.util.stream.*;

import javax.activation.*;

import org.w3c.dom.*;

import org.xml.sax.*;
import org.xml.sax.ext.*;

import javax.ws.rs.*;
import javax.ws.rs.core.*;

import org.apache.http.*;
import org.apache.http.entity.*;
import org.apache.http.message.*;
import org.apache.http.params.*;
import org.apache.http.protocol.*;

import org.apache.http.client.*;
import org.apache.http.client.config.*;
import org.apache.http.client.methods.*;
import org.apache.http.client.params.*;
import org.apache.http.client.protocol.*;
import org.apache.http.conn.*;
import org.apache.http.impl.client.*;

import org.eclipse.jdt.annotation.*;

import net.www_eee.util.misc.core.*;
import net.www_eee.util.misc.core.collection.*;
import net.www_eee.util.misc.core.function.*;
import net.www_eee.util.misc.core.io.*;
import net.www_eee.util.misc.core.net.*;
import net.www_eee.util.misc.core.xml.*;
import net.www_eee.util.misc.core.xml.dom.*;
import net.www_eee.util.misc.core.xml.html.*;
import net.www_eee.util.misc.core.xml.sax.*;
import net.www_eee.util.misc.core.xml.transform.sax.*;

import net.www_eee.util.misc.logging.*;

import net.www_eee.util.misc.http.*;

import net.www_eee.util.misc.ws.rs.*;

import net.www_eee.util.misc.ws.rs.properties.*;

import net.www_eee.portal.*;

/**
 * <p>
 * A {@link Channel} which provides content proxied via an {@link HttpClient}.
 * </p>
 * 
 * <p>
 * {@linkplain #BASE_URI_PROP Provided} a {@link URL}, a <code>ProxyChannel</code> will provide access to a single
 * document or entire website at that location.
 * </p>
 * 
 * <p>
 * This channel is implemented in a completely <em>generic</em> fashion, and is capable of rendering <em>any</em> type
 * of XML content into a {@link Page}. As such, you will probably desire the enhanced functionality provided by the
 * {@linkplain net.www_eee.portal.channelplugins.ProxyChannelHTMLSource HTML plugin}, in the likely case you will be
 * using the channel to proxy (X)HTML content.
 * </p>
 * 
 * <p>
 * Aside from actual retrieval of the content itself, a major function of this channel is to perform
 * {@linkplain #rewriteProxiedFileLink(Page.Request, URL, URI, boolean, boolean) link rewriting} within the proxied
 * documents.
 * </p>
 * 
 * <h3 id="configuration">Configuration</h3>
 * <p>
 * In addition to those inherited from the {@link Channel} class, the following {@linkplain ConfigManager configuration
 * properties} are supported by this class:
 * </p>
 * <ul>
 * <li>{@link #BASE_URI_PROP}</li>
 * <li>{@link #DEFAULT_PATH_PROP}</li>
 * <li>{@link #DEFAULT_PATH_RESTRICTION_ENABLE_PROP}</li>
 * <li>{@link #PARENT_FOLDERS_RESTRICTION_DISABLE_PROP}</li>
 * <li>{@link #LINK_REWRITING_HYPERLINKS_TO_CHANNEL_DISABLE_PROP}</li>
 * <li>{@link #LINK_REWRITING_RESOURCE_LINKS_TO_CHANNEL_ENABLE_PROP}</li>
 * <li>{@link #CONNECT_TIMEOUT_PROP}</li>
 * <li>{@link #READ_TIMEOUT_PROP}</li>
 * <li>{@link #FOLLOW_REDIRECTS_ENABLE_PROP}</li>
 * </ul>
 */
@NonNullByDefault
public class ProxyChannel extends Channel {
    /**
     * <p>
     * The key to a <strong>required</strong> {@link URI#create(String) URI} property defining the
     * {@linkplain #getProxiedBaseURI(Page.Request) base URI} of the document or site to be proxied.
     * </p>
     * 
     * <p>
     * If you wish to create a channel which will simply display a single document, proxied from a specified URL, then
     * this property can just contain the {@link URL} to that document (this value will essentially include the
     * {@linkplain #DEFAULT_PATH_PROP default path}, which is then not required).
     * </p>
     * 
     * <p>
     * If you wish to create a channel which will proxy an entire website, where users can navigate between the pages of
     * that site within the channel {@linkplain net.www_eee.portal.Channel.Mode#VIEW view}, then this property should
     * contain the {@link URL} of the top-most root <em>folder</em> which contains the documents for the entire site, and
     * a {@linkplain #DEFAULT_PATH_PROP default path} should then also be specified.
     * </p>
     * 
     * <p>
     * By default, this channel will proxy all documents within the leaf-most <em>folder</em> specified by this property,
     * though you can {@linkplain #DEFAULT_PATH_RESTRICTION_ENABLE_PROP restrict} it to just the document specified by
     * this URL (including the {@linkplain #DEFAULT_PATH_PROP default path}). Though discouraged, you may also
     * {@linkplain #PARENT_FOLDERS_RESTRICTION_DISABLE_PROP enable} proxying of documents on the server which are outside
     * of this folder.
     * </p>
     * 
     * <p>
     * Note that if you specify a value for this property which is not {@linkplain URI#isAbsolute() absolute}, it will be
     * {@linkplain ConfigManager#getContextResourceLocalHostURI(UriInfo, String, Map, String, boolean) resolved} within
     * the local portal context (relative to the root of the context). This is a very useful feature for creating a
     * self-contained portal context which includes all the documents it proxies.
     * </p>
     * 
     * @see #getProxiedBaseURI(Page.Request)
     * @see #DEFAULT_PATH_PROP
     * @category WWW_EEE_PORTAL_CONFIG_PROP
     */
    public static final String BASE_URI_PROP = "ProxyChannel.BaseURI";
    /**
     * <p>
     * The key to a String property defining a <strong>relative</strong> path under the proxied
     * {@linkplain #getProxiedBaseURI(Page.Request) base URI} to the document which should be displayed within this
     * channel by default (when {@linkplain net.www_eee.portal.Channel.Mode#VIEW viewed} as part of a
     * {@linkplain Page#doViewRequest(Page.Request) request} for the {@link Page}, or when no
     * {@linkplain net.www_eee.portal.Page.Request#getChannelLocalPath(Channel) local path} is specified).
     * </p>
     * 
     * <p>
     * This property is not required if the {@linkplain #getProxiedBaseURI(Page.Request) base URI} points directly to a
     * document (as opposed to a folder), as then that value will be interpreted as though it includes this path.
     * </p>
     * 
     * <p>
     * Note that this path is only really used during {@linkplain net.www_eee.portal.Channel.Mode#VIEW view mode}
     * requests, except when the {@linkplain #isDefaultPathRestrictionEnabled(Page.Request) default path} restriction is
     * {@linkplain #DEFAULT_PATH_RESTRICTION_ENABLE_PROP enabled}, in which case the
     * {@linkplain #getProxiedFileLocalURI(Page.Request, Channel.Mode, boolean) proxied file URI} will be validated
     * against it.
     * </p>
     * 
     * @see #BASE_URI_PROP
     * @category WWW_EEE_PORTAL_CONFIG_PROP
     */
    public static final String DEFAULT_PATH_PROP = "ProxyChannel.DefaultPath";
    /**
     * The key to a {@link Boolean#valueOf(String) Boolean} property indicating that this channel should only be
     * {@linkplain #isDefaultPathRestrictionEnabled(Page.Request) enabled} to proxy the single document located at the
     * {@linkplain #DEFAULT_PATH_PROP default path} location, and <em>not</em> anything else within the specified
     * {@linkplain #getProxiedBaseURI(Page.Request) base URI} folder (as allowed by default).
     * 
     * @see #isDefaultPathRestrictionEnabled(Page.Request)
     * @category WWW_EEE_PORTAL_CONFIG_PROP
     */
    public static final String DEFAULT_PATH_RESTRICTION_ENABLE_PROP = "ProxyChannel.DefaultPathRestriction.Enable";
    /**
     * The key to a {@link Boolean#valueOf(String) Boolean} property indicating the default restriction against the
     * proxying of documents from the source server which reside outside the {@linkplain #getProxiedBaseURI(Page.Request)
     * base URI} should be {@linkplain #isParentFoldersRestrictionDisabled(Page.Request) disabled}. This option should
     * never really need to be used, as the base URI should generally encompass all files required by a site.
     * 
     * @see #isParentFoldersRestrictionDisabled(Page.Request)
     * @category WWW_EEE_PORTAL_CONFIG_PROP
     */
    public static final String PARENT_FOLDERS_RESTRICTION_DISABLE_PROP = "ProxyChannel.ParentFoldersRestriction.Disable";
    /**
     * The key to a {@link Boolean#valueOf(String) Boolean} property which indicates that, during
     * {@linkplain #rewriteProxiedFileLink(Page.Request, URL, URI, boolean, boolean) link rewriting}, that any
     * <em>hyperlink</em> between this and another proxied site document should no longer be rewritten so that the client
     * browser {@linkplain net.www_eee.portal.Channel.Mode#VIEW views} the linked document within this portal channel (
     * {@linkplain #isLinkRewritingHyperlinksToChannelDisabled(Page.Request) default behaviour}), but rather so the client
     * is directed outside of the portal, to display the document directly from it's
     * {@linkplain #getProxiedBaseURI(Page.Request) origin/source location}.
     * 
     * @see #isLinkRewritingHyperlinksToChannelDisabled(Page.Request)
     * @see #rewriteProxiedFileLink(Page.Request, URL, URI, boolean, boolean)
     * @category WWW_EEE_PORTAL_CONFIG_PROP
     */
    public static final String LINK_REWRITING_HYPERLINKS_TO_CHANNEL_DISABLE_PROP = "ProxyChannel.LinkRewriting.Hyperlinks.ToChannel.Disable";
    /**
     * <p>
     * The key to a {@link Boolean#valueOf(String) Boolean} property which indicates that, during
     * {@linkplain #rewriteProxiedFileLink(Page.Request, URL, URI, boolean, boolean) link rewriting}, any relative link
     * from within the proxied document to an <em>external resource</em> should no longer be rewritten into an
     * {@linkplain URI#isAbsolute() absolute} link pointing directly back to the
     * {@linkplain #getProxiedBaseURI(Page.Request) origin/source location} (
     * {@linkplain #isLinkRewritingResourceLinksToChannelEnabled(Page.Request) default behaviour}), but rather so that the
     * client browser retrieves it as a {@linkplain net.www_eee.portal.Channel.Mode#RESOURCE resource} proxied via this
     * channel.
     * </p>
     * 
     * <p>
     * This option can be useful if there is a firewall preventing clients from direct access to an internal server
     * hosting the source resources, but should be exercised with care, as WWW-EEE-Portal is designed primarily for
     * aggregation of markup, and doesn't provide the most sophisticated HTTP proxy implementation (ie, conditional
     * requests <em>are</em> supported, but limited byte-range type requests against large resources are <em>not</em>).
     * Note that this option may be used to force resource links to be rewritten pointing back through the portal, and
     * then the container configured to intercept those requests and provide a more sophisticated proxy implementation
     * (ie, Apache <code>mod_proxy</code> and <code>mod_cache</code>) if required.
     * </p>
     * 
     * @see #isLinkRewritingResourceLinksToChannelEnabled(Page.Request)
     * @see #rewriteProxiedFileLink(Page.Request, URL, URI, boolean, boolean)
     * @category WWW_EEE_PORTAL_CONFIG_PROP
     */
    public static final String LINK_REWRITING_RESOURCE_LINKS_TO_CHANNEL_ENABLE_PROP = "ProxyChannel.LinkRewriting.ResourceLinksToChannel.Enable";
    /**
     * The key to an {@link Integer#valueOf(String) Integer} property (in milliseconds)
     * {@linkplain #getConnectTimeout(Page.Request) used} to {@linkplain HttpParams#setIntParameter(String, int) set} the
     * {@linkplain CoreConnectionPNames#CONNECTION_TIMEOUT connection timeout} {@linkplain HttpClient#getParams()
     * parameter} on any {@link HttpClient} created to proxy documents for this channel. If this property is not specified
     * then the {@linkplain #DEFAULT_CONNECT_TIMEOUT_MS default} will be used.
     * 
     * @see CoreConnectionPNames#CONNECTION_TIMEOUT
     * @see #DEFAULT_CONNECT_TIMEOUT_MS
     * @see #getConnectTimeout(Page.Request)
     * @category WWW_EEE_PORTAL_CONFIG_PROP
     */
    public static final String CONNECT_TIMEOUT_PROP = "ProxyChannel.ConnectTimeout";
    /**
     * Unless an explicit value is {@linkplain #CONNECT_TIMEOUT_PROP specified}, this value will be
     * {@linkplain #getConnectTimeout(Page.Request) used} as the default value (in milliseconds) to
     * {@linkplain HttpParams#setIntParameter(String, int) set} the {@linkplain CoreConnectionPNames#CONNECTION_TIMEOUT
     * connection timeout} {@linkplain HttpClient#getParams() parameter} on any {@link HttpClient} created to proxy
     * documents for this channel.
     * 
     * @see #CONNECT_TIMEOUT_PROP
     * @see #getConnectTimeout(Page.Request)
     */
    public static final Integer DEFAULT_CONNECT_TIMEOUT_MS = 30000;
    /**
     * The key to an {@link Integer#valueOf(String) Integer} property (in milliseconds)
     * {@linkplain #getReadTimeout(Page.Request) used} to {@linkplain HttpParams#setIntParameter(String, int) set} the
     * {@linkplain CoreConnectionPNames#SO_TIMEOUT socket timeout} {@linkplain HttpClient#getParams() parameter} on any
     * {@link HttpClient} created to proxy documents for this channel. If this property is not specified then the
     * {@linkplain #DEFAULT_READ_TIMEOUT_MS default} will be used.
     * 
     * @see CoreConnectionPNames#SO_TIMEOUT
     * @see #DEFAULT_READ_TIMEOUT_MS
     * @see #getReadTimeout(Page.Request)
     * @category WWW_EEE_PORTAL_CONFIG_PROP
     */
    public static final String READ_TIMEOUT_PROP = "ProxyChannel.ReadTimeout";
    /**
     * Unless an explicit value is {@linkplain #READ_TIMEOUT_PROP specified}, this value will be
     * {@linkplain #getReadTimeout(Page.Request) used} as the default value (in milliseconds) to
     * {@linkplain HttpParams#setIntParameter(String, int) set} the {@linkplain CoreConnectionPNames#SO_TIMEOUT socket
     * timeout} {@linkplain HttpClient#getParams() parameter} on any {@link HttpClient} created to proxy documents for
     * this channel.
     * 
     * @see #READ_TIMEOUT_PROP
     * @see #getReadTimeout(Page.Request)
     */
    public static final Integer DEFAULT_READ_TIMEOUT_MS = 30000;
    /**
     * <p>
     * The key to a {@link Boolean#valueOf(String) Boolean} property {@linkplain #isFollowRedirectsEnabled(Page.Request)
     * used} to {@linkplain HttpParams#setBooleanParameter(String, boolean) set} the automatic
     * {@linkplain ClientPNames#HANDLE_REDIRECTS redirect handling} {@linkplain HttpClient#getParams() parameter} on any
     * {@link HttpClient} created to proxy documents for this channel.
     * </p>
     * 
     * <p>
     * By default, any redirect URL returned by the proxied server will be
     * {@linkplain #rewriteProxiedFileLink(Page.Request, URL, URI, boolean, boolean) rewritten} and forwarded to the
     * client browser, as though it were returned as a link within a proxied document (redirects in
     * {@linkplain net.www_eee.portal.Channel.Mode#VIEW view mode} are treated as <em>hyperlinks</em>, redirects in
     * {@linkplain net.www_eee.portal.Channel.Mode#RESOURCE resource mode} as <em>external resource</em> links). It is
     * important to note that forwarding a redirect to the client is only possible within
     * {@linkplain net.www_eee.portal.Channel.Mode#RESOURCE resource mode}, or when the channel is being
     * {@linkplain net.www_eee.portal.Channel.Mode#VIEW viewed} while
     * {@linkplain net.www_eee.portal.Page.Request#isMaximized(Channel) maximized}, so any redirect returned by the
     * {@linkplain #DEFAULT_PATH_PROP default path} will generally result in a
     * {@link net.www_eee.portal.ConfigManager.ConfigException ConfigException}.
     * </p>
     * 
     * <p>
     * Enabling this property will cause <em>this channel</em> to follow any redirections while loading proxied documents,
     * instead of rewriting and forwarding them to the client browser, and also allow for successful redirection by the
     * {@linkplain #DEFAULT_PATH_PROP default path}.
     * </p>
     * 
     * <p>
     * This behavior is disabled by default as a security precaution, as when enabled, there will <strong>no longer be
     * <em>any</em> restrictions</strong> (ie, {@linkplain #PARENT_FOLDERS_RESTRICTION_DISABLE_PROP parent folders} or
     * {@linkplain #DEFAULT_PATH_RESTRICTION_ENABLE_PROP default path}) imposed on the redirection target URL. You should
     * ensure you trust the application at the {@linkplain #BASE_URI_PROP base URL}, otherwise it could cause the portal
     * to load data from <em>anywhere</em> and return it to clients from within your domain, mitigating a client browser's
     * <a href="http://en.wikipedia.org/wiki/Same_origin_policy">same-origin policy</a> and possibly enabling <a
     * href="http://en.wikipedia.org/wiki/Cross-site_request_forgery">CSRF</a> type attacks against your users.
     * </p>
     * 
     * @see ClientPNames#HANDLE_REDIRECTS
     * @see #isFollowRedirectsEnabled(Page.Request)
     * @category WWW_EEE_PORTAL_CONFIG_PROP
     */
    public static final String FOLLOW_REDIRECTS_ENABLE_PROP = "ProxyChannel.FollowRedirects.Enable";
    /**
     * Should the XML parser halt on {@linkplain ErrorHandler#warning(SAXParseException) warnings}?
     * 
     * @see ErrorHandler#warning(SAXParseException)
     * @see #isParserHaltOnWarningsEnabled(Page.Request)
     * @category WWW_EEE_PORTAL_CONFIG_PROP
     */
    public static final String PARSER_HALT_ON_WARNINGS_ENABLE_PROP = "ProxyChannel.Parser.Halt.Warnings.Enable";
    /**
     * Should the XML parser halt on {@linkplain ErrorHandler#error(SAXParseException) errors}?
     * 
     * @see ErrorHandler#error(SAXParseException)
     * @see #isParserHaltOnErrorsDisabled(Page.Request)
     * @category WWW_EEE_PORTAL_CONFIG_PROP
     */
    public static final String PARSER_HALT_ON_ERRORS_DISABLE_PROP = "ProxyChannel.Parser.Halt.Errors.Disable";
    /**
     * Should the XML parser halt on {@linkplain ErrorHandler#fatalError(SAXParseException) fatal errors}?
     * 
     * @see ErrorHandler#fatalError(SAXParseException)
     * @see #isParserHaltOnFatalErrorsDisabled(Page.Request)
     * @category WWW_EEE_PORTAL_CONFIG_PROP
     */
    public static final String PARSER_HALT_ON_FATAL_ERRORS_DISABLE_PROP = "ProxyChannel.Parser.Halt.FatalErrors.Disable";
    /**
     * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a
     * {@link net.www_eee.portal.Channel.Plugin Plugin} to either
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own
     * {@link #createProxyClientManager() ProxyClientManager} or to
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the
     * created one.
     * 
     * @see #createProxyClientManager()
     * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK
     */
    public static final WWWEEEPortal.PluginHook<ProxyChannel, HttpClientConnectionManager> PROXY_CLIENT_MANAGER_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, HttpClientConnectionManager>(
            ProxyChannel.class, 1, HttpClientConnectionManager.class, null);
    /**
     * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a
     * {@link net.www_eee.portal.Channel.Plugin Plugin} to either
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own
     * {@linkplain #getProxiedBaseURI(Page.Request) proxied base URI} or to
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the
     * {@linkplain #BASE_URI_PROP provided} value.
     * 
     * @see #getProxiedBaseURI(Page.Request)
     * @see #BASE_URI_PROP
     * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK
     */
    public static final WWWEEEPortal.PluginHook<ProxyChannel, URI> PROXIED_BASE_URI_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, URI>(
            ProxyChannel.class, 2, URI.class, null);
    /**
     * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a
     * {@link net.www_eee.portal.Channel.Plugin Plugin} to either
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own
     * {@linkplain #getProxiedFilePathDefault(Page.Request) proxied file default path} or to
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the
     * {@linkplain #DEFAULT_PATH_PROP provided} value.
     * 
     * @see #getProxiedFilePathDefault(Page.Request)
     * @see #DEFAULT_PATH_PROP
     * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK
     */
    public static final WWWEEEPortal.PluginHook<ProxyChannel, URI> PROXIED_FILE_PATH_DEFAULT_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, URI>(
            ProxyChannel.class, 3, URI.class, null);
    /**
     * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a
     * {@link net.www_eee.portal.Channel.Plugin Plugin} to either
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own
     * {@linkplain #getProxiedFileLocalURI(Page.Request, Channel.Mode, boolean) proxied file local URI} or to
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the
     * calculated value.
     * 
     * @see #getProxiedFileLocalURI(Page.Request, Channel.Mode, boolean)
     * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK
     */
    public static final WWWEEEPortal.PluginHook<ProxyChannel, URI> PROXIED_FILE_LOCAL_URI_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, URI>(
            ProxyChannel.class, 4, URI.class, new Class<?>[] { URI.class, Mode.class });
    /**
     * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a
     * {@link net.www_eee.portal.Channel.Plugin Plugin} to either
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own
     * {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied file URL} or to
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the
     * calculated value.
     * 
     * @see #getProxiedFileURL(Page.Request, Channel.Mode, boolean)
     * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK
     */
    public static final WWWEEEPortal.PluginHook<ProxyChannel, URL> PROXIED_FILE_URL_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, URL>(
            ProxyChannel.class, 5, URL.class, new Class<?>[] { Mode.class, Boolean.class });
    /**
     * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a
     * {@link net.www_eee.portal.Channel.Plugin Plugin} to either
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own
     * {@link #createProxyClientCookieStore(Page.Request) CookieStore} to the proxy client, or to
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the
     * created ones.
     * 
     * @see #createProxyClientCookieStore(Page.Request)
     * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK
     */
    public static final WWWEEEPortal.PluginHook<ProxyChannel, org.apache.http.client.CookieStore> PROXY_CLIENT_COOKIE_STORE_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, org.apache.http.client.CookieStore>(
            ProxyChannel.class, 6, org.apache.http.client.CookieStore.class, null);
    /**
     * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a
     * {@link net.www_eee.portal.Channel.Plugin Plugin} to either
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own
     * {@link #createProxyClient(Page.Request) HttpClient} to {@linkplain #doProxyRequest(Page.Request, Channel.Mode)
     * proxy} content, or to
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the
     * created one.
     * 
     * @see #createProxyClient(Page.Request)
     * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK
     */
    public static final WWWEEEPortal.PluginHook<ProxyChannel, HttpClientBuilder> PROXY_CLIENT_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, HttpClientBuilder>(
            ProxyChannel.class, 7, HttpClientBuilder.class,
            new Class<?>[] { HttpClientConnectionManager.class, org.apache.http.client.CookieStore.class });
    /**
     * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a
     * {@link net.www_eee.portal.Channel.Plugin Plugin} to either
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own
     * {@link #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient) HttpUriRequest} to
     * {@linkplain #doProxyRequest(Page.Request, Channel.Mode) proxy} content, or to
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the
     * configured one.
     * 
     * @see #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient)
     * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK
     */
    public static final WWWEEEPortal.PluginHook<ProxyChannel, HttpRequestBase> PROXY_REQUEST_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, HttpRequestBase>(
            ProxyChannel.class, 8, HttpRequestBase.class, new Class<?>[] { Mode.class, URL.class });
    /**
     * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a
     * {@link net.www_eee.portal.Channel.Plugin Plugin} to either
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own
     * {@link #createProxyClientRequestConfig(Page.Request, Channel.Mode) RequestConfig} to the proxy client, or to
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the
     * created ones.
     * 
     * @see #createProxyClientRequestConfig(Page.Request, Channel.Mode)
     * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK
     */
    public static final WWWEEEPortal.PluginHook<ProxyChannel, RequestConfig.Builder> PROXY_CLIENT_REQUEST_CONFIG_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, RequestConfig.Builder>(
            ProxyChannel.class, 9, RequestConfig.Builder.class, new Class<?>[] { Mode.class });
    /**
     * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a
     * {@link net.www_eee.portal.Channel.Plugin Plugin} to either
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own
     * {@link #createProxyRequestObject(Page.Request, Channel.Mode, URL) HttpUriRequest} to
     * {@linkplain #doProxyRequest(Page.Request, Channel.Mode) proxy} content, or to
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the
     * created one.
     * 
     * @see #createProxyRequestObject(Page.Request, Channel.Mode, URL)
     * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK
     */
    public static final WWWEEEPortal.PluginHook<ProxyChannel, HttpRequestBase> PROXY_REQUEST_OBJ_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, HttpRequestBase>(
            ProxyChannel.class, 10, HttpRequestBase.class,
            new Class<?>[] { Mode.class, URL.class, RequestConfig.class });
    /**
     * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a
     * {@link net.www_eee.portal.Channel.Plugin Plugin} to
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the
     * {@link HttpRequest} sent by the {@linkplain #createProxyClient(Page.Request) proxy client}.
     * 
     * @see #createProxyClient(Page.Request)
     * @see ProxyRequestInterceptor
     * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK
     */
    public static final WWWEEEPortal.PluginHook<ProxyChannel, HttpRequest> PROXY_REQUEST_INTERCEPTOR_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, HttpRequest>(
            ProxyChannel.class, 11, HttpRequest.class, new Class<?>[] { HttpClientContext.class });
    /**
     * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a
     * {@link net.www_eee.portal.Channel.Plugin Plugin} to
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the
     * {@link HttpResponse} received by the {@linkplain #createProxyClient(Page.Request) proxy client}.
     * 
     * @see #createProxyClient(Page.Request)
     * @see ProxyResponseInterceptor
     * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK
     */
    public static final WWWEEEPortal.PluginHook<ProxyChannel, HttpResponse> PROXY_RESPONSE_INTERCEPTOR_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, HttpResponse>(
            ProxyChannel.class, 12, HttpResponse.class, new Class<?>[] { HttpClientContext.class });
    /**
     * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a
     * {@link net.www_eee.portal.Channel.Plugin Plugin} to either
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) perform} the
     * {@linkplain #doProxyRequest(Page.Request, Channel.Mode) proxy request} on it's own, or to
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the
     * response.
     * 
     * @see #doProxyRequest(Page.Request, Channel.Mode)
     * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK
     */
    public static final WWWEEEPortal.PluginHook<ProxyChannel, CloseableHttpResponse> PROXY_RESPONSE_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, CloseableHttpResponse>(
            ProxyChannel.class, 13, CloseableHttpResponse.class,
            new Class<?>[] { Mode.class, HttpClientContext.class, HttpClient.class, HttpRequestBase.class });
    /**
     * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a
     * {@link net.www_eee.portal.Channel.Plugin Plugin} to either
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own
     * {@linkplain #getProxyResponseHeader(Page.Request, HttpResponse, String, Function) response header}, as if it had
     * {@linkplain HttpResponse#getHeaders(String) come} from the {@link #doProxyRequest(Page.Request, Channel.Mode)
     * proxied} response, or to
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the
     * returned value.
     * 
     * @see #getProxyResponseHeader(Page.Request, HttpResponse, String, Function)
     * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK
     */
    public static final WWWEEEPortal.PluginHook<ProxyChannel, Header> PROXY_RESPONSE_HEADER_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, Header>(
            ProxyChannel.class, 14, Header.class, new Class<?>[] { HttpResponse.class, String.class });
    /**
     * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a
     * {@link net.www_eee.portal.Channel.Plugin Plugin} to either
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own
     * {@link Boolean} value, to {@linkplain #isRenderedUsingXMLView(Page.Request, HttpResponse, MimeType) indicate} the
     * {@linkplain #doProxyRequest(Page.Request, Channel.Mode) proxied} response should be
     * {@linkplain #renderXMLView(Page.Request, Channel.ViewResponse, HttpResponse, URL, MimeType) rendered} as XML, or to
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the
     * calculated value.
     * 
     * @see #isRenderedUsingXMLView(Page.Request, HttpResponse, MimeType)
     * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK
     */
    public static final WWWEEEPortal.PluginHook<ProxyChannel, Boolean> IS_RENDERED_USING_XML_VIEW_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, Boolean>(
            ProxyChannel.class, 15, Boolean.class, new Class<?>[] { HttpResponse.class, MimeType.class });
    /**
     * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a
     * {@link net.www_eee.portal.Channel.Plugin Plugin} to either
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own
     * {@link #createProxiedDocumentInputSource(Page.Request, HttpResponse, URL, MimeType) TypedInputSource}, from which
     * to {@linkplain MarkupManager#parseXMLDocument(InputSource, DefaultHandler2, boolean, boolean, boolean, boolean)
     * parse} the {@linkplain #doProxyRequest(Page.Request, Channel.Mode) proxied} content being
     * {@linkplain #renderXMLView(Page.Request, Channel.ViewResponse, HttpResponse, URL, MimeType) rendered}, or to
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the
     * configured one.
     * 
     * @see #createProxiedDocumentInputSource(Page.Request, HttpResponse, URL, MimeType)
     * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK
     */
    public static final WWWEEEPortal.PluginHook<ProxyChannel, TypedInputSource> PROXIED_DOC_INPUT_SOURCE_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, TypedInputSource>(
            ProxyChannel.class, 16, TypedInputSource.class,
            new Class<?>[] { HttpResponse.class, URL.class, MimeType.class });
    /**
     * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a
     * {@link net.www_eee.portal.Channel.Plugin Plugin} to either
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own
     * {@link #createProxiedDocumentContentHandler(Page.Request, Channel.ViewResponse, URL, TypedInputSource)
     * DefaultHandler2}, to receive
     * {@linkplain MarkupManager#parseXMLDocument(InputSource, DefaultHandler2, boolean, boolean, boolean, boolean)
     * parsing} events from the {@linkplain #doProxyRequest(Page.Request, Channel.Mode) proxied} content being
     * {@linkplain #renderXMLView(Page.Request, Channel.ViewResponse, HttpResponse, URL, MimeType) rendered}, or to
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the
     * created one.
     * 
     * @see #createProxiedDocumentContentHandler(Page.Request, Channel.ViewResponse, URL, TypedInputSource)
     * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK
     */
    public static final WWWEEEPortal.PluginHook<ProxyChannel, DefaultHandler2> PROXIED_DOC_CONTENT_HANDLER_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, DefaultHandler2>(
            ProxyChannel.class, 17, DefaultHandler2.class,
            new Class<?>[] { ViewResponse.class, URL.class, TypedInputSource.class });
    /**
     * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a
     * {@link net.www_eee.portal.Channel.Plugin Plugin} to either
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own
     * {@link Boolean} value, indicating the {@linkplain #doProxyRequest(Page.Request, Channel.Mode) proxied}
     * {@linkplain #createProxiedDocumentInputSource(Page.Request, HttpResponse, URL, MimeType) input} should
     * <strong>not</strong> be
     * {@linkplain MarkupManager#parseXMLDocument(InputSource, DefaultHandler2, boolean, boolean, boolean, boolean)
     * parsed} into the
     * {@linkplain #createProxiedDocumentContentHandler(Page.Request, Channel.ViewResponse, URL, TypedInputSource) content
     * handler} during {@linkplain #renderXMLView(Page.Request, Channel.ViewResponse, HttpResponse, URL, MimeType)
     * rendering} (ie, parsing was already handled by the plugin), or to
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the
     * calculated value.
     * 
     * @see #renderXMLView(Page.Request, Channel.ViewResponse, HttpResponse, URL, MimeType)
     * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK
     */
    public static final WWWEEEPortal.PluginHook<ProxyChannel, Boolean> PARSE_XML_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, Boolean>(
            ProxyChannel.class, 18, Boolean.class,
            new Class<?>[] { ViewResponse.class, TypedInputSource.class, DefaultHandler2.class });
    /**
     * A {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook hook} which allows a
     * {@link net.www_eee.portal.Channel.Plugin Plugin} to either
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#value(List, Object[], Page.Request) provide} it's own
     * {@link Boolean} value, to {@linkplain #isRenderedUsingTextView(Page.Request, HttpResponse, MimeType) indicate} the
     * {@linkplain #doProxyRequest(Page.Request, Channel.Mode) proxied} response should be
     * {@linkplain #renderTextView(Page.Request, Channel.ViewResponse, HttpResponse, URL, MimeType) rendered} as text, or
     * to {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the
     * calculated value.
     * 
     * @see #isRenderedUsingTextView(Page.Request, HttpResponse, MimeType)
     * @category WWWEEE_PORTAL_CHANNEL_PLUGIN_HOOK
     */
    public static final WWWEEEPortal.PluginHook<ProxyChannel, Boolean> IS_RENDERED_USING_TEXT_VIEW_HOOK = new WWWEEEPortal.PluginHook<ProxyChannel, Boolean>(
            ProxyChannel.class, 19, Boolean.class, new Class<?>[] { HttpResponse.class, MimeType.class });
    /**
     * A constant used during
     * {@link #getProxyRequestUserAgentHeader(Page.Request, Channel.Mode, HttpClient, URL, HttpUriRequest) construction}
     * of the value for the &quot;User-Agent&quot; header to be {@link HttpUriRequest#setHeader(String, String) set} on
     * the {@link HttpUriRequest} being used to
     * {@linkplain #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient) proxy} content for this channel.
     * 
     * @see #getProxyRequestUserAgentHeader(Page.Request, Channel.Mode, HttpClient, URL, HttpUriRequest)
     */
    public static final String USER_AGENT_HEADER = "WWW-EEE-Portal/1.0";
    /**
     * The identifier used to store the {@link CloseableHttpClient} in the {@link HttpClientContext}.
     */
    public static final String HTTP_CLIENT_CONTEXT_ID = "http.client";
    /**
     * A {@link Function} constant for {@link URI#create(String)} which can be {@linkplain RSProperties#cache(Function)
     * cached}.
     */
    protected static final Function<String, URI> STRING_TO_URI_FUNCTION = URI::create;
    /**
     * The {@link MimeType} Object for the <code>"application/*"</code> mime type.
     * 
     * @see #getProxyRequestAcceptHeader(Page.Request, Channel.Mode, HttpClient, URL, HttpUriRequest)
     */
    protected static final MimeType APPLICATION_STAR_MIME_TYPE = IOUtil.newMimeType("application", "*");
    /**
     * The {@link MimeType} Object for the <code>"text/*"</code> mime type.
     * 
     * @see #getProxyRequestAcceptHeader(Page.Request, Channel.Mode, HttpClient, URL, HttpUriRequest)
     */
    protected static final MimeType TEXT_STAR_MIME_TYPE = IOUtil.newMimeType("text", "*");
    /**
     * The {@link HttpClientConnectionManager} to be used by all {@link HttpClient}'s
     * {@linkplain #createProxyClient(Page.Request) created} by this channel.
     * 
     * @see #PROXY_CLIENT_MANAGER_HOOK
     * @see #createProxyClientManager()
     * @see #createProxyClient(Page.Request)
     */
    protected @Nullable HttpClientConnectionManager proxyClientManager = null;

    /**
     * Construct a new <code>ProxyChannel</code> instance.
     * 
     * @param portal The {@link #getPortal() WWWEEEPortal} instance hosting this channel.
     * @param channelDef The {@linkplain net.www_eee.portal.ContentDef.Channel definition} which will be used to configure
     * this channel instance.
     * @param page The {@link Page} instance to which this
     * <em>{@linkplain net.www_eee.portal.ContentDef.LocalChannel local}</em> channel belongs, or <code>null</code> if
     * it's a {@linkplain net.www_eee.portal.ContentDef.GlobalChannel global channel}.
     * @throws WWWEEEPortal.Exception If a problem occurred while constructing the channel.
     */
    public ProxyChannel(final WWWEEEPortal portal, final ContentDef.Channel<?, ? extends ProxyChannel> channelDef,
            final @Nullable Page page) throws WWWEEEPortal.Exception {
        super(portal, channelDef, page);

        channelDef.getProps().cache(STRING_TO_URI_FUNCTION);

        return;
    }

    @Override
    public final Class<? extends Channel> getImmediateSubClass() {
        return ProxyChannel.class;
    }

    /**
     * Optionally construct an {@link HttpClientConnectionManager} to manage the {@link HttpClient}'s being
     * {@linkplain #createProxyClient(Page.Request) created} to {@linkplain #doProxyRequest(Page.Request, Channel.Mode)
     * proxy} content. The default implementation returns <code>null</code>.
     * 
     * @return The {@link HttpClientConnectionManager}.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @see #createProxyClient(Page.Request)
     * @see #PROXY_CLIENT_MANAGER_HOOK
     */
    protected @Nullable HttpClientConnectionManager createProxyClientManager() throws WWWEEEPortal.Exception {
        HttpClientConnectionManager proxyClientManager = PROXY_CLIENT_MANAGER_HOOK.value(plugins, null, null);
        proxyClientManager = PROXY_CLIENT_MANAGER_HOOK.filter(plugins, null, null, proxyClientManager);
        return proxyClientManager;
    }

    @Override
    protected void initInternal(final AnnotatedLogMessage logMessage) throws WWWEEEPortal.Exception {
        super.initInternal(logMessage);
        proxyClientManager = createProxyClientManager();
        return;
    }

    @Override
    protected void closeInternal(final AnnotatedLogMessage logMessage) {
        Optional.ofNullable(proxyClientManager).ifPresent((proxyClientManager) -> proxyClientManager.shutdown());
        super.closeInternal(logMessage);
        return;
    }

    /**
     * Get the {@link HttpClientConnectionManager} to be used by all {@link HttpClient}'s
     * {@linkplain #createProxyClient(Page.Request) created} by this channel.
     * 
     * @return The {@link HttpClientConnectionManager}.
     * @see #PROXY_CLIENT_MANAGER_HOOK
     */
    public HttpClientConnectionManager getProxyClientManager() {
        return proxyClientManager;
    }

    /**
     * Is the default-path restriction {@linkplain #DEFAULT_PATH_RESTRICTION_ENABLE_PROP enabled} for this channel?
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @return If the default-path restriction is {@linkplain #DEFAULT_PATH_RESTRICTION_ENABLE_PROP enabled}.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @see #DEFAULT_PATH_RESTRICTION_ENABLE_PROP
     */
    public boolean isDefaultPathRestrictionEnabled(final Page.Request pageRequest) throws WWWEEEPortal.Exception {
        return getConfigPropReq(DEFAULT_PATH_RESTRICTION_ENABLE_PROP, pageRequest, Boolean::valueOf, Boolean.FALSE);
    }

    /**
     * Is the parent-folders restriction {@linkplain #PARENT_FOLDERS_RESTRICTION_DISABLE_PROP disabled} for this channel?
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @return If the parent-folders restriction is {@linkplain #PARENT_FOLDERS_RESTRICTION_DISABLE_PROP disabled}.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @see #PARENT_FOLDERS_RESTRICTION_DISABLE_PROP
     */
    public boolean isParentFoldersRestrictionDisabled(final Page.Request pageRequest)
            throws WWWEEEPortal.Exception {
        return getConfigPropReq(PARENT_FOLDERS_RESTRICTION_DISABLE_PROP, pageRequest, Boolean::valueOf,
                Boolean.FALSE);
    }

    /**
     * Is the rewriting of hyperlinks within proxied documents back to this channel
     * {@linkplain #LINK_REWRITING_HYPERLINKS_TO_CHANNEL_DISABLE_PROP disabled}?
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @return If the rewriting of hyperlinks within proxied documents back to this channel is
     * {@linkplain #LINK_REWRITING_HYPERLINKS_TO_CHANNEL_DISABLE_PROP disabled}.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @see #LINK_REWRITING_HYPERLINKS_TO_CHANNEL_DISABLE_PROP
     */
    public boolean isLinkRewritingHyperlinksToChannelDisabled(final Page.Request pageRequest)
            throws WWWEEEPortal.Exception {
        return getConfigPropReq(LINK_REWRITING_HYPERLINKS_TO_CHANNEL_DISABLE_PROP, pageRequest, Boolean::valueOf,
                Boolean.FALSE);
    }

    /**
     * Is the rewriting of external resource links within proxied documents back to this channel
     * {@linkplain #LINK_REWRITING_RESOURCE_LINKS_TO_CHANNEL_ENABLE_PROP enabled}?
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @return If the rewriting of external resource links within proxied documents back to this channel is
     * {@linkplain #LINK_REWRITING_RESOURCE_LINKS_TO_CHANNEL_ENABLE_PROP enabled}.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @see #LINK_REWRITING_RESOURCE_LINKS_TO_CHANNEL_ENABLE_PROP
     */
    public boolean isLinkRewritingResourceLinksToChannelEnabled(final Page.Request pageRequest)
            throws WWWEEEPortal.Exception {
        return getConfigPropReq(LINK_REWRITING_RESOURCE_LINKS_TO_CHANNEL_ENABLE_PROP, pageRequest, Boolean::valueOf,
                Boolean.FALSE);
    }

    /**
     * Get the value (in milliseconds) used to {@linkplain HttpParams#setIntParameter(String, int) set} the
     * {@linkplain CoreConnectionPNames#CONNECTION_TIMEOUT connection timeout} {@linkplain HttpClient#getParams()
     * parameter} on any {@link HttpClient} created to proxy documents for this channel. If an explicit value is not
     * {@linkplain #CONNECT_TIMEOUT_PROP specified} then the {@linkplain #DEFAULT_CONNECT_TIMEOUT_MS default} will be
     * used.
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @return The value to be used for the {@linkplain CoreConnectionPNames#CONNECTION_TIMEOUT connection timeout}.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @see CoreConnectionPNames#CONNECTION_TIMEOUT
     * @see #CONNECT_TIMEOUT_PROP
     * @see #DEFAULT_CONNECT_TIMEOUT_MS
     */
    public int getConnectTimeout(final Page.Request pageRequest) throws WWWEEEPortal.Exception {
        return getConfigPropReq(CONNECT_TIMEOUT_PROP, pageRequest, Integer::valueOf, DEFAULT_CONNECT_TIMEOUT_MS);
    }

    /**
     * Get the value (in milliseconds) used to {@linkplain HttpParams#setIntParameter(String, int) set} the
     * {@linkplain CoreConnectionPNames#SO_TIMEOUT socket timeout} {@linkplain HttpClient#getParams() parameter} on any
     * {@link HttpClient} created to proxy documents for this channel. If an explicit value is not
     * {@linkplain #READ_TIMEOUT_PROP specified} then the {@linkplain #DEFAULT_READ_TIMEOUT_MS default} will be used.
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @return The value to be used for the {@linkplain CoreConnectionPNames#SO_TIMEOUT socket timeout}.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @see CoreConnectionPNames#SO_TIMEOUT
     * @see #READ_TIMEOUT_PROP
     * @see #DEFAULT_READ_TIMEOUT_MS
     */
    public int getReadTimeout(final Page.Request pageRequest) throws WWWEEEPortal.Exception {
        return getConfigPropReq(READ_TIMEOUT_PROP, pageRequest, Integer::valueOf, DEFAULT_READ_TIMEOUT_MS);
    }

    /**
     * Is the automatic handling of redirects by this channel's proxy client {@linkplain #FOLLOW_REDIRECTS_ENABLE_PROP
     * enabled}?
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @return If the automatic handling of redirects is {@linkplain #FOLLOW_REDIRECTS_ENABLE_PROP enabled}.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @see #FOLLOW_REDIRECTS_ENABLE_PROP
     */
    public boolean isFollowRedirectsEnabled(final Page.Request pageRequest) throws WWWEEEPortal.Exception {
        return getConfigPropReq(FOLLOW_REDIRECTS_ENABLE_PROP, pageRequest, Boolean::valueOf, Boolean.FALSE);
    }

    /**
     * Is halting of the XML parser on {@linkplain ErrorHandler#warning(SAXParseException) warnings}
     * {@linkplain #PARSER_HALT_ON_WARNINGS_ENABLE_PROP enabled}?
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @return If halting of the XML parser on {@linkplain ErrorHandler#warning(SAXParseException) warnings} is
     * {@linkplain #PARSER_HALT_ON_WARNINGS_ENABLE_PROP enabled}.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @see #PARSER_HALT_ON_WARNINGS_ENABLE_PROP
     * @see ErrorHandler#warning(SAXParseException)
     */
    public boolean isParserHaltOnWarningsEnabled(final Page.Request pageRequest) throws WWWEEEPortal.Exception {
        return getConfigPropReq(PARSER_HALT_ON_WARNINGS_ENABLE_PROP, pageRequest, Boolean::valueOf, Boolean.FALSE);
    }

    /**
     * Is halting of the XML parser on {@linkplain ErrorHandler#error(SAXParseException) errors}
     * {@linkplain #PARSER_HALT_ON_ERRORS_DISABLE_PROP disabled}?
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @return If halting of the XML parser on {@linkplain ErrorHandler#error(SAXParseException) errors} is
     * {@linkplain #PARSER_HALT_ON_ERRORS_DISABLE_PROP disabled}.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @see #PARSER_HALT_ON_ERRORS_DISABLE_PROP
     * @see ErrorHandler#error(SAXParseException)
     */
    public boolean isParserHaltOnErrorsDisabled(final Page.Request pageRequest) throws WWWEEEPortal.Exception {
        return getConfigPropReq(PARSER_HALT_ON_ERRORS_DISABLE_PROP, pageRequest, Boolean::valueOf, Boolean.FALSE);
    }

    /**
     * Is halting of the XML parser on {@linkplain ErrorHandler#fatalError(SAXParseException) fatal errors}
     * {@linkplain #PARSER_HALT_ON_FATAL_ERRORS_DISABLE_PROP disabled}?
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @return If halting of the XML parser on {@linkplain ErrorHandler#fatalError(SAXParseException) fatal errors} is
     * {@linkplain #PARSER_HALT_ON_FATAL_ERRORS_DISABLE_PROP disabled}.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @see #PARSER_HALT_ON_FATAL_ERRORS_DISABLE_PROP
     * @see ErrorHandler#error(SAXParseException)
     */
    public boolean isParserHaltOnFatalErrorsDisabled(final Page.Request pageRequest) throws WWWEEEPortal.Exception {
        return getConfigPropReq(PARSER_HALT_ON_FATAL_ERRORS_DISABLE_PROP, pageRequest, Boolean::valueOf,
                Boolean.FALSE);
    }

    /**
     * <p>
     * Is the supplied {@linkplain File#getPath() path} a relative path, which, if
     * {@linkplain java.net.URI#resolve(String) resolved} against some other path/folder, would point to a location
     * <em>within</em> that path's folder.
     * </p>
     * 
     * <ul>
     * <li><code>"/hello/"</code> returns <code>false</code>.</li>
     * <li><code>"./"</code> returns <code>true</code>.</li>
     * <li><code>"./hello.txt"</code> returns <code>true</code>.</li>
     * <li><code>"hello/"</code> returns <code>true</code>.</li>
     * <li><code>"hello/../"</code> returns <code>true</code>.</li>
     * <li><code>"hello/world/../"</code> returns <code>true</code>.</li>
     * <li><code>"hello/world/../../"</code> returns <code>true</code>.</li>
     * <li><code>"hello/world/../.././"</code> returns <code>true</code>.</li>
     * <li><code>"hello/world/../../../"</code> returns <code>false</code>.</li>
     * <li><code>".."</code> returns <code>false</code>.</li>
     * <li><code>"../"</code> returns <code>false</code>.</li>
     * </ul>
     * 
     * @param path The {@linkplain File#getPath() path} to some file or directory.
     * @return <code>true</code> if the supplied <code>path</code> is a relative subpath.
     * @throws IllegalArgumentException If <code>path</code> is {@linkplain String#isEmpty() empty}.
     */
    protected static final boolean isRelativeSubPath(final String path) throws IllegalArgumentException {
        if (path.isEmpty())
            throw new IllegalArgumentException("emtpy path");
        if (path.charAt(0) == '/')
            return false;
        int depth = 0;
        final StringBuffer currentComponent = new StringBuffer(path.length());
        for (int i = 0; i < path.length(); i++) {
            final char c = path.charAt(i);
            if (c != '/') {
                currentComponent.append(c);
            } else {
                final String currentComponentString = currentComponent.toString();
                if (currentComponentString.equals("..")) {
                    depth--;
                    if (depth < 0)
                        return false;
                } else if ((currentComponentString.length() > 0) && (!currentComponentString.equals("."))) {
                    depth++;
                }
                currentComponent.setLength(0);
            }
        }
        if ((currentComponent.length() == 2) && (currentComponent.toString().equals("..")) && (depth <= 0))
            return false;
        return true;
    }

    /**
     * <p>
     * Do the supplied {@link URL}'s both point to the same {@linkplain URL#getHost() host} and {@linkplain URL#getPort()
     * port}?
     * </p>
     * 
     * <p>
     * If either of the supplied URL's don't specify a {@linkplain URL#getPort() port}, the
     * {@linkplain URL#getDefaultPort() default} will be used. If there is no default port, the comparison will fail
     * unless both have no default.
     * </p>
     * 
     * @param url1 The first URL.
     * @param url2 The second URL.
     * @return <code>true</code> if the URL's point to the same host and port.
     * @throws IllegalArgumentException If <code>url1</code> or <code>url2</code> is <code>null</code>.
     */
    protected static final boolean equalHostAndPort(final @Nullable URL url1, final @Nullable URL url2)
            throws IllegalArgumentException {
        if (url1 == null)
            throw new IllegalArgumentException("null url1");
        if (url2 == null)
            throw new IllegalArgumentException("null url2");
        if (!StringUtil.equal(StringUtil.mkNull(url1.getHost()), StringUtil.mkNull(url2.getHost()), false))
            return false;
        final int url1Port = (url1.getPort() >= 0) ? url1.getPort() : url1.getDefaultPort();
        final int url2Port = (url2.getPort() >= 0) ? url2.getPort() : url2.getDefaultPort();
        if (url1Port != url2Port)
            return false;
        return true;
    }

    /**
     * Get the {@linkplain #BASE_URI_PROP base URI} of the document or site to be
     * {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied}.
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @return The {@linkplain #BASE_URI_PROP base URI} of the document or site to be
     * {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied}
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @see #BASE_URI_PROP
     * @see #getProxiedFileURL(Page.Request, Channel.Mode, boolean)
     * @see #PROXIED_BASE_URI_HOOK
     */
    public URI getProxiedBaseURI(final Page.Request pageRequest) throws WWWEEEPortal.Exception {
        URI baseURI = PROXIED_BASE_URI_HOOK.value(plugins, null, pageRequest);
        if (baseURI == null) {
            baseURI = getConfigPropReq(BASE_URI_PROP, pageRequest, STRING_TO_URI_FUNCTION, null);
        }
        baseURI = PROXIED_BASE_URI_HOOK
                .requireFilteredResult(PROXIED_BASE_URI_HOOK.filter(plugins, null, pageRequest, baseURI));
        return baseURI;
    }

    /**
     * Get the <em>relative</em> {@linkplain #DEFAULT_PATH_PROP path} (within the
     * {@linkplain #getProxiedBaseURI(Page.Request) proxied base URI}) to the document which should be displayed within
     * this channel by default.
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @return The {@linkplain #DEFAULT_PATH_PROP path} to the document which should be displayed within this channel by
     * default.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @see #DEFAULT_PATH_PROP
     * @see #getProxiedFileLocalURI(Page.Request, Channel.Mode, boolean)
     * @see #PROXIED_FILE_PATH_DEFAULT_HOOK
     */
    protected @Nullable URI getProxiedFilePathDefault(final Page.Request pageRequest)
            throws WWWEEEPortal.Exception {
        URI defaultPath = PROXIED_FILE_PATH_DEFAULT_HOOK.value(plugins, null, pageRequest);
        if (defaultPath == null) {
            defaultPath = getConfigPropOpt(DEFAULT_PATH_PROP, pageRequest, STRING_TO_URI_FUNCTION).orElse(null);
            if ((defaultPath != null) && ((defaultPath.isAbsolute()) || (defaultPath.getPath() == null)
                    || (defaultPath.getPath().startsWith("/")))) {
                throw new ConfigManager.ConfigException("The '" + DEFAULT_PATH_PROP
                        + "' property is not a relative path: '" + defaultPath.toString() + '\'', null);
            }
        }
        defaultPath = PROXIED_FILE_PATH_DEFAULT_HOOK.filter(plugins, null, pageRequest, defaultPath);
        return defaultPath;
    }

    /**
     * Perform any desired modifications to a link which is <em>not</em> being
     * {@linkplain #rewriteProxiedFileLink(Page.Request, URL, URI, boolean, boolean) rewritten} to point back through this
     * channel.
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @param proxiedFileURL The {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied file URL}.
     * @param linkURI The {@link URI} of the link to rewrite.
     * @param hyperlink Is the <code>linkURI</code> a hyperlink?
     * @param absoluteURLRequired Does the result need to be {@linkplain URI#isAbsolute() absolute}?
     * @param resolvedLinkURL The <code>linkURI</code> after being resolved to it's actual location.
     * @return The rewritten link {@link URI}.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @see #rewriteProxiedFileLink(Page.Request, URL, URI, boolean, boolean)
     */
    protected static final URI rewriteProxiedFileLinkOutsideChannel(final Page.Request pageRequest,
            final URL proxiedFileURL, final @Nullable URI linkURI, final boolean hyperlink,
            final boolean absoluteURLRequired, final URL resolvedLinkURL) throws WWWEEEPortal.Exception {
        if ((linkURI != null) && (linkURI.isAbsolute())) {
            return linkURI; // If a document author includes an absolute link, we generally want to just leave that as-is.
        }

        try {

            if ((!absoluteURLRequired) && (equalHostAndPort(proxiedFileURL, pageRequest.getBaseURL()))) {
                // They didn't author an absolute link, we don't require one, and since the resolved link points to our host/port, we have the opportunity to return a nice short non-absolute link...
                final StringBuffer sb = new StringBuffer();
                sb.append(resolvedLinkURL.getPath());
                if (resolvedLinkURL.getQuery() != null) {
                    sb.append('?');
                    sb.append(resolvedLinkURL.getQuery());
                }
                if (resolvedLinkURL.getRef() != null) {
                    sb.append('#');
                    sb.append(resolvedLinkURL.getRef());
                }
                return new URI(sb.toString());
            }

            return resolvedLinkURL.toURI();
        } catch (URISyntaxException urise) {
            throw new ContentManager.ContentException("Error constructing resolved link URI", urise);
        }
    }

    /**
     * <p>
     * Rewrite a <code>linkURI</code> associated with a <code>proxiedFileURL</code>, if required.
     * </p>
     * 
     * <p>
     * Technically, there are two types of links within a document. First, a link within a document can be to an
     * <em>external resource</em>, which is loaded automatically by the browser to augment that document (ie a link to an
     * image, style sheet, script, etc). Or, second, a link within a document can be a <em>hyperlink</em>, which, when
     * activated by the user, will cause the browser to stop displaying that document and navigate to displaying the
     * linked document instead.
     * </p>
     * 
     * <p>
     * If the portal is configured to display a website to clients through this <code>ProxyChannel</code>, it is generally
     * expected that if the client navigates a hyperlink from one document to another within the proxied site, that the
     * linked document would also be rendered within the channel ({@linkplain net.www_eee.portal.Channel.Mode#VIEW view
     * mode}), and that any external resource links will continue to resolve correctly. Link rewriting is required for
     * each of these two scenarios to work.
     * </p>
     * 
     * <p>
     * To continue rendering within {@linkplain net.www_eee.portal.Channel.Mode#VIEW view mode} while navigating between
     * website documents, any hyperlink from within a proxied document to another document within the
     * {@linkplain #getProxiedBaseURI(Page.Request) proxied site} will, by default, be rewritten to point back through
     * this channel (alternatively, hyperlinks may {@linkplain #isLinkRewritingHyperlinksToChannelDisabled(Page.Request)
     * optionally} be resolved into an {@linkplain URI#isAbsolute() absolute} link pointing directly back to their
     * {@linkplain #getProxiedBaseURI(Page.Request) origin/source location} instead).
     * </p>
     * 
     * <p>
     * If this channel were to blindly return unmodified source HTML from a proxied document for aggregation into a
     * {@link Page}, any relative link would break when it was incorrectly resolved relative to the
     * {@link net.www_eee.portal.ContentDef.Page.Key#getPageURI(UriInfo, Map, String, boolean) URL} of that page, instead
     * of relative to the {@linkplain #BASE_URI_PROP base URL} of the document providing it. To avoid this, any relative
     * link to an external resource from within a proxied document will, by default, be resolved into an
     * {@linkplain URI#isAbsolute() absolute} link pointing directly back to the
     * {@linkplain #getProxiedBaseURI(Page.Request) origin/source location} for that resource (alternatively, resource
     * links may {@linkplain #isLinkRewritingResourceLinksToChannelEnabled(Page.Request) optionally} be rewritten to point
     * back through this channel using {@linkplain net.www_eee.portal.Channel.Mode#RESOURCE resource mode} instead).
     * </p>
     * 
     * <p>
     * For link rewriting to work, the <code>ProxyChannel</code> obviously needs to know which attributes of a proxied
     * document constitute <em>links</em>. But since the implementation is generic, and doesn't actually understand any
     * particular dialect of markup language on it's own, <em>including HTML</em>, you will likely want to configure this
     * channel alongside a plugin which does, such as the
     * {@linkplain net.www_eee.portal.channelplugins.ProxyChannelHTMLSource HTML plugin}.
     * </p>
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @param proxiedFileURL The {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied file URL}.
     * @param linkURI The {@link URI} of the link to rewrite.
     * @param hyperlink Is the <code>linkURI</code> a hyperlink?
     * @param absoluteURLRequired Does the result need to be {@linkplain URI#isAbsolute() absolute}?
     * @return A Map.Entry containing the rewritten link value, and a Boolean specifying if the returned link points back
     * through the channel.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @see #isLinkRewritingHyperlinksToChannelDisabled(Page.Request)
     * @see #isLinkRewritingResourceLinksToChannelEnabled(Page.Request)
     * @see net.www_eee.portal.channelplugins.ProxyChannelHTMLSource
     */
    public Map.Entry<URI, Boolean> rewriteProxiedFileLink(final Page.Request pageRequest, final URL proxiedFileURL,
            final @Nullable URI linkURI, final boolean hyperlink, final boolean absoluteURLRequired)
            throws WWWEEEPortal.Exception {

        if ((linkURI != null) && (linkURI.isOpaque())) {
            return new AbstractMap.SimpleImmutableEntry<URI, Boolean>(linkURI, Boolean.FALSE); // Leave all the opaque ones alone (stuff like "mailto" links, etc), as we can't do anything with those.
        }

        final @NonNull URL resolvedLinkURL; // First, resolve the URL for the link so we know what server+file we are actually talking about here.
        try {
            if (linkURI == null) {
                resolvedLinkURL = proxiedFileURL; // An empty (null) link is equivalent to one back to the same proxied file.
            } else if (linkURI.isAbsolute()) {
                resolvedLinkURL = linkURI.toURL();
            } else {
                resolvedLinkURL = new URL(proxiedFileURL, linkURI.toString()); // Resolve the link relative to the file it came from.
            }
        } catch (MalformedURLException mue) {
            throw new ContentManager.ContentException("Error resolving proxied link URL", mue);
        }

        if (((hyperlink) && (isLinkRewritingHyperlinksToChannelDisabled(pageRequest)))
                || ((!hyperlink) && (!isLinkRewritingResourceLinksToChannelEnabled(pageRequest)))) {
            // We are configured to not write this link back through the portal.
            return new AbstractMap.SimpleImmutableEntry<URI, Boolean>(
                    rewriteProxiedFileLinkOutsideChannel(pageRequest, proxiedFileURL, linkURI, hyperlink,
                            absoluteURLRequired, resolvedLinkURL),
                    Boolean.FALSE);
        }

        /*
         * At this point, in order to determine what modifications to the link might be required, we need to figure out if
         * the link points to something either within, or outside of, the channel base URI's folder?
         */

        if ((linkURI != null) && (linkURI.isAbsolute()) && (!equalHostAndPort(resolvedLinkURL, proxiedFileURL))) {
            // This is an absolute link which doesn't even point to the same server as the proxied file.
            return new AbstractMap.SimpleImmutableEntry<URI, Boolean>(
                    rewriteProxiedFileLinkOutsideChannel(pageRequest, proxiedFileURL, linkURI, hyperlink,
                            absoluteURLRequired, resolvedLinkURL),
                    Boolean.FALSE);
        }

        /*
         * At this point we know the link at least points to the same server as the proxied file, but is it within the
         * channel base URI's folder?
         */

        final String resolvedLinkPath = StringUtil.toString(StringUtil.mkNull(resolvedLinkURL.getPath()), "/");

        final URI baseURI = getProxiedBaseURI(pageRequest);

        final URI resolvedBaseURI;
        if (baseURI.isAbsolute()) {
            resolvedBaseURI = baseURI;
        } else {
            resolvedBaseURI = ConfigManager.getContextResourceLocalHostURI(pageRequest.getUriInfo(),
                    baseURI.getPath(), NetUtil.getQueryParams(baseURI), baseURI.getFragment(), true);
        }

        final String baseURIPath = resolvedBaseURI.getPath();

        final String baseURIFolder;
        if ((baseURIPath.length() == 1) || (baseURIPath.charAt(baseURIPath.length() - 1) == '/')) {
            baseURIFolder = baseURIPath; // Path is a folder.
        } else {
            final int lastSlashIndex = baseURIPath.lastIndexOf('/');
            baseURIFolder = (lastSlashIndex > 0) ? baseURIPath.substring(0, lastSlashIndex + 1)
                    : String.valueOf('/');
        }

        if (!resolvedLinkPath.startsWith(baseURIFolder)) {
            // We have determined this link is not within the channel base URI folder.
            return new AbstractMap.SimpleImmutableEntry<URI, Boolean>(
                    rewriteProxiedFileLinkOutsideChannel(pageRequest, proxiedFileURL, linkURI, hyperlink,
                            absoluteURLRequired, resolvedLinkURL),
                    Boolean.FALSE);
        }

        /*
         * At this point we know the link points to within the channel base URI's folder, and that we need to rewrite it to
         * point back through the channel.
         */

        final String linkChannelLocalPath = StringUtil.mkNull(resolvedLinkPath.substring(baseURIFolder.length()));

        final Mode channelMode = ((hyperlink) && (!isMaximizationDisabled(pageRequest))) ? Mode.VIEW
                : Mode.RESOURCE;

        final ContentDef.ChannelSpec<?> channelSpec = pageRequest.getChannelSpec(this);
        return new AbstractMap.SimpleImmutableEntry<URI, Boolean>(
                channelSpec.getKey().getChannelURI(pageRequest.getUriInfo(), channelMode, linkChannelLocalPath,
                        (linkURI != null) ? NetUtil.getQueryParams(linkURI) : null,
                        (linkURI != null) ? linkURI.getFragment() : null, absoluteURLRequired),
                Boolean.TRUE);
    }

    /**
     * Combine the {@linkplain net.www_eee.portal.Page.Request#getChannelLocalPath(Channel) channel local path} (or the
     * {@linkplain #getProxiedFilePathDefault(Page.Request) default path} if that's <code>null</code>) and
     * {@linkplain UriInfo#getQueryParameters() query parameters} from the client <code>pageRequest</code>, to construct a
     * URI containing a <em>relative</em> path and query params, which can later be resolved against the
     * {@linkplain #getProxiedBaseURI(Page.Request) base URI} to create the
     * {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied file URL}. This method can also
     * <code>validate</code> the request against any {@linkplain #isParentFoldersRestrictionDisabled(Page.Request) parent
     * folder} or {@linkplain #isDefaultPathRestrictionEnabled(Page.Request) default path} restrictions.
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @param mode The {@link net.www_eee.portal.Channel.Mode Mode} of the request.
     * @param validate Should any {@linkplain #isParentFoldersRestrictionDisabled(Page.Request) parent folder} or
     * {@linkplain #isDefaultPathRestrictionEnabled(Page.Request) default path} restrictions be evaluated?
     * @return The proxied file "local {@link URI}".
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @throws WebApplicationException If a problem occurred while determining the result.
     * @see #getProxiedFileURL(Page.Request, Channel.Mode, boolean)
     * @see #PROXIED_FILE_LOCAL_URI_HOOK
     */
    protected @Nullable URI getProxiedFileLocalURI(final Page.Request pageRequest, final Mode mode,
            final boolean validate) throws WWWEEEPortal.Exception, WebApplicationException {
        final URI channelLocalPath = pageRequest.getChannelLocalPath(this);
        final Object[] context = new Object[] { channelLocalPath, mode };
        URI proxiedFileLocalURI = PROXIED_FILE_LOCAL_URI_HOOK.value(plugins, context, pageRequest);
        if (proxiedFileLocalURI == null) {

            final URI proxiedFilePath;

            if (channelLocalPath != null) {
                if ((validate) && (isDefaultPathRestrictionEnabled(pageRequest))) { // The default path restriction applies to both view-mode and resource-mode requests.
                    final URI proxiedFilePathDefault = getProxiedFilePathDefault(pageRequest);
                    if (!channelLocalPath.equals(proxiedFilePathDefault)) {
                        throw new WebApplicationException(Response.status(Response.Status.FORBIDDEN)
                                .entity("Request outside default path").type("text/plain").build());
                    }
                }
                if ((validate) && (!isParentFoldersRestrictionDisabled(pageRequest))) {
                    if (!isRelativeSubPath(channelLocalPath.getPath())) {
                        throw new WebApplicationException(Response.status(Response.Status.FORBIDDEN)
                                .entity("Request outside base URL folder").type("text/plain").build());
                    }
                }
                proxiedFilePath = channelLocalPath;
            } else if (Mode.VIEW.equals(mode)) {
                proxiedFilePath = getProxiedFilePathDefault(pageRequest);
            } else {
                proxiedFilePath = null; // The default path only applies to the view, and isn't used for resource requests.
            }

            final Map<String, List<String>> reqParameters = (pageRequest.isMaximized(this))
                    ? pageRequest.getUriInfo().getQueryParameters()
                    : null;
            if ((validate) && (isDefaultPathRestrictionEnabled(pageRequest)) && (reqParameters != null)
                    && (!reqParameters.isEmpty())) {
                throw new WebApplicationException(Response.status(Response.Status.FORBIDDEN)
                        .entity("Request outside default path").type("text/plain").build());
            }

            if ((proxiedFilePath == null) && ((reqParameters == null) || (reqParameters.isEmpty())))
                return null;

            final StringBuffer proxiedFileLocalURIBuffer = (proxiedFilePath != null)
                    ? new StringBuffer(proxiedFilePath.toString())
                    : new StringBuffer();
            if ((reqParameters != null) && (!reqParameters.isEmpty()))
                proxiedFileLocalURIBuffer.append(reqParameters.entrySet().stream()
                        .flatMap((entry) -> entry.getValue().stream().<Map.Entry<String, String>>map(
                                (i) -> new AbstractMap.SimpleImmutableEntry<String, String>(entry.getKey(), i)))
                        .map((entry) -> NetUtil.urlEncode(entry.getKey()) + '='
                                + NetUtil.urlEncode(entry.getValue()))
                        .collect(Collectors.joining("&", "?", ""))); //TODO proxied parameter blacklist

            try {
                proxiedFileLocalURI = new URI(proxiedFileLocalURIBuffer.toString());
            } catch (URISyntaxException urise) {
                throw new WWWEEEPortal.SoftwareException(urise);
            }

        }
        proxiedFileLocalURI = PROXIED_FILE_LOCAL_URI_HOOK.filter(plugins, context, pageRequest,
                proxiedFileLocalURI);
        return proxiedFileLocalURI;
    }

    /**
     * Construct the final {@linkplain URI#isAbsolute() absolute} {@link URL} for the
     * {@linkplain #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient) proxied} file by resolving the
     * relative {@linkplain #getProxiedFileLocalURI(Page.Request, Channel.Mode, boolean) proxied file local URI} against
     * the {@linkplain #getProxiedBaseURI(Page.Request) base URI}, and if the result isn't absolute, against the
     * {@linkplain ConfigManager#getContextResourceLocalHostURI(UriInfo, String, Map, String, boolean) local host context}
     * .
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @param mode The {@link net.www_eee.portal.Channel.Mode Mode} of the request.
     * @param validate Should any {@linkplain #isParentFoldersRestrictionDisabled(Page.Request) parent folder} or
     * {@linkplain #isDefaultPathRestrictionEnabled(Page.Request) default path} restrictions be evaluated?
     * @return The proxied file {@link URL}.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @throws WebApplicationException If a problem occurred while determining the result.
     * @see #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient)
     * @see #PROXIED_FILE_URL_HOOK
     */
    protected URL getProxiedFileURL(final Page.Request pageRequest, final Mode mode, final boolean validate)
            throws WWWEEEPortal.Exception, WebApplicationException {
        final Object[] context = new Object[] { mode, Boolean.valueOf(validate) };
        URL proxiedFileURL = PROXIED_FILE_URL_HOOK.value(plugins, context, pageRequest);
        if (proxiedFileURL == null) {

            try {
                final URI proxiedFileLocalURI = getProxiedFileLocalURI(pageRequest, mode, validate);
                final URI baseURI = getProxiedBaseURI(pageRequest);
                if (proxiedFileLocalURI != null) {

                    final URI proxiedFileURI = baseURI.resolve(proxiedFileLocalURI);
                    if (proxiedFileURI.isAbsolute()) {
                        proxiedFileURL = proxiedFileURI.toURL();
                    } else {
                        proxiedFileURL = ConfigManager
                                .getContextResourceLocalHostURI(pageRequest.getUriInfo(), proxiedFileURI.getPath(),
                                        NetUtil.getQueryParams(proxiedFileURI), proxiedFileURI.getFragment(), true)
                                .toURL();
                    }

                } else {

                    if (baseURI.isAbsolute()) {
                        proxiedFileURL = baseURI.toURL();
                    } else {
                        proxiedFileURL = ConfigManager.getContextResourceLocalHostURI(pageRequest.getUriInfo(),
                                baseURI.getPath(), NetUtil.getQueryParams(baseURI), baseURI.getFragment(), true)
                                .toURL();
                    }

                }
            } catch (MalformedURLException mue) {
                throw new WWWEEEPortal.SoftwareException(mue);
            }

        }
        proxiedFileURL = PROXIED_FILE_URL_HOOK
                .requireFilteredResult(PROXIED_FILE_URL_HOOK.filter(plugins, context, pageRequest, proxiedFileURL));
        return proxiedFileURL;
    }

    /**
     * Construct the {@link org.apache.http.client.CookieStore CookieStore} to be used for
     * {@linkplain #createProxyClient(Page.Request) creating} the {@link HttpClient}.
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @return The created {@link org.apache.http.client.CookieStore CookieStore}.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @see #createProxyClient(Page.Request)
     * @see #PROXY_CLIENT_COOKIE_STORE_HOOK
     */
    protected org.apache.http.client.CookieStore createProxyClientCookieStore(final Page.Request pageRequest)
            throws WWWEEEPortal.Exception {
        org.apache.http.client.CookieStore proxyClientCookieStore = PROXY_CLIENT_COOKIE_STORE_HOOK.value(plugins,
                null, pageRequest);
        if (proxyClientCookieStore == null) {

            proxyClientCookieStore = new BasicCookieStore();

        }
        proxyClientCookieStore = PROXY_CLIENT_COOKIE_STORE_HOOK.requireFilteredResult(
                PROXY_CLIENT_COOKIE_STORE_HOOK.filter(plugins, null, pageRequest, proxyClientCookieStore));
        return proxyClientCookieStore;
    }

    /**
     * Construct and configure the {@link HttpClient} to be used to
     * {@linkplain #doProxyRequest(Page.Request, Channel.Mode) proxy} content while fulfilling the specified
     * <code>pageRequest</code>. This method will also configure {@linkplain ProxyRequestInterceptor request} and
     * {@linkplain ProxyResponseInterceptor response} interceptors on the created client.
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @return The created {@link HttpClient}.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @see #doProxyRequest(Page.Request, Channel.Mode)
     * @see #PROXY_CLIENT_HOOK
     * @see ProxyRequestInterceptor
     * @see ProxyResponseInterceptor
     */
    protected CloseableHttpClient createProxyClient(final Page.Request pageRequest) throws WWWEEEPortal.Exception {
        final org.apache.http.client.CookieStore proxyClientCookieStore = createProxyClientCookieStore(pageRequest);

        Object[] context = new Object[] { proxyClientManager, proxyClientCookieStore };
        HttpClientBuilder proxyClientBuilder = PROXY_CLIENT_HOOK.value(plugins, context, pageRequest);
        if (proxyClientBuilder == null) {

            final HttpClientBuilder pcb = HttpClientBuilder.create();
            Optional.ofNullable(proxyClientManager)
                    .ifPresent((proxyClientManager) -> pcb.setConnectionManager(proxyClientManager));
            proxyClientBuilder = pcb;
            proxyClientBuilder.setDefaultCookieStore(proxyClientCookieStore);
            proxyClientBuilder = proxyClientBuilder.addInterceptorLast(new ProxyRequestInterceptor(pageRequest));
            proxyClientBuilder = proxyClientBuilder.addInterceptorLast(new ProxyResponseInterceptor(pageRequest));

        }
        proxyClientBuilder = PROXY_CLIENT_HOOK
                .requireFilteredResult(PROXY_CLIENT_HOOK.filter(plugins, context, pageRequest, proxyClientBuilder));
        return proxyClientBuilder.build();
    }

    /**
     * Construct and configure the {@link RequestConfig} to be used for
     * {@linkplain #createProxyRequestObject(Page.Request, Mode, URL) creating} the {@link HttpUriRequest} to
     * {@linkplain #doProxyRequest(Page.Request, Channel.Mode) proxy} content while fulfilling the specified
     * <code>pageRequest</code>.
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @param mode The {@link net.www_eee.portal.Channel.Mode Mode} of the request.
     * @return The created {@link RequestConfig}.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @see #createProxyClient(Page.Request)
     * @see #PROXY_CLIENT_REQUEST_CONFIG_HOOK
     */
    protected RequestConfig createProxyClientRequestConfig(final Page.Request pageRequest, final Mode mode)
            throws WWWEEEPortal.Exception {
        final Object[] context = new Object[] { mode };
        RequestConfig.Builder requestConfig = PROXY_CLIENT_REQUEST_CONFIG_HOOK.value(plugins, context, pageRequest);
        if (requestConfig == null) {

            requestConfig = RequestConfig.copy(RequestConfig.DEFAULT);
            requestConfig = requestConfig.setConnectTimeout(getConnectTimeout(pageRequest));
            requestConfig = requestConfig.setSocketTimeout(getReadTimeout(pageRequest));
            requestConfig = requestConfig.setRedirectsEnabled(isFollowRedirectsEnabled(pageRequest));
            requestConfig = requestConfig.setAuthenticationEnabled(false);

        }
        requestConfig = PROXY_CLIENT_REQUEST_CONFIG_HOOK.requireFilteredResult(
                PROXY_CLIENT_REQUEST_CONFIG_HOOK.filter(plugins, context, pageRequest, requestConfig));
        return requestConfig.build();
    }

    /**
     * Construct the {@link HttpUriRequest} implementation appropriate to
     * {@linkplain #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient) proxy} the specified
     * <code>pageRequest</code> to the <code>proxiedFileURL</code>.
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @param mode The {@link net.www_eee.portal.Channel.Mode Mode} of the request.
     * @param proxiedFileURL The {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied file URL}.
     * @return The created {@link HttpUriRequest}.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @throws WebApplicationException If a problem occurred while determining the result.
     * @see #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient)
     * @see #PROXY_REQUEST_OBJ_HOOK
     */
    protected HttpRequestBase createProxyRequestObject(final Page.Request pageRequest, final Mode mode,
            final URL proxiedFileURL) throws WWWEEEPortal.Exception, WebApplicationException {
        final RequestConfig requestConfig = createProxyClientRequestConfig(pageRequest, mode);

        final Object[] context = new Object[] { mode, proxiedFileURL, requestConfig };
        HttpRequestBase proxyRequest = PROXY_REQUEST_OBJ_HOOK.value(plugins, context, pageRequest);
        if (proxyRequest == null) {

            final URI proxiedFileURI;
            try {
                proxiedFileURI = proxiedFileURL.toURI();
            } catch (URISyntaxException urise) {
                throw new WWWEEEPortal.SoftwareException(urise);
            }

            final String method = pageRequest.getRSRequest().getMethod();
            if ((!pageRequest.isMaximized(this)) || (method.equalsIgnoreCase("GET"))) {
                proxyRequest = new HttpGet(proxiedFileURI);
            } else if (method.equalsIgnoreCase("HEAD")) {
                proxyRequest = new HttpHead(proxiedFileURI);
            } else if (method.equalsIgnoreCase("POST")) {
                proxyRequest = new HttpPost(proxiedFileURI);
            } else if (method.equalsIgnoreCase("PUT")) {
                proxyRequest = new HttpPut(proxiedFileURI);
            } else if (method.equalsIgnoreCase("DELETE")) {
                proxyRequest = new HttpDelete(proxiedFileURI);
            } else {
                throw new WebApplicationException(
                        Response.status(RESTUtil.Response.Status.METHOD_NOT_ALLOWED).build());
            }

            proxyRequest.setConfig(requestConfig);

        }
        proxyRequest = PROXY_REQUEST_OBJ_HOOK
                .requireFilteredResult(PROXY_REQUEST_OBJ_HOOK.filter(plugins, context, pageRequest, proxyRequest));
        return proxyRequest;
    }

    /**
     * Construct the value for the &quot;User-Agent&quot; header to be {@link HttpUriRequest#setHeader(String, String)
     * set} on the {@link HttpUriRequest} being used to
     * {@linkplain #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient) proxy} content to the
     * <code>proxiedFileURL</code>. This method starts with the {@linkplain javax.ws.rs.core.HttpHeaders#USER_AGENT
     * USER_AGENT} header provided by the client, appends the WWW-EEE-Portal {@link #USER_AGENT_HEADER}, followed by the
     * {@linkplain CoreProtocolPNames#USER_AGENT USER_AGENT} for the {@link HttpClient} being used.
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @param mode The {@link net.www_eee.portal.Channel.Mode Mode} of the request.
     * @param proxyClient The {@link #createProxyClient(Page.Request) HttpClient} performing the proxy request.
     * @param proxiedFileURL The {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied file URL}.
     * @param proxyRequest The proxy {@link #createProxyRequestObject(Page.Request, Channel.Mode, URL) HttpUriRequest}
     * object.
     * @return The &quot;User-Agent&quot; header value.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @throws WebApplicationException If a problem occurred while determining the result.
     * @see #USER_AGENT_HEADER
     * @see #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient)
     */
    protected @Nullable String getProxyRequestUserAgentHeader(final Page.Request pageRequest, final Mode mode,
            final HttpClient proxyClient, final URL proxiedFileURL, final HttpUriRequest proxyRequest)
            throws WWWEEEPortal.Exception, WebApplicationException {
        final StringBuffer value = new StringBuffer();
        final Optional<String> clientRequestHeader = RESTUtil.getFirstHeaderValue(pageRequest.getHttpHeaders(),
                javax.ws.rs.core.HttpHeaders.USER_AGENT, Function.identity());
        if (clientRequestHeader.isPresent())
            value.append(clientRequestHeader.get());
        if (value.length() > 0)
            value.append(' ');
        value.append(USER_AGENT_HEADER);
        //FIXME Include original User-Agent?
        // final String proxyRequestUserAgent = (String)proxyClient.getParams().getParameter(CoreProtocolPNames.USER_AGENT);
        // if (proxyRequestUserAgent != null) {
        //   value.append(' ');
        //   value.append(proxyRequestUserAgent);
        // }
        return value.toString();
    }

    /**
     * Construct the value for the &quot;Accept-Language&quot; header to be
     * {@link HttpUriRequest#setHeader(String, String) set} on the {@link HttpUriRequest} being used to
     * {@linkplain #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient) proxy} content to the
     * <code>proxiedFileURL</code> . This method basically just fowards the
     * {@link javax.ws.rs.core.HttpHeaders#getAcceptableLanguages() acceptable} to the client.
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @param mode The {@link net.www_eee.portal.Channel.Mode Mode} of the request.
     * @param proxyClient The {@link #createProxyClient(Page.Request) HttpClient} performing the proxy request.
     * @param proxiedFileURL The {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied file URL}.
     * @param proxyRequest The proxy {@link #createProxyRequestObject(Page.Request, Channel.Mode, URL) HttpUriRequest}
     * object.
     * @return The &quot;Accept-Language&quot; header value.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @throws WebApplicationException If a problem occurred while determining the result.
     * @see javax.ws.rs.core.HttpHeaders#getAcceptableLanguages()
     * @see #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient)
     */
    protected @Nullable String getProxyRequestAcceptLanguageHeader(final Page.Request pageRequest, final Mode mode,
            final HttpClient proxyClient, final URL proxiedFileURL, final HttpUriRequest proxyRequest)
            throws WWWEEEPortal.Exception, WebApplicationException {
        final List<Locale> acceptableLanguages = pageRequest.getHttpHeaders().getAcceptableLanguages();
        if ((acceptableLanguages == null) || (acceptableLanguages.isEmpty()))
            return null;
        final StringBuffer acceptLanguage = new StringBuffer();
        for (Locale locale : acceptableLanguages) {
            if (acceptLanguage.length() > 0)
                acceptLanguage.append(',');
            acceptLanguage.append(locale.getLanguage());
            if (!locale.getCountry().isEmpty()) {
                acceptLanguage.append('-');
                acceptLanguage.append(locale.getCountry());
            }
        }
        return acceptLanguage.toString();
    }

    /**
     * Construct the value for the &quot;Accept&quot; header to be {@link HttpUriRequest#setHeader(String, String) set} on
     * the {@link HttpUriRequest} being used to
     * {@linkplain #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient) proxy} content to the
     * <code>proxiedFileURL</code>.
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @param mode The {@link net.www_eee.portal.Channel.Mode Mode} of the request.
     * @param proxyClient The {@link #createProxyClient(Page.Request) HttpClient} performing the proxy request.
     * @param proxiedFileURL The {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied file URL}.
     * @param proxyRequest The proxy {@link #createProxyRequestObject(Page.Request, Channel.Mode, URL) HttpUriRequest}
     * object.
     * @return The &quot;Accept&quot; header value.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @throws WebApplicationException If a problem occurred while determining the result.
     * @see #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient)
     */
    protected @Nullable String getProxyRequestAcceptHeader(final Page.Request pageRequest, final Mode mode,
            final HttpClient proxyClient, final URL proxiedFileURL, final HttpUriRequest proxyRequest)
            throws WWWEEEPortal.Exception, WebApplicationException {
        if (Mode.VIEW.equals(mode)) {
            final StringBuffer accept = new StringBuffer();
            accept.append(HTMLUtil.APPLICATION_XHTML_XML_MIME_TYPE); // Defaults to q=1.
            accept.append(',');
            accept.append(XMLUtil.APPLICATION_XML_MIME_TYPE); // Defaults to q=1.
            accept.append(',');
            accept.append(XMLUtil.TEXT_XML_MIME_TYPE); // They should likely be serving this as "application/something+xml" instead.
            accept.append(";q=0.9,");
            accept.append(APPLICATION_STAR_MIME_TYPE); // Ideally this would be "application/*+xml;q=1", but since we can't do that, at least do this.
            accept.append(";q=0.5,");
            accept.append(TEXT_STAR_MIME_TYPE); // ProxyChannel can render text, but it's not anywhere near as ideal as HTML.
            accept.append(";q=0.4");
            return accept.toString();
        }
        return RESTUtil.getFirstHeaderValue(pageRequest.getHttpHeaders(), "Accept", Function.identity())
                .orElse(null); // The ProxyChannel doesn't really interact with content through resource mode, so just forward whatever the client wants.
    }

    /**
     * Construct the value for the &quot;Authorization&quot; header to be {@link HttpUriRequest#setHeader(String, String)
     * set} on the {@link HttpUriRequest} being used to
     * {@linkplain #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient) proxy} content to the
     * <code>proxiedFileURL</code>.
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @param mode The {@link net.www_eee.portal.Channel.Mode Mode} of the request.
     * @param proxyClient The {@link #createProxyClient(Page.Request) HttpClient} performing the proxy request.
     * @param proxiedFileURL The {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied file URL}.
     * @param proxyRequest The proxy {@link #createProxyRequestObject(Page.Request, Channel.Mode, URL) HttpUriRequest}
     * object.
     * @return The &quot;Authorization&quot; header value.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @throws WebApplicationException If a problem occurred while determining the result.
     * @see #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient)
     */
    protected @Nullable String getProxyRequestAuthorizationHeader(final Page.Request pageRequest, final Mode mode,
            final HttpClient proxyClient, final URL proxiedFileURL, final HttpUriRequest proxyRequest)
            throws WWWEEEPortal.Exception, WebApplicationException {
        if ((!proxyRequest.containsHeader("Authorization")) && (RESTUtil
                .getFirstHeaderValue(pageRequest.getHttpHeaders(), "Authorization", Function.identity()) != null)) {
            return RESTUtil.getFirstHeaderValue(pageRequest.getHttpHeaders(), "Authorization", Function.identity())
                    .orElse(null);
        }
        return null;
    }

    /**
     * Construct the {@link HttpUriRequest} that will be used to {@linkplain #doProxyRequest(Page.Request, Channel.Mode)
     * proxy} content while fulfilling the specified <code>pageRequest</code>. This method will
     * {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) calculate} the proxied file URL,
     * {@linkplain #createProxyRequestObject(Page.Request, Channel.Mode, URL) create} the appropriate type of request
     * object, set it's {@linkplain HttpUriRequest#setHeader(String, String) headers}, and, if this channel is
     * {@linkplain net.www_eee.portal.Page.Request#isMaximized(Channel) maximized}, set any
     * {@linkplain HttpEntityEnclosingRequest#setEntity(HttpEntity) entity} that was
     * {@linkplain net.www_eee.portal.Page.Request#getEntity() provided} by the <code>pageRequest</code>.
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @param mode The {@link net.www_eee.portal.Channel.Mode Mode} of the request.
     * @param proxyClient The {@link #createProxyClient(Page.Request) HttpClient} performing the proxy request.
     * @return The proxy {@link HttpUriRequest} object.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @throws WebApplicationException If a problem occurred while determining the result.
     * @see #doProxyRequest(Page.Request, Channel.Mode)
     * @see #PROXY_REQUEST_HOOK
     */
    protected HttpRequestBase createProxyRequest(final Page.Request pageRequest, final Mode mode,
            final CloseableHttpClient proxyClient) throws WWWEEEPortal.Exception, WebApplicationException {
        final URL proxiedFileURL = getProxiedFileURL(pageRequest, mode, true);

        final Object[] context = new Object[] { mode, proxiedFileURL };
        HttpRequestBase proxyRequest = PROXY_REQUEST_HOOK.value(plugins, context, pageRequest);
        if (proxyRequest == null) {

            proxyRequest = createProxyRequestObject(pageRequest, mode, proxiedFileURL);

            final String userAgent = getProxyRequestUserAgentHeader(pageRequest, mode, proxyClient, proxiedFileURL,
                    proxyRequest);
            if (userAgent != null)
                proxyRequest.setHeader("User-Agent", userAgent);
            final String acceptLanguage = getProxyRequestAcceptLanguageHeader(pageRequest, mode, proxyClient,
                    proxiedFileURL, proxyRequest);
            if (acceptLanguage != null)
                proxyRequest.setHeader("Accept-Language", acceptLanguage);
            final String accept = getProxyRequestAcceptHeader(pageRequest, mode, proxyClient, proxiedFileURL,
                    proxyRequest);
            if (accept != null)
                proxyRequest.setHeader("Accept", accept);
            final String authorization = getProxyRequestAuthorizationHeader(pageRequest, mode, proxyClient,
                    proxiedFileURL, proxyRequest);
            if (authorization != null)
                proxyRequest.setHeader("Authorization", authorization);

            if (Mode.RESOURCE.equals(mode)) {
                final Optional<String> ifMatch = RESTUtil.getFirstHeaderValue(pageRequest.getHttpHeaders(),
                        "If-Match", Function.identity());
                if (ifMatch.isPresent())
                    proxyRequest.setHeader("If-Match", ifMatch.get());
                final Optional<String> ifModifiedSince = RESTUtil.getFirstHeaderValue(pageRequest.getHttpHeaders(),
                        "If-Modified-Since", Function.identity());
                if (ifModifiedSince.isPresent())
                    proxyRequest.setHeader("If-Modified-Since", ifModifiedSince.get());
                final Optional<String> ifNoneMatch = RESTUtil.getFirstHeaderValue(pageRequest.getHttpHeaders(),
                        "If-None-Match", Function.identity());
                if (ifNoneMatch.isPresent())
                    proxyRequest.setHeader("If-None-Match", ifNoneMatch.get());
                final Optional<String> ifUnmodifiedSince = RESTUtil.getFirstHeaderValue(
                        pageRequest.getHttpHeaders(), "If-Unmodified-Since", Function.identity());
                if (ifUnmodifiedSince.isPresent())
                    proxyRequest.setHeader("If-Unmodified-Since", ifUnmodifiedSince.get());
            }

            if (!isCacheControlClientDirectivesDisabled(pageRequest)) {
                final Optional<CacheControl> cacheControl = RESTUtil
                        .getFirstHeaderValue(pageRequest.getHttpHeaders(), "Cache-Control", CacheControl::valueOf);
                if (cacheControl.isPresent())
                    proxyRequest.setHeader("Cache-Control", cacheControl.get().toString());
            }

            if (pageRequest.isMaximized(this)) {

                final MediaType contentType = pageRequest.getHttpHeaders().getMediaType();
                if (contentType != null)
                    proxyRequest.setHeader("Content-Type", contentType.toString());

                final DataSource entity = pageRequest.getEntity();
                if ((entity != null) && (proxyRequest instanceof HttpEntityEnclosingRequest)) {
                    try {
                        final Optional<String> contentLengthString = RESTUtil.getFirstHeaderValue(
                                pageRequest.getHttpHeaders(), "Content-Length", Function.identity());
                        final HttpEntity httpEntity = new InputStreamEntity(entity.getInputStream(),
                                (contentLengthString.isPresent()) ? Long.parseLong(contentLengthString.get()) : -1);
                        ((HttpEntityEnclosingRequest) proxyRequest).setEntity(httpEntity);
                    } catch (NumberFormatException nfe) {
                        throw new WebApplicationException(nfe, Response.Status.INTERNAL_SERVER_ERROR);
                    } catch (IOException ioe) {
                        throw new WWWEEEPortal.OperationalException(ioe);
                    }
                }

            } // if (pageRequest.isMaximized(this))

        } // if (proxyRequest == null)
        proxyRequest = PROXY_REQUEST_HOOK
                .requireFilteredResult(PROXY_REQUEST_HOOK.filter(plugins, context, pageRequest, proxyRequest));
        return proxyRequest;
    }

    /**
     * Construct a {@link Reader} for the {@linkplain HttpResponse#getEntity() entity} within the supplied
     * <code>proxyResponse</code>. This method will attempt to use the given <code>responseContentType</code> to
     * {@linkplain IOUtil#getCharsetParameter(MimeType) determine} the correct {@link Charset} to use for
     * {@linkplain InputStreamReader#InputStreamReader(InputStream, Charset) decoding} the
     * {@linkplain HttpEntity#getContent() byte stream}.
     * 
     * @param proxyResponse The {@link HttpResponse} containing the proxied file.
     * @param responseContentType The {@link MimeType} of the proxied file.
     * @return A {@link Reader} for the proxied content.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     */
    protected @Nullable Reader createProxiedFileReader(final HttpResponse proxyResponse,
            final @Nullable MimeType responseContentType) throws WWWEEEPortal.Exception {
        if (proxyResponse.getEntity() == null)
            return null;
        final Optional<Charset> charset;
        try {
            charset = (responseContentType != null) ? IOUtil.getCharsetParameter(responseContentType)
                    : Optional.empty();
        } catch (UnsupportedCharsetException uce) {
            throw new ContentManager.ContentException("Proxied file has unsupported charset", uce);
        }
        try {
            return (charset.isPresent())
                    ? new InputStreamReader(proxyResponse.getEntity().getContent(), charset.get())
                    : new InputStreamReader(proxyResponse.getEntity().getContent());
        } catch (IOException ioe) {
            throw new WWWEEEPortal.OperationalException(ioe);
        }
    }

    /**
     * Examine the {@linkplain HttpResponse#getStatusLine() status} {@link StatusLine#getStatusCode() code} from the
     * response to the {@linkplain #doProxyRequest(Page.Request, Channel.Mode) proxy request} and throw an exception if
     * something went wrong.
     * 
     * @param proxyContext The {@link HttpClientContext} containing the {@linkplain ExecutionContext#HTTP_RESPONSE
     * response} from the proxied server.
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @param mode The {@link net.www_eee.portal.Channel.Mode Mode} of the request.
     * @return The supplied <code>proxyClientContext</code> argument.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @throws WebApplicationException If a problem occurred while determining the result.
     * @see #doProxyRequest(Page.Request, Channel.Mode)
     */
    protected HttpClientContext validateProxyResponse(final HttpClientContext proxyContext,
            final Page.Request pageRequest, final Mode mode)
            throws WWWEEEPortal.Exception, WebApplicationException {
        final HttpResponse proxyResponse = proxyContext.getResponse();
        final int responseCode = proxyResponse.getStatusLine().getStatusCode();
        if (responseCode == Response.Status.OK.getStatusCode())
            return proxyContext;
        try {
            if (responseCode == Response.Status.NOT_MODIFIED.getStatusCode()) {
                throw new WebApplicationException(Response.Status.NOT_MODIFIED);
            } else if (((responseCode >= Response.Status.MOVED_PERMANENTLY.getStatusCode())
                    && (responseCode <= Response.Status.SEE_OTHER.getStatusCode()))
                    || (responseCode == Response.Status.TEMPORARY_REDIRECT.getStatusCode())) { // Moved Permanently, Found, See Other, Temporary Redirect
                if (pageRequest.isMaximized(this)) {
                    final URI fixedLocation;
                    try {
                        final Optional<URI> locationURI = HttpUtil.getValue(proxyResponse.getLastHeader("Location"),
                                URI::create);
                        final URL proxiedFileURL = HttpUtil.getRequestTargetURL(proxyContext);
                        fixedLocation = rewriteProxiedFileLink(pageRequest, proxiedFileURL,
                                locationURI.orElse(null), Mode.VIEW.equals(mode), true).getKey();
                    } catch (Exception e) {
                        throw new ContentManager.ContentException("Error rewriting 'Location' header", e);
                    }
                    throw new WebApplicationException(
                            Response.status(RESTUtil.Response.Status.fromStatusCode(responseCode))
                                    .location(fixedLocation).build());
                }
            } else if (responseCode == Response.Status.UNAUTHORIZED.getStatusCode()) {
                if (pageRequest.isMaximized(this)) {
                    throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED)
                            .header("WWW-Authenticate", HttpUtil
                                    .getValue(proxyResponse.getLastHeader("WWW-Authenticate"), Function.identity())
                                    .orElse(null))
                            .build());
                }
            } else if ((responseCode == Response.Status.NOT_FOUND.getStatusCode())
                    || (responseCode == Response.Status.GONE.getStatusCode())) {
                final URI channelLocalPath = pageRequest.getChannelLocalPath(this);
                if (channelLocalPath != null) {
                    throw new WebApplicationException(Response.Status.fromStatusCode(responseCode));
                }
            } else if (responseCode == RESTUtil.Response.Status.METHOD_NOT_ALLOWED.getStatusCode()) {
                final URI channelLocalPath = pageRequest.getChannelLocalPath(this);
                if (channelLocalPath != null) {
                    throw new WebApplicationException(
                            Response.status(RESTUtil.Response.Status.METHOD_NOT_ALLOWED)
                                    .header("Allow", HttpUtil
                                            .getValue(proxyResponse.getLastHeader("Allow"), Function.identity())
                                            .orElse(null))
                                    .build());
                }
            } else if (responseCode == RESTUtil.Response.Status.REQUEST_TIMEOUT.getStatusCode()) { // we didn't send it to the proxied server fast enough!?
                throw new WWWEEEPortal.OperationalException(new WebApplicationException(responseCode));
            } else if (responseCode == Response.Status.BAD_REQUEST.getStatusCode()) {
                throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST).build());
            } else if ((responseCode >= 400) && (responseCode < 500)) { // All other 4XX errors
                if (pageRequest.isMaximized(this)) {
                    throw new WebApplicationException(
                            Response.status(RESTUtil.Response.Status.fromStatusCode(responseCode)).build());
                }
            } else if ((responseCode >= RESTUtil.Response.Status.BAD_GATEWAY.getStatusCode())
                    && (responseCode <= RESTUtil.Response.Status.GATEWAY_TIMEOUT.getStatusCode())) { // Bad Gateway, Service Unavailable, Gateway Timeout
                throw new WWWEEEPortal.OperationalException(new WebApplicationException(responseCode));
            }
            final Response.StatusType statusType = RESTUtil.Response.Status.fromStatusCode(responseCode);
            final String codePhrase = (statusType != null) ? " (" + statusType.getReasonPhrase() + ")" : "";
            final String responsePhrase = (proxyResponse.getStatusLine().getReasonPhrase() != null)
                    ? ": " + proxyResponse.getStatusLine().getReasonPhrase()
                    : "";
            final ConfigManager.ConfigException configurationException = new ConfigManager.ConfigException(
                    "The proxied file server returned code '" + responseCode + "'" + codePhrase + responsePhrase,
                    null);
            if (getLogger().isLoggable(Level.FINE)) {
                try {
                    final Reader reader = createProxiedFileReader(proxyResponse, getProxyResponseHeader(pageRequest,
                            proxyResponse, "Content-Type", IOUtil::newMimeType));
                    LogAnnotation.annotate(configurationException, "ProxiedFileResponseContent",
                            (reader != null) ? IOUtil.toString(reader) : null, Level.FINE, false);
                } catch (Exception e) {
                }
            }
            throw configurationException;
        } catch (WWWEEEPortal.Exception wpe) {
            try {
                LogAnnotation.annotate(wpe, "ProxiedFileURL", HttpUtil.getRequestTargetURL(proxyContext), null,
                        false);
            } catch (Exception e) {
            }
            LogAnnotation.annotate(wpe, "ProxiedFileResponseCode", responseCode, null, false);
            LogAnnotation.annotate(wpe, "ProxiedFileResponseCodeReasonPhrase",
                    RESTUtil.Response.Status.fromStatusCode(responseCode), null, false);
            LogAnnotation.annotate(wpe, "ProxiedFileResponseReasonPhrase",
                    proxyResponse.getStatusLine().getReasonPhrase(), null, false);
            throw wpe;
        }
    }

    /**
     * {@linkplain #createProxyClient(Page.Request) Create} an {@link HttpClient} and use it to
     * {@linkplain HttpClient#execute(HttpUriRequest, HttpContext) execute} a
     * {@linkplain #createProxyRequest(Page.Request, Channel.Mode, CloseableHttpClient) proxy request} to retrieve the
     * content to be returned/rendered in response to the supplied <code>pageRequest</code>.
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @param mode The {@link net.www_eee.portal.Channel.Mode Mode} of the request.
     * @return An {@link HttpClientContext} containing the {@linkplain ExecutionContext#HTTP_RESPONSE response} from the
     * proxied server.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @throws WebApplicationException If a problem occurred while determining the result.
     * @see #PROXY_RESPONSE_HOOK
     */
    protected HttpClientContext doProxyRequest(final Page.Request pageRequest, final Mode mode)
            throws WWWEEEPortal.Exception, WebApplicationException {
        final HttpClientContext proxyContext = HttpClientContext.create();
        final CloseableHttpClient proxyClient = createProxyClient(pageRequest);
        proxyContext.setAttribute(HTTP_CLIENT_CONTEXT_ID, proxyClient);

        try {

            CloseableHttpResponse proxyResponse = null;

            while (proxyResponse == null) { // Keep trying again if a plugin filter null's out the previous response.

                final HttpRequestBase proxyRequest = createProxyRequest(pageRequest, mode, proxyClient);
                try {

                    final Object[] context = new Object[] { mode, proxyContext, proxyClient, proxyRequest };
                    proxyResponse = PROXY_RESPONSE_HOOK.value(plugins, context, pageRequest);

                    if (proxyResponse == null) {
                        try {

                            proxyResponse = proxyClient.execute(proxyRequest, proxyContext);

                        } catch (UnknownHostException uhe) {
                            throw new ConfigManager.ConfigException(uhe);
                        } catch (IOException ioe) {
                            throw new WWWEEEPortal.OperationalException(ioe);
                        }
                    }

                    proxyResponse = PROXY_RESPONSE_HOOK.filter(plugins, context, pageRequest, proxyResponse);

                } catch (WWWEEEPortal.Exception wpe) {
                    LogAnnotation.annotate(wpe, "ProxyRequest", proxyRequest, null, false);
                    LogAnnotation.annotate(wpe, "ProxyResponse", proxyResponse, null, false);
                    LogAnnotation.annotate(wpe, "ProxiedFileURL", proxyRequest.getURI(), null, false);
                    throw wpe;
                }

            } // while (proxyResponse == null)

            return validateProxyResponse(proxyContext, pageRequest, mode);

        } catch (WWWEEEPortal.Exception wpe) {
            LogAnnotation.annotate(wpe, "ProxyContext", proxyContext, null, false);
            try {
                LogAnnotation.annotate(wpe, "ProxiedFileURL", HttpUtil.getRequestTargetURL(proxyContext), null,
                        false); // This wouldn't be necessary if any of the previous annotations could actually toString() themselves usefully.
            } catch (Exception e) {
            }
            throw wpe;
        }

    }

    /**
     * {@linkplain HttpResponse#getHeaders(String) Retrieve} the value of the named header (multiple values will be
     * {@link HttpUtil#consolidate(Header[]) consolidated}), using the supplied <code>converter</code> to handle the data
     * type.
     * 
     * @param <R> The type of value returned for this header.
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @param proxyResponse The {@link HttpResponse} received for the proxied file.
     * @param headerName The name of the response header desired.
     * @param headerReadingFunction A {@link Function} capable of converting the header value to the desired result type.
     * @return The converted value of the header.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @see #PROXY_RESPONSE_HEADER_HOOK
     */

    public <R> @Nullable R getProxyResponseHeader(final Page.Request pageRequest, final HttpResponse proxyResponse,
            final String headerName, final Function<String, R> headerReadingFunction)
            throws WWWEEEPortal.Exception {
        final Object[] context = new Object[] { proxyResponse, headerName };
        Header header = PROXY_RESPONSE_HEADER_HOOK.value(plugins, context, pageRequest);
        if (header == null) {

            header = HttpUtil.consolidate(proxyResponse.getHeaders(headerName)).orElse(null);

        }
        if (header == null)
            header = new BasicHeader(headerName, null); // Give filters access to the headerName
        header = PROXY_RESPONSE_HEADER_HOOK.filter(plugins, context, pageRequest, header);
        return HttpUtil
                .getValue(header, FuncUtil.mapEx(headerReadingFunction, ContentManager.ContentException::new))
                .orElse(null);
    }

    /**
     * Should the content contained within the supplied <code>proxyResponse</code> having the supplied
     * <code>responseContentType</code> be
     * {@linkplain #renderXMLView(Page.Request, Channel.ViewResponse, HttpResponse, URL, MimeType) rendered using the XML
     * view}? By default, this method will return <code>true</code> for any {@linkplain XMLUtil#isXML(MimeType) XML}
     * MimeType.
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @param proxyResponse The {@link HttpResponse} received for the proxied file.
     * @param responseContentType The {@link MimeType} of the proxied file.
     * @return If the <code>proxyResponse</code> content should be
     * {@linkplain #renderXMLView(Page.Request, Channel.ViewResponse, HttpResponse, URL, MimeType) rendered using the XML
     * view}.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @see #renderXMLView(Page.Request, Channel.ViewResponse, HttpResponse, URL, MimeType)
     * @see #IS_RENDERED_USING_XML_VIEW_HOOK
     */
    protected boolean isRenderedUsingXMLView(final Page.Request pageRequest, final HttpResponse proxyResponse,
            final @Nullable MimeType responseContentType) throws WWWEEEPortal.Exception {
        final Object[] context = new Object[] { proxyResponse, responseContentType };
        Boolean isRenderedUsingXMLView = IS_RENDERED_USING_XML_VIEW_HOOK.value(plugins, context, pageRequest);
        if (isRenderedUsingXMLView == null) {

            isRenderedUsingXMLView = Boolean.valueOf(XMLUtil.isXML(responseContentType));

        }
        return Boolean.TRUE.equals(
                IS_RENDERED_USING_XML_VIEW_HOOK.filter(plugins, context, pageRequest, isRenderedUsingXMLView));
    }

    /**
     * Create a {@link #createProxiedFileReader(HttpResponse, MimeType) Reader} for the <code>proxyResponse</code> and
     * wrap it in a {@link TypedInputSource}.
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @param proxyResponse The {@link HttpResponse} received for the proxied file.
     * @param proxiedFileURL The {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied file URL}.
     * @param responseContentType The {@link MimeType} of the proxied file.
     * @return A {@link TypedInputSource} containing the proxied document.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @see #renderXMLView(Page.Request, Channel.ViewResponse, HttpResponse, URL, MimeType)
     * @see #PROXIED_DOC_INPUT_SOURCE_HOOK
     */
    protected TypedInputSource createProxiedDocumentInputSource(final Page.Request pageRequest,
            final HttpResponse proxyResponse, final URL proxiedFileURL,
            final @Nullable MimeType responseContentType) throws WWWEEEPortal.Exception {
        final Object[] context = new Object[] { proxyResponse, proxiedFileURL, responseContentType };
        TypedInputSource proxiedDocumentInputSource = PROXIED_DOC_INPUT_SOURCE_HOOK.value(plugins, context,
                pageRequest);
        if (proxiedDocumentInputSource == null) {

            final Reader proxiedFileReader = createProxiedFileReader(proxyResponse, responseContentType);
            proxiedDocumentInputSource = new TypedInputSource(new InputSource(proxiedFileReader),
                    responseContentType);

        }
        proxiedDocumentInputSource = PROXIED_DOC_INPUT_SOURCE_HOOK.requireFilteredResult(
                PROXIED_DOC_INPUT_SOURCE_HOOK.filter(plugins, context, pageRequest, proxiedDocumentInputSource));
        return proxiedDocumentInputSource;
    }

    /**
     * Create a {@linkplain DOMBuildingHandler} to add the parsed XML to the channel
     * {@linkplain net.www_eee.portal.Channel.ViewResponse#getContentContainerElement() content}.
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @param viewResponse The {@link net.www_eee.portal.Channel.ViewResponse ViewResponse} currently being generated.
     * @param proxiedFileURL The {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied file URL}.
     * @param proxiedDocumentInputSource The
     * {@linkplain #createProxiedDocumentInputSource(Page.Request, HttpResponse, URL, MimeType) input source} containing
     * the proxied file content.
     * @return A {@link DefaultHandler2} to handle the parsing events for the proxied content.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @see #renderXMLView(Page.Request, Channel.ViewResponse, HttpResponse, URL, MimeType)
     * @see #PROXIED_DOC_CONTENT_HANDLER_HOOK
     */
    protected DefaultHandler2 createProxiedDocumentContentHandler(final Page.Request pageRequest,
            final ViewResponse viewResponse, final URL proxiedFileURL,
            final TypedInputSource proxiedDocumentInputSource) throws WWWEEEPortal.Exception {
        final Object[] context = new Object[] { viewResponse, proxiedFileURL, proxiedDocumentInputSource };
        DefaultHandler2 contentHandler = PROXIED_DOC_CONTENT_HANDLER_HOOK.value(plugins, context, pageRequest);
        if (contentHandler == null) {

            contentHandler = new DOMBuildingHandler((DefaultHandler2) null,
                    viewResponse.getContentContainerElement());

        }
        contentHandler = PROXIED_DOC_CONTENT_HANDLER_HOOK.requireFilteredResult(
                PROXIED_DOC_CONTENT_HANDLER_HOOK.filter(plugins, context, pageRequest, contentHandler));
        return contentHandler;
    }

    /**
     * Create an {@linkplain #createProxiedDocumentInputSource(Page.Request, HttpResponse, URL, MimeType) input source},
     * {@linkplain MarkupManager#parseXMLDocument(InputSource, DefaultHandler2, boolean, boolean, boolean, boolean) feed}
     * it to an XML parser, and
     * {@linkplain #createProxiedDocumentContentHandler(Page.Request, Channel.ViewResponse, URL, TypedInputSource) handle}
     * the results.
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @param viewResponse The {@link net.www_eee.portal.Channel.ViewResponse ViewResponse} currently being generated.
     * @param proxyResponse The {@link HttpResponse} received for the proxied file.
     * @param proxiedFileURL The {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied file URL}.
     * @param responseContentType The {@link MimeType} of the proxied file.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @see #isRenderedUsingXMLView(Page.Request, HttpResponse, MimeType)
     * @see #createProxiedDocumentInputSource(Page.Request, HttpResponse, URL, MimeType)
     * @see MarkupManager#parseXMLDocument(InputSource, DefaultHandler2, boolean, boolean, boolean, boolean)
     * @see #createProxiedDocumentContentHandler(Page.Request, Channel.ViewResponse, URL, TypedInputSource)
     * @see #PARSE_XML_HOOK
     */
    protected void renderXMLView(final Page.Request pageRequest, final ViewResponse viewResponse,
            final HttpResponse proxyResponse, final URL proxiedFileURL,
            final @Nullable MimeType responseContentType) throws WWWEEEPortal.Exception {
        final TypedInputSource proxiedDocumentInputSource = createProxiedDocumentInputSource(pageRequest,
                proxyResponse, proxiedFileURL, responseContentType);

        final DefaultHandler2 contentHandler = createProxiedDocumentContentHandler(pageRequest, viewResponse,
                proxiedFileURL, proxiedDocumentInputSource);

        final Object[] context = new Object[] { viewResponse, proxiedDocumentInputSource, contentHandler };
        Boolean parsedXML = PARSE_XML_HOOK.value(plugins, context, pageRequest);
        if (!Boolean.TRUE.equals(parsedXML)) {

            MarkupManager.parseXMLDocument(proxiedDocumentInputSource.getInputSource(), contentHandler,
                    isParserHaltOnWarningsEnabled(pageRequest), !isParserHaltOnErrorsDisabled(pageRequest),
                    !isParserHaltOnFatalErrorsDisabled(pageRequest), false);

        }

        PARSE_XML_HOOK.filter(plugins, context, pageRequest, Boolean.TRUE);

        final Optional<Element> contentRootElement = DOMUtil
                .getChildElement(viewResponse.getContentContainerElement(), null, null);
        if (contentRootElement.isPresent()) {
            final Locale contentLocale = DOMUtil.getXMLLangAttr(contentRootElement.get(), Locale.ROOT);
            if (!Locale.ROOT.equals(contentLocale))
                viewResponse.setLocale(contentLocale);
        }

        return;
    }

    /**
     * Should the content contained within the supplied <code>proxyResponse</code> having the supplied
     * <code>responseContentType</code> be
     * {@linkplain #renderTextView(Page.Request, Channel.ViewResponse, HttpResponse, URL, MimeType) rendered using the
     * Text view}?
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @param proxyResponse The {@link HttpResponse} received for the proxied file.
     * @param responseContentType The {@link MimeType} of the proxied file.
     * @return If the <code>proxyResponse</code> content should be
     * {@linkplain #renderTextView(Page.Request, Channel.ViewResponse, HttpResponse, URL, MimeType) rendered using the
     * Text view}.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @see #renderTextView(Page.Request, Channel.ViewResponse, HttpResponse, URL, MimeType)
     * @see #IS_RENDERED_USING_TEXT_VIEW_HOOK
     */
    protected boolean isRenderedUsingTextView(final Page.Request pageRequest, final HttpResponse proxyResponse,
            final @Nullable MimeType responseContentType) throws WWWEEEPortal.Exception {
        final Object[] context = new Object[] { proxyResponse, responseContentType };
        Boolean isRenderedUsingTextView = IS_RENDERED_USING_TEXT_VIEW_HOOK.value(plugins, context, pageRequest);
        if (isRenderedUsingTextView == null) {

            isRenderedUsingTextView = Boolean.valueOf((responseContentType != null)
                    && (IOUtil.TEXT_PLAIN_MIME_TYPE.getBaseType().equals(responseContentType.getBaseType())));

        }
        return Boolean.TRUE.equals(
                IS_RENDERED_USING_TEXT_VIEW_HOOK.filter(plugins, context, pageRequest, isRenderedUsingTextView));
    }

    /**
     * {@link #createProxiedFileReader(HttpResponse, MimeType) Read} the <code>proxyResponse</code> into a
     * {@link IOUtil#toString(Reader) String} and add it to the channel
     * {@link net.www_eee.portal.Channel.ViewResponse#getContentContainerElement() content} within a
     * <code>&lt;pre&gt;</code> element.
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @param viewResponse The {@link net.www_eee.portal.Channel.ViewResponse ViewResponse} currently being generated.
     * @param proxyResponse The {@link HttpResponse} received for the proxied file.
     * @param proxiedFileURL The {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied file URL}.
     * @param responseContentType The {@link MimeType} of the proxied file.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @see #isRenderedUsingTextView(Page.Request, HttpResponse, MimeType)
     * @see #createProxiedFileReader(HttpResponse, MimeType)
     */
    protected void renderTextView(final Page.Request pageRequest, final ViewResponse viewResponse,
            final HttpResponse proxyResponse, final URL proxiedFileURL,
            final @Nullable MimeType responseContentType) throws WWWEEEPortal.Exception {
        final Reader proxiedDocumentReader = createProxiedFileReader(proxyResponse, responseContentType);
        final String textString = (proxiedDocumentReader != null) ? IOUtil.toString(proxiedDocumentReader) : null;

        final Element channelContentContainerElement = viewResponse.getContentContainerElement();
        final Element preElement = DOMUtil.createElement(HTMLUtil.HTML_NS_URI, HTMLUtil.HTML_NS_PREFIX, "pre",
                channelContentContainerElement);
        setIDAndClassAttrs(preElement, Arrays.asList("proxy", "text"), null, null);
        DOMUtil.createAttr(XMLUtil.XML_NS_URI, "xml", "space", "preserve", preElement);
        if ((textString != null) && (!textString.isEmpty()))
            DOMUtil.appendText(preElement, textString);

        return;
    }

    /**
     * Add an <code>&lt;object&gt;</code> element to the channel
     * {@link net.www_eee.portal.Channel.ViewResponse#getContentContainerElement() content} creating an external resource
     * reference to the content at the given <code>proxiedFileURL</code> (link
     * {@linkplain #rewriteProxiedFileLink(Page.Request, URL, URI, boolean, boolean) rewritten} if necessary).
     * 
     * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
     * @param viewResponse The {@link net.www_eee.portal.Channel.ViewResponse ViewResponse} currently being generated.
     * @param proxiedFileURL The {@linkplain #getProxiedFileURL(Page.Request, Channel.Mode, boolean) proxied file URL}.
     * @param contentType The {@link MimeType} of the proxied file.
     * @throws WWWEEEPortal.Exception If a problem occurred while determining the result.
     * @throws WebApplicationException If a problem occurred while determining the result.
     */
    protected void renderResourceReferenceView(final Page.Request pageRequest, final ViewResponse viewResponse,
            final URL proxiedFileURL, final @Nullable MimeType contentType)
            throws WWWEEEPortal.Exception, WebApplicationException {
        final Element contentContainerElement = viewResponse.getContentContainerElement();
        final Document contentContainerDocument = DOMUtil.getDocument(contentContainerElement);

        final Element objectElement = DOMUtil.createElement(HTMLUtil.HTML_NS_URI, HTMLUtil.HTML_NS_PREFIX, "object",
                contentContainerElement, contentContainerDocument, true, true);

        final String[][] extraObjectClasses;
        if (contentType != null) {
            final String contentTypeString = contentType.toString();

            DOMUtil.createAttr(null, null, "type", contentTypeString, objectElement);

            final StringBuffer safeTypeClass = new StringBuffer();
            for (int i = 0; i < contentTypeString.length(); i++) {
                char c = contentTypeString.charAt(i);
                if (Character.isLetterOrDigit(c)) {
                    safeTypeClass.append(c);
                } else {
                    safeTypeClass.append('_');
                }
            }
            extraObjectClasses = new String[][] { new String[] { portal.getPortalID(), "channel",
                    channelDef.getID(), "resource", "type", safeTypeClass.toString() } };

        } else {
            extraObjectClasses = null;
        }
        setIDAndClassAttrs(objectElement, Arrays.asList("proxy", "resource"), extraObjectClasses, null);

        final URI resourceURI = rewriteProxiedFileLink(pageRequest, proxiedFileURL, null, false, false).getKey();
        DOMUtil.createAttr(null, null, "data", resourceURI.toString(), objectElement);

        return;
    }

    /**
     * This method will create a new {@link CacheControl} by intelligently merging the values from the
     * <code>newCacheControl</code> into the <code>defaultCacheControl</code> values, removing a default 'public'
     * directive if the input specifies a 'private' one, etc.
     * 
     * @param defaultCacheControl The base {@link CacheControl} values.
     * @param newCacheControl The {@link CacheControl} to use to update the <code>defaultCacheControl</code> values with.
     * @return A merged {@link CacheControl}.
     */
    protected static final @Nullable CacheControl mergeCacheControl(
            final @Nullable CacheControl defaultCacheControl, final @Nullable CacheControl newCacheControl) {
        if ((defaultCacheControl == null) || (defaultCacheControl.equals(newCacheControl)))
            return newCacheControl;
        final CacheControl mergedCacheControl = RESTUtil.copy(defaultCacheControl);
        if (newCacheControl == null)
            return mergedCacheControl;
        if (newCacheControl.getCacheExtension().containsKey("public")) {
            mergedCacheControl.getCacheExtension().put("public", null);
            mergedCacheControl.setPrivate(false);
            mergedCacheControl.getPrivateFields().clear();
            mergedCacheControl.setNoStore(false);
            mergedCacheControl.setNoCache(false);
            mergedCacheControl.getNoCacheFields().clear();
        }
        if (newCacheControl.isPrivate()) {
            mergedCacheControl.setPrivate(true);
            mergedCacheControl.getPrivateFields().clear();
            mergedCacheControl.getPrivateFields().addAll(newCacheControl.getPrivateFields());
            mergedCacheControl.getCacheExtension().remove("public");
        }
        if (newCacheControl.isNoCache()) {
            mergedCacheControl.setNoCache(true);
            mergedCacheControl.getNoCacheFields().clear();
            mergedCacheControl.getNoCacheFields().addAll(newCacheControl.getPrivateFields());
            mergedCacheControl.getCacheExtension().remove("public");
            mergedCacheControl.setMaxAge(-1);
            mergedCacheControl.setSMaxAge(-1);
        }
        if (newCacheControl.isNoStore()) {
            mergedCacheControl.setNoStore(true);
            mergedCacheControl.getCacheExtension().remove("public");
            mergedCacheControl.setMaxAge(-1);
            mergedCacheControl.setSMaxAge(-1);
        }
        if (newCacheControl.isNoTransform()) {
            mergedCacheControl.setNoTransform(true);
        }
        if (newCacheControl.isMustRevalidate()) {
            mergedCacheControl.setMustRevalidate(true);
        }
        if (newCacheControl.isProxyRevalidate()) {
            mergedCacheControl.setProxyRevalidate(true);
        }
        if (newCacheControl.getMaxAge() >= 0) {
            mergedCacheControl.setMaxAge(newCacheControl.getMaxAge());
            mergedCacheControl.setNoCache(false);
            mergedCacheControl.setNoStore(false);
        }
        if (newCacheControl.getSMaxAge() >= 0) {
            mergedCacheControl.setSMaxAge(newCacheControl.getSMaxAge());
            mergedCacheControl.setNoCache(false);
            mergedCacheControl.setNoStore(false);
        }
        return mergedCacheControl;
    }

    @Override
    protected void doViewRequestImpl(final Page.Request pageRequest, final ViewResponse viewResponse)
            throws WWWEEEPortal.Exception, WebApplicationException {
        final HttpClientContext proxyContext = doProxyRequest(pageRequest, Mode.VIEW);

        final @NonNull HttpResponse proxyResponse = proxyContext.getResponse();
        final URL proxiedFileURL = HttpUtil.getRequestTargetURL(proxyContext);

        try (final CloseableHttpClient proxyClient = Objects
                .requireNonNull((CloseableHttpClient) proxyContext.getAttribute(HTTP_CLIENT_CONTEXT_ID))) {

            final MimeType responseContentType = getProxyResponseHeader(pageRequest, proxyResponse, "Content-Type",
                    IOUtil::newMimeType);
            viewResponse.setContentType(
                    (responseContentType != null) ? RESTUtil.getMediaType(responseContentType) : null);

            viewResponse.setLastModified(getProxyResponseHeader(pageRequest, proxyResponse, "Last-Modified",
                    (s) -> ZonedDateTime.parse(s, DateTimeFormatter.RFC_1123_DATE_TIME).toInstant()));
            viewResponse.setCacheControl(mergeCacheControl(viewResponse.getCacheControl(),
                    getProxyResponseHeader(pageRequest, proxyResponse, "Cache-Control", CacheControl::valueOf)));
            viewResponse.setExpires(getProxyResponseHeader(pageRequest, proxyResponse, "Expires",
                    (s) -> ZonedDateTime.parse(s, DateTimeFormatter.RFC_1123_DATE_TIME).toInstant()));
            viewResponse
                    .setEntityTag(getProxyResponseHeader(pageRequest, proxyResponse, "ETag", EntityTag::valueOf));
            viewResponse.setLocale(getProxyResponseHeader(pageRequest, proxyResponse, "Content-Language",
                    (h) -> Locale.forLanguageTag(
                            CollUtil.first(Arrays.asList(StringUtil.COMMA_SEPARATED_PATTERN.split(h))).get())));

            if (isRenderedUsingXMLView(pageRequest, proxyResponse, responseContentType)) {
                renderXMLView(pageRequest, viewResponse, proxyResponse, proxiedFileURL, responseContentType);
            } else if (isRenderedUsingTextView(pageRequest, proxyResponse, responseContentType)) {
                renderTextView(pageRequest, viewResponse, proxyResponse, proxiedFileURL, responseContentType);
            } else {
                renderResourceReferenceView(pageRequest, viewResponse, proxiedFileURL, responseContentType);
            }

        } catch (WWWEEEPortal.Exception wpe) {
            LogAnnotation.annotate(wpe, "ProxyContext", proxyContext, null, false);
            LogAnnotation.annotate(wpe, "ProxyResponse", proxyResponse, null, false);
            LogAnnotation.annotate(wpe, "ProxiedFileURL", proxiedFileURL, null, false); // This wouldn't be necessary if any of the previous annotations could actually toString() themselves usefully.
            throw wpe;
        } catch (IOException ioe) {
            throw new WWWEEEPortal.OperationalException(ioe);
        }

        return;
    }

    @Override
    protected Response doResourceRequestImpl(final Page.Request pageRequest)
            throws WWWEEEPortal.Exception, WebApplicationException {
        final HttpClientContext proxyContext = doProxyRequest(pageRequest, Mode.RESOURCE);
        @SuppressWarnings("resource")
        final CloseableHttpClient proxyClient = Objects
                .requireNonNull((CloseableHttpClient) proxyContext.getAttribute(HTTP_CLIENT_CONTEXT_ID));

        final @NonNull HttpResponse proxyResponse = proxyContext.getResponse();

        try {

            final Response.ResponseBuilder responseBuilder = Response.ok();

            responseBuilder.lastModified(getProxyResponseHeader(pageRequest, proxyResponse, "Last-Modified",
                    (s) -> Date.from(ZonedDateTime.parse(s, DateTimeFormatter.RFC_1123_DATE_TIME).toInstant())));
            final MimeType responseContentType = getProxyResponseHeader(pageRequest, proxyResponse, "Content-Type",
                    IOUtil::newMimeType);
            responseBuilder.type((responseContentType != null) ? RESTUtil.getMediaType(responseContentType) : null);
            responseBuilder.cacheControl(mergeCacheControl(getCacheControlDefault().orElse(null),
                    getProxyResponseHeader(pageRequest, proxyResponse, "Cache-Control", CacheControl::valueOf)));
            responseBuilder.expires(getProxyResponseHeader(pageRequest, proxyResponse, "Expires",
                    (s) -> Date.from(ZonedDateTime.parse(s, DateTimeFormatter.RFC_1123_DATE_TIME).toInstant())));
            responseBuilder.tag(getProxyResponseHeader(pageRequest, proxyResponse, "ETag", EntityTag::valueOf));
            responseBuilder.language(getProxyResponseHeader(pageRequest, proxyResponse, "Content-Language",
                    (h) -> Locale.forLanguageTag(StringUtil.COMMA_SEPARATED_PATTERN.split("")[0]).toString()));

            final HttpEntity proxyResponseEntity = proxyResponse.getEntity();
            final Long contentLength = (proxyResponseEntity != null)
                    ? Long.valueOf(proxyResponseEntity.getContentLength())
                    : null;
            responseBuilder.header("Content-Length", contentLength);

            if (proxyResponseEntity != null) {
                responseBuilder.entity(HttpUtil.getDataSource(proxyResponseEntity, proxyClient));
            } else {
                try {
                    proxyClient.close();
                } catch (IOException ioe) {
                    throw new WWWEEEPortal.OperationalException(ioe);
                }
            }

            return responseBuilder.build();

        } catch (WWWEEEPortal.Exception wpe) {
            LogAnnotation.annotate(wpe, "ProxyContext", proxyContext, null, false);
            LogAnnotation.annotate(wpe, "ProxyResponse", proxyResponse, null, false);
            try {
                LogAnnotation.annotate(wpe, "ProxiedFileURL", HttpUtil.getRequestTargetURL(proxyContext), null,
                        false); // This wouldn't be necessary if any of the previous annotations could actually toString() themselves usefully.
            } catch (Exception e) {
            }
            throw wpe;
        }
    }

    /**
     * An {@link HttpRequestInterceptor} which is {@link AbstractHttpClient#addRequestInterceptor(HttpRequestInterceptor)
     * added} to all {@link HttpClient}'s {@linkplain ProxyChannel#createProxyClient(Page.Request) created} to
     * {@linkplain ProxyChannel#doProxyRequest(Page.Request, Channel.Mode) proxy} content for this channel, which, when
     * {@linkplain #process(HttpRequest, HttpContext) processed}, allows {@link net.www_eee.portal.Channel.Plugin 
     * plugin's} a {@linkplain #PROXY_REQUEST_INTERCEPTOR_HOOK hook} they can use to
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the
     * {@link HttpRequest}.
     * 
     * @see ProxyChannel#PROXY_REQUEST_INTERCEPTOR_HOOK
     */
    public final class ProxyRequestInterceptor implements HttpRequestInterceptor {
        /**
         * The {@link net.www_eee.portal.Page.Request Request} currently being processed.
         */
        protected final Page.Request pageRequest;

        /**
         * Construct a new <code>ProxyRequestInterceptor</code>.
         * 
         * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
         */
        public ProxyRequestInterceptor(final Page.Request pageRequest) {
            this.pageRequest = pageRequest;
            return;
        }

        @Override
        @SuppressWarnings("synthetic-access")
        public void process(final HttpRequest proxyRequest, final HttpContext proxyContext)
                throws HttpException, IOException {
            try {
                PROXY_REQUEST_INTERCEPTOR_HOOK.filter(plugins,
                        new Object[] { HttpClientContext.adapt(proxyContext) }, pageRequest, proxyRequest);
            } catch (WWWEEEPortal.Exception wpe) {
                final HttpException httpException = new HttpException();
                httpException.initCause(wpe);
                throw httpException;
            }
            return;
        }

    } // ProxyRequestInterceptor

    /**
     * An {@link HttpResponseInterceptor} which is
     * {@link AbstractHttpClient#addResponseInterceptor(HttpResponseInterceptor) added} to all {@link HttpClient}'s
     * {@linkplain ProxyChannel#createProxyClient(Page.Request) created} to
     * {@linkplain ProxyChannel#doProxyRequest(Page.Request, Channel.Mode) proxy} content for this channel, which, when
     * {@linkplain #process(HttpResponse, HttpContext) processed}, allows {@link net.www_eee.portal.Channel.Plugin 
     * plugin's} a {@linkplain ProxyChannel#PROXY_RESPONSE_INTERCEPTOR_HOOK hook} they can use to
     * {@linkplain net.www_eee.portal.WWWEEEPortal.PluginHook#filter(List, Object[], Page.Request, Object) filter} the
     * {@link HttpResponse}.
     * 
     * @see ProxyChannel#PROXY_RESPONSE_INTERCEPTOR_HOOK
     */
    public final class ProxyResponseInterceptor implements HttpResponseInterceptor {
        /**
         * The {@link net.www_eee.portal.Page.Request Request} currently being processed.
         */
        protected final Page.Request pageRequest;

        /**
         * Construct a new <code>ProxyResponseInterceptor</code>.
         * 
         * @param pageRequest The {@link net.www_eee.portal.Page.Request Request} currently being processed.
         */
        public ProxyResponseInterceptor(final Page.Request pageRequest) {
            this.pageRequest = pageRequest;
            return;
        }

        @Override
        @SuppressWarnings("synthetic-access")
        public void process(final HttpResponse proxyResponse, final HttpContext proxyContext)
                throws HttpException, IOException {
            try {
                PROXY_RESPONSE_INTERCEPTOR_HOOK.filter(plugins,
                        new Object[] { HttpClientContext.adapt(proxyContext) }, pageRequest, proxyResponse);
            } catch (WWWEEEPortal.Exception wpe) {
                final HttpException httpException = new HttpException();
                httpException.initCause(wpe);
                throw httpException;
            }
            return;
        }

    } // ProxyResponseInterceptor

}