001package co.codewizards.cloudstore.rest.client.request; 002 003import static java.util.Objects.*; 004 005import java.net.URI; 006 007import javax.ws.rs.WebApplicationException; 008import javax.ws.rs.client.Client; 009import javax.ws.rs.client.Invocation; 010import javax.ws.rs.client.WebTarget; 011import javax.ws.rs.core.Response; 012 013import org.slf4j.Logger; 014import org.slf4j.LoggerFactory; 015 016import co.codewizards.cloudstore.core.dto.Error; 017import co.codewizards.cloudstore.core.dto.RemoteException; 018import co.codewizards.cloudstore.core.dto.RemoteExceptionUtil; 019import co.codewizards.cloudstore.core.util.UrlEncoder; 020import co.codewizards.cloudstore.rest.client.CloudStoreRestClient; 021 022/** 023 * Abstract base class for REST requests. 024 * <p> 025 * Implementors are encouraged to sub-class {@code AbstractRequest} or {@link VoidRequest} instead of 026 * directly implementing {@link Request}. 027 * 028 * @author Marco หงุ่ยตระกูล-Schulze - marco at codewizards dot co 029 * 030 * @param <R> the response type, i.e. the type of the object sent from the server back to the client. 031 */ 032public abstract class AbstractRequest<R> implements Request<R> { 033 private static final Logger logger = LoggerFactory.getLogger(AbstractRequest.class); 034 035 private CloudStoreRestClient cloudStoreRestClient; 036 037 @Override 038 public CloudStoreRestClient getCloudStoreRestClient() { 039 return cloudStoreRestClient; 040 } 041 042 @Override 043 public void setCloudStoreRestClient(final CloudStoreRestClient cloudStoreRestClient) { 044 this.cloudStoreRestClient = cloudStoreRestClient; 045 } 046 047 /** 048 * Gets the {@link CloudStoreRestClient} or throws an exception, if it was not assigned. 049 * <p> 050 * Implementors are encouraged to use this method instead of {@link #getCloudStoreRestClient()} in their 051 * {@link #execute()} method. 052 * @return the {@link CloudStoreRestClient}. Never <code>null</code>. 053 */ 054 protected CloudStoreRestClient getCloudStoreRestClientOrFail() { 055 final CloudStoreRestClient cloudStoreRestClient = getCloudStoreRestClient(); 056 requireNonNull(cloudStoreRestClient, "cloudStoreRestClient"); 057 return cloudStoreRestClient; 058 } 059 060// protected void handleException(final RuntimeException x) { 061// getCloudStoreRestClientOrFail().handleAndRethrowException(x); 062// } 063 064 protected Invocation.Builder assignCredentials(final Invocation.Builder builder) { 065 return getCloudStoreRestClientOrFail().assignCredentials(builder); 066 } 067 068 protected String getPath(final Class<?> dtoClass) { 069 return "_" + dtoClass.getSimpleName(); 070 } 071 072 /** 073 * Encodes the given {@code string}. 074 * <p> 075 * This method does <i>not</i> use {@link java.net.URLEncoder URLEncoder}, because of 076 * <a href="https://java.net/jira/browse/JERSEY-417">JERSEY-417</a>. 077 * <p> 078 * The result of this method can be used in both URL-paths and URL-query-parameters. 079 * @param string the {@code String} to be encoded. Must not be <code>null</code>. 080 * @return the encoded {@code String}. 081 */ 082 protected static String urlEncode(final String string) { 083 requireNonNull(string, "string"); 084 // This UriComponent method is safe. It does not try to handle the '{' and '}' 085 // specially and with type PATH_SEGMENT, it encodes spaces using '%20' instead of '+'. 086 // It can therefore be used for *both* path segments *and* query parameters. 087// return org.glassfish.jersey.uri.UriComponent.encode(string, UriComponent.Type.PATH_SEGMENT); 088 return UrlEncoder.encode(string); 089 } 090 091 /** 092 * Create a {@link WebTarget} from the given path segments. 093 * <p> 094 * This method prefixes the path with the {@link #getBaseURL() base-URL} and appends 095 * all path segments separated via slashes ('/'). 096 * <p> 097 * We do not use <code>client.target(getBaseURL()).path("...")</code>, because the 098 * {@link WebTarget#path(String) path(...)} method does not encode curly braces 099 * (which might be part of a file name!). 100 * Instead it resolves them using {@linkplain WebTarget#matrixParam(String, Object...) matrix-parameters}. 101 * The matrix-parameters need to be encoded manually, too (at least I tried it and it failed, if I didn't). 102 * Because of these reasons and in order to make the calls more compact, we assemble the path 103 * ourselves here. 104 * @param pathSegments the parts of the path. May be <code>null</code>. The path segments are 105 * appended to the path as they are. They are not encoded at all! If you require encoding, 106 * use {@link #encodePath(String)} or {@link #urlEncode(String)} before! Furthermore, all path segments 107 * are separated with a slash inbetween them, but <i>not</i> at the end. If a single path segment 108 * already contains a slash, duplicate slashes might occur. 109 * @return the target. Never <code>null</code>. 110 */ 111 protected WebTarget createWebTarget(final String ... pathSegments) { 112 final Client client = getClientOrFail(); 113 114 final StringBuilder sb = new StringBuilder(); 115 sb.append(getBaseURL()); 116 117 boolean first = true; 118 if (pathSegments != null && pathSegments.length != 0) { 119 for (final String pathSegment : pathSegments) { 120 if (!first) // the base-URL already ends with a slash! 121 sb.append('/'); 122 first = false; 123 sb.append(pathSegment); 124 } 125 } 126 127 final WebTarget webTarget = client.target(URI.create(sb.toString())); 128 return webTarget; 129 } 130 131 /** 132 * Get the server's base-URL. 133 * <p> 134 * This base-URL is the base of the <code>CloudStoreREST</code> application. Hence all URLs 135 * beneath this base-URL are processed by the <code>CloudStoreREST</code> application. 136 * <p> 137 * In other words: All repository-names are located directly beneath this base-URL. The special services, too, 138 * are located directly beneath this base-URL. 139 * <p> 140 * For example, if the server's base-URL is "https://host.domain:8443/", then the test-service is 141 * available via "https://host.domain:8443/_test" and the repository with the alias "myrepo" is 142 * "https://host.domain:8443/myrepo". 143 * @return the base-URL. This URL always ends with "/". 144 */ 145 protected String getBaseURL() { 146 return getCloudStoreRestClientOrFail().getBaseUrl(); 147 } 148 149 protected Client getClientOrFail() { 150 return getCloudStoreRestClientOrFail().getClientOrFail(); 151 } 152 153 /** 154 * Encodes the given {@code path} (using {@link #urlEncode(String)}) and removes leading & trailing slashes. 155 * <p> 156 * Slashes are not encoded, but retained as they are; only the path segments (the strings between the slashes) are 157 * encoded. 158 * <p> 159 * Duplicate slashes are removed. 160 * <p> 161 * The result of this method can be used in both URL-paths and URL-query-parameters. 162 * <p> 163 * For example the input "/some//ex ample///path/" becomes "some/ex%20ample/path". 164 * @param path the path to be encoded. Must not be <code>null</code>. 165 * @return the encoded path. Never <code>null</code>. 166 */ 167 protected String encodePath(final String path) { 168 requireNonNull(path, "path"); 169 170 final StringBuilder sb = new StringBuilder(); 171 final String[] segments = path.split("/"); 172 for (final String segment : segments) { 173 if (segment.isEmpty()) 174 continue; 175 176 if (sb.length() != 0) 177 sb.append('/'); 178 179 sb.append(urlEncode(segment)); 180 } 181 182 return sb.toString(); 183 } 184 185 protected void assertResponseIndicatesSuccess(final Response response) { 186 if (400 <= response.getStatus() && response.getStatus() <= 599) { 187 response.bufferEntity(); 188 if (response.hasEntity()) { 189 Error error = null; 190 try { 191 error = response.readEntity(Error.class); 192 } catch (final Exception y) { 193 logger.error("handleException: " + y, y); 194 } 195 if (error != null) { 196 throwOriginalExceptionIfPossible(error); 197 throw new RemoteException(error); 198 } 199 } 200 throw new WebApplicationException(response); 201 } 202 } 203 204 protected void throwOriginalExceptionIfPossible(final Error error) { 205 RemoteExceptionUtil.throwOriginalExceptionIfPossible(error); 206 } 207}