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. Logging

    6. Interceptors

    7. Remoteable Proxies

    8. 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:

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 mimimum, the following jars are required:

Example:

// Examples below use the Juneau Address Book resource example // Create a reusable client with JSON support RestClient client = new RestClient(JsonSerializer.class, JsonParser.class); // GET request, ignoring output try { int rc = client.doGet("http://localhost:9080/sample/addressBook").execute(); // 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").execute(); // 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 ObjectMap // 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 a ObjectList // 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> ClassMeta cm = BeanContext.DEFAULT.getCollectionClassMeta(LinkedList.class, Person.class); List<Person> pl = client.doGet("http://localhost:9080/sample/addressBook") .getResponse(cm); // 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.getParser().addPojoSwaps(CalendarSwap.ISO8601.class); 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).execute(); // 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).execute(); // 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).execute();

Notes:

1.1 - SSL Support

The simplest way to enable SSL support in the client is to use the {@link org.apache.juneau.rest.client.RestClient#enableSSL(SSLOpts)} method and one of the predefined {@link org.apache.juneau.rest.client.SSLOpts} instances:

Example:

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

This is functionally equivalent to the following:

RestClient restClient = new RestClient(); 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); restClient.setSSLSocketFactory(sf); Registry<ConnectionSocketFactory> r = RegistryBuilder.<ConnectionSocketFactory>.create().register("https", sf).build(); restClient.setConnectionManager(new PoolingHttpClientConnectionManager(r)); }

More complex SSL support can be enabled through the various {@link org.apache.http.impl.client.HttpClientBuilder} methods defined on the class.

1.1.1 - SSLOpts Bean

The {@link org.apache.juneau.rest.client.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 {@link org.apache.juneau.ini.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 = ConfigMgr.DEFAULT.get("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 {@link org.apache.juneau.rest.client.RestClient#setBasicAuth(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 RestClient() .setBasicAuth(HOST, PORT, USER, PW);

This is functionally equivalent to the following:

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

1.2.2 - FORM-based Authentication

The {@link org.apache.juneau.rest.client.RestClient} 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 {@link org.apache.juneau.rest.client.RestClient} and override the {@link org.apache.juneau.rest.client.RestClient#createHttpClient()} method to provide an authenticated client.

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

/** * Constructor. */ public JazzRestClient(URI jazzUri, String user, String pw) throws IOException { ... } /** * Override the createHttpClient() method to return an authenticated client. */ @Override /* RestClient */ 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 JazzRestClient(URI jazzUri, String user, String pw) throws IOException { ... } /** * Override the createHttpClient() method to return an authenticated client. */ @Override /* RestClient */ 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 {@link org.apache.juneau.rest.client.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 {@link org.apache.juneau.rest.client.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 {@link org.apache.juneau.rest.client.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 {@link org.apache.juneau.rest.client.RestCall#addResponsePattern(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 {@link org.apache.juneau.rest.client.RestCall#getResponseAsString()} or {@link org.apache.juneau.rest.client.RestCall#getResponse(Class)}.
HOWEVER, if you want to retrieve the entire text of the response from inside the match methods, use {@link org.apache.juneau.rest.client.RestCall#getCapturedResponse()} since this method will not absorb the response for those other methods.

1.4 - Piping Response Output

The {@link org.apache.juneau.rest.client.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 {@link org.apache.juneau.rest.client.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 - Logging

Use the {@link org.apache.juneau.rest.client.RestClient#logTo(Level,Logger)} and {@link org.apache.juneau.rest.client.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 subclassing the {@link org.apache.juneau.rest.client.RestCallLogger} class and using the {@link org.apache.juneau.rest.client.RestCall#addInterceptor(RestCallInterceptor)} method.

1.6 - Interceptors

The {@link org.apache.juneau.rest.client.RestClient#addInterceptor(RestCallInterceptor)} and {@link org.apache.juneau.rest.client.RestCall#addInterceptor(RestCallInterceptor)} methods can be used to intercept responses during specific connection lifecycle events.

The {@link org.apache.juneau.rest.client.RestCallLogger} class is an example of an interceptor that uses the various lifecycle methods to log HTTP requests.

/** * Specialized interceptor 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 =================================================================="); 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.7 - Remotable 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 {@link org.apache.juneau.rest.client.RestClient#getRemoteableProxy(Class)} method. The {@link org.apache.juneau.rest.client.RestClient#setRemoteableServletUri(String)} method is used to specify the location of the remoteable services servlet running on the server. The remoteable servlet is a specialized subclass of {@link org.apache.juneau.rest.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 RestClient(JsonSerializer.class, JsonParser.class) .setRemoteableServletUri("https://localhost:9080/juneau/sample/remoteable"); // 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 {@link org.apache.juneau.rest.remoteable} for more information.

1.8 - Other Useful Methods

The {@link org.apache.juneau.rest.client.RestClient#setRootUrl(String)} 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 RestClient().setRootUrl("http://localhost:9080/foobar"); String r = rc.doGet("/baz").getResponseAsString(); // Gets "http://localhost:9080/foobar/baz"

The {@link org.apache.juneau.rest.client.RestClient#setProperty(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 {@link org.apache.juneau.BeanContext#BEAN_ignoreUnknownBeanProperties} property.

// Create a rest client that ignores unknown fields in the response RestClient rc = new RestClient(JsonSerializer.class, JsonParser.class) .setProperty(BEAN_ignoreUnknownBeanProperties, true); MyPojo myPojo = rc.doGet(URL).getResponse(MyPojo.class);

The {@link org.apache.juneau.rest.client.RestCall#setRetryable(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) .setRetryable(180, 10000, RetryOn.DEFAULT) .run();