package co.codewizards.cloudstore.rest.client.request;
import java.net.URI;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import co.codewizards.cloudstore.core.dto.Error;
import co.codewizards.cloudstore.core.dto.RemoteException;
import co.codewizards.cloudstore.core.dto.RemoteExceptionUtil;
import co.codewizards.cloudstore.core.util.AssertUtil;
import co.codewizards.cloudstore.core.util.UrlEncoder;
import co.codewizards.cloudstore.rest.client.CloudStoreRestClient;
/**
* Abstract base class for REST requests.
* <p>
* Implementors are encouraged to sub-class {@code AbstractRequest} or {@link VoidRequest} instead of
* directly implementing {@link Request}.
*
* @author Marco หงุ่ยตระกูล-Schulze - marco at codewizards dot co
*
* @param <R> the response type, i.e. the type of the object sent from the server back to the client.
*/
public abstract class AbstractRequest<R> implements Request<R> {
private static final Logger logger = LoggerFactory.getLogger(AbstractRequest.class);
private CloudStoreRestClient cloudStoreRestClient;
@Override
public CloudStoreRestClient getCloudStoreRestClient() {
return cloudStoreRestClient;
}
@Override
public void setCloudStoreRestClient(final CloudStoreRestClient cloudStoreRestClient) {
this.cloudStoreRestClient = cloudStoreRestClient;
}
/**
* Gets the {@link CloudStoreRestClient} or throws an exception, if it was not assigned.
* <p>
* Implementors are encouraged to use this method instead of {@link #getCloudStoreRestClient()} in their
* {@link #execute()} method.
* @return the {@link CloudStoreRestClient}. Never <code>null</code>.
*/
protected CloudStoreRestClient getCloudStoreRestClientOrFail() {
final CloudStoreRestClient cloudStoreRestClient = getCloudStoreRestClient();
AssertUtil.assertNotNull(cloudStoreRestClient, "cloudStoreRestClient");
return cloudStoreRestClient;
}
protected void handleException(final RuntimeException x) {
getCloudStoreRestClientOrFail().handleAndRethrowException(x);
}
protected Invocation.Builder assignCredentials(final Invocation.Builder builder) {
return getCloudStoreRestClientOrFail().assignCredentials(builder);
}
protected String getPath(final Class<?> dtoClass) {
return "_" + dtoClass.getSimpleName();
}
/**
* Encodes the given {@code string}.
* <p>
* This method does <i>not</i> use {@link java.net.URLEncoder URLEncoder}, because of
* <a href="https://java.net/jira/browse/JERSEY-417">JERSEY-417</a>.
* <p>
* The result of this method can be used in both URL-paths and URL-query-parameters.
* @param string the {@code String} to be encoded. Must not be <code>null</code>.
* @return the encoded {@code String}.
*/
protected static String urlEncode(final String string) {
AssertUtil.assertNotNull(string, "string");
// This UriComponent method is safe. It does not try to handle the '{' and '}'
// specially and with type PATH_SEGMENT, it encodes spaces using '%20' instead of '+'.
// It can therefore be used for *both* path segments *and* query parameters.
// return org.glassfish.jersey.uri.UriComponent.encode(string, UriComponent.Type.PATH_SEGMENT);
return UrlEncoder.encode(string);
}
/**
* Create a {@link WebTarget} from the given path segments.
* <p>
* This method prefixes the path with the {@link #getBaseURL() base-URL} and appends
* all path segments separated via slashes ('/').
* <p>
* We do not use <code>client.target(getBaseURL()).path("...")</code>, because the
* {@link WebTarget#path(String) path(...)} method does not encode curly braces
* (which might be part of a file name!).
* Instead it resolves them using {@linkplain WebTarget#matrixParam(String, Object...) matrix-parameters}.
* The matrix-parameters need to be encoded manually, too (at least I tried it and it failed, if I didn't).
* Because of these reasons and in order to make the calls more compact, we assemble the path
* ourselves here.
* @param pathSegments the parts of the path. May be <code>null</code>. The path segments are
* appended to the path as they are. They are not encoded at all! If you require encoding,
* use {@link #encodePath(String)} or {@link #urlEncode(String)} before! Furthermore, all path segments
* are separated with a slash inbetween them, but <i>not</i> at the end. If a single path segment
* already contains a slash, duplicate slashes might occur.
* @return the target. Never <code>null</code>.
*/
protected WebTarget createWebTarget(final String ... pathSegments) {
final Client client = getClientOrFail();
final StringBuilder sb = new StringBuilder();
sb.append(getBaseURL());
boolean first = true;
if (pathSegments != null && pathSegments.length != 0) {
for (final String pathSegment : pathSegments) {
if (!first) // the base-URL already ends with a slash!
sb.append('/');
first = false;
sb.append(pathSegment);
}
}
final WebTarget webTarget = client.target(URI.create(sb.toString()));
return webTarget;
}
/**
* Get the server's base-URL.
* <p>
* This base-URL is the base of the <code>CloudStoreREST</code> application. Hence all URLs
* beneath this base-URL are processed by the <code>CloudStoreREST</code> application.
* <p>
* In other words: All repository-names are located directly beneath this base-URL. The special services, too,
* are located directly beneath this base-URL.
* <p>
* For example, if the server's base-URL is "https://host.domain:8443/", then the test-service is
* available via "https://host.domain:8443/_test" and the repository with the alias "myrepo" is
* "https://host.domain:8443/myrepo".
* @return the base-URL. This URL always ends with "/".
*/
protected String getBaseURL() {
return getCloudStoreRestClientOrFail().getBaseUrl();
}
protected Client getClientOrFail() {
return getCloudStoreRestClientOrFail().getClientOrFail();
}
/**
* Encodes the given {@code path} (using {@link #urlEncode(String)}) and removes leading & trailing slashes.
* <p>
* Slashes are not encoded, but retained as they are; only the path segments (the strings between the slashes) are
* encoded.
* <p>
* Duplicate slashes are removed.
* <p>
* The result of this method can be used in both URL-paths and URL-query-parameters.
* <p>
* For example the input "/some//ex ample///path/" becomes "some/ex%20ample/path".
* @param path the path to be encoded. Must not be <code>null</code>.
* @return the encoded path. Never <code>null</code>.
*/
protected String encodePath(final String path) {
AssertUtil.assertNotNull(path, "path");
final StringBuilder sb = new StringBuilder();
final String[] segments = path.split("/");
for (final String segment : segments) {
if (segment.isEmpty())
continue;
if (sb.length() != 0)
sb.append('/');
sb.append(urlEncode(segment));
}
return sb.toString();
}
protected void assertResponseIndicatesSuccess(final Response response) {
if (400 <= response.getStatus() && response.getStatus() <= 599) {
response.bufferEntity();
if (response.hasEntity()) {
Error error = null;
try {
error = response.readEntity(Error.class);
} catch (final Exception y) {
logger.error("handleException: " + y, y);
}
if (error != null) {
throwOriginalExceptionIfPossible(error);
throw new RemoteException(error);
}
}
throw new WebApplicationException(response);
}
}
protected void throwOriginalExceptionIfPossible(final Error error) {
RemoteExceptionUtil.throwOriginalExceptionIfPossible(error);
}
}