/* * Copyright 1998-2009 University Corporation for Atmospheric Research/Unidata * * Portions of this software were developed by the Unidata Program at the * University Corporation for Atmospheric Research. * * Access and use of this software shall impose the following obligations * and understandings on the user. The user is granted the right, without * any fee or cost, to use, copy, modify, alter, enhance and distribute * this software, and any derivative works thereof, and its supporting * documentation for any purpose whatsoever, provided that this entire * notice appears in all copies of the software, derivative works and * supporting documentation. Further, UCAR requests that the user credit * UCAR/Unidata in any publications that result from the use of this * software or in any product that includes this software. The names UCAR * and/or Unidata, however, may not be used in any advertising or publicity * to endorse or promote any products or commercial entity unless specific * written permission is obtained from UCAR/Unidata. The user also * understands that UCAR/Unidata is not obligated to provide the user with * any support, consulting, training or assistance of any kind with regard * to the use, operation and performance of this software nor to provide * the user with any updates, revisions, new versions or "bug fixes." * * THIS SOFTWARE IS PROVIDED BY UCAR/UNIDATA "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL UCAR/UNIDATA BE LIABLE FOR ANY SPECIAL, * INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING * FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION * WITH THE ACCESS, USE OR PERFORMANCE OF THIS SOFTWARE. */ package ucar.httpservices; import org.apache.http.*; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.*; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.message.BasicHeader; import org.apache.http.message.BasicHttpResponse; import org.apache.http.util.EntityUtils; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.Set; /** * HTTPMethod is the encapsulation of specific * kind of server request: GET, HEAD, POST, etc. * The general processing sequence is as follows. * <ol> * <li> Create an HTTPMethod object using one of the * methods of HTTPFactory (e.g. HTTPFactory.Get()). * <p> * <li> Set parameters and headers of the returned HTTPMethod instance. * <p> * <li> Invoke the execute() method to actually make * the request. * <p> * <li> Extract response headers. * <p> * <li> Extract any body of the response in one of several forms: * an Inputstream, a byte array, or a String. * <p> * <li> Close the method. * </ol> * In practice, one has an HTTPMethod instance, one can * repeat the steps 2-5. Of course this assumes that one is * doing the same kind of action (e.g. GET). * <p> * The arguments to the factory method are as follows. * <ul> * <li> An HTTPSession instance (optional). * <li> A URL. * </ul> * An HTTPMethod instance is assumed to be operating in the context * of an HTTPSession instance as specified by the session argument. * If not present, the HTTPMethod instance will create a session * that will be reclaimed when the method is closed * (see the discussion about one-shot operation below). * <p> * Method URLs may be specified in any of three ways. * <ol> * <li> It may be inherited from the URL specified when * the session was created. * <p> * <li> It may be specified as part of the HTTPMethod * constructor (via the factory). If none is specified, * then the session URL is used. * <p> * <li> It may be specified as an argument to the * execute() method. If none is specified, then * the factory constructor URL is used (which might, * in turn have come from the session). * </ol> * <p> * Legal url arguments to HTTPMethod are constrained by the URL * specified in creating the HTTPSession instance, if any. If * the session was constructed with a specified URL, then any * url specified to HTTMethod (via the factory or via * execute()) must be "compatible" with the session URL). The * term "compatible" basically means that the session url's host+port * is the same as that of the specified method url. This * maintains the semantics of the Session but allows * flexibility in accessing data from the server. * <p> * <u>One-Shot Operation:</u> * A reasonably common use case is when a client * wants to create a method, execute it, get the response, * and close the method. For this use case, creating a session * and making sure it gets closed can be a tricky proposition. * To support this use case, HTTPMethod supports what amounts * to a one-shot use. The steps are as follows: * <ol> * <li> HTTPMethod method = HTTPFactory.Get(<url string>); note * that this implicitly creates a session internal to the * method instance. * <p> * <li> Set any session parameters or headers using method.getSession().setXXX * <p> * <li> Set any parameters and headers on method * <p> * <li> method.execute(); * <p> * <li> Get any response method headers * <p> * <li> InputStream stream = method.getResponseBodyAsStream() * <p> * <li> process the stream * <p> * <li> stream.close() * </ol> * There are several things to note. * <ul> * <li> Closing the stream will close the underlying method, so it is not * necessary to call method.close(). * <li> However, if you, for example, get the response body using getResponseBodyAsString(), * then you need to explicitly call method.close(). * <li> Closing the method (directly or through stream.close()) * will close the one-shot session created by the method. * </ul> */ public class HTTPMethod implements Closeable { ////////////////////////////////////////////////// // Static Methods static public Set<String> getAllowedMethods() { HttpResponse rs = new BasicHttpResponse(new ProtocolVersion("http", 1, 1), 0, ""); Set<String> set = new HttpOptions().getAllowedMethods(rs); return set; } ////////////////////////////////////////////////// // Instance fields protected HTTPSession session = null; protected boolean localsession = false; protected URI methodurl = null; protected String userinfo = null; protected HttpEntity content = null; protected HTTPSession.Methods methodkind = null; protected HTTPMethodStream methodstream = null; // wrapper for strm protected boolean closed = false; protected HttpRequestBase request = null; protected CloseableHttpResponse response = null; protected long[] range = null; protected List<Header> headers = new ArrayList<Header>(); ////////////////////////////////////////////////// // Constructor(s) // These are package scope to prevent public instantiation HTTPMethod() throws HTTPException { } HTTPMethod(HTTPSession.Methods m, String url) throws HTTPException { this(m, null, url); } HTTPMethod(HTTPSession.Methods m, HTTPSession session) throws HTTPException { this(m, session, null); } HTTPMethod(HTTPSession.Methods m, HTTPSession session, String url) throws HTTPException { url = HTTPUtil.nullify(url); if(url == null && session != null) url = session.getSessionURI(); if(url == null) throw new HTTPException("HTTPMethod: cannot find usable url"); try { this.methodurl = HTTPUtil.parseToURI(url); /// validate } catch (URISyntaxException mue) { throw new HTTPException("Malformed URL: " + url, mue); } // Check method and url compatibiltiy if(session == null) { session = HTTPFactory.newSession(url); localsession = true; } this.session = session; this.userinfo = HTTPUtil.nullify(this.methodurl.getUserInfo()); if(this.userinfo != null) { this.methodurl = HTTPUtil.uriExclude(this.methodurl, HTTPUtil.URIPart.USERINFO); // convert userinfo to credentials this.session.setCredentials( new UsernamePasswordCredentials(this.userinfo)); } this.session.addMethod(this); this.methodkind = m; } protected RequestBuilder buildrequest() throws HTTPException { if(this.methodurl == null) throw new HTTPException("Null url"); RequestBuilder rb = null; switch (this.methodkind) { case Put: rb = RequestBuilder.put(); break; case Post: rb = RequestBuilder.post(); break; case Head: rb = RequestBuilder.head(); break; case Options: rb = RequestBuilder.options(); break; case Get: default: rb = RequestBuilder.get(); break; } rb.setUri(this.methodurl); return rb; } protected void setheaders(RequestBuilder rb) { if(range != null) { rb.addHeader("Range", "bytes=" + range[0] + "-" + range[1]); range = null; } // Add any defined headers if(this.headers.size() > 0) { for(Header h : this.headers) { rb.addHeader(h); } } } protected void setcontent(RequestBuilder rb) { switch (this.methodkind) { case Put: if(this.content != null) rb.setEntity(this.content); break; case Post: if(this.content != null) rb.setEntity(this.content); break; case Head: case Get: case Options: default: break; } this.content = null; // do not reuse } ////////////////////////////////////////////////// // Execution support /** * Create a request, add headers, and content, * then send to HTTPSession to do the bulk of the work. */ public int execute() throws HTTPException { if(closed) throw new HTTPException("HTTPMethod: attempt to execute closed method"); if(this.methodurl == null) throw new HTTPException("HTTPMethod: no url specified"); if(!localsession && !sessionCompatible(this.methodurl)) throw new HTTPException("HTTPMethod: session incompatible url: " + this.methodurl); RequestBuilder rb = buildrequest(); try { // Apply settings setcontent(rb); // Add any user defined headers setheaders(rb); // use the session to do the heavy lifting. HTTPSession.ExecState estate = session.execute(this, methodurl, rb); this.request = estate.request; this.response = estate.response; if(this.request == null || this.response == null) throw new IllegalStateException("HTTPMethod.execute: request or response was null"); HttpClientContext execcontext = session.getExecutionContext(); int code = this.response.getStatusLine().getStatusCode(); return code; } catch (Exception ie) { throw new HTTPException(ie); } } /** * Calling close will force the method to close, and will * force any open stream to terminate. If the session is local, * Then that too will be closed. */ public synchronized void close() { if(closed) return; // multiple calls ok closed = true; // mark as closed to prevent recursive calls if(methodstream != null) { try { methodstream.close(); } catch (IOException ioe) {/*failure is ok*/} ; methodstream = null; } //this.request = null; if(session != null) { session.removeMethod(this); if(localsession) { session.close(); session = null; } } // finally, make this reusable if(this.request != null) this.request.reset(); } ////////////////////////////////////////////////// // Accessors public String getMethodKind() { return this.methodkind.name(); } public int getStatusCode() { return (this.response == null) ? 0 : this.response.getStatusLine().getStatusCode(); } public String getStatusLine() { return (this.response == null) ? null : this.response.getStatusLine().toString(); } public String getRequestLine() { //fix: return (method == null ? null : request.getRequestLine().toString()); throw new UnsupportedOperationException("getrequestline not implemented"); } public String getPath() { return this.methodurl.toString(); } public boolean canHoldContent() { return (this.methodkind == HTTPSession.Methods.Head); } public InputStream getResponseBodyAsStream() { return getResponseAsStream(); } public InputStream getResponseAsStream() { if(closed) throw new IllegalStateException("HTTPMethod: method is closed"); if(this.methodstream != null) { // duplicate: caller's problem HTTPSession.log.warn("HTTPRequest.getResponseBodyAsStream: Getting method stream multiple times"); } else { // first time HTTPMethodStream stream = null; try { if(this.response == null) return null; stream = new HTTPMethodStream(this.response.getEntity().getContent(), this); } catch (Exception e) { stream = null; } this.methodstream = stream; } return this.methodstream; } public byte[] getResponseAsBytes(int maxbytes) { byte[] contents = getResponseAsBytes(); if(contents != null && contents.length > maxbytes) { byte[] result = new byte[maxbytes]; System.arraycopy(contents, 0, result, 0, maxbytes); contents = result; } return contents; } public byte[] getResponseAsBytes() { if(closed) throw new IllegalStateException("HTTPMethod: method is closed"); byte[] content = null; if(this.response != null) try { content = EntityUtils.toByteArray(this.response.getEntity()); } catch (Exception e) {/*ignore*/} return content; } public String getResponseAsString(String charset) { if(closed) throw new IllegalStateException("HTTPMethod: method is closed"); String content = null; if(this.response != null) try { Charset cset = Charset.forName(charset); content = EntityUtils.toString(this.response.getEntity(), cset); } catch (Exception e) { throw new IllegalArgumentException(e.getMessage()); } close();//getting the response will disallow later stream return content; } public String getResponseAsString() { return getResponseAsString("UTF-8"); } public Header getRequestHeader(String name) { Header[] hdrs = getRequestHeaders(); for(Header h : hdrs) { if(h.getName().equals(name)) return h; } return null; } public Header getResponseHeader(String name) { try { return this.response == null ? null : this.response.getFirstHeader(name); } catch (Exception e) { return null; } } public Header[] getResponseHeaders() { try { if(this.response == null) return null; Header[] hs = this.response.getAllHeaders(); return hs; } catch (Exception e) { return null; } } public HTTPMethod setRequestContent(HttpEntity content) { this.content = content; return this; } public String getCharSet() { return "UTF-8"; } public String getURL() { return this.methodurl.toString(); } /* public String getProtocolVersion() { String ver = null; if(request != null) { ver = request.getProtocolVersion().toString(); } return ver; } */ public String getStatusText() { return getStatusLine(); } // Convenience methods to minimize changes elsewhere public String getResponseCharSet() { return "UTF-8"; } public HTTPSession getSession() { return this.session; } public boolean isSessionLocal() { return this.localsession; } public boolean hasStreamOpen() { return methodstream != null; } public boolean isClosed() { return this.closed; } public HTTPMethod setRange(long lo, long hi) { range = new long[]{lo, hi}; return this; } ////////////////////////////////////////////////// // Pass thru's to HTTPSession public Header[] getRequestHeaders() { return this.session.getRequestHeaders(); } public RequestConfig getDebugConfig() { return this.session.getDebugConfig(); } public HTTPMethod setCompression(String compressors) { this.session.setGlobalCompression(compressors); return this; } public HTTPMethod setFollowRedirects(boolean tf) { this.session.setFollowRedirects(tf); return this; } public HTTPMethod setUserAgent(String agent) { this.session.setUserAgent(agent); return this; } public HTTPMethod setUseSessions(boolean tf) { this.session.setUseSessions(tf); return this; } ////////////////////////////////////////////////// // Utilities /** * Test that the given url is "compatible" with the * session specified dataset. Wrapper around * HTTPAuthUtil.httphostCompatible(). * * @param other to test for compatibility against this method's * @return true if compatible, false otherwise. */ protected boolean sessionCompatible(AuthScope other) { return HTTPAuthUtil.authscopeCompatible(session.getAuthScope(), other); } protected boolean sessionCompatible(URI otheruri) { AuthScope other = HTTPAuthUtil.uriToAuthScope(otheruri); return sessionCompatible(other); } @Deprecated protected boolean sessionCompatible(HttpHost otherhost) { AuthScope other = HTTPAuthUtil.hostToAuthScope(otherhost); return sessionCompatible(other); } ////////////////////////////////////////////////// // debug interface public HttpMessage debugRequest() { return (this.request); } public HttpResponse debugResponse() { return (this.response); } ////////////////////////////////////////////////// // Deprecated but for back compatibility /** * Deprecated: use getMethodKind * * @return Name of the method: e.g. GET, HEAD, ... */ @Deprecated public String getName() { return getMethodKind(); } @Deprecated public HTTPMethod setMethodHeaders(List<Header> headers) throws HTTPException { try { for(Header h : headers) { this.headers.add(h); } } catch (Exception e) { throw new HTTPException(e); } return this; } @Deprecated public HTTPMethod setRequestHeader(String name, String value) throws HTTPException { return setRequestHeader(new BasicHeader(name, value)); } @Deprecated protected HTTPMethod setRequestHeader(Header h) throws HTTPException { try { headers.add(h); } catch (Exception e) { throw new HTTPException("cause", e); } return this; } }