/** Copyright (C) SYSTAP, LLC DBA Blazegraph 2014. All rights reserved. Contact: SYSTAP, LLC DBA Blazegraph 2501 Calvert ST NW #106 Washington, DC 20008 licenses@blazegraph.com This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package com.bigdata.rdf.sail.webapp.client; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.StringWriter; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.apache.log4j.Logger; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.util.InputStreamResponseListener; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; /** * Class handles the jetty {@link Response} input stream. */ public class JettyResponseListener extends InputStreamResponseListener { private static final transient Logger log = Logger .getLogger(JettyResponseListener.class); private final long queryTimeoutMillis; private volatile Request m_request; private volatile Response m_response; private volatile InputStream m_cachedStream = null; /** * Note: This is the default content encoding for <code>text/*</code> per * Section 3.7.1 Canonicalization and Text Defaults of the HTTP 1.1 * specification. */ private static final String ISO_8859_1 = "ISO-8859-1"; /** * * @param request * @param queryTimeoutMillis * the timeout in milliseconds (if non-positive, then an infinite * timeout is used). */ public JettyResponseListener(final Request request, long queryTimeoutMillis) { if (request == null) throw new IllegalArgumentException(); m_request = request; this.queryTimeoutMillis = queryTimeoutMillis <= 0L ? Long.MAX_VALUE : queryTimeoutMillis; } /** * Blocks (up to a timeout) for the response to arrive (http status code, * etc.) * * @throws IOException if there is a problem, if there is a timeout, etc. */ private void ensureResponse() throws IOException { if (m_response == null) { try { final boolean traceEnabled = log.isTraceEnabled(); final long start = traceEnabled ? System.currentTimeMillis() : 0; m_response = get(queryTimeoutMillis, TimeUnit.MILLISECONDS); if (traceEnabled) log.trace("Response in " + (System.currentTimeMillis() - start) + "ms"); } catch (InterruptedException | TimeoutException | ExecutionException e) { throw new IOException(e); } } } /** * Return the value of the <code>Content-Type</code> header. * @return * @throws IOException */ public String getContentType() throws IOException { ensureResponse(); final HttpFields headers = m_response.getHeaders(); return headers.get(HttpHeader.CONTENT_TYPE); } /** * Return the content encoding specified by the <code>charset</code> MIME * parameter for the <code>Content-Type</code> header and <code>null</code> * if that MIME type parameter was not specified. * <p> * Note: Per Section 3.7.1 Canonicalization and Text Defaults of the HTTP * 1.1 specification: * <ul> * <li> * The "charset" parameter is used with some media types to define the * character set (section 3.4) of the data. When no explicit charset * parameter is provided by the sender, media subtypes of the "text" type * are defined to have a default charset value of "ISO-8859-1" when received * via HTTP. Data in character sets other than "ISO-8859-1" or its subsets * MUST be labeled with an appropriate charset value. See section 3.4.1 for * compatibility problems.</li> * </ul> * * @return The content encoding if the <code>charset</code> parameter was * specified and otherwise <code>null</code>. * * @throws IOException */ public String getContentEncoding() throws IOException { ensureResponse(); final HttpFields headers = m_response.getHeaders(); final String contentType = headers.get(HttpHeader.CONTENT_TYPE); if (contentType == null) return null; final MiniMime mimeType = new MiniMime(contentType); return mimeType.getContentEncoding(); } /** * The http status code. */ public int getStatus() throws IOException { ensureResponse(); return m_response.getStatus(); } /** * The http reason line. */ public String getReason() throws IOException { ensureResponse(); return m_response.getReason(); } /** The http headers. */ public HttpFields getHeaders() throws IOException { ensureResponse(); return m_response.getHeaders(); } /** * Return the response body as a string. */ public String getResponseBody() throws IOException { final Reader r; { final String contentEncoding = getContentEncoding(); if (contentEncoding != null ) { /* * Explicit content encoding. */ r = new InputStreamReader(getInputStream(), contentEncoding); } else if (getContentType()!=null && getContentType().startsWith("text/")) { /** * Note: Per Section 3.7.1 Canonicalization and Text Defaults of * the HTTP 1.1 specification: * <p> * The "charset" parameter is used with some media types to * define the character set (section 3.4) of the data. When no * explicit charset parameter is provided by the sender, media * subtypes of the "text" type are defined to have a default * charset value of "ISO-8859-1" when received via HTTP. Data in * character sets other than "ISO-8859-1" or its subsets MUST be * labeled with an appropriate charset value. See section 3.4.1 * for compatibility problems. */ r = new InputStreamReader(getInputStream(),ISO_8859_1); } else { /* * Also per that section, no default otherwise. */ r = new InputStreamReader(getInputStream()); } } try { final StringWriter w = new StringWriter(); final char[] buf = new char[1024]; int rdlen = 0; while ((rdlen = r.read(buf)) != -1) { w.write(buf, 0, rdlen); } return w.toString(); } finally { r.close(); } } @Override public InputStream getInputStream() { if (m_cachedStream != null) { /* * This preserves the semantics of the method on the base class * since Jetty will return a closed input stream if you invoke this * method more than once. */ return super.getInputStream(); } // await a response up to a timeout. try { ensureResponse(); } catch (IOException e) { throw new RuntimeException(e); } // request the input stream. m_cachedStream = super.getInputStream(); return m_cachedStream; } /** * Abort the request/response. The request is associated with the http * request/response is aborted. If we already have the response, then it's * {@link InputStream} is closed. */ public void abort() { // Note: jetty requires a cause for request.abort(Throwable). abort(new IOException()); } /** * Abort the request/response. The request is associated with the http * request/response is aborted. If we already have the response, then it's * {@link InputStream} is closed. * * @param cause * The cause (required). */ public void abort(final Throwable cause) { final InputStream is = m_cachedStream; if (is != null) { m_cachedStream = null; try { is.close(); } catch (IOException ex) { log.warn(ex); } } final Request r = m_request; if (r != null) { m_request = null; r.abort(cause); } } }