org.opengeoportal.proxy.controllers.OldDynamicOgcController.java Source code

Java tutorial

Introduction

Here is the source code for org.opengeoportal.proxy.controllers.OldDynamicOgcController.java

Source

package org.opengeoportal.proxy.controllers;

/**
 * Adapted from David Smiley's HTTP reverse proxy/gateway servlet
 */
/**
* Copyright MITRE
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import org.apache.commons.lang.StringEscapeUtils;
import org.apache.http.*;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.AbortableHttpRequest;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.client.utils.URIUtils;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicHttpEntityEnclosingRequest;
import org.apache.http.message.HeaderGroup;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.util.EntityUtils;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.opengeoportal.metadata.*;
import org.opengeoportal.solr.*;
import org.opengeoportal.utilities.LocationFieldUtils;
import org.opengeoportal.utilities.OgpUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.w3c.dom.Node;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.BitSet;
import java.util.Enumeration;
import java.util.Formatter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/*@Controller
@RequestMapping("/dynamic")*/
public class OldDynamicOgcController {
    /* INIT PARAMETER NAME CONSTANTS */

    final Logger logger = LoggerFactory.getLogger(this.getClass());

    /* MISC */
    protected URI targetUri;
    protected HttpClient proxyClient;

    @Autowired
    private LayerInfoRetriever layerInfoRetriever;

    private DocumentBuilderFactory factory;
    private TransformerFactory transformerFactory;

    /**
    * An HTTP reverse proxy/gateway servlet. It is designed to be extended for customization
    * if desired. Most of the work is handled by
    * <a href="http://hc.apache.org/httpcomponents-client-ga/">Apache HttpClient</a>.
    * <p>
    * There are alternatives to a servlet based proxy such as Apache mod_proxy if that is available to you. However
    * this servlet is easily customizable by Java, secure-able by your web application's security (e.g. spring-security),
    * portable across servlet engines, and is embeddable into another web application.
    * </p>
    * <p>
    * Inspiration: http://httpd.apache.org/docs/2.0/mod/mod_proxy.html
    * </p>
    *
    * @author David Smiley dsmiley@mitre.org>
    */

