All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.juneau.rest.client.RestClient Maven / Gradle / Ivy

There is a newer version: 9.0.1
Show newest version
// ***************************************************************************************************************************
// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
// * with the License.  You may obtain a copy of the License at                                                              *
// *                                                                                                                         *
// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
// *                                                                                                                         *
// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
// * specific language governing permissions and limitations under the License.                                              *
// ***************************************************************************************************************************
package org.apache.juneau.rest.client;

import static org.apache.juneau.internal.StringUtils.*;
import static org.apache.juneau.httppart.HttpPartType.*;

import java.io.*;
import java.lang.reflect.*;
import java.lang.reflect.Proxy;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.regex.*;

import org.apache.http.*;
import org.apache.http.client.methods.*;
import org.apache.http.client.utils.*;
import org.apache.http.entity.*;
import org.apache.http.impl.client.*;
import org.apache.juneau.*;
import org.apache.juneau.httppart.*;
import org.apache.juneau.httppart.bean.*;
import org.apache.juneau.internal.*;
import org.apache.juneau.json.*;
import org.apache.juneau.oapi.*;
import org.apache.juneau.parser.*;
import org.apache.juneau.remote.*;
import org.apache.juneau.rest.client.remote.*;
import org.apache.juneau.serializer.*;
import org.apache.juneau.urlencoding.*;

