001 package com.thetransactioncompany.jsonrpc2.client;
002
003
004 import java.io.IOException;
005 import java.io.OutputStreamWriter;
006
007 import java.net.CookieManager;
008 import java.net.CookiePolicy;
009 import java.net.HttpCookie;
010 import java.net.HttpURLConnection;
011 import java.net.URISyntaxException;
012 import java.net.URL;
013 import java.net.URLConnection;
014
015 import java.security.SecureRandom;
016
017 import java.security.cert.X509Certificate;
018
019 import java.util.Collections;
020 import java.util.List;
021
022 import javax.net.ssl.HttpsURLConnection;
023 import javax.net.ssl.SSLContext;
024 import javax.net.ssl.SSLSocketFactory;
025 import javax.net.ssl.TrustManager;
026 import javax.net.ssl.X509TrustManager;
027
028 import com.thetransactioncompany.jsonrpc2.JSONRPC2Notification;
029 import com.thetransactioncompany.jsonrpc2.JSONRPC2ParseException;
030 import com.thetransactioncompany.jsonrpc2.JSONRPC2Request;
031 import com.thetransactioncompany.jsonrpc2.JSONRPC2Response;
032
033
034 /**
035 * Sends requests and / or notifications to a specified JSON-RPC 2.0 server
036 * URL. The JSON-RPC 2.0 messages are dispatched by means of HTTP(S) POST.
037 * This class is thread-safe.
038 *
039 * <p>The client-session class has a number of {@link JSONRPC2SessionOptions
040 * optional settings}. To change them pass a modified options instance to the
041 * {@link #setOptions setOptions()} method.
042 *
043 * <p>Example JSON-RPC 2.0 client session:
044 *
045 * <pre>
046 * // First, import the required packages:
047 *
048 * // The Client sessions package
049 * import com.thetransactioncompany.jsonrpc2.client.*;
050 *
051 * // The Base package for representing JSON-RPC 2.0 messages
052 * import com.thetransactioncompany.jsonrpc2.*;
053 *
054 * // The JSON Smart package for JSON encoding/decoding (optional)
055 * import net.minidev.json.*;
056 *
057 * // For creating URLs
058 * import java.net.*;
059 *
060 * // ...
061 *
062 *
063 * // Creating a new session to a JSON-RPC 2.0 web service at a specified URL
064 *
065 * // The JSON-RPC 2.0 server URL
066 * URL serverURL = null;
067 *
068 * try {
069 * serverURL = new URL("http://jsonrpc.example.com:8080");
070 *
071 * } catch (MalformedURLException e) {
072 * // handle exception...
073 * }
074 *
075 * // Create new JSON-RPC 2.0 client session
076 * JSONRPC2Session mySession = new JSONRPC2Session(serverURL);
077 *
078 *
079 * // Once the client session object is created, you can use to send a series
080 * // of JSON-RPC 2.0 requests and notifications to it.
081 *
082 * // Sending an example "getServerTime" request:
083 *
084 * // Construct new request
085 * String method = "getServerTime";
086 * int requestID = 0;
087 * JSONRPC2Request request = new JSONRPC2Request(method, requestID);
088 *
089 * // Send request
090 * JSONRPC2Response response = null;
091 *
092 * try {
093 * response = mySession.send(request);
094 *
095 * } catch (JSONRPC2SessionException e) {
096 *
097 * System.err.println(e.getMessage());
098 * // handle exception...
099 * }
100 *
101 * // Print response result / error
102 * if (response.indicatesSuccess())
103 * System.out.println(response.getResult());
104 * else
105 * System.out.println(response.getError().getMessage());
106 *
107 * </pre>
108 *
109 * @author Vladimir Dzhuvinov
110 */
111 public class JSONRPC2Session {
112
113
114 /**
115 * The server URL, which protocol must be HTTP or HTTPS.
116 *
117 * <p>Example URL: "http://jsonrpc.example.com:8080"
118 */
119 private URL url;
120
121
122 /**
123 * The client-session options.
124 */
125 private JSONRPC2SessionOptions options;
126
127
128 /**
129 * Custom HTTP URL connection configurator.
130 */
131 private ConnectionConfigurator connectionConfigurator;
132
133
134 /**
135 * Optional HTTP raw response inspector.
136 */
137 private RawResponseInspector responseInspector;
138
139
140 /**
141 * Optional HTTP cookie manager.
142 */
143 private CookieManager cookieManager;
144
145
146 /**
147 * Trust-all-certs (including self-signed) SSL socket factory.
148 */
149 private static SSLSocketFactory trustAllSocketFactory = createTrustAllSocketFactory();
150
151
152 /**
153 * Creates a new client session to a JSON-RPC 2.0 server at the
154 * specified URL. Uses a default {@link JSONRPC2SessionOptions}
155 * instance.
156 *
157 * @param url The server URL, e.g. "http://jsonrpc.example.com:8080".
158 * Must not be {@code null}.
159 */
160 public JSONRPC2Session(final URL url) {
161
162 if (! url.getProtocol().equalsIgnoreCase("http") &&
163 ! url.getProtocol().equalsIgnoreCase("https") )
164 throw new IllegalArgumentException("The URL protocol must be HTTP or HTTPS");
165
166 this.url = url;
167
168 // Default session options
169 options = new JSONRPC2SessionOptions();
170
171 // No initial connection configurator
172 connectionConfigurator = null;
173 }
174
175
176 /**
177 * Creates a trust-all-certificates SSL socket factory. Encountered
178 * exceptions are not rethrown.
179 *
180 * @return The SSL socket factory.
181 */
182 public static SSLSocketFactory createTrustAllSocketFactory() {
183
184 TrustManager[] trustAllCerts = new TrustManager[] {
185
186 new X509TrustManager() {
187
188 public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[]{}; }
189
190 public void checkClientTrusted(X509Certificate[] certs, String authType) { }
191
192 public void checkServerTrusted(X509Certificate[] certs, String authType) { }
193 }
194 };
195
196 try {
197 SSLContext sc = SSLContext.getInstance("SSL");
198 sc.init(null, trustAllCerts, new SecureRandom());
199 return sc.getSocketFactory();
200
201 } catch (Exception e) {
202
203 // Ignore
204 return null;
205 }
206 }
207
208
209 /**
210 * Gets the JSON-RPC 2.0 server URL.
211 *
212 * @return The server URL.
213 */
214 public URL getURL() {
215
216 return url;
217 }
218
219
220 /**
221 * Sets the JSON-RPC 2.0 server URL.
222 *
223 * @param url The server URL. Must not be {@code null}.
224 */
225 public void setURL(final URL url) {
226
227 if (url == null)
228 throw new IllegalArgumentException("The server URL must not be null");
229
230 this.url = url;
231 }
232
233
234 /**
235 * Gets the JSON-RPC 2.0 client session options.
236 *
237 * @return The client session options.
238 */
239 public JSONRPC2SessionOptions getOptions() {
240
241 return options;
242 }
243
244
245 /**
246 * Sets the JSON-RPC 2.0 client session options.
247 *
248 * @param options The client session options, must not be {@code null}.
249 */
250 public void setOptions(final JSONRPC2SessionOptions options) {
251
252 if (options == null)
253 throw new IllegalArgumentException("The client session options must not be null");
254
255 this.options = options;
256 }
257
258
259 /**
260 * Gets the custom HTTP URL connection configurator.
261 *
262 * @since 1.5
263 *
264 * @return The connection configurator, {@code null} if none is set.
265 */
266 public ConnectionConfigurator getConnectionConfigurator() {
267
268 return connectionConfigurator;
269 }
270
271
272 /**
273 * Specifies a custom HTTP URL connection configurator. It will be
274 * {@link ConnectionConfigurator#configure applied} to each new HTTP
275 * connection after the {@link JSONRPC2SessionOptions session options}
276 * are applied and before the connection is established.
277 *
278 * <p>This method may be used to set custom HTTP request headers,
279 * timeouts or other properties.
280 *
281 * @since 1.5
282 *
283 * @param connectionConfigurator A custom HTTP URL connection
284 * configurator, {@code null} to remove
285 * a previously set one.
286 */
287 public void setConnectionConfigurator(final ConnectionConfigurator connectionConfigurator) {
288
289 this.connectionConfigurator = connectionConfigurator;
290 }
291
292
293 /**
294 * Gets the optional inspector for the raw HTTP responses.
295 *
296 * @since 1.6
297 *
298 * @return The optional inspector for the raw HTTP responses,
299 * {@code null} if none is set.
300 */
301 public RawResponseInspector getRawResponseInspector() {
302
303 return responseInspector;
304 }
305
306
307 /**
308 * Specifies an optional inspector for the raw HTTP responses to
309 * JSON-RPC 2.0 requests and notifications. Its
310 * {@link RawResponseInspector#inspect inspect} method will be called
311 * upon reception of a HTTP response.
312 *
313 * <p>You can use the {@link RawResponseInspector} interface to
314 * retrieve the unparsed response content and headers.
315 *
316 * @since 1.6
317 *
318 * @param responseInspector An optional inspector for the raw HTTP
319 * responses, {@code null} to remove a
320 * previously set one.
321 */
322 public void setRawResponseInspector(final RawResponseInspector responseInspector) {
323
324 this.responseInspector = responseInspector;
325 }
326
327
328 /**
329 * Gets all non-expired HTTP cookies currently stored in the client.
330 *
331 * @return The HTTP cookies, or empty list if none were set by the
332 * server or cookies are not
333 * {@link JSONRPC2SessionOptions#acceptCookies accepted}.
334 */
335 public List<HttpCookie> getCookies() {
336
337 if (cookieManager == null) {
338
339 List<HttpCookie> emptyList = Collections.emptyList();
340 return emptyList;
341 }
342
343 return cookieManager.getCookieStore().getCookies();
344 }
345
346
347 /**
348 * Applies the required headers to the specified URL connection.
349 *
350 * @param con The URL connection which must be open.
351 *
352 * @throws JSONRPC2SessionException If an exception is encountered.
353 */
354 private void applyHeaders(final URLConnection con)
355 throws JSONRPC2SessionException {
356
357 // Expect UTF-8 for JSON
358 con.setRequestProperty("Accept-Charset", "UTF-8");
359
360 // Add "Content-Type" header?
361 if (options.getRequestContentType() != null)
362 con.setRequestProperty("Content-Type", options.getRequestContentType());
363
364 // Add "Origin" header?
365 if (options.getOrigin() != null)
366 con.setRequestProperty("Origin", options.getOrigin());
367
368 // Add "Accept-Encoding: gzip, deflate" header?
369 if (options.enableCompression())
370 con.setRequestProperty("Accept-Encoding", "gzip, deflate");
371
372 // Add "Cookie" headers?
373 if (options.acceptCookies()) {
374
375 StringBuilder buf = new StringBuilder();
376
377 for (HttpCookie cookie: getCookies()) {
378
379 if (buf.length() > 0)
380 buf.append("; ");
381
382 buf.append(cookie.toString());
383 }
384
385 con.setRequestProperty("Cookie", buf.toString());
386 }
387 }
388
389
390 /**
391 * Creates and configures a new URL connection to the JSON-RPC 2.0
392 * server endpoint according to the session settings.
393 *
394 * @return The URL connection, configured and ready for output (HTTP
395 * POST).
396 *
397 * @throws JSONRPC2SessionException If the URL connection couldn't be
398 * created or configured.
399 */
400 private URLConnection createURLConnection()
401 throws JSONRPC2SessionException {
402
403 // Open HTTP connection
404 URLConnection con = null;
405
406 try {
407 // Use proxy?
408 if (options.getProxy() != null)
409 con = url.openConnection(options.getProxy());
410 else
411 con = url.openConnection();
412
413 } catch (IOException e) {
414
415 throw new JSONRPC2SessionException(
416 "Network exception: " + e.getMessage(),
417 JSONRPC2SessionException.NETWORK_EXCEPTION,
418 e);
419 }
420
421 con.setConnectTimeout(options.getConnectTimeout());
422 con.setReadTimeout(options.getReadTimeout());
423
424 applyHeaders(con);
425
426 // Set POST mode
427 con.setDoOutput(true);
428
429 // Set trust all certs SSL factory?
430 if (con instanceof HttpsURLConnection && options.trustsAllCerts()) {
431
432 if (trustAllSocketFactory == null)
433 throw new JSONRPC2SessionException("Couldn't obtain trust-all SSL socket factory");
434
435 ((HttpsURLConnection)con).setSSLSocketFactory(trustAllSocketFactory);
436 }
437
438 // Apply connection configurator?
439 if (connectionConfigurator != null)
440 connectionConfigurator.configure((HttpURLConnection)con);
441
442 return con;
443 }
444
445
446 /**
447 * Posts string data (i.e. JSON string) to the specified URL
448 * connection.
449 *
450 * @param con The URL connection. Must be in HTTP POST mode. Must not
451 * be {@code null}.
452 * @param data The string data to post. Must not be {@code null}.
453 *
454 * @throws JSONRPC2SessionException If an I/O exception is encountered.
455 */
456 private static void postString(final URLConnection con, final String data)
457 throws JSONRPC2SessionException {
458
459 try {
460 OutputStreamWriter wr = new OutputStreamWriter(con.getOutputStream(), "UTF-8");
461 wr.write(data);
462 wr.flush();
463 wr.close();
464
465 } catch (IOException e) {
466
467 throw new JSONRPC2SessionException(
468 "Network exception: " + e.getMessage(),
469 JSONRPC2SessionException.NETWORK_EXCEPTION,
470 e);
471 }
472 }
473
474
475 /**
476 * Reads the raw response from an URL connection (after HTTP POST).
477 * Invokes the {@link RawResponseInspector} if configured and stores
478 * any cookies {@link JSONRPC2SessionOptions#storeCookies if required}.
479 *
480 * @param con The URL connection. It should contain ready data for
481 * retrieval. Must not be {@code null}.
482 *
483 * @return The raw response.
484 *
485 * @throws JSONRPC2SessionException If an exception is encountered.
486 */
487 private RawResponse readRawResponse(final URLConnection con)
488 throws JSONRPC2SessionException {
489
490 RawResponse rawResponse = null;
491
492 try {
493 rawResponse = RawResponse.parse((HttpURLConnection)con);
494
495 } catch (IOException e) {
496
497 throw new JSONRPC2SessionException(
498 "Network exception: " + e.getMessage(),
499 JSONRPC2SessionException.NETWORK_EXCEPTION,
500 e);
501 }
502
503 if (responseInspector != null)
504 responseInspector.inspect(rawResponse);
505
506 if (options.acceptCookies()) {
507
508 // Init cookie manager?
509 if (cookieManager == null)
510 cookieManager = new CookieManager(null, CookiePolicy.ACCEPT_ALL);
511
512 try {
513 cookieManager.put(con.getURL().toURI(), rawResponse.getHeaderFields());
514
515 } catch (URISyntaxException e) {
516
517 throw new JSONRPC2SessionException(
518 "Network exception: " + e.getMessage(),
519 JSONRPC2SessionException.NETWORK_EXCEPTION,
520 e);
521
522 } catch (IOException e) {
523
524 throw new JSONRPC2SessionException(
525 "I/O exception: " + e.getMessage(),
526 JSONRPC2SessionException.NETWORK_EXCEPTION,
527 e);
528 }
529 }
530
531 return rawResponse;
532 }
533
534
535 /**
536 * Sends a JSON-RPC 2.0 request using HTTP POST and returns the server
537 * response.
538 *
539 * @param request The JSON-RPC 2.0 request to send. Must not be
540 * {@code null}.
541 *
542 * @return The JSON-RPC 2.0 response returned by the server.
543 *
544 * @throws JSONRPC2SessionException On a network error, unexpected HTTP
545 * response content type or invalid
546 * JSON-RPC 2.0 response.
547 */
548 public JSONRPC2Response send(final JSONRPC2Request request)
549 throws JSONRPC2SessionException {
550
551 // Create and configure URL connection to server endpoint
552 URLConnection con = createURLConnection();
553
554 // Send request encoded as JSON
555 postString(con, request.toString());
556
557 // Get the response
558 RawResponse rawResponse = readRawResponse(con);
559
560 // Check response content type
561 String contentType = rawResponse.getContentType();
562
563 if (! options.isAllowedResponseContentType(contentType)) {
564
565 String msg = null;
566
567 if (contentType == null)
568 msg = "Missing Content-Type header in the HTTP response";
569 else
570 msg = "Unexpected \"" + contentType + "\" content type of the HTTP response";
571
572 throw new JSONRPC2SessionException(msg, JSONRPC2SessionException.UNEXPECTED_CONTENT_TYPE);
573 }
574
575 // Parse and return the response
576 JSONRPC2Response response = null;
577
578 try {
579 response = JSONRPC2Response.parse(rawResponse.getContent(),
580 options.preservesParseOrder(),
581 options.ignoresVersion(),
582 options.parsesNonStdAttributes());
583
584 } catch (JSONRPC2ParseException e) {
585
586 throw new JSONRPC2SessionException(
587 "Invalid JSON-RPC 2.0 response",
588 JSONRPC2SessionException.BAD_RESPONSE,
589 e);
590 }
591
592 // Response ID must match the request ID, except for
593 // -32700 (parse error), -32600 (invalid request) and
594 // -32603 (internal error)
595
596 Object reqID = request.getID();
597 Object resID = response.getID();
598
599 if (reqID != null && resID !=null && reqID.toString().equals(resID.toString()) ) {
600 // ok
601 }
602 else if (reqID == null && resID == null) {
603 // ok
604 }
605 else if (! response.indicatesSuccess() && ( response.getError().getCode() == -32700 ||
606 response.getError().getCode() == -32600 ||
607 response.getError().getCode() == -32603 )) {
608 // ok
609 }
610 else {
611 throw new JSONRPC2SessionException(
612 "Invalid JSON-RPC 2.0 response: ID mismatch: Returned " +
613 resID.toString() + ", expected " + reqID.toString(),
614 JSONRPC2SessionException.BAD_RESPONSE);
615 }
616
617 return response;
618 }
619
620
621 /**
622 * Sends a JSON-RPC 2.0 notification using HTTP POST. Note that
623 * contrary to requests, notifications produce no server response.
624 *
625 * @param notification The JSON-RPC 2.0 notification to send. Must not
626 * be {@code null}.
627 *
628 * @throws JSONRPC2SessionException On a network error.
629 */
630 public void send(final JSONRPC2Notification notification)
631 throws JSONRPC2SessionException {
632
633 // Create and configure URL connection to server endpoint
634 URLConnection con = createURLConnection();
635
636 // Send notification encoded as JSON
637 postString(con, notification.toString());
638
639 // Get the response /for the inspector only/
640 RawResponse rawResponse = readRawResponse(con);
641 }
642 }
643