Skip navigation links

Package org.apache.juneau.rest.client

REST client API

See: Description

Package org.apache.juneau.rest.client Description

REST client API

Table of Contents
  1. REST Client API

    1. SSL Support

      1. SSLOpts Bean

    2. Authentication

      1. BASIC Authentication

      2. FORM-based Authentication

      3. OIDC Authentication

    3. Using Response Patterns

    4. Piping Response Output

    5. Debugging

    6. Logging

    7. Interceptors

    8. Remoteable Proxies

    9. Other Useful Methods

1 - REST Client API

Juneau provides an HTTP client API that makes it extremely simple to connect to remote REST interfaces and seemlessly send and receive serialized POJOs in requests and responses.

Features:
  • Converts POJOs directly to HTTP request message bodies using Serializer classes.
  • Converts HTTP response message bodies directly to POJOs using Parser classes.
  • Exposes the full functionality of the Apache HttpClient API by exposing all methods defined on the HttpClientBuilder class.
  • Provides various convenience methods for setting up common SSL and authentication methods.
  • Provides a fluent interface that allows you to make complex REST calls in a single line of code.

The client API is designed to work as a thin layer on top of the proven Apache HttpClient API. By leveraging the HttpClient library, details such as SSL certificate negotiation, proxies, encoding, etc... are all handled in Apache code.

The Juneau client API prereq's Apache HttpClient 4.1.2+. At a minimum, the following jars are required:

  • httpclient-4.5.jar
  • httpcore-4.4.1.jar
  • httpmime-4.5.jar
Example:

// Examples below use the Juneau Address Book resource example // Create a reusable client with JSON support RestClient client = new RestClientBuilder().build(); // GET request, ignoring output try { int rc = client.doGet("http://localhost:9080/sample/addressBook").run(); // Succeeded! } catch (RestCallException e) { // Failed! System.err.println( String.format("status=%s, message=%s", e.getResponseStatus(), e.getResponseMessage()) ); } // Remaining examples ignore thrown exceptions. // GET request, secure, ignoring output client.doGet("https://localhost:9443/sample/addressBook").run(); // GET request, getting output as a String. No POJO parsing is performed. // Note that when calling one of the getX() methods, you don't need to call connect() or disconnect(), since // it's automatically called for you. String output = client.doGet("http://localhost:9080/sample/addressBook") .getResponseAsString(); // GET request, getting output as a Reader Reader r = client.doGet("http://localhost:9080/sample/addressBook") .getReader(); // GET request, getting output as an untyped map // Input must be an object (e.g. "{...}") ObjectMap m = client.doGet("http://localhost:9080/sample/addressBook/0") .getResponse(ObjectMap.class); // GET request, getting output as an untyped list // Input must be an array (e.g. "[...]") ObjectList l = client.doGet("http://localhost:9080/sample/addressBook") .getResponse(ObjectList.class); // GET request, getting output as a parsed bean // Input must be an object (e.g. "{...}") // Note that you don't have to do any casting! Person p = client.doGet("http://localhost:9080/sample/addressBook/0") .getResponse(Person.class); // GET request, getting output as a parsed bean // Input must be an array of objects (e.g. "[{...},{...}]") Person[] pa = client.doGet("http://localhost:9080/sample/addressBook") .getResponse(Person[].class); // Same as above, except as a List<Person> List<Person> pl = client.doGet("http://localhost:9080/sample/addressBook") .getResponse(List.class, Person.class); // GET request, getting output as a parsed string // Input must be a string (e.g. "<string>foo</string>" or "'foo'") String name = client.doGet("http://localhost:9080/sample/addressBook/0/name") .getResponse(String.class); // GET request, getting output as a parsed number // Input must be a number (e.g. "<number>123</number>" or "123") int age = client.doGet("http://localhost:9080/sample/addressBook/0/age") .getResponse(Integer.class); // GET request, getting output as a parsed boolean // Input must be a boolean (e.g. "<boolean>true</boolean>" or "true") boolean isCurrent = client.doGet("http://localhost:9080/sample/addressBook/0/addresses/0/isCurrent") .getResponse(Boolean.class); // GET request, getting a filtered object client = new RestClientBuilder().pojoSwaps(CalendarSwap.ISO8601.class).build(); Calendar birthDate = client.doGet("http://localhost:9080/sample/addressBook/0/birthDate") .getResponse(GregorianCalendar.class); // PUT request on regular field String newName = "John Smith"; int rc = client.doPut("http://localhost:9080/addressBook/0/name", newName).run(); // PUT request on filtered field Calendar newBirthDate = new GregorianCalendar(1, 2, 3, 4, 5, 6); rc = client.doPut("http://localhost:9080/sample/addressBook/0/birthDate", newBirthDate).run(); // POST of a new entry to a list Address newAddress = new Address("101 Main St", "Anywhere", "NY", 12121, false); rc = client.doPost("http://localhost:9080/addressBook/0/addresses", newAddress).run();

