Fri, 14 Feb 2014 11:13:45 +0100
8026188: Enhance envelope factory
Summary: Avoiding caching data initialized via TCCL in static context; fix also reviewed by Alexander Fomin
Reviewed-by: ahgross, mgrebac, skoivu
1 /*
2 * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
26 package com.sun.xml.internal.ws.transport.http.client;
28 import com.sun.istack.internal.NotNull;
29 import com.sun.xml.internal.ws.api.SOAPVersion;
30 import com.sun.xml.internal.ws.api.WSBinding;
31 import com.sun.xml.internal.ws.api.ha.StickyFeature;
32 import com.sun.xml.internal.ws.api.message.Packet;
33 import com.sun.xml.internal.ws.api.pipe.*;
34 import com.sun.xml.internal.ws.api.pipe.helper.AbstractTubeImpl;
35 import com.sun.xml.internal.ws.client.ClientTransportException;
36 import com.sun.xml.internal.ws.developer.HttpConfigFeature;
37 import com.sun.xml.internal.ws.resources.ClientMessages;
38 import com.sun.xml.internal.ws.resources.WsservletMessages;
39 import com.sun.xml.internal.ws.transport.Headers;
40 import com.sun.xml.internal.ws.transport.http.HttpAdapter;
41 import com.sun.xml.internal.ws.util.ByteArrayBuffer;
42 import com.sun.xml.internal.ws.util.RuntimeVersion;
43 import com.sun.xml.internal.ws.util.StreamUtils;
45 import javax.xml.bind.DatatypeConverter;
46 import javax.xml.ws.BindingProvider;
47 import javax.xml.ws.WebServiceException;
48 import javax.xml.ws.WebServiceFeature;
49 import javax.xml.ws.handler.MessageContext;
50 import javax.xml.ws.soap.SOAPBinding;
51 import java.io.*;
52 import java.net.CookieHandler;
53 import java.net.HttpURLConnection;
54 import java.util.*;
55 import java.util.Map.Entry;
56 import java.util.logging.Level;
57 import java.util.logging.Logger;
59 /**
60 * {@link Tube} that sends a request to a remote HTTP server.
61 *
62 * TODO: need to create separate HTTP transport pipes for binding. SOAP1.1, SOAP1.2,
63 * TODO: XML/HTTP differ in handling status codes.
64 *
65 * @author Jitendra Kotamraju
66 */
67 public class HttpTransportPipe extends AbstractTubeImpl {
69 private static final List<String> USER_AGENT = Collections.singletonList(RuntimeVersion.VERSION.toString());
70 private static final Logger LOGGER = Logger.getLogger(HttpTransportPipe.class.getName());
72 /**
73 * Dumps what goes across HTTP transport.
74 */
75 public static boolean dump;
77 private final Codec codec;
78 private final WSBinding binding;
79 private final CookieHandler cookieJar; // shared object among the tubes
80 private final boolean sticky;
82 static {
83 boolean b;
84 try {
85 b = Boolean.getBoolean(HttpTransportPipe.class.getName()+".dump");
86 } catch( Throwable t ) {
87 b = false;
88 }
89 dump = b;
90 }
92 public HttpTransportPipe(Codec codec, WSBinding binding) {
93 this.codec = codec;
94 this.binding = binding;
95 this.sticky = isSticky(binding);
96 HttpConfigFeature configFeature = binding.getFeature(HttpConfigFeature.class);
97 if (configFeature == null) {
98 configFeature = new HttpConfigFeature();
99 }
100 this.cookieJar = configFeature.getCookieHandler();
101 }
103 private static boolean isSticky(WSBinding binding) {
104 boolean tSticky = false;
105 WebServiceFeature[] features = binding.getFeatures().toArray();
106 for(WebServiceFeature f : features) {
107 if (f instanceof StickyFeature) {
108 tSticky = true;
109 break;
110 }
111 }
112 return tSticky;
113 }
115 /*
116 * Copy constructor for {@link Tube#copy(TubeCloner)}.
117 */
118 private HttpTransportPipe(HttpTransportPipe that, TubeCloner cloner) {
119 this(that.codec.copy(), that.binding);
120 cloner.add(that,this);
121 }
123 @Override
124 public NextAction processException(@NotNull Throwable t) {
125 return doThrow(t);
126 }
128 @Override
129 public NextAction processRequest(@NotNull Packet request) {
130 return doReturnWith(process(request));
131 }
133 @Override
134 public NextAction processResponse(@NotNull Packet response) {
135 return doReturnWith(response);
136 }
138 protected HttpClientTransport getTransport(Packet request, Map<String, List<String>> reqHeaders) {
139 return new HttpClientTransport(request, reqHeaders);
140 }
142 @Override
143 public Packet process(Packet request) {
144 HttpClientTransport con;
145 try {
146 // get transport headers from message
147 Map<String, List<String>> reqHeaders = new Headers();
148 @SuppressWarnings("unchecked")
149 Map<String, List<String>> userHeaders = (Map<String, List<String>>) request.invocationProperties.get(MessageContext.HTTP_REQUEST_HEADERS);
150 boolean addUserAgent = true;
151 if (userHeaders != null) {
152 // userHeaders may not be modifiable like SingletonMap, just copy them
153 reqHeaders.putAll(userHeaders);
154 // application wants to use its own User-Agent header
155 if (userHeaders.get("User-Agent") != null) {
156 addUserAgent = false;
157 }
158 }
159 if (addUserAgent) {
160 reqHeaders.put("User-Agent", USER_AGENT);
161 }
163 addBasicAuth(request, reqHeaders);
164 addCookies(request, reqHeaders);
166 con = getTransport(request, reqHeaders);
167 request.addSatellite(new HttpResponseProperties(con));
169 ContentType ct = codec.getStaticContentType(request);
170 if (ct == null) {
171 ByteArrayBuffer buf = new ByteArrayBuffer();
173 ct = codec.encode(request, buf);
174 // data size is available, set it as Content-Length
175 reqHeaders.put("Content-Length", Collections.singletonList(Integer.toString(buf.size())));
176 reqHeaders.put("Content-Type", Collections.singletonList(ct.getContentType()));
177 if (ct.getAcceptHeader() != null) {
178 reqHeaders.put("Accept", Collections.singletonList(ct.getAcceptHeader()));
179 }
180 if (binding instanceof SOAPBinding) {
181 writeSOAPAction(reqHeaders, ct.getSOAPActionHeader());
182 }
184 if (dump || LOGGER.isLoggable(Level.FINER)) {
185 dump(buf, "HTTP request", reqHeaders);
186 }
188 buf.writeTo(con.getOutput());
189 } else {
190 // Set static Content-Type
191 reqHeaders.put("Content-Type", Collections.singletonList(ct.getContentType()));
192 if (ct.getAcceptHeader() != null) {
193 reqHeaders.put("Accept", Collections.singletonList(ct.getAcceptHeader()));
194 }
195 if (binding instanceof SOAPBinding) {
196 writeSOAPAction(reqHeaders, ct.getSOAPActionHeader());
197 }
199 if(dump || LOGGER.isLoggable(Level.FINER)) {
200 ByteArrayBuffer buf = new ByteArrayBuffer();
201 codec.encode(request, buf);
202 dump(buf, "HTTP request - "+request.endpointAddress, reqHeaders);
203 OutputStream out = con.getOutput();
204 if (out != null) {
205 buf.writeTo(out);
206 }
207 } else {
208 OutputStream os = con.getOutput();
209 if (os != null) {
210 codec.encode(request, os);
211 }
212 }
213 }
215 con.closeOutput();
217 return createResponsePacket(request, con);
218 } catch(WebServiceException wex) {
219 throw wex;
220 } catch(Exception ex) {
221 throw new WebServiceException(ex);
222 }
223 }
225 private Packet createResponsePacket(Packet request, HttpClientTransport con) throws IOException {
226 con.readResponseCodeAndMessage(); // throws IOE
227 recordCookies(request, con);
229 InputStream responseStream = con.getInput();
230 if (dump || LOGGER.isLoggable(Level.FINER)) {
231 ByteArrayBuffer buf = new ByteArrayBuffer();
232 if (responseStream != null) {
233 buf.write(responseStream);
234 responseStream.close();
235 }
236 dump(buf,"HTTP response - "+request.endpointAddress+" - "+con.statusCode, con.getHeaders());
237 responseStream = buf.newInputStream();
238 }
240 // Check if stream contains any data
241 int cl = con.contentLength;
242 InputStream tempIn = null;
243 if (cl == -1) { // No Content-Length header
244 tempIn = StreamUtils.hasSomeData(responseStream);
245 if (tempIn != null) {
246 responseStream = tempIn;
247 }
248 }
249 if (cl == 0 || (cl == -1 && tempIn == null)) {
250 if(responseStream != null) {
251 responseStream.close(); // No data, so close the stream
252 responseStream = null;
253 }
255 }
257 // Allows only certain http status codes for a binding. For all
258 // other status codes, throws exception
259 checkStatusCode(responseStream, con); // throws ClientTransportException
261 Packet reply = request.createClientResponse(null);
262 reply.wasTransportSecure = con.isSecure();
263 if (responseStream != null) {
264 String contentType = con.getContentType();
265 if (contentType != null && contentType.contains("text/html") && binding instanceof SOAPBinding) {
266 throw new ClientTransportException(ClientMessages.localizableHTTP_STATUS_CODE(con.statusCode, con.statusMessage));
267 }
268 codec.decode(responseStream, contentType, reply);
269 }
270 return reply;
271 }
273 /*
274 * Allows the following HTTP status codes.
275 * SOAP 1.1/HTTP - 200, 202, 500
276 * SOAP 1.2/HTTP - 200, 202, 400, 500
277 * XML/HTTP - all
278 *
279 * For all other status codes, it throws an exception
280 */
281 private void checkStatusCode(InputStream in, HttpClientTransport con) throws IOException {
282 int statusCode = con.statusCode;
283 String statusMessage = con.statusMessage;
284 // SOAP1.1 and SOAP1.2 differ here
285 if (binding instanceof SOAPBinding) {
286 if (binding.getSOAPVersion() == SOAPVersion.SOAP_12) {
287 //In SOAP 1.2, Fault messages can be sent with 4xx and 5xx error codes
288 if (statusCode == HttpURLConnection.HTTP_OK || statusCode == HttpURLConnection.HTTP_ACCEPTED || isErrorCode(statusCode)) {
289 // acceptable status codes for SOAP 1.2
290 if (isErrorCode(statusCode) && in == null) {
291 // No envelope for the error, so throw an exception with http error details
292 throw new ClientTransportException(ClientMessages.localizableHTTP_STATUS_CODE(statusCode, statusMessage));
293 }
294 return;
295 }
296 } else {
297 // SOAP 1.1
298 if (statusCode == HttpURLConnection.HTTP_OK || statusCode == HttpURLConnection.HTTP_ACCEPTED || statusCode == HttpURLConnection.HTTP_INTERNAL_ERROR) {
299 // acceptable status codes for SOAP 1.1
300 if (statusCode == HttpURLConnection.HTTP_INTERNAL_ERROR && in == null) {
301 // No envelope for the error, so throw an exception with http error details
302 throw new ClientTransportException(ClientMessages.localizableHTTP_STATUS_CODE(statusCode, statusMessage));
303 }
304 return;
305 }
306 }
307 if (in != null) {
308 in.close();
309 }
310 throw new ClientTransportException(ClientMessages.localizableHTTP_STATUS_CODE(statusCode, statusMessage));
311 }
312 // Every status code is OK for XML/HTTP
313 }
315 private boolean isErrorCode(int code) {
316 //if(code/100 == 5/*Server-side error*/ || code/100 == 4 /*client error*/ ) {
317 return code == 500 || code == 400;
318 }
320 private void addCookies(Packet context, Map<String, List<String>> reqHeaders) throws IOException {
321 Boolean shouldMaintainSessionProperty =
322 (Boolean) context.invocationProperties.get(BindingProvider.SESSION_MAINTAIN_PROPERTY);
323 if (shouldMaintainSessionProperty != null && !shouldMaintainSessionProperty) {
324 return; // explicitly turned off
325 }
326 if (sticky || (shouldMaintainSessionProperty != null && shouldMaintainSessionProperty)) {
327 Map<String, List<String>> rememberedCookies = cookieJar.get(context.endpointAddress.getURI(), reqHeaders);
328 processCookieHeaders(reqHeaders, rememberedCookies, "Cookie");
329 processCookieHeaders(reqHeaders, rememberedCookies, "Cookie2");
330 }
331 }
333 private void processCookieHeaders(Map<String, List<String>> requestHeaders, Map<String, List<String>> rememberedCookies, String cookieHeader) {
334 List<String> jarCookies = rememberedCookies.get(cookieHeader);
335 if (jarCookies != null && !jarCookies.isEmpty()) {
336 List<String> resultCookies = mergeUserCookies(jarCookies, requestHeaders.get(cookieHeader));
337 requestHeaders.put(cookieHeader, resultCookies);
338 }
339 }
341 private List<String> mergeUserCookies(List<String> rememberedCookies, List<String> userCookies) {
343 // nothing to merge
344 if (userCookies == null || userCookies.isEmpty()) {
345 return rememberedCookies;
346 }
348 Map<String, String> map = new HashMap<String, String>();
349 cookieListToMap(rememberedCookies, map);
350 cookieListToMap(userCookies, map);
352 return new ArrayList<String>(map.values());
353 }
355 private void cookieListToMap(List<String> cookieList, Map<String, String> targetMap) {
356 for(String cookie : cookieList) {
357 int index = cookie.indexOf("=");
358 String cookieName = cookie.substring(0, index);
359 targetMap.put(cookieName, cookie);
360 }
361 }
363 private void recordCookies(Packet context, HttpClientTransport con) throws IOException {
364 Boolean shouldMaintainSessionProperty =
365 (Boolean) context.invocationProperties.get(BindingProvider.SESSION_MAINTAIN_PROPERTY);
366 if (shouldMaintainSessionProperty != null && !shouldMaintainSessionProperty) {
367 return; // explicitly turned off
368 }
369 if (sticky || (shouldMaintainSessionProperty != null && shouldMaintainSessionProperty)) {
370 cookieJar.put(context.endpointAddress.getURI(), con.getHeaders());
371 }
372 }
374 private void addBasicAuth(Packet context, Map<String, List<String>> reqHeaders) {
375 String user = (String) context.invocationProperties.get(BindingProvider.USERNAME_PROPERTY);
376 if (user != null) {
377 String pw = (String) context.invocationProperties.get(BindingProvider.PASSWORD_PROPERTY);
378 if (pw != null) {
379 StringBuilder buf = new StringBuilder(user);
380 buf.append(":");
381 buf.append(pw);
382 String creds = DatatypeConverter.printBase64Binary(buf.toString().getBytes());
383 reqHeaders.put("Authorization", Collections.singletonList("Basic "+creds));
384 }
385 }
386 }
388 /*
389 * write SOAPAction header if the soapAction parameter is non-null or BindingProvider properties set.
390 * BindingProvider properties take precedence.
391 */
392 private void writeSOAPAction(Map<String, List<String>> reqHeaders, String soapAction) {
393 //dont write SOAPAction HTTP header for SOAP 1.2 messages.
394 if(SOAPVersion.SOAP_12.equals(binding.getSOAPVersion())) {
395 return;
396 }
397 if (soapAction != null) {
398 reqHeaders.put("SOAPAction", Collections.singletonList(soapAction));
399 } else {
400 reqHeaders.put("SOAPAction", Collections.singletonList("\"\""));
401 }
402 }
404 @Override
405 public void preDestroy() {
406 // nothing to do. Intentionally left empty.
407 }
409 @Override
410 public HttpTransportPipe copy(TubeCloner cloner) {
411 return new HttpTransportPipe(this,cloner);
412 }
415 private void dump(ByteArrayBuffer buf, String caption, Map<String, List<String>> headers) throws IOException {
416 ByteArrayOutputStream baos = new ByteArrayOutputStream();
417 PrintWriter pw = new PrintWriter(baos, true);
418 pw.println("---["+caption +"]---");
419 for (Entry<String,List<String>> header : headers.entrySet()) {
420 if(header.getValue().isEmpty()) {
421 // I don't think this is legal, but let's just dump it,
422 // as the point of the dump is to uncover problems.
423 pw.println(header.getValue());
424 } else {
425 for (String value : header.getValue()) {
426 pw.println(header.getKey()+": "+value);
427 }
428 }
429 }
431 if (buf.size() > HttpAdapter.dump_threshold) {
432 byte[] b = buf.getRawData();
433 baos.write(b, 0, HttpAdapter.dump_threshold);
434 pw.println();
435 pw.println(WsservletMessages.MESSAGE_TOO_LONG(HttpAdapter.class.getName() + ".dumpTreshold"));
436 } else {
437 buf.writeTo(baos);
438 }
439 pw.println("--------------------");
441 String msg = baos.toString();
442 if (dump) {
443 System.out.println(msg);
444 }
445 if (LOGGER.isLoggable(Level.FINER)) {
446 LOGGER.log(Level.FINER, msg);
447 }
448 }
450 }