001/* 002 * Copyright 2002-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.remoting.httpinvoker; 018 019import java.io.ByteArrayOutputStream; 020import java.io.IOException; 021import java.io.InputStream; 022import java.net.HttpURLConnection; 023import java.net.URL; 024import java.net.URLConnection; 025import java.util.Locale; 026import java.util.zip.GZIPInputStream; 027 028import org.springframework.context.i18n.LocaleContext; 029import org.springframework.context.i18n.LocaleContextHolder; 030import org.springframework.remoting.support.RemoteInvocationResult; 031 032/** 033 * {@link org.springframework.remoting.httpinvoker.HttpInvokerRequestExecutor} implementation 034 * that uses standard Java facilities to execute POST requests, without support for HTTP 035 * authentication or advanced configuration options. 036 * 037 * <p>Designed for easy subclassing, customizing specific template methods. However, 038 * consider {@code HttpComponentsHttpInvokerRequestExecutor} for more sophisticated needs: 039 * The standard {@link HttpURLConnection} class is rather limited in its capabilities. 040 * 041 * @author Juergen Hoeller 042 * @since 1.1 043 * @see java.net.HttpURLConnection 044 */ 045public class SimpleHttpInvokerRequestExecutor extends AbstractHttpInvokerRequestExecutor { 046 047 private int connectTimeout = -1; 048 049 private int readTimeout = -1; 050 051 052 /** 053 * Set the underlying URLConnection's connect timeout (in milliseconds). 054 * A timeout value of 0 specifies an infinite timeout. 055 * <p>Default is the system's default timeout. 056 * @see URLConnection#setConnectTimeout(int) 057 */ 058 public void setConnectTimeout(int connectTimeout) { 059 this.connectTimeout = connectTimeout; 060 } 061 062 /** 063 * Set the underlying URLConnection's read timeout (in milliseconds). 064 * A timeout value of 0 specifies an infinite timeout. 065 * <p>Default is the system's default timeout. 066 * @see URLConnection#setReadTimeout(int) 067 */ 068 public void setReadTimeout(int readTimeout) { 069 this.readTimeout = readTimeout; 070 } 071 072 073 /** 074 * Execute the given request through a standard {@link HttpURLConnection}. 075 * <p>This method implements the basic processing workflow: 076 * The actual work happens in this class's template methods. 077 * @see #openConnection 078 * @see #prepareConnection 079 * @see #writeRequestBody 080 * @see #validateResponse 081 * @see #readResponseBody 082 */ 083 @Override 084 protected RemoteInvocationResult doExecuteRequest( 085 HttpInvokerClientConfiguration config, ByteArrayOutputStream baos) 086 throws IOException, ClassNotFoundException { 087 088 HttpURLConnection con = openConnection(config); 089 prepareConnection(con, baos.size()); 090 writeRequestBody(config, con, baos); 091 validateResponse(config, con); 092 InputStream responseBody = readResponseBody(config, con); 093 094 return readRemoteInvocationResult(responseBody, config.getCodebaseUrl()); 095 } 096 097 /** 098 * Open an {@link HttpURLConnection} for the given remote invocation request. 099 * @param config the HTTP invoker configuration that specifies the 100 * target service 101 * @return the HttpURLConnection for the given request 102 * @throws IOException if thrown by I/O methods 103 * @see java.net.URL#openConnection() 104 */ 105 protected HttpURLConnection openConnection(HttpInvokerClientConfiguration config) throws IOException { 106 URLConnection con = new URL(config.getServiceUrl()).openConnection(); 107 if (!(con instanceof HttpURLConnection)) { 108 throw new IOException( 109 "Service URL [" + config.getServiceUrl() + "] does not resolve to an HTTP connection"); 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 */ 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 133 connection.setDoOutput(true); 134 connection.setRequestMethod(HTTP_METHOD_POST); 135 connection.setRequestProperty(HTTP_HEADER_CONTENT_TYPE, getContentType()); 136 connection.setRequestProperty(HTTP_HEADER_CONTENT_LENGTH, Integer.toString(contentLength)); 137 138 LocaleContext localeContext = LocaleContextHolder.getLocaleContext(); 139 if (localeContext != null) { 140 Locale locale = localeContext.getLocale(); 141 if (locale != null) { 142 connection.setRequestProperty(HTTP_HEADER_ACCEPT_LANGUAGE, locale.toLanguageTag()); 143 } 144 } 145 146 if (isAcceptGzipEncoding()) { 147 connection.setRequestProperty(HTTP_HEADER_ACCEPT_ENCODING, ENCODING_GZIP); 148 } 149 } 150 151 /** 152 * Set the given serialized remote invocation as request body. 153 * <p>The default implementation simply write the serialized invocation to the 154 * HttpURLConnection's OutputStream. This can be overridden, for example, to write 155 * a specific encoding and potentially set appropriate HTTP request headers. 156 * @param config the HTTP invoker configuration that specifies the target service 157 * @param con the HttpURLConnection to write the request body to 158 * @param baos the ByteArrayOutputStream that contains the serialized 159 * RemoteInvocation object 160 * @throws IOException if thrown by I/O methods 161 * @see java.net.HttpURLConnection#getOutputStream() 162 * @see java.net.HttpURLConnection#setRequestProperty 163 */ 164 protected void writeRequestBody( 165 HttpInvokerClientConfiguration config, HttpURLConnection con, ByteArrayOutputStream baos) 166 throws IOException { 167 168 baos.writeTo(con.getOutputStream()); 169 } 170 171 /** 172 * Validate the given response as contained in the {@link HttpURLConnection} object, 173 * throwing an exception if it does not correspond to a successful HTTP response. 174 * <p>Default implementation rejects any HTTP status code beyond 2xx, to avoid 175 * parsing the response body and trying to deserialize from a corrupted stream. 176 * @param config the HTTP invoker configuration that specifies the target service 177 * @param con the HttpURLConnection to validate 178 * @throws IOException if validation failed 179 * @see java.net.HttpURLConnection#getResponseCode() 180 */ 181 protected void validateResponse(HttpInvokerClientConfiguration config, HttpURLConnection con) 182 throws IOException { 183 184 if (con.getResponseCode() >= 300) { 185 throw new IOException( 186 "Did not receive successful HTTP response: status code = " + con.getResponseCode() + 187 ", status message = [" + con.getResponseMessage() + "]"); 188 } 189 } 190 191 /** 192 * Extract the response body from the given executed remote invocation 193 * request. 194 * <p>The default implementation simply reads the serialized invocation 195 * from the HttpURLConnection's InputStream. If the response is recognized 196 * as GZIP response, the InputStream will get wrapped in a GZIPInputStream. 197 * @param config the HTTP invoker configuration that specifies the target service 198 * @param con the HttpURLConnection to read the response body from 199 * @return an InputStream for the response body 200 * @throws IOException if thrown by I/O methods 201 * @see #isGzipResponse 202 * @see java.util.zip.GZIPInputStream 203 * @see java.net.HttpURLConnection#getInputStream() 204 * @see java.net.HttpURLConnection#getHeaderField(int) 205 * @see java.net.HttpURLConnection#getHeaderFieldKey(int) 206 */ 207 protected InputStream readResponseBody(HttpInvokerClientConfiguration config, HttpURLConnection con) 208 throws IOException { 209 210 if (isGzipResponse(con)) { 211 // GZIP response found - need to unzip. 212 return new GZIPInputStream(con.getInputStream()); 213 } 214 else { 215 // Plain response found. 216 return con.getInputStream(); 217 } 218 } 219 220 /** 221 * Determine whether the given response is a GZIP response. 222 * <p>Default implementation checks whether the HTTP "Content-Encoding" 223 * header contains "gzip" (in any casing). 224 * @param con the HttpURLConnection to check 225 */ 226 protected boolean isGzipResponse(HttpURLConnection con) { 227 String encodingHeader = con.getHeaderField(HTTP_HEADER_CONTENT_ENCODING); 228 return (encodingHeader != null && encodingHeader.toLowerCase().contains(ENCODING_GZIP)); 229 } 230 231}