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