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