    OldDynamicOgcController() {
        HttpParams hcParams = new BasicHttpParams();
        hcParams.setBooleanParameter(ClientPNames.HANDLE_REDIRECTS, true);
        proxyClient = createHttpClient(hcParams);

        // Create a factory
        factory = DocumentBuilderFactory.newInstance();
        //ignore validation, dtd
        factory.setAttribute("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
        factory.setValidating(false);

        transformerFactory = TransformerFactory.newInstance();
    }

    /** Called from {@link #init(javax.servlet.ServletConfig)}. HttpClient offers many opportunities for customization.
    * @param hcParams*/
    @SuppressWarnings("deprecation")
    protected HttpClient createHttpClient(HttpParams hcParams) {
        return new DefaultHttpClient(new ThreadSafeClientConnManager(), hcParams);
    }

    public void destroy() {
        //shutdown() must be called according to documentation.
        if (proxyClient != null)
            proxyClient.getConnectionManager().shutdown();
    }

    private class UrlToNameContainer {
        Set<String> qualifiedNames;
        String wfsUrl;
    }

    @RequestMapping(value = "/wfs", method = RequestMethod.GET, params = "request=GetCapabilities")
    public ModelAndView doWfsGetCapabilitiesCase(@RequestParam("ogpids") Set<String> layerIds,
            HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws Exception {
        return doWfsGetCapabilities(layerIds, servletRequest, servletResponse);
    }

    @RequestMapping(value = "/wfs", method = RequestMethod.GET, params = "REQUEST=GetCapabilities")
    public ModelAndView doWfsGetCapabilities(@RequestParam("ogpids") Set<String> layerIds,
            HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws Exception {
        logger.info("wfs get capabilities requested");
        List<SolrRecord> solrRecords = null;
        try {
            solrRecords = this.layerInfoRetriever.fetchAllLayerInfo(layerIds);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ServletException("Unable to retrieve layer info.");
        }
        //need to pass a model to the caps document

        //parse the returned XML
        // Use document builder factory
        DocumentBuilder builder = factory.newDocumentBuilder();

        Map<String, UrlToNameContainer> recordMap = new HashMap<String, UrlToNameContainer>();
        for (SolrRecord solrRecord : solrRecords) {
            //we have to get all of the wfs service points for the passed layerids.  match layerids to service points, so we only have to process each caps document once
            //in the future, we should cache these caps documents
            String workspaceName = solrRecord.getWorkspaceName();
            String layerName = solrRecord.getName();

            String qualifiedName = OgpUtils.getLayerNameNS(workspaceName, layerName);
            String wfsUrl = LocationFieldUtils.getWfsUrl(solrRecord.getLocation());

            URI currentURI = new URI(wfsUrl);
            //is it ok to call these equivalent?
            String currentURIString = currentURI.getScheme() + currentURI.getHost() + currentURI.getPath();
            if (recordMap.containsKey(currentURIString)) {
                UrlToNameContainer urlMap = recordMap.get(currentURIString);
                logger.info(qualifiedName);
                urlMap.qualifiedNames.add(qualifiedName);
            } else {
                UrlToNameContainer urlMap = new UrlToNameContainer();
                urlMap.wfsUrl = wfsUrl;
                Set<String> qNamesSet = new HashSet<String>();
                qNamesSet.add(qualifiedName);
                logger.info(qualifiedName);
                urlMap.qualifiedNames = qNamesSet;

                recordMap.put(currentURIString, urlMap);
            }
        }

        String version = "1.0.0";
        String currentUrl = "";
        String wfsQueryBoilerPlate = "?version=" + version + "&service=wfs";
        String capabilitiesQuery = "&request=GetCapabilities";
        String featureTypeInfo = "";
        for (UrlToNameContainer container : recordMap.values()) {
            //this should happen asynchronously
            currentUrl = container.wfsUrl;
            HttpResponse response = proxyClient
                    .execute(new HttpGet(currentUrl + wfsQueryBoilerPlate + capabilitiesQuery));
            InputStream inputStream = response.getEntity().getContent();
            //Parse the document
            Document document = builder.parse(inputStream);
            inputStream.close();

            NodeList layerNodeList = document.getElementsByTagName("Name");
            if (layerNodeList.getLength() == 0) {
                throw new Exception("Malformed GetCapabilities Document.");
            }
            /*
             * <FeatureType><Name>sde:GISPORTAL.GISOWNER01.AFGHANISTANRIVERREGION97</Name><Title>GISPORTAL.GISOWNER01.AFGHANISTANRIVERREGION97</Title><Abstract/><Keywords>ArcSDE, GISPORTAL.GISOWNER01.AFGHANISTANRIVERREGION97</Keywords><SRS>EPSG:100004</SRS><LatLongBoundingBox minx="60.82625305019409" miny="29.95629731861914" maxx="74.6959181471344" maxy="38.59658289704833"/></FeatureType>
             * 
             */
            for (int j = 0; j < layerNodeList.getLength(); j++) {
                Node currentLayerNode = layerNodeList.item(j);
                String layerName = currentLayerNode.getTextContent().toLowerCase();
                if (OgpUtils.getSetAsLowerCase(container.qualifiedNames).contains(layerName)) {
                    featureTypeInfo += xmlToString(currentLayerNode.getParentNode());
                }

            }

        }

        String onlineResource = "";
        String describeFeatureUrl = "";
        String getFeatureUrl = "";

        if (recordMap.values().size() == 1) {
            //this is a special case...
            //if every layer is from a single server, pass that server value into the caps doc for describelayer and getfeature.  that way, clients that do the right thing will bypass this ogp service
            //otherwise, everything must be proxied
            onlineResource = currentUrl;
            describeFeatureUrl = currentUrl + wfsQueryBoilerPlate + "&request=DescribeFeatureType";
            getFeatureUrl = currentUrl + wfsQueryBoilerPlate + "&request=GetFeature";
        } else {
            //values for describelayer and getFeature should refer back to this controller
            String thisUrl = servletRequest.getRequestURL().toString() + "?";
            onlineResource = thisUrl + "ogpids=" + servletRequest.getParameter("ogpids");
            describeFeatureUrl = thisUrl + "request=DescribeFeatureType";
            getFeatureUrl = thisUrl + "request=GetFeature";
        }
        ModelAndView mav = new ModelAndView("wfs_caps_1_0_0");

        mav.addObject("onlineResource", StringEscapeUtils.escapeXml(onlineResource));
        mav.addObject("getCapabilities", StringEscapeUtils
                .escapeXml(servletRequest.getRequestURL().toString() + "?" + servletRequest.getQueryString()));
        mav.addObject("describeFeatureUrl", StringEscapeUtils.escapeXml(describeFeatureUrl));
        mav.addObject("getFeatureUrl", StringEscapeUtils.escapeXml(getFeatureUrl));
        mav.addObject("featureTypeInfo", featureTypeInfo);

        servletResponse.setHeader("Content-Disposition", "inline;filename=GetCapabilities.xml");
        return mav;

    }

    @RequestMapping(value = "/wfs", method = RequestMethod.GET)
    public void doWfsRequest(@RequestParam("ogpids") Set<String> layerIds, HttpServletRequest servletRequest,
            HttpServletResponse servletResponse) throws Exception {
        Enumeration paramNames = servletRequest.getParameterNames();
        String ogcRequest = "";
        String typeName = "";
        while (paramNames.hasMoreElements()) {
            String param = (String) paramNames.nextElement();
            if (param.equalsIgnoreCase("version")) {

            } else if (param.equalsIgnoreCase("request")) {
                logger.info("request: " + servletRequest.getParameter(param));
                ogcRequest = servletRequest.getParameter(param);
            } else if (param.equalsIgnoreCase("typename")) {
                typeName = servletRequest.getParameter(param);
            }
        }

        if (ogcRequest.equalsIgnoreCase("describefeaturetype") || ogcRequest.equalsIgnoreCase("getfeature")) {
            //TODO: strip all the params and rebuild the request with only sanctioned parameters, in case of fussy servers
            String remoteUrl = getOgcUrlFromLayerName(typeName, "wfs");
            String newQuery = removeParamFromQuery(servletRequest.getQueryString(), "ogpids");
            if (ogcRequest.equalsIgnoreCase("describefeaturetype")) {
                newQuery = removeParamFromQuery(newQuery, "srsname");
            }
            remoteUrl += "?" + newQuery;
            logger.info("remote url:" + remoteUrl);
            doProxy(remoteUrl, servletRequest, servletResponse);
        }

    }

    private String xmlToString(Node node) throws TransformerException {
        StringWriter stw = new StringWriter();
        Transformer transformer = transformerFactory.newTransformer();
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
        transformer.transform(new DOMSource(node), new StreamResult(stw));
        return stw.toString();
    }

    public static String removeParamFromQuery(String query, String param) {
        if (query.startsWith("?")) {
            query = query.substring(1);
        }
        String[] arrQuery = query.split("&");
        String newQuery = "";
        for (int i = 0; i < arrQuery.length; i++) {
            String currentParam = arrQuery[i].substring(0, arrQuery[i].indexOf("="));
            if (!currentParam.equalsIgnoreCase(param)) {
                newQuery += arrQuery[i] + "&";
            }
        }
        newQuery = newQuery.substring(0, newQuery.length() - 1);
        return newQuery;
    }

    private String getOgcUrlFromLayerName(String layerName, String ogcProtocol) throws Exception {
        SolrQuery query = new SolrQuery();

        if (layerName.contains(":")) {
            String[] arrName = layerName.split(":");
            layerName = arrName[1];
        }

        String queryText = "Name:" + layerName;

        query.setQuery(queryText);
        QueryResponse queryResponse = this.layerInfoRetriever.getSolrServer().query(query);
        List<SolrRecord> records = queryResponse.getBeans(SolrRecord.class);
        if (records.isEmpty()) {
            throw new Exception("No matching record found in Solr Index for ['" + layerName + "']");
        }
        String location = records.get(0).getLocation();

        if (ogcProtocol.equalsIgnoreCase("wfs")) {
            return LocationFieldUtils.getWfsUrl(location);

        } else if (ogcProtocol.equalsIgnoreCase("wms")) {
            return LocationFieldUtils.getWmsUrl(location);

        } else if (ogcProtocol.equalsIgnoreCase("wcs")) {
            return LocationFieldUtils.getWcsUrl(location);
        } else {
            throw new Exception("Unsupported OGC Protocol ['" + ogcProtocol + "']");
        }

    }

    @SuppressWarnings("deprecation")
    private void doProxy(String remoteUrl, HttpServletRequest servletRequest, HttpServletResponse servletResponse)
            throws ServletException, IOException {
        // Make the Request
        //note: we won't transfer the protocol version because I'm not sure it would truly be compatible
        try {
            this.targetUri = new URI(remoteUrl);
        } catch (URISyntaxException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        }

        //Need to handle https, but think about "restricted" layers for now.  Some institutions don't really have good protection for restricted layers.  Does this open up potential for security
        //problems for those folks?
        if (servletRequest.getScheme().equals("https")) {
            //actually, what matters the most is if the remote url is https
        }

        BasicHttpEntityEnclosingRequest proxyRequest = new BasicHttpEntityEnclosingRequest(
                servletRequest.getMethod(), rewriteUrlFromRequest(servletRequest));

        copyRequestHeaders(servletRequest, proxyRequest);

        // Add the input entity (streamed) then execute the request.
        HttpResponse proxyResponse = null;
        InputStream servletRequestInputStream = servletRequest.getInputStream();
        try {
            try {
                proxyRequest.setEntity(
                        new InputStreamEntity(servletRequestInputStream, servletRequest.getContentLength()));

                // Execute the request
                logger.debug("proxy " + servletRequest.getMethod() + " uri: " + servletRequest.getRequestURI()
                        + " -- " + proxyRequest.getRequestLine().getUri());

                proxyResponse = proxyClient.execute(URIUtils.extractHost(targetUri), proxyRequest);
            } finally {
                closeQuietly(servletRequestInputStream);
            }

            // Process the response
            int statusCode = proxyResponse.getStatusLine().getStatusCode();
            logger.info("Status from remote server: " + Integer.toString(statusCode));
            if (doResponseRedirectOrNotModifiedLogic(servletRequest, servletResponse, proxyResponse, statusCode)) {
                EntityUtils.consume(proxyResponse.getEntity());
                return;
            }

            // Pass the response code. This method with the "reason phrase" is deprecated but it's the only way to pass the
            // reason along too.
            //noinspection deprecation
            servletResponse.setStatus(statusCode, proxyResponse.getStatusLine().getReasonPhrase());

            copyResponseHeaders(proxyResponse, servletResponse);

            // Send the content to the client
            copyResponseEntity(proxyResponse, servletResponse);

        } catch (Exception e) {
            //abort request, according to best practice with HttpClient
            if (proxyRequest instanceof AbortableHttpRequest) {
                AbortableHttpRequest abortableHttpRequest = (AbortableHttpRequest) proxyRequest;
                abortableHttpRequest.abort();
            }
            if (e instanceof RuntimeException)
                throw (RuntimeException) e;
            if (e instanceof ServletException)
                throw (ServletException) e;
            throw new RuntimeException(e);
        }
    }

    private boolean doResponseRedirectOrNotModifiedLogic(HttpServletRequest servletRequest,
            HttpServletResponse servletResponse, HttpResponse proxyResponse, int statusCode)
            throws ServletException, IOException {
        // Check if the proxy response is a redirect
        // The following code is adapted from org.tigris.noodle.filters.CheckForRedirect
        if (statusCode >= HttpServletResponse.SC_MULTIPLE_CHOICES /* 300 */
                && statusCode < HttpServletResponse.SC_NOT_MODIFIED /* 304 */) {
            Header locationHeader = proxyResponse.getLastHeader(HttpHeaders.LOCATION);
            if (locationHeader == null) {
                throw new ServletException("Received status code: " + statusCode + " but no " + HttpHeaders.LOCATION
                        + " header was found in the response");
            }
            // Modify the redirect to go to this proxy servlet rather that the proxied host
            String locStr = rewriteUrlFromResponse(servletRequest, locationHeader.getValue());

            servletResponse.sendRedirect(locStr);
            return true;
        }
        // 304 needs special handling. See:
        // http://www.ics.uci.edu/pub/ietf/http/rfc1945.html#Code304
        // We get a 304 whenever passed an 'If-Modified-Since'
        // header and the data on disk has not changed; server
        // responds w/ a 304 saying I'm not going to send the
        // body because the file has not changed.
        if (statusCode == HttpServletResponse.SC_NOT_MODIFIED) {
            servletResponse.setIntHeader(HttpHeaders.CONTENT_LENGTH, 0);
            servletResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            return true;
        }
        return false;
    }

    protected void closeQuietly(Closeable closeable) {
        try {
            closeable.close();
        } catch (IOException e) {
            logger.error(e.getMessage(), e);
        }
    }

    /** These are the "hop-by-hop" headers that should not be copied.
    * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
    * I use an HttpClient HeaderGroup class instead of Set<String> because this
    * approach does case insensitive lookup faster.
    */
    private static final HeaderGroup hopByHopHeaders;
    static {
        hopByHopHeaders = new HeaderGroup();
        String[] headers = new String[] { "Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization",
                "TE", "Trailers", "Transfer-Encoding", "Upgrade" };
        for (String header : headers) {
            hopByHopHeaders.addHeader(new BasicHeader(header, null));
        }
    }

    /** Copy request headers from the servlet client to the proxy request. */
    protected void copyRequestHeaders(HttpServletRequest servletRequest, HttpRequest proxyRequest) {
        // Get an Enumeration of all of the header names sent by the client
        Enumeration enumerationOfHeaderNames = servletRequest.getHeaderNames();
        while (enumerationOfHeaderNames.hasMoreElements()) {
            String headerName = (String) enumerationOfHeaderNames.nextElement();
            //TODO why?
            // if (headerName.equalsIgnoreCase(HttpHeaders.CONTENT_LENGTH))
            // continue;
            if (hopByHopHeaders.containsHeader(headerName))
                continue;
            // As per the Java Servlet API 2.5 documentation:
            // Some headers, such as Accept-Language can be sent by clients
            // as several headers each with a different value rather than
            // sending the header as a comma separated list.
            // Thus, we get an Enumeration of the header values sent by the client
            Enumeration headers = servletRequest.getHeaders(headerName);
            while (headers.hasMoreElements()) {
                String headerValue = (String) headers.nextElement();
                //Don't do this unless we need to
                /*if (headerName.equalsIgnoreCase(HttpHeaders.USER_AGENT)){
                   headerValue = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:24.0) Gecko/20100101 Firefox/24.0";
                }*/
                // In case the proxy host is running multiple virtual servers,
                // rewrite the Host header to ensure that we get content from
                // the correct virtual server
                if (headerName.equalsIgnoreCase(HttpHeaders.HOST)) {
                    HttpHost host = URIUtils.extractHost(this.targetUri);
                    headerValue = host.getHostName();
                    if (host.getPort() != -1)
                        headerValue += ":" + host.getPort();
                }
                proxyRequest.addHeader(headerName, headerValue);
            }
        }
    }

    /** Copy proxied response headers back to the servlet client. */
    protected void copyResponseHeaders(HttpResponse proxyResponse, HttpServletResponse servletResponse) {
        for (Header header : proxyResponse.getAllHeaders()) {
            if (hopByHopHeaders.containsHeader(header.getName()))
                continue;
            servletResponse.addHeader(header.getName(), header.getValue());
        }
    }

    /** Copy response body data (the entity) from the proxy to the servlet client. */
    private void copyResponseEntity(HttpResponse proxyResponse, HttpServletResponse servletResponse)
            throws IOException {
        HttpEntity entity = proxyResponse.getEntity();
        if (entity != null) {
            OutputStream servletOutputStream = servletResponse.getOutputStream();
            try {
                entity.writeTo(servletOutputStream);
            } finally {
                closeQuietly(servletOutputStream);
            }
        }
    }

    private String rewriteUrlFromRequest(HttpServletRequest servletRequest) {
        StringBuilder uri = new StringBuilder(500);
        uri.append(this.targetUri.toString());

        // Handle the query string
        /* String queryString = servletRequest.getQueryString();//ex:(following '?'): name=value&foo=bar#fragment
         if (queryString != null && queryString.length() > 0) {
           uri.append('?');
           int fragIdx = queryString.indexOf('#');
           String queryNoFrag = (fragIdx < 0 ? queryString : queryString.substring(0,fragIdx));
           uri.append(encodeUriQuery(queryNoFrag));
           if (fragIdx >= 0) {
             uri.append('#');
             uri.append(encodeUriQuery(queryString.substring(fragIdx + 1)));
           }
         }*/
        //skip this for now

        //  http://giswebservices.massgis.state.ma.us/geoserver/wfs?service=wfs&version=1.0.0&request=getFeature&typename=massgis:MORIS.RFI_AIS_GT50_POLY
        //?ogpids=MassGIS.MORIS.RFI_AIS_GT50_POLY&service=wfs&version=1.0.0&request=getFeature&typename=massgis:MORIS.RFI_AIS_GT50_POLY
        logger.info("new url string: " + uri.toString());
        return uri.toString();
    }

    private String rewriteUrlFromResponse(HttpServletRequest servletRequest, String theUrl) {
        //TODO document example paths
        if (theUrl.startsWith(this.targetUri.toString())) {
            String curUrl = servletRequest.getRequestURL().toString();//no query
            String pathInfo = servletRequest.getPathInfo();
            if (pathInfo != null) {
                assert curUrl.endsWith(pathInfo);
                curUrl = curUrl.substring(0, curUrl.length() - pathInfo.length());//take pathInfo off
            }
            theUrl = curUrl + theUrl.substring(this.targetUri.toString().length());
        }
        return theUrl;
    }

    /**
    * <p>Encodes characters in the query or fragment part of the URI.
    *
    * <p>Unfortunately, an incoming URI sometimes has characters disallowed by the spec. HttpClient
    * insists that the outgoing proxied request has a valid URI because it uses Java's {@link URI}. To be more
    * forgiving, we must escape the problematic characters. See the URI class for the spec.
    *
    * @param in example: name=value&foo=bar#fragment
    */
    static CharSequence encodeUriQuery(CharSequence in) {
        //Note that I can't simply use URI.java to encode because it will escape pre-existing escaped things.
        StringBuilder outBuf = null;
        Formatter formatter = null;
        for (int i = 0; i < in.length(); i++) {
            char c = in.charAt(i);
            boolean escape = true;
            if (c < 128) {
                if (asciiQueryChars.get((int) c)) {
                    escape = false;
                }
            } else if (!Character.isISOControl(c) && !Character.isSpaceChar(c)) {//not-ascii
                escape = false;
            }
            if (!escape) {
                if (outBuf != null)
                    outBuf.append(c);
            } else {
                //escape
                if (outBuf == null) {
                    outBuf = new StringBuilder(in.length() + 5 * 3);
                    outBuf.append(in, 0, i);
                    formatter = new Formatter(outBuf);
                }
                //leading %, 0 padded, width 2, capital hex
                formatter.format("%%%02X", (int) c);//TODO
                formatter.close();
            }
        }
        return outBuf != null ? outBuf : in;
    }

    static final BitSet asciiQueryChars;
    static {
        char[] c_unreserved = "_-!.~'()*".toCharArray();//plus alphanum
        char[] c_punct = ",;:$&+=".toCharArray();
        char[] c_reserved = "?/[]@".toCharArray();//plus punct

        asciiQueryChars = new BitSet(128);
        for (char c = 'a'; c <= 'z'; c++)
            asciiQueryChars.set((int) c);
        for (char c = 'A'; c <= 'Z'; c++)
            asciiQueryChars.set((int) c);
        for (char c = '0'; c <= '9'; c++)
            asciiQueryChars.set((int) c);
        for (char c : c_unreserved)
            asciiQueryChars.set((int) c);
        for (char c : c_punct)
            asciiQueryChars.set((int) c);
        for (char c : c_reserved)
            asciiQueryChars.set((int) c);

        asciiQueryChars.set((int) '%');//leave existing percent escapes in place
    }

}