1 /* 2 * Copyright 2002-2011 the original author or authors. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package org.springframework.remoting.httpinvoker; 18 19 import java.io.ByteArrayOutputStream; 20 import java.io.IOException; 21 import java.io.InputStream; 22 import java.net.HttpURLConnection; 23 import java.net.URL; 24 import java.net.URLConnection; 25 import java.util.zip.GZIPInputStream; 26 27 import org.springframework.context.i18n.LocaleContext; 28 import org.springframework.context.i18n.LocaleContextHolder; 29 import org.springframework.remoting.support.RemoteInvocationResult; 30 import org.springframework.util.StringUtils; 31 32 /** 33 * HttpInvokerRequestExecutor implementation that uses standard J2SE facilities 34 * to execute POST requests, without support for HTTP authentication or 35 * advanced configuration options. 36 * 37 * <p>Designed for easy subclassing, customizing specific template methods. 38 * However, consider CommonsHttpInvokerRequestExecutor for more sophisticated 39 * needs: The J2SE HttpURLConnection is rather limited in its capabilities. 40 * 41 * @author Juergen Hoeller 42 * @since 1.1 43 * @see CommonsHttpInvokerRequestExecutor 44 * @see java.net.HttpURLConnection 45 */ 46 public class SimpleHttpInvokerRequestExecutor extends AbstractHttpInvokerRequestExecutor { 47 48 private int connectTimeout = -1; 49 50 private int readTimeout = -1; 51 52 53 /** 54 * Set the underlying URLConnection's connect timeout (in milliseconds). 55 * A timeout value of 0 specifies an infinite timeout. 56 * <p>Default is the system's default timeout. 57 * @see URLConnection#setConnectTimeout(int) 58 */ setConnectTimeout(int connectTimeout)59 public void setConnectTimeout(int connectTimeout) { 60 this.connectTimeout = connectTimeout; 61 } 62 63 /** 64 * Set the underlying URLConnection's read timeout (in milliseconds). 65 * A timeout value of 0 specifies an infinite timeout. 66 * <p>Default is the system's default timeout. 67 * @see URLConnection#setReadTimeout(int) 68 */ setReadTimeout(int readTimeout)69 public void setReadTimeout(int readTimeout) { 70 this.readTimeout = readTimeout; 71 } 72 73 74 /** 75 * Execute the given request through a standard J2SE HttpURLConnection. 76 * <p>This method implements the basic processing workflow: 77 * The actual work happens in this class's template methods. 78 * @see #openConnection 79 * @see #prepareConnection 80 * @see #writeRequestBody 81 * @see #validateResponse 82 * @see #readResponseBody 83 */ 84 @Override doExecuteRequest( HttpInvokerClientConfiguration config, ByteArrayOutputStream baos)85 protected RemoteInvocationResult doExecuteRequest( 86 HttpInvokerClientConfiguration config, ByteArrayOutputStream baos) 87 throws IOException, ClassNotFoundException { 88 89 HttpURLConnection con = openConnection(config); 90 prepareConnection(con, baos.size()); 91 writeRequestBody(config, con, baos); 92 validateResponse(config, con); 93 InputStream responseBody = readResponseBody(config, con); 94 95 return readRemoteInvocationResult(responseBody, config.getCodebaseUrl()); 96 } 97 98 /** 99 * Open an HttpURLConnection for the given remote invocation request. 100 * @param config the HTTP invoker configuration that specifies the 101 * target service 102 * @return the HttpURLConnection for the given request 103 * @throws IOException if thrown by I/O methods 104 * @see java.net.URL#openConnection() 105 */ openConnection(HttpInvokerClientConfiguration config)106 protected HttpURLConnection openConnection(HttpInvokerClientConfiguration config) throws IOException { 107 URLConnection con = new URL(config.getServiceUrl()).openConnection(); 108 if (!(con instanceof HttpURLConnection)) { 109 throw new IOException("Service URL [" + config.getServiceUrl() + "] is not an HTTP URL"); 110 } 111 return (HttpURLConnection) con; 112 } 113 114 /** 115 * Prepare the given HTTP connection. 116 * <p>The default implementation specifies POST as method, 117 * "application/x-java-serialized-object" as "Content-Type" header, 118 * and the given content length as "Content-Length" header. 119 * @param connection the HTTP connection to prepare 120 * @param contentLength the length of the content to send 121 * @throws IOException if thrown by HttpURLConnection methods 122 * @see java.net.HttpURLConnection#setRequestMethod 123 * @see java.net.HttpURLConnection#setRequestProperty 124 */ prepareConnection(HttpURLConnection connection, int contentLength)125 protected void prepareConnection(HttpURLConnection connection, int contentLength) throws IOException { 126 if (this.connectTimeout >= 0) { 127 connection.setConnectTimeout(this.connectTimeout); 128 } 129 if (this.readTimeout >= 0) { 130 connection.setReadTimeout(this.readTimeout); 131 } 132 connection.setDoOutput(true); 133 connection.setRequestMethod(HTTP_METHOD_POST); 134 connection.setRequestProperty(HTTP_HEADER_CONTENT_TYPE, getContentType()); 135 connection.setRequestProperty(HTTP_HEADER_CONTENT_LENGTH, Integer.toString(contentLength)); 136 LocaleContext locale = LocaleContextHolder.getLocaleContext(); 137 if (locale != null) { 138 connection.setRequestProperty(HTTP_HEADER_ACCEPT_LANGUAGE, StringUtils.toLanguageTag(locale.getLocale())); 139 } 140 if (isAcceptGzipEncoding()) { 141 connection.setRequestProperty(HTTP_HEADER_ACCEPT_ENCODING, ENCODING_GZIP); 142 } 143 } 144 145 /** 146 * Set the given serialized remote invocation as request body. 147 * <p>The default implementation simply write the serialized invocation to the 148 * HttpURLConnection's OutputStream. This can be overridden, for example, to write 149 * a specific encoding and potentially set appropriate HTTP request headers. 150 * @param config the HTTP invoker configuration that specifies the target service 151 * @param con the HttpURLConnection to write the request body to 152 * @param baos the ByteArrayOutputStream that contains the serialized 153 * RemoteInvocation object 154 * @throws IOException if thrown by I/O methods 155 * @see java.net.HttpURLConnection#getOutputStream() 156 * @see java.net.HttpURLConnection#setRequestProperty 157 */ writeRequestBody( HttpInvokerClientConfiguration config, HttpURLConnection con, ByteArrayOutputStream baos)158 protected void writeRequestBody( 159 HttpInvokerClientConfiguration config, HttpURLConnection con, ByteArrayOutputStream baos) 160 throws IOException { 161 162 baos.writeTo(con.getOutputStream()); 163 } 164 165 /** 166 * Validate the given response as contained in the HttpURLConnection object, 167 * throwing an exception if it does not correspond to a successful HTTP response. 168 * <p>Default implementation rejects any HTTP status code beyond 2xx, to avoid 169 * parsing the response body and trying to deserialize from a corrupted stream. 170 * @param config the HTTP invoker configuration that specifies the target service 171 * @param con the HttpURLConnection to validate 172 * @throws IOException if validation failed 173 * @see java.net.HttpURLConnection#getResponseCode() 174 */ validateResponse(HttpInvokerClientConfiguration config, HttpURLConnection con)175 protected void validateResponse(HttpInvokerClientConfiguration config, HttpURLConnection con) 176 throws IOException { 177 178 if (con.getResponseCode() >= 300) { 179 throw new IOException( 180 "Did not receive successful HTTP response: status code = " + con.getResponseCode() + 181 ", status message = [" + con.getResponseMessage() + "]"); 182 } 183 } 184 185 /** 186 * Extract the response body from the given executed remote invocation 187 * request. 188 * <p>The default implementation simply reads the serialized invocation 189 * from the HttpURLConnection's InputStream. If the response is recognized 190 * as GZIP response, the InputStream will get wrapped in a GZIPInputStream. 191 * @param config the HTTP invoker configuration that specifies the target service 192 * @param con the HttpURLConnection to read the response body from 193 * @return an InputStream for the response body 194 * @throws IOException if thrown by I/O methods 195 * @see #isGzipResponse 196 * @see java.util.zip.GZIPInputStream 197 * @see java.net.HttpURLConnection#getInputStream() 198 * @see java.net.HttpURLConnection#getHeaderField(int) 199 * @see java.net.HttpURLConnection#getHeaderFieldKey(int) 200 */ readResponseBody(HttpInvokerClientConfiguration config, HttpURLConnection con)201 protected InputStream readResponseBody(HttpInvokerClientConfiguration config, HttpURLConnection con) 202 throws IOException { 203 204 if (isGzipResponse(con)) { 205 // GZIP response found - need to unzip. 206 return new GZIPInputStream(con.getInputStream()); 207 } 208 else { 209 // Plain response found. 210 return con.getInputStream(); 211 } 212 } 213 214 /** 215 * Determine whether the given response is a GZIP response. 216 * <p>Default implementation checks whether the HTTP "Content-Encoding" 217 * header contains "gzip" (in any casing). 218 * @param con the HttpURLConnection to check 219 */ isGzipResponse(HttpURLConnection con)220 protected boolean isGzipResponse(HttpURLConnection con) { 221 String encodingHeader = con.getHeaderField(HTTP_HEADER_CONTENT_ENCODING); 222 return (encodingHeader != null && encodingHeader.toLowerCase().contains(ENCODING_GZIP)); 223 } 224 225 } 226