001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common Development
008: * and Distribution License("CDDL") (collectively, the "License"). You
009: * may not use this file except in compliance with the License. You can obtain
010: * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
011: * or glassfish/bootstrap/legal/LICENSE.txt. See the License for the specific
012: * language governing permissions and limitations under the License.
013: *
014: * When distributing the software, include this License Header Notice in each
015: * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
016: * Sun designates this particular file as subject to the "Classpath" exception
017: * as provided by Sun in the GPL Version 2 section of the License file that
018: * accompanied this code. If applicable, add the following below the License
019: * Header, with the fields enclosed by brackets [] replaced by your own
020: * identifying information: "Portions Copyrighted [year]
021: * [name of copyright owner]"
022: *
023: * Contributor(s):
024: *
025: * If you wish your version of this file to be governed by only the CDDL or
026: * only the GPL Version 2, indicate your decision by adding "[Contributor]
027: * elects to include this software in this distribution under the [CDDL or GPL
028: * Version 2] license." If you don't indicate a single choice of license, a
029: * recipient has the option to distribute your version of this file under
030: * either the CDDL, the GPL Version 2 or to extend the choice of license to
031: * its licensees as provided above. However, if you add GPL Version 2 code
032: * and therefore, elected the GPL Version 2 license, then the option applies
033: * only if the new code is made subject to such option by the copyright
034: * holder.
035: */
036:
037: package com.sun.xml.ws.transport.http.client;
038:
039: import com.sun.xml.ws.api.EndpointAddress;
040: import com.sun.xml.ws.api.message.Packet;
041: import com.sun.xml.ws.client.BindingProviderProperties;
042: import static com.sun.xml.ws.client.BindingProviderProperties.*;
043: import com.sun.xml.ws.client.ClientTransportException;
044: import com.sun.xml.ws.resources.ClientMessages;
045: import com.sun.xml.ws.transport.Headers;
046: import com.sun.xml.ws.developer.JAXWSProperties;
047: import com.sun.xml.ws.util.RuntimeVersion;
048: import com.sun.istack.Nullable;
049: import com.sun.istack.NotNull;
050:
051: import javax.net.ssl.SSLSocketFactory;
052: import javax.net.ssl.HttpsURLConnection;
053: import javax.net.ssl.HostnameVerifier;
054: import javax.net.ssl.SSLSession;
055: import static javax.xml.bind.DatatypeConverter.printBase64Binary;
056: import javax.xml.bind.JAXBContext;
057: import javax.xml.bind.JAXBException;
058: import javax.xml.ws.BindingProvider;
059: import static javax.xml.ws.BindingProvider.SESSION_MAINTAIN_PROPERTY;
060: import javax.xml.ws.WebServiceException;
061: import javax.xml.ws.handler.MessageContext;
062: import java.io.IOException;
063: import java.io.InputStream;
064: import java.io.OutputStream;
065: import java.net.HttpURLConnection;
066: import java.util.Collections;
067: import java.util.List;
068: import java.util.Map;
069: import java.util.zip.GZIPOutputStream;
070: import java.util.zip.GZIPInputStream;
071:
072: /**
073: * TODO: this class seems to be pointless. Just merge it with {@link HttpTransportPipe}.
074: *
075: * @author WS Development Team
076: */
077: final class HttpClientTransport {
078:
079: // Need to use JAXB first to register DatatypeConverter
080: static {
081: try {
082: JAXBContext.newInstance().createUnmarshaller();
083: } catch (JAXBException je) {
084: // Nothing much can be done. Intentionally left empty
085: }
086: }
087: private static String LAST_ENDPOINT = "";
088: private static boolean redirect = true;
089: private static final int START_REDIRECT_COUNT = 3;
090: private static int redirectCount = START_REDIRECT_COUNT;
091:
092: /*package*/int statusCode;
093: private final Map<String, List<String>> reqHeaders;
094: private Map<String, List<String>> respHeaders = null;
095:
096: private OutputStream outputStream;
097: private boolean https;
098: private HttpURLConnection httpConnection = null;
099: private EndpointAddress endpoint = null;
100: private Packet context = null;
101: private CookieJar cookieJar = null;
102: private boolean isFailure = false;
103: private final Integer chunkSize;
104:
105: public HttpClientTransport(@NotNull
106: Packet packet, @NotNull
107: Map<String, List<String>> reqHeaders) {
108: endpoint = packet.endpointAddress;
109: context = packet;
110: this .reqHeaders = reqHeaders;
111: chunkSize = (Integer) context.invocationProperties
112: .get(JAXWSProperties.HTTP_CLIENT_STREAMING_CHUNK_SIZE);
113: }
114:
115: /**
116: * Prepare the stream for HTTP request
117: */
118: public OutputStream getOutput() {
119: try {
120: createHttpConnection();
121: sendCookieAsNeeded();
122: // for "GET" request no need to get outputStream
123: if (requiresOutputStream()) {
124: outputStream = httpConnection.getOutputStream();
125: if (chunkSize != null) {
126: outputStream = new WSChunkedOuputStream(
127: outputStream, chunkSize);
128: }
129: List<String> contentEncoding = reqHeaders
130: .get("Content-Encoding");
131: // TODO need to find out correct encoding based on q value - RFC 2616
132: if (contentEncoding != null
133: && contentEncoding.get(0).contains("gzip")) {
134: outputStream = new GZIPOutputStream(outputStream);
135: }
136: }
137: httpConnection.connect();
138: } catch (Exception ex) {
139: throw new ClientTransportException(ClientMessages
140: .localizableHTTP_CLIENT_FAILED(ex), ex);
141: }
142:
143: return outputStream;
144: }
145:
146: public void closeOutput() throws IOException {
147: if (outputStream != null) {
148: outputStream.close();
149: outputStream = null;
150: }
151: }
152:
153: /**
154: * Get the response from HTTP connection and prepare the input stream for response
155: */
156: public InputStream getInput() {
157: // response processing
158:
159: InputStream in;
160: try {
161: in = readResponse();
162: String contentEncoding = httpConnection
163: .getContentEncoding();
164: if (contentEncoding != null
165: && contentEncoding.contains("gzip")) {
166: in = new GZIPInputStream(in);
167: }
168: } catch (IOException e) {
169: if (statusCode == HttpURLConnection.HTTP_NO_CONTENT
170: || (isFailure && statusCode != HttpURLConnection.HTTP_INTERNAL_ERROR)) {
171: try {
172: throw new ClientTransportException(
173: ClientMessages.localizableHTTP_STATUS_CODE(
174: statusCode, httpConnection
175: .getResponseMessage()));
176: } catch (IOException ex) {
177: throw new ClientTransportException(
178: ClientMessages.localizableHTTP_STATUS_CODE(
179: statusCode, ex));
180: }
181: }
182: throw new ClientTransportException(ClientMessages
183: .localizableHTTP_CLIENT_FAILED(e), e);
184: }
185: return in;
186: }
187:
188: public Map<String, List<String>> getHeaders() {
189: if (respHeaders != null) {
190: return respHeaders;
191: }
192: respHeaders = new Headers();
193: respHeaders.putAll(httpConnection.getHeaderFields());
194: return respHeaders;
195: }
196:
197: protected InputStream readResponse() throws IOException {
198: return (isFailure ? httpConnection.getErrorStream()
199: : httpConnection.getInputStream());
200: }
201:
202: /*
203: * Will throw an exception instead of returning 'false' if there is no
204: * return message to be processed (i.e., in the case of an UNAUTHORIZED
205: * response from the servlet or 404 not found)
206: */
207: /*package*/void checkResponseCode() {
208: try {
209:
210: statusCode = httpConnection.getResponseCode();
211:
212: if ((httpConnection.getResponseCode() == HttpURLConnection.HTTP_INTERNAL_ERROR)) {
213: isFailure = true;
214: //added HTTP_ACCEPT for 1-way operations
215: } else if (httpConnection.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED) {
216:
217: // no soap message returned, so skip reading message and throw exception
218: throw new ClientTransportException(
219: ClientMessages
220: .localizableHTTP_CLIENT_UNAUTHORIZED(httpConnection
221: .getResponseMessage()));
222: } else if (httpConnection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
223:
224: // no message returned, so skip reading message and throw exception
225: throw new ClientTransportException(ClientMessages
226: .localizableHTTP_NOT_FOUND(httpConnection
227: .getResponseMessage()));
228: } else if ((statusCode == HttpURLConnection.HTTP_MOVED_TEMP)
229: || (statusCode == HttpURLConnection.HTTP_MOVED_PERM)) {
230: isFailure = true;
231:
232: if (!redirect || (redirectCount <= 0)) {
233: throw new ClientTransportException(ClientMessages
234: .localizableHTTP_STATUS_CODE(statusCode,
235: getStatusMessage()));
236: }
237: } else if (statusCode < 200
238: || (statusCode >= 303 && statusCode < 500)) {
239: throw new ClientTransportException(ClientMessages
240: .localizableHTTP_STATUS_CODE(statusCode,
241: getStatusMessage()));
242: } else if (statusCode >= 500) {
243: isFailure = true;
244: }
245: } catch (IOException e) {
246: throw new WebServiceException(e);
247: }
248: // Hack for now
249: saveCookieAsNeeded();
250: }
251:
252: private String getStatusMessage() throws IOException {
253: int statusCode = httpConnection.getResponseCode();
254: String message = httpConnection.getResponseMessage();
255: if (statusCode == HttpURLConnection.HTTP_CREATED
256: || (statusCode >= HttpURLConnection.HTTP_MULT_CHOICE
257: && statusCode != HttpURLConnection.HTTP_NOT_MODIFIED && statusCode < HttpURLConnection.HTTP_BAD_REQUEST)) {
258: String location = httpConnection.getHeaderField("Location");
259: if (location != null)
260: message += " - Location: " + location;
261: }
262: return message;
263: }
264:
265: protected void sendCookieAsNeeded() {
266: Boolean shouldMaintainSessionProperty = (Boolean) context.invocationProperties
267: .get(SESSION_MAINTAIN_PROPERTY);
268: if (shouldMaintainSessionProperty != null
269: && shouldMaintainSessionProperty) {
270: cookieJar = (CookieJar) context.invocationProperties
271: .get(HTTP_COOKIE_JAR);
272: if (cookieJar == null) {
273: cookieJar = new CookieJar();
274:
275: // need to store in binding's context so it is not lost
276: context.proxy.getRequestContext().put(HTTP_COOKIE_JAR,
277: cookieJar);
278: }
279: cookieJar.applyRelevantCookies(httpConnection);
280: }
281: }
282:
283: private void saveCookieAsNeeded() {
284: if (cookieJar != null) {
285: cookieJar.recordAnyCookies(httpConnection);
286: }
287: }
288:
289: private void createHttpConnection() throws IOException {
290:
291: // does the client want request redirection to occur
292: String redirectProperty = (String) context.invocationProperties
293: .get(REDIRECT_REQUEST_PROPERTY);
294: if (redirectProperty != null) {
295: if (redirectProperty.equalsIgnoreCase("false"))
296: redirect = false;
297: }
298:
299: checkEndpoints();
300:
301: httpConnection = (HttpURLConnection) endpoint.openConnection();
302: if (httpConnection instanceof HttpsURLConnection) {
303: https = true;
304:
305: boolean verification = false;
306: // does the client want client hostname verification by the service
307: String verificationProperty = (String) context.invocationProperties
308: .get(HOSTNAME_VERIFICATION_PROPERTY);
309: if (verificationProperty != null) {
310: if (verificationProperty.equalsIgnoreCase("true"))
311: verification = true;
312: }
313: if (!verification) {
314: ((HttpsURLConnection) httpConnection)
315: .setHostnameVerifier(new HttpClientVerifier());
316: }
317:
318: // Set application's HostNameVerifier for this connection
319: HostnameVerifier verifier = (HostnameVerifier) context.invocationProperties
320: .get(JAXWSProperties.HOSTNAME_VERIFIER);
321: if (verifier != null) {
322: ((HttpsURLConnection) httpConnection)
323: .setHostnameVerifier(verifier);
324: }
325:
326: // Set application's SocketFactory for this connection
327: SSLSocketFactory sslSocketFactory = (SSLSocketFactory) context.invocationProperties
328: .get(JAXWSProperties.SSL_SOCKET_FACTORY);
329: if (sslSocketFactory != null) {
330: ((HttpsURLConnection) httpConnection)
331: .setSSLSocketFactory(sslSocketFactory);
332: }
333:
334: }
335:
336: writeBasicAuthAsNeeded(context, reqHeaders);
337:
338: // allow interaction with the web page - user may have to supply
339: // username, password id web page is accessed from web browser
340: httpConnection.setAllowUserInteraction(true);
341:
342: // enable input, output streams
343: httpConnection.setDoOutput(true);
344: httpConnection.setDoInput(true);
345:
346: String requestMethod = (String) context.invocationProperties
347: .get(MessageContext.HTTP_REQUEST_METHOD);
348: String method = (requestMethod != null) ? requestMethod
349: : "POST";
350: httpConnection.setRequestMethod(method);
351:
352: //this code or something similiar needs t be moved elsewhere for error checking
353: /*if (context.invocationProperties.get(BindingProviderProperties.BINDING_ID_PROPERTY).equals(HTTPBinding.HTTP_BINDING)){
354: method = (requestMethod != null)?requestMethod:method;
355: } else if
356: (context.invocationProperties.get(BindingProviderProperties.BINDING_ID_PROPERTY).equals(SOAPBinding.SOAP12HTTP_BINDING) &&
357: "GET".equalsIgnoreCase(requestMethod)) {
358: }
359: */
360:
361: Integer reqTimeout = (Integer) context.invocationProperties
362: .get(BindingProviderProperties.REQUEST_TIMEOUT);
363: if (reqTimeout != null) {
364: httpConnection.setReadTimeout(reqTimeout);
365: }
366:
367: Integer connectTimeout = (Integer) context.invocationProperties
368: .get(JAXWSProperties.CONNECT_TIMEOUT);
369: if (connectTimeout != null) {
370: httpConnection.setConnectTimeout(connectTimeout);
371: }
372:
373: Integer chunkSize = (Integer) context.invocationProperties
374: .get(JAXWSProperties.HTTP_CLIENT_STREAMING_CHUNK_SIZE);
375: if (chunkSize != null) {
376: httpConnection.setChunkedStreamingMode(chunkSize);
377: }
378:
379: // set the properties on HttpURLConnection
380: for (Map.Entry<String, List<String>> entry : reqHeaders
381: .entrySet()) {
382: httpConnection.addRequestProperty(entry.getKey(), entry
383: .getValue().get(0));
384: }
385: httpConnection.addRequestProperty("User-Agent",
386: RuntimeVersion.VERSION.toString());
387: }
388:
389: public boolean isSecure() {
390: return https;
391: }
392:
393: // private void redirectRequest(HttpURLConnection httpConnection, SOAPMessageContext context) {
394: // String redirectEndpoint = httpConnection.getHeaderField("Location");
395: // if (redirectEndpoint != null) {
396: // httpConnection.disconnect();
397: // invoke(redirectEndpoint, context);
398: // } else
399: // System.out.println("redirection Failed");
400: // }
401:
402: private boolean checkForRedirect(int statusCode) {
403: return (((statusCode == 301) || (statusCode == 302))
404: && redirect && (redirectCount-- > 0));
405: }
406:
407: private void checkEndpoints() {
408: if (!LAST_ENDPOINT.equalsIgnoreCase(endpoint.toString())) {
409: redirectCount = START_REDIRECT_COUNT;
410: LAST_ENDPOINT = endpoint.toString();
411: }
412: }
413:
414: private void writeBasicAuthAsNeeded(Packet context,
415: Map<String, List<String>> reqHeaders) {
416: String user = (String) context.invocationProperties
417: .get(BindingProvider.USERNAME_PROPERTY);
418: if (user != null) {
419: String pw = (String) context.invocationProperties
420: .get(BindingProvider.PASSWORD_PROPERTY);
421: if (pw != null) {
422: StringBuffer buf = new StringBuffer(user);
423: buf.append(":");
424: buf.append(pw);
425: String creds = printBase64Binary(buf.toString()
426: .getBytes());
427: reqHeaders.put("Authorization", Collections
428: .singletonList("Basic " + creds));
429: }
430: }
431: }
432:
433: private boolean requiresOutputStream() {
434: return !(httpConnection.getRequestMethod().equalsIgnoreCase(
435: "GET")
436: || httpConnection.getRequestMethod().equalsIgnoreCase(
437: "HEAD") || httpConnection.getRequestMethod()
438: .equalsIgnoreCase("DELETE"));
439: }
440:
441: public @Nullable
442: String getContentType() {
443: return httpConnection.getContentType();
444: }
445:
446: // overide default SSL HttpClientVerifier to always return true
447: // effectively overiding Hostname client verification when using SSL
448: private static class HttpClientVerifier implements HostnameVerifier {
449: public boolean verify(String s, SSLSession sslSession) {
450: return true;
451: }
452: }
453:
454: /**
455: * HttpURLConnection.getOuputStream() returns sun.net.www.http.ChunkedOuputStream in chunked
456: * streaming mode. If you call ChunkedOuputStream.write(byte[20MB], int, int), then the whole data
457: * is kept in memory. This wraps the ChunkedOuputStream so that it writes only small
458: * chunks.
459: */
460: private static final class WSChunkedOuputStream extends
461: OutputStream {
462: final OutputStream actual;
463: final int chunkSize;
464:
465: WSChunkedOuputStream(OutputStream actual, int chunkSize) {
466: this .actual = actual;
467: this .chunkSize = chunkSize;
468: }
469:
470: @Override
471: public void write(byte b[], int off, int len)
472: throws IOException {
473: int sent = 0;
474: while (sent < len) {
475: int chunk = len - sent;
476: if (chunk > chunkSize) {
477: chunk = chunkSize;
478: }
479: actual.write(b, off, chunk);
480: off += chunk;
481: sent += chunk;
482: }
483: }
484:
485: @Override
486: public void write(byte b[]) throws IOException {
487: write(b, 0, b.length);
488: }
489:
490: @Override
491: public void write(int b) throws IOException {
492: actual.write(b);
493: }
494:
495: @Override
496: public void flush() throws IOException {
497: actual.flush();
498: }
499:
500: @Override
501: public void close() throws IOException {
502: actual.close();
503: }
504: }
505:
506: }
|