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 &amp; 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}