Notes:
  • The RestClient class exposes all the builder methods on the Apache HttpClient HttpClientBuilder class. Use these methods to provide any customized HTTP client behavior.

1.1 - SSL Support

The simplest way to enable SSL support in the client is to use the RestClientBuilder.enableSSL(SSLOpts) method and one of the predefined SSLOpts instances:

Example:

// Create a client that ignores self-signed or otherwise invalid certificates. RestClientBuilder builder = new RestClientBuilder() .enableSSL(SSLOpts.LAX); // ...or... RestClientBuilder builder = new RestClientBuilder() .enableLaxSSL();

This is functionally equivalent to the following:

RestClientBuilder builder = new RestClientBuilder(); HostnameVerifier hv = new NoopHostnameVerifier(); TrustManager tm = new SimpleX509TrustManager(true); for (String p : new String[]{"SSL","TLS","SSL_TLS"}) { SSLContext ctx = SSLContext.getInstance(p); ctx.init(null, new TrustManager[] { tm }, null); SSLConnectionSocketFactory sf = new SSLConnectionSocketFactory(ctx, hv); builder.setSSLSocketFactory(sf); Registry<ConnectionSocketFactory> r = RegistryBuilder.<ConnectionSocketFactory>.create() .register("https", sf).build(); builder.setConnectionManager(new PoolingHttpClientConnectionManager(r)); }

More complex SSL support can be enabled through the various HttpClientBuilder methods defined on the class.

1.1.1 - SSLOpts Bean

The SSLOpts class itself is a bean that can be created by the parsers. For example, SSL options can be specified in a config file and retrieved as a bean using the ConfigFile class.

Contents of MyConfig.cfg

#================================================================================ # My Connection Settings #================================================================================ [Connection] url = https://myremotehost:9443 ssl = {certValidate:'LAX',hostVerify:'LAX'}

Code that reads an SSLOpts bean from the config file

// Read config file and set SSL options based on what's in that file. ConfigFile cf = new ConfigFileBuilder().build("MyConfig.cfg"); SSLOpts ssl = cf.getObject(SSLOpts.class, "Connection/ssl"); RestClient rc = new RestClient().enableSSL(ssl);

1.2 - Authentication

1.2.1 - BASIC Authentication

The RestClientBuilder.basicAuth(String,int,String,String) method can be used to quickly enable BASIC authentication support.

Example:

// Create a client that performs BASIC authentication using the specified user/pw. RestClient restClient = new RestClientBuilder() .basicAuth(HOST, PORT, USER, PW) .build();

This is functionally equivalent to the following:

RestClientBuilder builder = new RestClientBuilder(); AuthScope scope = new AuthScope(HOST, PORT); Credentials up = new UsernamePasswordCredentials(USER, PW); CredentialsProvider p = new BasicCredentialsProvider(); p.setCredentials(scope, up); builder.setDefaultCredentialsProvider(p);

1.2.2 - FORM-based Authentication

The RestClientBuilder class does not itself provide FORM-based authentication since there is no standard way of providing such support. Typically, to perform FORM-based or other types of authentication, you'll want to create your own subclass of RestClientBuilder and override the RestClientBuilder.createHttpClient() method to provide an authenticated client.

The following example shows how the JazzRestClient class provides FORM-based authentication support.