/**
 * Utility class for interfacing with remote REST interfaces.
 *
 * 
Features
*
    *
  • * Convert POJOs directly to HTTP request message bodies using {@link Serializer} class. *
  • * Convert HTTP response message bodies directly to POJOs using {@link Parser} class. *
  • * Fluent interface. *
  • * Thread safe. *
  • * API for interacting with remote services. *
* * *
See Also:
*
    *
*/ @SuppressWarnings("rawtypes") public class RestClient extends BeanContext implements Closeable { //------------------------------------------------------------------------------------------------------------------- // Configurable properties //------------------------------------------------------------------------------------------------------------------- private static final String PREFIX = "RestClient."; /** * Configuration property: Debug. * *
Property:
*
    *
  • Name: "RestClient.debug.b" *
  • Data type: Boolean *
  • Default: false *
  • Methods: *
      *
    • {@link RestClientBuilder#debug()} *
    *
* *
Description:
*

* Enable debug mode. */ public static final String RESTCLIENT_debug = PREFIX + "debug.b"; /** * Configuration property: Executor service. * *

Property:
*
    *
  • Name: "RestClient.executorService.o" *
  • Data type: Class<? implements ExecutorService> or {@link ExecutorService}. *
  • Default: null. *
  • Methods: *
      *
    • {@link RestClientBuilder#executorService(ExecutorService, boolean)} *
    *
* *
Description:
*

* Defines the executor service to use when calling future methods on the {@link RestCall} class. * *

* This executor service is used to create {@link Future} objects on the following methods: *

    *
  • {@link RestCall#runFuture()} *
  • {@link RestCall#getResponseFuture(Class)} *
  • {@link RestCall#getResponseFuture(Type,Type...)} *
  • {@link RestCall#getResponseAsString()} *
* *

* The default executor service is a single-threaded {@link ThreadPoolExecutor} with a 30 second timeout * and a queue size of 10. */ public static final String RESTCLIENT_executorService = PREFIX + "executorService.o"; /** * Configuration property: Shut down executor service on close. * *

Property:
*
    *
  • Name: "RestClient.executorServiceShutdownOnClose.b" *
  • Data type: Boolean *
  • Default: false *
  • Methods: *
      *
    • {@link RestClientBuilder#executorService(ExecutorService, boolean)} *
    *
* *
Description:
*

* Call {@link ExecutorService#shutdown()} when {@link RestClient#close()} is called. */ public static final String RESTCLIENT_executorServiceShutdownOnClose = PREFIX + "executorServiceShutdownOnClose.b"; /** * Configuration property: Request headers. * *

Property:
*
    *
  • Name: "RestClient.requestHeader.sms" *
  • Data type: Map<String,String> *
  • Default: empty map *
  • Methods: *
      *
    • {@link RestClientBuilder#defaultHeaders(Collection)} *
    • {@link RestClientBuilder#header(String, Object)} *
    *
* *
Description:
*

* Headers to add to every request. */ public static final String RESTCLIENT_headers = PREFIX + "headers.sms"; /** * Configuration property: Call interceptors. * *

Property:
*
    *
  • Name: "RestClient.interceptors.lo" *
  • Data type: List<Class<? implements RestCallInterceptor> | RestCallInterceptor> *
  • Default: empty list. *
  • Methods: *
      *
    • {@link RestClientBuilder#interceptors(RestCallInterceptor...)} *
    *
* *
Description:
*

* Interceptors that get called immediately after a connection is made. */ public static final String RESTCLIENT_interceptors = PREFIX + "interceptors.lo"; /** * Add to the Call interceptors property. */ public static final String RESTCLIENT_interceptors_add = PREFIX + "interceptors.lo/add"; /** * Configuration property: Keep HttpClient open. * *

Property:
*
    *
  • Name: "RestClient.keepHttpClientOpen.b" *
  • Data type: Boolean *
  • Default: false *
  • Methods: *
      *
    • {@link RestClientBuilder#keepHttpClientOpen(boolean)} *
    *
* *
Description:
*

* Don't close this client when the {@link RestClient#close()} method is called. */ public static final String RESTCLIENT_keepHttpClientOpen = PREFIX + "keepHttpClientOpen.b"; /** * Configuration property: Parser. * *

Property:
*
    *
  • Name: "RestClient.parser.o" *
  • Data type: Class<? extends Parser> or {@link Parser}. *
  • Default: {@link JsonParser}; *
  • Methods: *
      *
    • {@link RestClientBuilder#parser(Class)} *
    • {@link RestClientBuilder#parser(Parser)} *
    *
* *
Description:
*

* The parser to use for parsing POJOs in response bodies. */ public static final String RESTCLIENT_parser = PREFIX + "parser.o"; /** * Configuration property: Part parser. * *

Property:
*
    *
  • Name: "RestClient.partParser.o" *
  • Data type: Class<? implements HttpPartParser> or {@link HttpPartParser}. *
  • Default: {@link OpenApiParser}; *
  • Methods: *
      *
    • {@link RestClientBuilder#partParser(Class)} *
    • {@link RestClientBuilder#partParser(HttpPartParser)} *
    *
* *
Description:
*

* The parser to use for parsing POJOs from form data, query parameters, headers, and path variables. */ public static final String RESTCLIENT_partParser = PREFIX + "partParser.o"; /** * Configuration property: Part serializer. * *

Property:
*
    *
  • Name: "RestClient.partSerializer.o" *
  • Data type: Class<? implements HttpPartSerializer> or {@link HttpPartSerializer}. *
  • Default: {@link OpenApiSerializer}; *
  • Methods: *
      *
    • {@link RestClientBuilder#partSerializer(Class)} *
    • {@link RestClientBuilder#partSerializer(HttpPartSerializer)} *
    *
* *
Description:
*

* The serializer to use for serializing POJOs in form data, query parameters, headers, and path variables. */ public static final String RESTCLIENT_partSerializer = PREFIX + "partSerializer.o"; /** * Configuration property: Request query parameters. * *

Property:
*
    *
  • Name: "RestClient.query.sms" *
  • Data type: Map<String,String> *
  • Default: empty map *
  • Methods: *
      *
    • {@link RestClientBuilder#query(String, Object)} *
    *
* *
Description:
*

* Query parameters to add to every request. */ public static final String RESTCLIENT_query = PREFIX + "query.sms"; /** * Configuration property: Number of retries to attempt. * *

Property:
*
    *
  • Name: "RestClient.retries.i" *
  • Data type: Integer *
  • Default: 1 *
  • Methods: *
      *
    • {@link RestClientBuilder#retryable(int, int, RetryOn)} *
    *
* *
Description:
*

* The number of retries to attempt when the connection cannot be made or a >400 response is received. */ public static final String RESTCLIENT_retries = PREFIX + "retries.i"; /** * Configuration property: The time in milliseconds between retry attempts. * *

Property:
*
    *
  • Name: "RestClient.retryInterval.i" *
  • Data type: Integer *
  • Default: -1 *
  • Methods: *
      *
    • {@link RestClientBuilder#retryable(int, int, RetryOn)} *
    *
* *
Description:
*

* The time in milliseconds between retry attempts. * -1 means retry immediately. */ public static final String RESTCLIENT_retryInterval = PREFIX + "retryInterval.i"; /** * Configuration property: Retry-on determination object. * *

Property:
*
    *
  • Name: "RestClient.retryOn.o" *
  • Data type: Class<? extends {@link RetryOn} or {@link RetryOn} *
  • Default: {@link RetryOn#DEFAULT} *
  • Methods: *
      *
    • {@link RestClientBuilder#retryable(int, int, RetryOn)} *
    *
* *
Description:
*

* Object used for determining whether a retry should be attempted. */ public static final String RESTCLIENT_retryOn = PREFIX + "retryOn.o"; /** * Configuration property: Root URI. * *

Property:
*
    *
  • Name: "RestClient.rootUri.s" *
  • Data type: String *
  • Default: false *
  • Methods: *
      *
    • {@link RestClientBuilder#rootUrl(Object)} *
    *
* *
Description:
*

* When set, relative URL strings passed in through the various rest call methods (e.g. {@link RestClient#doGet(Object)} * will be prefixed with the specified root. *
This root URL is ignored on those methods if you pass in a {@link URL}, {@link URI}, or an absolute URL string. *
Trailing slashes are trimmed. */ public static final String RESTCLIENT_rootUri = PREFIX + "rootUri.s"; /** * Configuration property: Serializer. * *

Property:
*
    *
  • Name: "RestClient.serializer.o" *
  • Data type: Class<? extends Serializer> or {@link Serializer}. *
  • Default: {@link JsonSerializer}; *
  • Methods: *
      *
    • {@link RestClientBuilder#serializer(Class)} *
    • {@link RestClientBuilder#serializer(Serializer)} *
    *
* *
Description:
*

* The serializer to use for serializing POJOs in request bodies. */ public static final String RESTCLIENT_serializer = PREFIX + "serializer.o"; private static final ConcurrentHashMap partSerializerCache = new ConcurrentHashMap<>(); private final Map headers, query; private final HttpClientBuilder httpClientBuilder; private final CloseableHttpClient httpClient; private final boolean keepHttpClientOpen, debug; private final UrlEncodingSerializer urlEncodingSerializer; // Used for form posts only. private final HttpPartSerializer partSerializer; private final HttpPartParser partParser; private final String rootUrl; private volatile boolean isClosed = false; private final StackTraceElement[] creationStack; private StackTraceElement[] closedStack; // These are read directly by RestCall. final Serializer serializer; final Parser parser; final RetryOn retryOn; final int retries; final long retryInterval; final RestCallInterceptor[] interceptors; // This is lazy-created. private volatile ExecutorService executorService; private final boolean executorServiceShutdownOnClose; /** * Instantiates a new clean-slate {@link RestClientBuilder} object. * * @return A new {@link RestClientBuilder} object. */ public static RestClientBuilder create() { return new RestClientBuilder(PropertyStore.DEFAULT, null); } /** * Instantiates a new {@link RestClientBuilder} object using the specified serializer and parser. * *

* Shortcut for calling RestClient.create().serializer(s).parser(p); * * @param s The serializer to use for output. * @param p The parser to use for input. * @return A new {@link RestClientBuilder} object. */ public static RestClientBuilder create(Serializer s, Parser p) { return create().serializer(s).parser(p); } /** * Instantiates a new {@link RestClientBuilder} object using the specified serializer and parser. * *

* Shortcut for calling RestClient.create().serializer(s).parser(p); * * @param s The serializer class to use for output. * @param p The parser class to use for input. * @return A new {@link RestClientBuilder} object. */ public static RestClientBuilder create(Class s, Class p) { return create().serializer(s).parser(p); } @Override /* Context */ public RestClientBuilder builder() { return new RestClientBuilder(getPropertyStore(), httpClientBuilder); } /** * Constructor. * * @param ps * Configuration properties for this client. *
Can be null. * @param httpClientBuilder * The HTTP client builder to use to create the HTTP client. *
Can be null. * @param httpClient * The HTTP client. *
Must not be null. */ @SuppressWarnings("unchecked") protected RestClient( PropertyStore ps, HttpClientBuilder httpClientBuilder, CloseableHttpClient httpClient) { super(ps); if (ps == null) ps = PropertyStore.DEFAULT; this.httpClientBuilder = httpClientBuilder; this.httpClient = httpClient; this.keepHttpClientOpen = getBooleanProperty(RESTCLIENT_keepHttpClientOpen, false); this.headers = getMapProperty(RESTCLIENT_headers, String.class); this.query = getMapProperty(RESTCLIENT_query, String.class); this.retries = getIntegerProperty(RESTCLIENT_retries, 1); this.retryInterval = getIntegerProperty(RESTCLIENT_retryInterval, -1); this.retryOn = getInstanceProperty(RESTCLIENT_retryOn, RetryOn.class, RetryOn.DEFAULT); this.debug = getBooleanProperty(RESTCLIENT_debug, false); this.executorServiceShutdownOnClose = getBooleanProperty(RESTCLIENT_executorServiceShutdownOnClose, false); this.rootUrl = StringUtils.nullIfEmpty(getStringProperty(RESTCLIENT_rootUri, "").replaceAll("\\/$", "")); Object o = getProperty(RESTCLIENT_serializer, Object.class, null); if (o instanceof Serializer) { this.serializer = ((Serializer)o).builder().apply(ps).build(); } else if (o instanceof Class) { this.serializer = ContextCache.INSTANCE.create((Class)o, ps); } else { this.serializer = null; } o = getProperty(RESTCLIENT_parser, Object.class, null); if (o instanceof Parser) { this.parser = ((Parser)o).builder().apply(ps).build(); } else if (o instanceof Class) { this.parser = ContextCache.INSTANCE.create((Class)o, ps); } else { this.parser = null; } this.urlEncodingSerializer = new SerializerBuilder(ps).build(UrlEncodingSerializer.class); this.partSerializer = getInstanceProperty(RESTCLIENT_partSerializer, HttpPartSerializer.class, OpenApiSerializer.class, true, ps); this.partParser = getInstanceProperty(RESTCLIENT_partParser, HttpPartParser.class, OpenApiParser.class, true, ps); this.executorService = getInstanceProperty(RESTCLIENT_executorService, ExecutorService.class, null); RestCallInterceptor[] rci = getInstanceArrayProperty(RESTCLIENT_interceptors, RestCallInterceptor.class, new RestCallInterceptor[0]); if (debug) rci = ArrayUtils.append(rci, RestCallLogger.DEFAULT); this.interceptors = rci; if (Boolean.getBoolean("org.apache.juneau.rest.client.RestClient.trackLifecycle")) creationStack = Thread.currentThread().getStackTrace(); else creationStack = null; } /** * Calls {@link CloseableHttpClient#close()} on the underlying {@link CloseableHttpClient}. * *

* It's good practice to call this method after the client is no longer used. * * @throws IOException */ @Override public void close() throws IOException { isClosed = true; if (httpClient != null && ! keepHttpClientOpen) httpClient.close(); if (executorService != null && executorServiceShutdownOnClose) executorService.shutdown(); if (creationStack != null) closedStack = Thread.currentThread().getStackTrace(); } /** * Same as {@link #close()}, but ignores any exceptions. */ public void closeQuietly() { isClosed = true; try { if (httpClient != null && ! keepHttpClientOpen) httpClient.close(); if (executorService != null && executorServiceShutdownOnClose) executorService.shutdown(); } catch (Throwable t) {} if (creationStack != null) closedStack = Thread.currentThread().getStackTrace(); } /** * Execute the specified request. * *

* Subclasses can override this method to provide specialized handling. * * @param req The HTTP request. * @return The HTTP response. * @throws Exception */ protected HttpResponse execute(HttpUriRequest req) throws Exception { return httpClient.execute(req); } /** * Perform a GET request against the specified URL. * * @param url * The URL of the remote REST resource. * Can be any of the following: {@link String}, {@link URI}, {@link URL}. * @return * A {@link RestCall} object that can be further tailored before executing the request and getting the response * as a parsed object. * @throws RestCallException If any authentication errors occurred. */ public RestCall doGet(Object url) throws RestCallException { return doCall("GET", url, false); } /** * Perform a PUT request against the specified URL. * * @param url * The URL of the remote REST resource. * Can be any of the following: {@link String}, {@link URI}, {@link URL}. * @param o * The object to serialize and transmit to the URL as the body of the request. * Can be of the following types: *

    *
  • * {@link Reader} - Raw contents of {@code Reader} will be serialized to remote resource. *
  • * {@link InputStream} - Raw contents of {@code InputStream} will be serialized to remote resource. *
  • * {@link Object} - POJO to be converted to text using the {@link Serializer} registered with the * {@link RestClient}. *
  • * {@link HttpEntity} - Bypass Juneau serialization and pass HttpEntity directly to HttpClient. *
* @return * A {@link RestCall} object that can be further tailored before executing the request * and getting the response as a parsed object. * @throws RestCallException If any authentication errors occurred. */ public RestCall doPut(Object url, Object o) throws RestCallException { return doCall("PUT", url, true).body(o); } /** * Same as {@link #doPut(Object, Object)} but don't specify the input yet. * *

* You must call either {@link RestCall#body(Object)} or {@link RestCall#formData(String, Object)} * to set the contents on the result object. * * @param url * The URL of the remote REST resource. * Can be any of the following: {@link String}, {@link URI}, {@link URL}. * @return * A {@link RestCall} object that can be further tailored before executing the request and getting the response * as a parsed object. * @throws RestCallException */ public RestCall doPut(Object url) throws RestCallException { return doCall("PUT", url, true); } /** * Perform a POST request against the specified URL. * *

Notes:
*
    *
  • Use {@link #doFormPost(Object, Object)} for application/x-www-form-urlencoded form posts. *
* * @param url * The URL of the remote REST resource. * Can be any of the following: {@link String}, {@link URI}, {@link URL}. * @param o * The object to serialize and transmit to the URL as the body of the request. * Can be of the following types: *
    *
  • * {@link Reader} - Raw contents of {@code Reader} will be serialized to remote resource. *
  • * {@link InputStream} - Raw contents of {@code InputStream} will be serialized to remote resource. *
  • * {@link Object} - POJO to be converted to text using the {@link Serializer} registered with the {@link RestClient}. *
  • * {@link HttpEntity} - Bypass Juneau serialization and pass HttpEntity directly to HttpClient. *
* @return * A {@link RestCall} object that can be further tailored before executing the request and getting the response * as a parsed object. * @throws RestCallException If any authentication errors occurred. */ public RestCall doPost(Object url, Object o) throws RestCallException { return doCall("POST", url, true).body(o); } /** * Same as {@link #doPost(Object, Object)} but don't specify the input yet. * *

* You must call either {@link RestCall#body(Object)} or {@link RestCall#formData(String, Object)} to set the * contents on the result object. * *

Notes:
*
    *
  • Use {@link #doFormPost(Object, Object)} for application/x-www-form-urlencoded form posts. *
* * @param url * The URL of the remote REST resource. * Can be any of the following: {@link String}, {@link URI}, {@link URL}. * @return * A {@link RestCall} object that can be further tailored before executing the request and getting the response * as a parsed object. * @throws RestCallException */ public RestCall doPost(Object url) throws RestCallException { return doCall("POST", url, true); } /** * Perform a DELETE request against the specified URL. * * @param url * The URL of the remote REST resource. * Can be any of the following: {@link String}, {@link URI}, {@link URL}. * @return * A {@link RestCall} object that can be further tailored before executing the request and getting the response * as a parsed object. * @throws RestCallException If any authentication errors occurred. */ public RestCall doDelete(Object url) throws RestCallException { return doCall("DELETE", url, false); } /** * Perform an OPTIONS request against the specified URL. * * @param url * The URL of the remote REST resource. * Can be any of the following: {@link String}, {@link URI}, {@link URL}. * @return * A {@link RestCall} object that can be further tailored before executing the request and getting the response * as a parsed object. * @throws RestCallException If any authentication errors occurred. */ public RestCall doOptions(Object url) throws RestCallException { return doCall("OPTIONS", url, true); } /** * Perform a POST request with a content type of application/x-www-form-urlencoded * against the specified URL. * * @param url * The URL of the remote REST resource. * Can be any of the following: {@link String}, {@link URI}, {@link URL}. * @param o * The object to serialize and transmit to the URL as the body of the request, serialized as a form post * using the {@link UrlEncodingSerializer#DEFAULT} serializer. * @return * A {@link RestCall} object that can be further tailored before executing the request and getting the response * as a parsed object. * @throws RestCallException If any authentication errors occurred. */ public RestCall doFormPost(Object url, Object o) throws RestCallException { return doCall("POST", url, true) .body(o instanceof HttpEntity ? o : new RestRequestEntity(o, urlEncodingSerializer, null)); } /** * Performs a REST call where the entire call is specified in a simple string. * *

* This method is useful for performing callbacks when the target of a callback is passed in * on an initial request, for example to signal when a long-running process has completed. * *

* The call string can be any of the following formats: *

    *
  • * "[method] [url]" - e.g. "GET http://localhost/callback" *
  • * "[method] [url] [payload]" - e.g. "POST http://localhost/callback some text payload" *
  • * "[method] [headers] [url] [payload]" - e.g. "POST {'Content-Type':'text/json'} http://localhost/callback {'some':'json'}" *
*

* The payload will always be sent using a simple {@link StringEntity}. * * @param callString The call string. * @return * A {@link RestCall} object that can be further tailored before executing the request and getting the response * as a parsed object. * @throws RestCallException */ public RestCall doCallback(String callString) throws RestCallException { String s = callString; try { RestCall rc = null; String method = null, uri = null, content = null; ObjectMap h = null; int i = s.indexOf(' '); if (i != -1) { method = s.substring(0, i).trim(); s = s.substring(i).trim(); if (s.length() > 0) { if (s.charAt(0) == '{') { i = s.indexOf('}'); if (i != -1) { String json = s.substring(0, i+1); h = JsonParser.DEFAULT.parse(json, ObjectMap.class); s = s.substring(i+1).trim(); } } if (s.length() > 0) { i = s.indexOf(' '); if (i == -1) uri = s; else { uri = s.substring(0, i).trim(); s = s.substring(i).trim(); if (s.length() > 0) content = s; } } } } if (method != null && uri != null) { rc = doCall(method, uri, content != null); if (content != null) rc.body(new StringEntity(content)); if (h != null) for (Map.Entry e : h.entrySet()) rc.header(e.getKey(), e.getValue()); return rc; } } catch (Exception e) { throw new RestCallException(e); } throw new RestCallException("Invalid format for call string."); } /** * Perform a generic REST call. * * @param method The HTTP method. * @param url * The URL of the remote REST resource. * Can be any of the following: {@link String}, {@link URI}, {@link URL}. * @param content * The HTTP body content. * Can be of the following types: *

    *
  • * {@link Reader} - Raw contents of {@code Reader} will be serialized to remote resource. *
  • * {@link InputStream} - Raw contents of {@code InputStream} will be serialized to remote resource. *
  • * {@link Object} - POJO to be converted to text using the {@link Serializer} registered with the * {@link RestClient}. *
  • * {@link HttpEntity} - Bypass Juneau serialization and pass HttpEntity directly to HttpClient. *
  • * {@link NameValuePairs} - Converted to a URL-encoded FORM post. *
* This parameter is IGNORED if {@link HttpMethod#hasContent()} is false. * @return * A {@link RestCall} object that can be further tailored before executing the request and getting the response * as a parsed object. * @throws RestCallException If any authentication errors occurred. */ public RestCall doCall(HttpMethod method, Object url, Object content) throws RestCallException { RestCall rc = doCall(method.name(), url, method.hasContent()); if (method.hasContent()) rc.body(content); return rc; } /** * Perform a generic REST call. * * @param method The method name (e.g. "GET", "OPTIONS"). * @param url * The URL of the remote REST resource. * Can be any of the following: {@link String}, {@link URI}, {@link URL}. * @param hasContent Boolean flag indicating if the specified request has content associated with it. * @return * A {@link RestCall} object that can be further tailored before executing the request and getting the response * as a parsed object. * @throws RestCallException If any authentication errors occurred. */ public RestCall doCall(String method, Object url, boolean hasContent) throws RestCallException { if (isClosed) { Exception e2 = null; if (closedStack != null) { e2 = new Exception("Creation stack:"); e2.setStackTrace(closedStack); throw new RestCallException(e2, "RestClient.close() has already been called. This client cannot be reused."); } throw new RestCallException("RestClient.close() has already been called. This client cannot be reused. Closed location stack trace can be displayed by setting the system property 'org.apache.juneau.rest.client.RestClient.trackCreation' to true."); } HttpRequestBase req = null; RestCall restCall = null; final String methodUC = method.toUpperCase(Locale.ENGLISH); try { if (hasContent) { req = new HttpEntityEnclosingRequestBase() { @Override /* HttpRequest */ public String getMethod() { return methodUC; } }; restCall = new RestCall(this, req, toURI(url)); } else { req = new HttpRequestBase() { @Override /* HttpRequest */ public String getMethod() { return methodUC; } }; restCall = new RestCall(this, req, toURI(url)); } } catch (URISyntaxException e1) { throw new RestCallException(e1); } for (Map.Entry e : query.entrySet()) restCall.query(e.getKey(), e.getValue()); for (Map.Entry e : headers.entrySet()) restCall.header(e.getKey(), e.getValue()); if (parser != null && ! req.containsHeader("Accept")) req.setHeader("Accept", parser.getPrimaryMediaType().toString()); return restCall; } /** * Create a new proxy interface against a 3rd-party REST interface. * *

* The URL to the REST interface is based on the following values: *

    *
  • The {@link RemoteResource#path() @RemoteResource(path)} annotation on the interface (remote-path). *
  • The {@link RestClientBuilder#rootUrl(Object) rootUrl} on the client (root-url). *
  • The fully-qualified class name of the interface (class-name). *
* *

* The URL calculation is as follows: *

    *
  • remote-path - If remote path is absolute. *
  • root-url/remote-path - If remote path is relative and root-url has been specified. *
  • root-url/class-name - If remote path is not specified. *
* *

* If the information is not available to resolve to an absolute URL, a {@link RemoteMetadataException} is thrown. * *

* Examples: *

* package org.apache.foo; * * @RemoteResource(path="http://hostname/resturl/myinterface1") * public interface MyInterface1 { ... } * * @RemoteResource(path="/myinterface2") * public interface MyInterface2 { ... } * * public interface MyInterface3 { ... } * * // Resolves to "http://localhost/resturl/myinterface1" * MyInterface1 i1 = RestClient * .create() * .build() * .getRemoteResource(MyInterface1.class); * * // Resolves to "http://hostname/resturl/myinterface2" * MyInterface2 i2 = RestClient * .create() * .rootUrl("http://hostname/resturl") * .build() * .getRemoteResource(MyInterface2.class); * * // Resolves to "http://hostname/resturl/org.apache.foo.MyInterface3" * MyInterface3 i3 = RestClient * .create() * .rootUrl("http://hostname/resturl") * .build() * .getRemoteResource(MyInterface3.class); *

* *
Notes:
*
    *
  • * If you plan on using your proxy in a multi-threaded environment, you'll want to use an underlying * pooling client connection manager. * The easiest way to do this is to use the {@link RestClientBuilder#pooled()} method. * If you don't do this, you may end up seeing "Connection still allocated" exceptions. *
* * @param interfaceClass The interface to create a proxy for. * @return The new proxy interface. * @throws RemoteMetadataException If the REST URI cannot be determined based on the information given. */ public T getRemoteResource(final Class interfaceClass) { return getRemoteResource(interfaceClass, null); } /** * Same as {@link #getRemoteResource(Class)} except explicitly specifies the URL of the REST interface. * * @param interfaceClass The interface to create a proxy for. * @param restUrl The URL of the REST interface. * @return The new proxy interface. */ public T getRemoteResource(final Class interfaceClass, final Object restUrl) { return getRemoteResource(interfaceClass, restUrl, serializer, parser); } /** * Same as {@link #getRemoteResource(Class, Object)} but allows you to override the serializer and parser used. * * @param interfaceClass The interface to create a proxy for. * @param restUrl The URL of the REST interface. * @param serializer The serializer used to serialize POJOs to the body of the HTTP request. * @param parser The parser used to parse POJOs from the body of the HTTP response. * @return The new proxy interface. */ @SuppressWarnings({ "unchecked" }) public T getRemoteResource(final Class interfaceClass, Object restUrl, final Serializer serializer, final Parser parser) { if (restUrl == null) restUrl = rootUrl; final String restUrl2 = trimSlashes(emptyIfNull(restUrl)); try { return (T)Proxy.newProxyInstance( interfaceClass.getClassLoader(), new Class[] { interfaceClass }, new InvocationHandler() { final RemoteResourceMeta rm = new RemoteResourceMeta(interfaceClass); @Override /* InvocationHandler */ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { RemoteMethodMeta rmm = rm.getMethodMeta(method); if (rmm == null) throw new RuntimeException("Method is not exposed as a remote method."); String url = rmm.getFullPath(); if (url.indexOf("://") == -1) url = restUrl2 + '/' + url; if (url.indexOf("://") == -1) throw new RemoteMetadataException(interfaceClass, "Root URI has not been specified. Cannot construct absolute path to remote resource."); String httpMethod = rmm.getHttpMethod(); HttpPartSerializer s = getPartSerializer(); try (RestCall rc = doCall(httpMethod, url, httpMethod.equals("POST") || httpMethod.equals("PUT"))) { rc.serializer(serializer).parser(parser); for (RemoteMethodArg a : rmm.getPathArgs()) rc.path(a.getName(), args[a.getIndex()], a.getSerializer(s), a.getSchema()); for (RemoteMethodArg a : rmm.getQueryArgs()) rc.query(a.getName(), args[a.getIndex()], a.isSkipIfEmpty(), a.getSerializer(s), a.getSchema()); for (RemoteMethodArg a : rmm.getFormDataArgs()) rc.formData(a.getName(), args[a.getIndex()], a.isSkipIfEmpty(), a.getSerializer(s), a.getSchema()); for (RemoteMethodArg a : rmm.getHeaderArgs()) rc.header(a.getName(), args[a.getIndex()], a.isSkipIfEmpty(), a.getSerializer(s), a.getSchema()); RemoteMethodArg ba = rmm.getBodyArg(); if (ba != null) rc.requestBodySchema(ba.getSchema()).body(args[ba.getIndex()]); if (rmm.getRequestArgs().length > 0) { for (RemoteMethodBeanArg rmba : rmm.getRequestArgs()) { RequestBeanMeta rbm = rmba.getMeta(); Object bean = args[rmba.getIndex()]; if (bean != null) { for (RequestBeanPropertyMeta p : rbm.getProperties()) { Object val = p.getGetter().invoke(bean); HttpPartType pt = p.getPartType(); HttpPartSerializer ps = p.getSerializer(s); String pn = p.getPartName(); HttpPartSchema schema = p.getSchema(); boolean sie = schema.isSkipIfEmpty(); if (pt == PATH) rc.path(pn, val, p.getSerializer(s), schema); else if (val != null) { if (pt == QUERY) rc.query(pn, val, sie, ps, schema); else if (pt == FORMDATA) rc.formData(pn, val, sie, ps, schema); else if (pt == HEADER) rc.header(pn, val, sie, ps, schema); else if (pt == HttpPartType.BODY) rc.requestBodySchema(schema).body(val); } } } } } if (rmm.getOtherArgs().length > 0) { Object[] otherArgs = new Object[rmm.getOtherArgs().length]; int i = 0; for (RemoteMethodArg a : rmm.getOtherArgs()) otherArgs[i++] = args[a.getIndex()]; rc.body(otherArgs); } RemoteMethodReturn rmr = rmm.getReturns(); if (rmr.getReturnValue() == RemoteReturn.NONE) { rc.run(); return null; } else if (rmr.getReturnValue() == RemoteReturn.STATUS) { rc.ignoreErrors(); int returnCode = rc.run(); Class rt = method.getReturnType(); if (rt == Integer.class || rt == int.class) return returnCode; if (rt == Boolean.class || rt == boolean.class) return returnCode < 400; throw new RestCallException("Invalid return type on method annotated with @RemoteMethod(returns=HTTP_STATUS). Only integer and booleans types are valid."); } else if (rmr.getReturnValue() == RemoteReturn.BEAN) { return rc.getResponse(rmr.getResponseBeanMeta()); } else { Object v = rc.getResponseBody(method.getGenericReturnType()); if (v == null && method.getReturnType().isPrimitive()) v = ClassUtils.getPrimitiveDefault(method.getReturnType()); return v; } } catch (RestCallException e) { // Try to throw original exception if possible. e.throwServerException(interfaceClass.getClassLoader()); throw new RuntimeException(e); } catch (Exception e) { throw new RuntimeException(e); } } }); } catch (Exception e) { throw new RuntimeException(e); } } /** * Create a new Remote Interface against a {@link RemoteInterface @RemoteInterface}-annotated class. * *

* Remote interfaces are interfaces exposed on the server side using either the RemoteInterfaceServlet * or PROXY REST methods. * *

* The URL to the REST interface is based on the following values: *

    *
  • The {@link RemoteResource#path() @RemoteResource(path)} annotation on the interface (remote-path). *
  • The {@link RestClientBuilder#rootUrl(Object) rootUrl} on the client (root-url). *
  • The fully-qualified class name of the interface (class-name). *
* *

* The URL calculation is as follows: *

    *
  • remote-path - If remote path is absolute. *
  • root-url/remote-path - If remote path is relative and root-url has been specified. *
  • root-url/class-name - If remote path is not specified. *
* *

* If the information is not available to resolve to an absolute URL, a {@link RemoteMetadataException} is thrown. * *

Notes:
*
    *
  • * If you plan on using your proxy in a multi-threaded environment, you'll want to use an underlying * pooling client connection manager. * The easiest way to do this is to use the {@link RestClientBuilder#pooled()} method. * If you don't do this, you may end up seeing "Connection still allocated" exceptions. *
* * @param interfaceClass The interface to create a proxy for. * @return The new proxy interface. * @throws RemoteMetadataException If the REST URI cannot be determined based on the information given. */ public T getRemoteInterface(final Class interfaceClass) { return getRemoteInterface(interfaceClass, null); } /** * Same as {@link #getRemoteInterface(Class)} except explicitly specifies the URL of the REST interface. * * @param interfaceClass The interface to create a proxy for. * @param restUrl The URL of the REST interface. * @return The new proxy interface. */ public T getRemoteInterface(final Class interfaceClass, final Object restUrl) { return getRemoteInterface(interfaceClass, restUrl, serializer, parser); } /** * Same as {@link #getRemoteInterface(Class, Object)} but allows you to override the serializer and parser used. * * @param interfaceClass The interface to create a proxy for. * @param restUrl The URL of the REST interface. * @param serializer The serializer used to serialize POJOs to the body of the HTTP request. * @param parser The parser used to parse POJOs from the body of the HTTP response. * @return The new proxy interface. */ @SuppressWarnings({ "unchecked" }) public T getRemoteInterface(final Class interfaceClass, Object restUrl, final Serializer serializer, final Parser parser) { if (restUrl == null) { RemoteInterfaceMeta rm = new RemoteInterfaceMeta(interfaceClass, asString(restUrl)); String path = rm.getPath(); if (path.indexOf("://") == -1) { if (rootUrl == null) throw new RemoteMetadataException(interfaceClass, "Root URI has not been specified. Cannot construct absolute path to remote interface."); path = trimSlashes(rootUrl) + '/' + path; } restUrl = path; } final String restUrl2 = asString(restUrl); try { return (T)Proxy.newProxyInstance( interfaceClass.getClassLoader(), new Class[] { interfaceClass }, new InvocationHandler() { final RemoteInterfaceMeta rm = new RemoteInterfaceMeta(interfaceClass, restUrl2); @Override /* InvocationHandler */ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { RemoteInterfaceMethod rim = rm.getMethodMeta(method); if (rim == null) throw new RuntimeException("Method is not exposed as a remote method."); String url = rim.getUrl(); try (RestCall rc = doCall("POST", url, true)) { rc.serializer(serializer).parser(parser).body(args); Object v = rc.getResponse(method.getGenericReturnType()); if (v == null && method.getReturnType().isPrimitive()) v = ClassUtils.getPrimitiveDefault(method.getReturnType()); return v; } catch (RestCallException e) { // Try to throw original exception if possible. e.throwServerException(interfaceClass.getClassLoader()); throw new RuntimeException(e); } catch (Exception e) { throw new RuntimeException(e); } } }); } catch (Exception e) { throw new RuntimeException(e); } } static final String getName(String name1, String name2, BeanPropertyMeta pMeta) { String n = name1.isEmpty() ? name2 : name1; ClassMeta cm = pMeta.getClassMeta(); if (n.isEmpty() && (cm.isMapOrBean() || cm.isReader() || cm.isInstanceOf(NameValuePairs.class))) n = "*"; if (n.isEmpty()) n = pMeta.getName(); return n; } final HttpPartSerializer getPartSerializer(Class c, HttpPartSerializer c2) { if (c2 != null) return c2; if (c == HttpPartSerializer.Null.class) return null; HttpPartSerializer pf = partSerializerCache.get(c); if (pf == null) { partSerializerCache.putIfAbsent(c, newInstance(HttpPartSerializer.class, c, true, getPropertyStore())); pf = partSerializerCache.get(c); } return pf; } private Pattern absUrlPattern = Pattern.compile("^\\w+\\:\\/\\/.*"); HttpPartSerializer getPartSerializer() { return partSerializer; } HttpPartParser getPartParser() { return partParser; } URI toURI(Object url) throws URISyntaxException { if (url instanceof URI) return (URI)url; if (url instanceof URL) ((URL)url).toURI(); if (url instanceof URIBuilder) return ((URIBuilder)url).build(); String s = url == null ? "" : url.toString(); if (rootUrl != null && ! absUrlPattern.matcher(s).matches()) { if (s.isEmpty()) s = rootUrl; else { StringBuilder sb = new StringBuilder(rootUrl); if (! s.startsWith("/")) sb.append('/'); sb.append(s); s = sb.toString(); } } if (s.indexOf('{') != -1) s = s.replace("{", "%7B").replace("}", "%7D"); return new URI(s); } ExecutorService getExecutorService(boolean create) { if (executorService != null || ! create) return executorService; synchronized(this) { if (executorService == null) executorService = new ThreadPoolExecutor(1, 1, 30, TimeUnit.SECONDS, new ArrayBlockingQueue(10)); return executorService; } } @Override protected void finalize() throws Throwable { if (! isClosed && ! keepHttpClientOpen) { System.err.println("WARNING: RestClient garbage collected before it was finalized."); // NOT DEBUG if (creationStack != null) { System.err.println("Creation Stack:"); // NOT DEBUG for (StackTraceElement e : creationStack) System.err.println(e); // NOT DEBUG } else { System.err.println("Creation stack traces can be displayed by setting the system property 'org.apache.juneau.rest.client.RestClient.trackLifecycle' to true."); // NOT DEBUG } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy