001/* 002 * Copyright 2002-2017 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.http.client; 018 019import java.io.IOException; 020import java.net.HttpURLConnection; 021import java.net.Proxy; 022import java.net.URI; 023import java.net.URL; 024import java.net.URLConnection; 025 026import org.springframework.core.task.AsyncListenableTaskExecutor; 027import org.springframework.http.HttpMethod; 028import org.springframework.util.Assert; 029 030/** 031 * {@link ClientHttpRequestFactory} implementation that uses standard JDK facilities. 032 * 033 * @author Arjen Poutsma 034 * @author Juergen Hoeller 035 * @since 3.0 036 * @see java.net.HttpURLConnection 037 * @see HttpComponentsClientHttpRequestFactory 038 */ 039public class SimpleClientHttpRequestFactory implements ClientHttpRequestFactory, AsyncClientHttpRequestFactory { 040 041 private static final int DEFAULT_CHUNK_SIZE = 4096; 042 043 044 private Proxy proxy; 045 046 private boolean bufferRequestBody = true; 047 048 private int chunkSize = DEFAULT_CHUNK_SIZE; 049 050 private int connectTimeout = -1; 051 052 private int readTimeout = -1; 053 054 private boolean outputStreaming = true; 055 056 private AsyncListenableTaskExecutor taskExecutor; 057 058 059 /** 060 * Set the {@link Proxy} to use for this request factory. 061 */ 062 public void setProxy(Proxy proxy) { 063 this.proxy = proxy; 064 } 065 066 /** 067 * Indicate whether this request factory should buffer the 068 * {@linkplain ClientHttpRequest#getBody() request body} internally. 069 * <p>Default is {@code true}. When sending large amounts of data via POST or PUT, 070 * it is recommended to change this property to {@code false}, so as not to run 071 * out of memory. This will result in a {@link ClientHttpRequest} that either 072 * streams directly to the underlying {@link HttpURLConnection} (if the 073 * {@link org.springframework.http.HttpHeaders#getContentLength() Content-Length} 074 * is known in advance), or that will use "Chunked transfer encoding" 075 * (if the {@code Content-Length} is not known in advance). 076 * @see #setChunkSize(int) 077 * @see HttpURLConnection#setFixedLengthStreamingMode(int) 078 */ 079 public void setBufferRequestBody(boolean bufferRequestBody) { 080 this.bufferRequestBody = bufferRequestBody; 081 } 082 083 /** 084 * Set the number of bytes to write in each chunk when not buffering request 085 * bodies locally. 086 * <p>Note that this parameter is only used when 087 * {@link #setBufferRequestBody(boolean) bufferRequestBody} is set to {@code false}, 088 * and the {@link org.springframework.http.HttpHeaders#getContentLength() Content-Length} 089 * is not known in advance. 090 * @see #setBufferRequestBody(boolean) 091 */ 092 public void setChunkSize(int chunkSize) { 093 this.chunkSize = chunkSize; 094 } 095 096 /** 097 * Set the underlying URLConnection's connect timeout (in milliseconds). 098 * A timeout value of 0 specifies an infinite timeout. 099 * <p>Default is the system's default timeout. 100 * @see URLConnection#setConnectTimeout(int) 101 */ 102 public void setConnectTimeout(int connectTimeout) { 103 this.connectTimeout = connectTimeout; 104 } 105 106 /** 107 * Set the underlying URLConnection's read timeout (in milliseconds). 108 * A timeout value of 0 specifies an infinite timeout. 109 * <p>Default is the system's default timeout. 110 * @see URLConnection#setReadTimeout(int) 111 */ 112 public void setReadTimeout(int readTimeout) { 113 this.readTimeout = readTimeout; 114 } 115 116 /** 117 * Set if the underlying URLConnection can be set to 'output streaming' mode. 118 * Default is {@code true}. 119 * <p>When output streaming is enabled, authentication and redirection cannot be handled automatically. 120 * If output streaming is disabled, the {@link HttpURLConnection#setFixedLengthStreamingMode} and 121 * {@link HttpURLConnection#setChunkedStreamingMode} methods of the underlying connection will never 122 * be called. 123 * @param outputStreaming if output streaming is enabled 124 */ 125 public void setOutputStreaming(boolean outputStreaming) { 126 this.outputStreaming = outputStreaming; 127 } 128 129 /** 130 * Set the task executor for this request factory. Setting this property is required 131 * for {@linkplain #createAsyncRequest(URI, HttpMethod) creating asynchronous requests}. 132 * @param taskExecutor the task executor 133 */ 134 public void setTaskExecutor(AsyncListenableTaskExecutor taskExecutor) { 135 this.taskExecutor = taskExecutor; 136 } 137 138 139 @Override 140 public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException { 141 HttpURLConnection connection = openConnection(uri.toURL(), this.proxy); 142 prepareConnection(connection, httpMethod.name()); 143 144 if (this.bufferRequestBody) { 145 return new SimpleBufferingClientHttpRequest(connection, this.outputStreaming); 146 } 147 else { 148 return new SimpleStreamingClientHttpRequest(connection, this.chunkSize, this.outputStreaming); 149 } 150 } 151 152 /** 153 * {@inheritDoc} 154 * <p>Setting the {@link #setTaskExecutor taskExecutor} property is required before calling this method. 155 */ 156 @Override 157 public AsyncClientHttpRequest createAsyncRequest(URI uri, HttpMethod httpMethod) throws IOException { 158 Assert.state(this.taskExecutor != null, "Asynchronous execution requires TaskExecutor to be set"); 159 160 HttpURLConnection connection = openConnection(uri.toURL(), this.proxy); 161 prepareConnection(connection, httpMethod.name()); 162 163 if (this.bufferRequestBody) { 164 return new SimpleBufferingAsyncClientHttpRequest( 165 connection, this.outputStreaming, this.taskExecutor); 166 } 167 else { 168 return new SimpleStreamingAsyncClientHttpRequest( 169 connection, this.chunkSize, this.outputStreaming, this.taskExecutor); 170 } 171 } 172 173 /** 174 * Opens and returns a connection to the given URL. 175 * <p>The default implementation uses the given {@linkplain #setProxy(java.net.Proxy) proxy} - 176 * if any - to open a connection. 177 * @param url the URL to open a connection to 178 * @param proxy the proxy to use, may be {@code null} 179 * @return the opened connection 180 * @throws IOException in case of I/O errors 181 */ 182 protected HttpURLConnection openConnection(URL url, Proxy proxy) throws IOException { 183 URLConnection urlConnection = (proxy != null ? url.openConnection(proxy) : url.openConnection()); 184 if (!HttpURLConnection.class.isInstance(urlConnection)) { 185 throw new IllegalStateException("HttpURLConnection required for [" + url + "] but got: " + urlConnection); 186 } 187 return (HttpURLConnection) urlConnection; 188 } 189 190 /** 191 * Template method for preparing the given {@link HttpURLConnection}. 192 * <p>The default implementation prepares the connection for input and output, and sets the HTTP method. 193 * @param connection the connection to prepare 194 * @param httpMethod the HTTP request method ({@code GET}, {@code POST}, etc.) 195 * @throws IOException in case of I/O errors 196 */ 197 protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException { 198 if (this.connectTimeout >= 0) { 199 connection.setConnectTimeout(this.connectTimeout); 200 } 201 if (this.readTimeout >= 0) { 202 connection.setReadTimeout(this.readTimeout); 203 } 204 205 connection.setDoInput(true); 206 207 if ("GET".equals(httpMethod)) { 208 connection.setInstanceFollowRedirects(true); 209 } 210 else { 211 connection.setInstanceFollowRedirects(false); 212 } 213 214 if ("POST".equals(httpMethod) || "PUT".equals(httpMethod) || 215 "PATCH".equals(httpMethod) || "DELETE".equals(httpMethod)) { 216 connection.setDoOutput(true); 217 } 218 else { 219 connection.setDoOutput(false); 220 } 221 222 connection.setRequestMethod(httpMethod); 223 } 224 225}