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; 031import org.springframework.util.StringUtils; 032 033/** 034 * {@link org.springframework.remoting.httpinvoker.HttpInvokerRequestExecutor} implementation 035 * that uses standard Java facilities to execute POST requests, without support for HTTP 036 * authentication or advanced configuration options. 037 * 038 * <p>Designed for easy subclassing, customizing specific template methods. However, 039 * consider {@code HttpComponentsHttpInvokerRequestExecutor} for more sophisticated needs: 040 * The standard {@link HttpURLConnection} class is rather limited in its capabilities. 041 * 042 * @author Juergen Hoeller 043 * @since 1.1 044 * @see java.net.HttpURLConnection 045 */ 046public class SimpleHttpInvokerRequestExecutor extends AbstractHttpInvokerRequestExecutor { 047 048 private int connectTimeout = -1; 049 050 private int readTimeout = -1; 051 052 053 /** 054 * Set the underlying URLConnection's connect timeout (in milliseconds). 055 * A timeout value of 0 specifies an infinite timeout. 056 * <p>Default is the system's default timeout. 057 * @see URLConnection#setConnectTimeout(int) 058 */ 059 public void setConnectTimeout(int connectTimeout) { 060 this.connectTimeout = connectTimeout; 061 } 062 063 /** 064 * Set the underlying URLConnection's read timeout (in milliseconds). 065 * A timeout value of 0 specifies an infinite timeout. 066 * <p>Default is the system's default timeout. 067 * @see URLConnection#setReadTimeout(int) 068 */ 069 public void setReadTimeout(int readTimeout) { 070 this.readTimeout = readTimeout; 071 } 072 073 074 /** 075 * Execute the given request through a standard {@link HttpURLConnection}. 076 * <p>This method implements the basic processing workflow: 077 * The actual work happens in this class's template methods. 078 * @see #openConnection 079 * @see #prepareConnection 080 * @see #writeRequestBody 081 * @see #validateResponse 082 * @see #readResponseBody 083 */ 084 @Override 085 protected RemoteInvocationResult doExecuteRequest( 086 HttpInvokerClientConfiguration config, ByteArrayOutputStream baos) 087 throws IOException, ClassNotFoundException { 088 089 HttpURLConnection con = openConnection(config); 090 prepareConnection(con, baos.size()); 091 writeRequestBody(config, con, baos); 092 validateResponse(config, con); 093 InputStream responseBody = readResponseBody(config, con); 094 095 return readRemoteInvocationResult(responseBody, config.getCodebaseUrl()); 096 } 097 098 /** 099 * Open an {@link 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 */ 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( 110 "Service URL [" + config.getServiceUrl() + "] does not resolve to an HTTP connection"); 111 } 112 return (HttpURLConnection) con; 113 } 114 115 /** 116 * Prepare the given HTTP connection. 117 * <p>The default implementation specifies POST as method, 118 * "application/x-java-serialized-object" as "Content-Type" header, 119 * and the given content length as "Content-Length" header. 120 * @param connection the HTTP connection to prepare 121 * @param contentLength the length of the content to send 122 * @throws IOException if thrown by HttpURLConnection methods 123 * @see java.net.HttpURLConnection#setRequestMethod 124 * @see java.net.HttpURLConnection#setRequestProperty 125 */ 126 protected void prepareConnection(HttpURLConnection connection, int contentLength) throws IOException { 127 if (this.connectTimeout >= 0) { 128 connection.setConnectTimeout(this.connectTimeout); 129 } 130 if (this.readTimeout >= 0) { 131 connection.setReadTimeout(this.readTimeout); 132 } 133 134 connection.setDoOutput(true); 135 connection.setRequestMethod(HTTP_METHOD_POST); 136 connection.setRequestProperty(HTTP_HEADER_CONTENT_TYPE, getContentType()); 137 connection.setRequestProperty(HTTP_HEADER_CONTENT_LENGTH, Integer.toString(contentLength)); 138 139 LocaleContext localeContext = LocaleContextHolder.getLocaleContext(); 140 if (localeContext != null) { 141 Locale locale = localeContext.getLocale(); 142 if (locale != null) { 143 connection.setRequestProperty(HTTP_HEADER_ACCEPT_LANGUAGE, StringUtils.toLanguageTag(locale)); 144 } 145 } 146 147 if (isAcceptGzipEncoding()) { 148 connection.setRequestProperty(HTTP_HEADER_ACCEPT_ENCODING, ENCODING_GZIP); 149 } 150 } 151 152 /** 153 * Set the given serialized remote invocation as request body. 154 * <p>The default implementation simply write the serialized invocation to the 155 * HttpURLConnection's OutputStream. This can be overridden, for example, to write 156 * a specific encoding and potentially set appropriate HTTP request headers. 157 * @param config the HTTP invoker configuration that specifies the target service 158 * @param con the HttpURLConnection to write the request body to 159 * @param baos the ByteArrayOutputStream that contains the serialized 160 * RemoteInvocation object 161 * @throws IOException if thrown by I/O methods 162 * @see java.net.HttpURLConnection#getOutputStream() 163 * @see java.net.HttpURLConnection#setRequestProperty 164 */ 165 protected void writeRequestBody( 166 HttpInvokerClientConfiguration config, HttpURLConnection con, ByteArrayOutputStream baos) 167 throws IOException { 168 169 baos.writeTo(con.getOutputStream()); 170 } 171 172 /** 173 * Validate the given response as contained in the {@link HttpURLConnection} object, 174 * throwing an exception if it does not correspond to a successful HTTP response. 175 * <p>Default implementation rejects any HTTP status code beyond 2xx, to avoid 176 * parsing the response body and trying to deserialize from a corrupted stream. 177 * @param config the HTTP invoker configuration that specifies the target service 178 * @param con the HttpURLConnection to validate 179 * @throws IOException if validation failed 180 * @see java.net.HttpURLConnection#getResponseCode() 181 */ 182 protected void validateResponse(HttpInvokerClientConfiguration config, HttpURLConnection con) 183 throws IOException { 184 185 if (con.getResponseCode() >= 300) { 186 throw new IOException( 187 "Did not receive successful HTTP response: status code = " + con.getResponseCode() + 188 ", status message = [" + con.getResponseMessage() + "]"); 189 } 190 } 191 192 /** 193 * Extract the response body from the given executed remote invocation 194 * request. 195 * <p>The default implementation simply reads the serialized invocation 196 * from the HttpURLConnection's InputStream. If the response is recognized 197 * as GZIP response, the InputStream will get wrapped in a GZIPInputStream. 198 * @param config the HTTP invoker configuration that specifies the target service 199 * @param con the HttpURLConnection to read the response body from 200 * @return an InputStream for the response body 201 * @throws IOException if thrown by I/O methods 202 * @see #isGzipResponse 203 * @see java.util.zip.GZIPInputStream 204 * @see java.net.HttpURLConnection#getInputStream() 205 * @see java.net.HttpURLConnection#getHeaderField(int) 206 * @see java.net.HttpURLConnection#getHeaderFieldKey(int) 207 */ 208 protected InputStream readResponseBody(HttpInvokerClientConfiguration config, HttpURLConnection con) 209 throws IOException { 210 211 if (isGzipResponse(con)) { 212 // GZIP response found - need to unzip. 213 return new GZIPInputStream(con.getInputStream()); 214 } 215 else { 216 // Plain response found. 217 return con.getInputStream(); 218 } 219 } 220 221 /** 222 * Determine whether the given response is a GZIP response. 223 * <p>Default implementation checks whether the HTTP "Content-Encoding" 224 * header contains "gzip" (in any casing). 225 * @param con the HttpURLConnection to check 226 */ 227 protected boolean isGzipResponse(HttpURLConnection con) { 228 String encodingHeader = con.getHeaderField(HTTP_HEADER_CONTENT_ENCODING); 229 return (encodingHeader != null && encodingHeader.toLowerCase().contains(ENCODING_GZIP)); 230 } 231 232}