/** * Constructor. */ public JazzRestClientBuilder(URI jazzUri, String user, String pw) throws IOException { ... } /** * Override the createHttpClient() method to return an authenticated client. */ @Override /* RestClientBuilder */ protected CloseableHttpClient createHttpClient() throws Exception { CloseableHttpClient client = super.createHttpClient(); formBasedAuthenticate(client); visitAuthenticatedURL(client); return client; } /* * Performs form-based authentication against the Jazz server. */ private void formBasedAuthenticate(HttpClient client) throws IOException { URI uri2 = jazzUri.resolve("j_security_check"); HttpPost request = new HttpPost(uri2); request.setConfig(RequestConfig.custom().setRedirectsEnabled(false).build()); // Charset must explicitly be set to UTF-8 to handle user/pw with non-ascii characters. request.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8"); NameValuePairs params = new NameValuePairs() .append(new BasicNameValuePair("j_username"", user)) .append(new BasicNameValuePair("j_password", pw)); request.setEntity(new UrlEncodedFormEntity(params)); HttpResponse response = client.execute(request); try { int rc = response.getStatusLine().getStatusCode(); Header authMsg = response.getFirstHeader("X-com-ibm-team-repository-web-auth-msg"); if (authMsg != null) throw new IOException(authMsg.getValue()); // The form auth request should always respond with a 200 ok or 302 redirect code if (rc == SC_MOVED_TEMPORARILY) { if (response.getFirstHeader("Location").getValue().matches("^.*/auth/authfailed.*$")) throw new IOException("Invalid credentials."); } else if (rc != SC_OK) { throw new IOException("Unexpected HTTP status: " + rc); } } finally { EntityUtils.consume(response.getEntity()); } } /* * This is needed for Tomcat because it responds with SC_BAD_REQUEST when the j_security_check URL is visited before an * authenticated URL has been visited. This same URL must also be visited after authenticating with j_security_check * otherwise tomcat will not consider the session authenticated */ private int visitAuthenticatedURL(HttpClient httpClient) throws IOException { HttpGet authenticatedURL = new HttpGet(jazzUri.resolve("authenticated/identity")); HttpResponse response = httpClient.execute(authenticatedURL); try { return response.getStatusLine().getStatusCode(); } finally { EntityUtils.consume(response.getEntity()); } }

1.2.3 - OIDC Authentication

The following example shows how the JazzRestClient class provides OIDC authentication support.

/** * Constructor. */ public JazzRestClientBuilder(URI jazzUri, String user, String pw) throws IOException { ... } /** * Override the createHttpClient() method to return an authenticated client. */ @Override /* RestClientBuilder */ protected CloseableHttpClient createHttpClient() throws Exception { CloseableHttpClient client = super.createHttpClient(); oidcAuthenticate(client); return client; } private void oidcAuthenticate(HttpClient client) throws IOException { HttpGet request = new HttpGet(jazzUri); request.setConfig(RequestConfig.custom().setRedirectsEnabled(false).build()); // Charset must explicitly be set to UTF-8 to handle user/pw with non-ascii characters. request.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8"); HttpResponse response = client.execute(request); try { int code = response.getStatusLine().getStatusCode(); // Already authenticated if (code == SC_OK) return; if (code != SC_UNAUTHORIZED) throw new RestCallException("Unexpected response during OIDC authentication: " + response.getStatusLine()); // x-jsa-authorization-redirect String redirectUri = getHeader(response, "X-JSA-AUTHORIZATION-REDIRECT"); if (redirectUri == null) throw new RestCallException("Expected a redirect URI during OIDC authentication: " + response.getStatusLine()); // Handle Bearer Challenge HttpGet method = new HttpGet(redirectUri + "&prompt=none"); addDefaultOidcHeaders(method); response = client.execute(method); code = response.getStatusLine().getStatusCode(); if (code != SC_OK) throw new RestCallException("Unexpected response during OIDC authentication phase 2: " + response.getStatusLine()); String loginRequired = getHeader(response, "X-JSA-LOGIN-REQUIRED"); if (! "true".equals(loginRequired)) throw new RestCallException("X-JSA-LOGIN-REQUIRED header not found on response during OIDC authentication phase 2: " + response.getStatusLine()); method = new HttpGet(redirectUri + "&prompt=none"); addDefaultOidcHeaders(method); response = client.execute(method); code = response.getStatusLine().getStatusCode(); if (code != SC_OK) throw new RestCallException("Unexpected response during OIDC authentication phase 3: " + response.getStatusLine()); // Handle JAS Challenge method = new HttpGet(redirectUri); addDefaultOidcHeaders(method); response = client.execute(method); code = response.getStatusLine().getStatusCode(); if (code != SC_OK) throw new RestCallException("Unexpected response during OIDC authentication phase 4: " + response.getStatusLine()); cookie = getHeader(response, "Set-Cookie"); Header[] defaultHeaders = new Header[] { new BasicHeader("User-Agent", "Jazz Native Client"), new BasicHeader("X-com-ibm-team-configuration-versions", "com.ibm.team.rtc=6.0.0,com.ibm.team.jazz.foundation=6.0"), new BasicHeader("Accept", "text/json"), new BasicHeader("Authorization", "Basic " + StringUtils.base64EncodeToString(user + ":" + pw)), new BasicHeader("Cookie", cookie) }; setDefaultHeaders(Arrays.asList(defaultHeaders)); } finally { EntityUtils.consume(response.getEntity()); } } private void addDefaultOidcHeaders(HttpRequestBase method) { method.addHeader("User-Agent", "Jazz Native Client"); method.addHeader("X-com-ibm-team-configuration-versions", "com.ibm.team.rtc=6.0.0,com.ibm.team.jazz.foundation=6.0"); method.addHeader("Accept", "text/json"); if (cookie != null) { method.addHeader("Authorization", "Basic " + StringUtils.base64EncodeToString(user + ":" + pw)); method.addHeader("Cookie", cookie); } }

1.3 - Using Response Patterns

One issue with REST (and HTTP in general) is that the HTTP response code must be set as a header before the body of the request is sent. This can be problematic when REST calls invoke long-running processes, pipes the results through the connection, and then fails after an HTTP 200 has already been sent.

One common solution is to serialize some text at the end to indicate whether the long-running process succeeded (e.g. "FAILED" or "SUCCEEDED").

The RestClient class has convenience methods for scanning the response without interfering with the other methods used for retrieving output.

The following example shows how the RestCall.successPattern(String) method can be used to look for a SUCCESS message in the output:

Example:

// Throw a RestCallException if SUCCESS is not found in the output. restClient.doPost(URL) .successPattern("SUCCESS") .run();

The RestCall.failurePattern(String) method does the opposite. It throws an exception if a failure message is detected.

Example:

// Throw a RestCallException if FAILURE or ERROR is found in the output. restClient.doPost(URL) .failurePattern("FAILURE|ERROR") .run();

These convenience methods are specialized methods that use the RestCall.responsePattern(ResponsePattern) method which uses regular expression matching against the response body. This method can be used to search for arbitrary patterns in the response body.

The following example shows how to use a response pattern finder to find and capture patterns for "x=number" and "y=string" from a response body.

Example:

final List<Number> xList = new ArrayList<Number>(); final List<String> yList = new ArrayList<String>(); String responseText = restClient.doGet(URL) .addResponsePattern( new ResponsePattern("x=(\\d+)") { @Override public void onMatch(RestCall restCall, Matcher m) throws RestCallException { xList.add(Integer.parseInt(m.group(1))); } @Override public void onNoMatch(RestCall restCall) throws RestCallException { throw new RestCallException("No X's found!"); } } ) .addResponsePattern( new ResponsePattern("y=(\\S+)") { @Override public void onMatch(RestCall restCall, Matcher m) throws RestCallException { yList.add(m.group(1)); } @Override public void onNoMatch(RestCall restCall) throws RestCallException { throw new RestCallException("No Y's found!"); } } ) .getResponseAsString();

Using response patterns does not affect the functionality of any of the other methods used to retrieve the response such as RestCall.getResponseAsString() or RestCall.getResponse(Class).
HOWEVER, if you want to retrieve the entire text of the response from inside the match methods, use RestCall.getCapturedResponse() since this method will not absorb the response for those other methods.

1.4 - Piping Response Output

The RestCall class provides various convenience pipeTo() methods to pipe output to output streams and writers.

If you want to pipe output without any intermediate buffering, you can use the RestCall.byLines() method. This will cause the output to be piped and flushed after every line. This can be useful if you want to display the results in real-time from a long running process producing output on a REST call.

Example:

// Pipe output from REST call to System.out in real-time. restClient.doPost(URL).byLines().pipeTo(new PrintWriter(System.out)).run();

1.5 - Debugging

Use the RestClientBuilder.debug() method to enable logging for HTTP requests made from the client.

Under-the-covers, this is simply a shortcut for adding the RestCallLogger.DEFAULT intercepter to the client. This causes the following output to be generated by the Java org.apache.juneau.rest.client logger at WARNING level:

=== HTTP Call (outgoing) ======================================================= === REQUEST === POST http://localhost:10000/testUrl HTTP/1.1 ---request headers--- Debug: true No-Trace: true Accept: application/json ---request entity--- Content-Type: application/json ---request content--- {"foo":"bar","baz":123} === RESPONSE === HTTP/1.1 200 OK ---response headers--- Content-Type: application/json;charset=utf-8 Content-Length: 21 Server: Jetty(8.1.0.v20120127) ---response content--- {"message":"OK then"} === END ========================================================================

This setting also causes a Debug: true header value to trigger logging of the request on the server side as well.

=== HTTP Request (incoming) ==================================================== HTTP POST /testUrl ---Headers--- Host: localhost:10000 Transfer-Encoding: chunked Accept: application/json Content-Type: application/json User-Agent: Apache-HttpClient/4.5 (Java/1.6.0_65) Connection: keep-alive Debug: true Accept-Encoding: gzip,deflate ---Default Servlet Headers--- ---Body--- {"foo":"bar","baz":123} === END ========================================================================

1.6 - Logging

Use the RestClientBuilder.logTo(Level,Logger) and RestCall.logTo(Level,Logger) methods to log HTTP calls. These methods will cause the HTTP request and response headers and body to be logged to the specified logger.

Example:

// Log the HTTP request/response to the specified logger. int rc = restClient.doGet(URL).logTo(INFO, getLogger()).run();

The method call is ignored if the logger level is below the specified level.

Customized logging can be handled by sub-classing the RestCallLogger class and using the RestCall.intercepter(RestCallInterceptor) method.

1.7 - Interceptors

The RestClientBuilder.intercepter(RestCallInterceptor) and RestCall.intercepter(RestCallInterceptor) methods can be used to intercept responses during specific connection lifecycle events.

The RestCallLogger class is an example of an intercepter that uses the various lifecycle methods to log HTTP requests.

/** * Specialized intercepter for logging calls to a log file. */ public class RestCallLogger extends RestCallInterceptor { private Level level; private Logger log; /** * Constructor. * * @param level The log level to log messages at. * @param log The logger to log to. */ protected RestCallLogger(Level level, Logger log) { this.level = level; this.log = log; } @Override /* RestCallInterceptor */ public void onInit(RestCall restCall) { if (log.isLoggable(level)) restCall.captureResponse(); } @Override /* RestCallInterceptor */ public void onConnect(RestCall restCall, int statusCode, HttpRequest req, HttpResponse res) { // Do nothing. } @Override /* RestCallInterceptor */ public void onRetry(RestCall restCall, int statusCode, HttpRequest req, HttpResponse res) { if (log.isLoggable(level)) log.log(level, MessageFormat.format("Call to {0} returned {1}. Will retry.", req.getRequestLine().getUri(), statusCode)); } @Override /* RestCallInterceptor */ public void onClose(RestCall restCall) throws RestCallException { try { if (log.isLoggable(level)) { String output = restCall.getCapturedResponse(); StringBuilder sb = new StringBuilder(); HttpUriRequest req = restCall.getRequest(); HttpResponse res = restCall.getResponse(); if (req != null) { sb.append("\n=== HTTP Call (outgoing) ========================================================="); sb.append("\n=== REQUEST ===\n").append(req); sb.append("\n---request headers---"); for (Header h : req.getAllHeaders()) sb.append("\n").append(h); if (req instanceof HttpEntityEnclosingRequestBase) { sb.append("\n---request entity---"); HttpEntityEnclosingRequestBase req2 = (HttpEntityEnclosingRequestBase)req; HttpEntity e = req2.getEntity(); if (e == null) sb.append("\nEntity is null"); else { if (e.getContentType() != null) sb.append("\n").append(e.getContentType()); if (e.getContentEncoding() != null) sb.append("\n").append(e.getContentEncoding()); if (e.isRepeatable()) { try { sb.append("\n---request content---\n").append(EntityUtils.toString(e)); } catch (Exception ex) { throw new RuntimeException(ex); } } } } } if (res != null) { sb.append("\n=== RESPONSE ===\n").append(res.getStatusLine()); sb.append("\n---response headers---"); for (Header h : res.getAllHeaders()) sb.append("\n").append(h); sb.append("\n---response content---\n").append(output); sb.append("\n=== END ========================================================================"); } log.log(level, sb.toString()); } } catch (IOException e) { log.log(Level.SEVERE, e.getLocalizedMessage(), e); } } }

1.8 - Remoteable Proxies

Juneau provides the capability of calling methods on POJOs on a server through client-side proxy interfaces. It offers a number of advantages over other similar remote proxy interfaces, such as being much simpler to use and allowing much more flexibility.

Proxy interfaces are retrieved using the RestClient.getRemoteableProxy(Class) method. The remoteable servlet is a specialized subclass of RestServlet that provides a full-blown REST interface for calling interfaces remotely.

In this example, we have the following interface defined that we want to call from the client side against a POJO on the server side (i.e. a Remoteable Service)...

public interface IAddressBook { Person createPerson(CreatePerson cp) throws Exception; }

The client side code for invoking this method is shown below...

// Create a RestClient using JSON for serialization, and point to the server-side remoteable servlet. RestClient client = new RestClientBuilder() .rootUrl("https://localhost:9080/juneau/sample/remoteable") .build(); // Create a proxy interface. IAddressBook ab = client.getRemoteableProxy(IAddressBook.class); // Invoke a method on the server side and get the returned result. Person p = ab.createPerson( new CreatePerson("Test Person", AddressBook.toCalendar("Aug 1, 1999"), new CreateAddress("Test street", "Test city", "Test state", 12345, true)) );

The requirements for a method to be callable through a remoteable service are:

One significant feature is that the remoteable services servlet is a full-blown REST interface. Therefore, in cases where the interface classes are not available on the client side, the same method calls can be made through pure REST calls. This can also aid significantly in debugging since calls to the remoteable service can be called directly from a browser with no code involved.

See org.apache.juneau.rest.remoteable for more information.

1.9 - Other Useful Methods

The RestClientBuilder.rootUrl(Object) method can be used to specify a root URL on all requests so that you don't have to use absolute paths on individual calls.

// Create a rest client with a root URL RestClient rc = new RestClientBuilder().rootUrl("http://localhost:9080/foobar").build(); String r = rc.doGet("/baz").getResponseAsString(); // Gets "http://localhost:9080/foobar/baz"

The RestClientBuilder.property(String,Object) method can be used to set serializer and parser properties. For example, if you're parsing a response into POJOs and you want to ignore fields that aren't on the POJOs, you can use the BeanContext.BEAN_ignoreUnknownBeanProperties property.

// Create a rest client that ignores unknown fields in the response RestClient rc = new RestClientBuilder() .property(BEAN_ignoreUnknownBeanProperties, true) // or .ignoreUnknownBeanProperties(true) .build(); MyPojo myPojo = rc.doGet(URL).getResponse(MyPojo.class);

The RestCall.retryable(int,long,RetryOn) method can be used to automatically retry requests on failures. This can be particularly useful if you're attempting to connect to a REST resource that may be in the process of still initializing.

// Create a rest call that retries every 10 seconds for up to 30 minutes as long as a connection fails // or a 400+ is received. restClient.doGet(URL) .retryable(180, 10000, RetryOn.DEFAULT) .run();

Skip navigation links

Copyright © 2017 Apache. All rights reserved.