001/*
002 * The contents of this file are subject to the license and copyright
003 * detailed in the LICENSE and NOTICE files at the root of the source
004 * tree.
005 */
006package org.fcrepo.client;
007
008import static org.slf4j.LoggerFactory.getLogger;
009
010import java.io.Closeable;
011import java.io.IOException;
012import java.io.InputStream;
013import java.net.URI;
014import java.time.Instant;
015import java.util.ArrayList;
016import java.util.List;
017import java.util.Map;
018import java.util.TreeMap;
019
020import org.apache.http.Header;
021import org.apache.http.HttpEntity;
022import org.apache.http.HttpResponse;
023import org.apache.http.HttpStatus;
024import org.apache.http.client.methods.CloseableHttpResponse;
025import org.apache.http.client.methods.HttpRequestBase;
026import org.apache.http.impl.client.CloseableHttpClient;
027import org.slf4j.Logger;
028
029/**
030 * Represents a client to interact with Fedora's HTTP API.
031 * <p>
032 * Users of the {@code FcrepoClient} are responsible for managing connection resources. Specifically, the underlying
033 * HTTP connections of this client must be freed. Suggested usage is to create the {@code FcrepoResponse} within a
034 * {@code try-with-resources} block, insuring that any resources held by the response are freed automatically.
035 * </p>
036 * <pre>
037 * FcrepoClient client = ...;
038 * try (FcrepoResponse res = client.get(...).perform()) {
039 *     // do something with the response
040 * } catch (FcrepoOperationFailedException|IOException e) {
041 *     // handle any exceptions
042 * }
043 * </pre>
044 *
045 * @author Aaron Coburn
046 * @since October 20, 2014
047 */
048public class FcrepoClient implements Closeable {
049
050    private CloseableHttpClient httpclient;
051
052    private Boolean throwExceptionOnFailure = true;
053
054    private static final Logger LOGGER = getLogger(FcrepoClient.class);
055
056    /**
057     * Build a FcrepoClient
058     *
059     * @return a client builder
060     */
061    public static FcrepoClientBuilder client() {
062        return new FcrepoClientBuilder();
063    }
064
065    /**
066     * Create a FcrepoClient with a set of authentication values.
067     *
068     * @param username the username for the repository
069     * @param password the password for the repository
070     * @param host the authentication hostname (realm) for the repository
071     * @param throwExceptionOnFailure whether to throw an exception on any non-2xx or 3xx HTTP responses
072     */
073    protected FcrepoClient(final String username, final String password, final String host,
074            final Boolean throwExceptionOnFailure) {
075        this(new FcrepoHttpClientBuilder(username, password, host).build(), throwExceptionOnFailure);
076    }
077
078    /**
079     * Create a FcrepoClient which uses the given {@link org.apache.http.impl.client.CloseableHttpClient}.
080     * FcrepoClient will close the httpClient when {@link #close()} is called.
081     *
082     * @param httpClient http client to use to connect to the repository
083     * @param throwExceptionOnFailure whether to throw an exception on any non-2xx or 3xx HTTP responses
084     */
085    protected FcrepoClient(final CloseableHttpClient httpClient, final Boolean throwExceptionOnFailure) {
086        this.throwExceptionOnFailure = throwExceptionOnFailure;
087        this.httpclient = httpClient;
088    }
089
090    /**
091     * Make a PUT request to create a resource with a specified path, or replace the triples associated with a
092     * resource with the triples provided in the request body.
093     *
094     * @param url the URL of the resource to which to PUT
095     * @return a put request builder object
096     */
097    public PutBuilder put(final URI url) {
098        return new PutBuilder(url, this);
099    }
100
101    /**
102     * Make a PATCH request to modify the triples associated with a resource with SPARQL-Update.
103     *
104     * @param url the URL of the resource to which to PATCH
105     * @return a patch request builder object
106     */
107    public PatchBuilder patch(final URI url) {
108        return new PatchBuilder(url, this);
109    }
110
111    /**
112     * Make a POST request to create a new resource within an LDP container.
113     *
114     * @param url the URL of the resource to which to POST
115     * @return a post request builder object
116     */
117    public PostBuilder post(final URI url) {
118        return new PostBuilder(url, this);
119    }
120
121    /**
122     * Make a POST request to create a new memento (LDPRm) within an LDPCv of the current version of a resource.
123     *
124     * @param url the URL of the LDPCv in which to create the LDPRm.
125     * @return a memento creation request builder object
126     */
127    public OriginalMementoBuilder createMemento(final URI url) {
128        return new OriginalMementoBuilder(url, this);
129    }
130
131    /**
132     * Make a POST request to create a new memento (LDPRm) within an LDPCv using the given memento-datetime and the
133     * request body.
134     *
135     * @param url the URL of the LDPCv in which to create the LDPRm.
136     * @param mementoInstant the memento datetime as an Instant.
137     * @return a memento creation request builder object
138     */
139    public HistoricMementoBuilder createMemento(final URI url, final Instant mementoInstant) {
140        return new HistoricMementoBuilder(url, this, mementoInstant);
141    }
142
143    /**
144     * Make a POST request to create a new memento (LDPRm) within an LDPCv using the given memento-datetime and the
145     * request body.
146     *
147     * @param url the URL of the LDPCv in which to create the LDPRm.
148     * @param mementoDatetime the RFC1123 formatted memento datetime.
149     * @return a memento creation request builder object
150     */
151    public HistoricMementoBuilder createMemento(final URI url, final String mementoDatetime) {
152        return new HistoricMementoBuilder(url, this, mementoDatetime);
153    }
154
155    /**
156     * Make a DELETE request to delete a resource
157     *
158     * @param url the URL of the resource to which to DELETE
159     * @return a delete request builder object
160     */
161    public DeleteBuilder delete(final URI url) {
162        return new DeleteBuilder(url, this);
163    }
164
165    /**
166     * Make a GET request to retrieve the content of a resource
167     *
168     * @param url the URL of the resource to which to GET
169     * @return a get request builder object
170     */
171    public GetBuilder get(final URI url) {
172        return new GetBuilder(url, this);
173    }
174
175    /**
176     * Make a HEAD request to retrieve resource headers.
177     *
178     * @param url the URL of the resource to make the HEAD request on.
179     * @return a HEAD request builder object
180     */
181    public HeadBuilder head(final URI url) {
182        return new HeadBuilder(url, this);
183    }
184
185    /**
186     * Make a OPTIONS request to output information about the supported HTTP methods, etc.
187     *
188     * @param url the URL of the resource to make the OPTIONS request on.
189     * @return a OPTIONS request builder object
190     */
191    public OptionsBuilder options(final URI url) {
192        return new OptionsBuilder(url, this);
193    }
194
195    @Override
196    public void close() throws IOException {
197        this.httpclient.close();
198    }
199
200    /**
201     * Execute a HTTP request
202     *
203     * @param url URI the request is made to
204     * @param request the request
205     * @return the repository response
206     * @throws FcrepoOperationFailedException when the underlying HTTP request results in an error
207     */
208    public FcrepoResponse executeRequest(final URI url, final HttpRequestBase request)
209            throws FcrepoOperationFailedException {
210        LOGGER.debug("Fcrepo {} request to resource {}", request.getMethod(), url);
211        final CloseableHttpResponse response = executeRequest(request);
212
213        return fcrepoGenericResponse(url, response, throwExceptionOnFailure);
214    }
215
216    /**
217     * Execute the HTTP request
218     */
219    private CloseableHttpResponse executeRequest(final HttpRequestBase request)
220            throws FcrepoOperationFailedException {
221        try {
222            return httpclient.execute(request);
223        } catch (final IOException ex) {
224            LOGGER.debug("HTTP Operation failed: ", ex);
225            throw new FcrepoOperationFailedException(request.getURI(), -1, ex.getMessage());
226        }
227    }
228
229    /**
230     * Handle the general case with responses.
231     */
232    private FcrepoResponse fcrepoGenericResponse(final URI url, final CloseableHttpResponse response,
233            final Boolean throwExceptionOnFailure) throws FcrepoOperationFailedException {
234        final int status = response.getStatusLine().getStatusCode();
235        final Map<String, List<String>> headers = getHeaders(response);
236
237        if ((status >= HttpStatus.SC_OK && status < HttpStatus.SC_BAD_REQUEST) || !throwExceptionOnFailure) {
238            return new FcrepoResponse(url, status, headers, getEntityContent(response));
239        } else {
240            free(response);
241            throw new FcrepoOperationFailedException(url, status,
242                    response.getStatusLine().getReasonPhrase());
243        }
244    }
245
246    /**
247     * Frees resources associated with the HTTP response. Specifically, closing the {@code response} frees the
248     * connection of the {@link org.apache.http.conn.HttpClientConnectionManager} underlying this {@link #httpclient}.
249     *
250     * @param response the response object to close
251     */
252    private void free(final CloseableHttpResponse response) {
253        // Free resources associated with the response.
254        try {
255            response.close();
256        } catch (final IOException e) {
257            LOGGER.warn("Unable to close HTTP response.", e);
258        }
259    }
260
261    /**
262     * Extract the response body as an input stream
263     */
264    private static InputStream getEntityContent(final HttpResponse response) {
265        try {
266            final HttpEntity entity = response.getEntity();
267            if (entity == null) {
268                return null;
269            } else {
270                return entity.getContent();
271            }
272        } catch (final IOException ex) {
273            LOGGER.debug("Unable to extract HttpEntity response into an InputStream: ", ex);
274            return null;
275        }
276    }
277
278    /**
279     * Retrieve all header values
280     *
281     * @param response response from request
282     * @return Map of all values for all response headers
283     */
284    private static Map<String, List<String>> getHeaders(final HttpResponse response) {
285        final Map<String, List<String>> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
286
287        for (final Header header : response.getAllHeaders()) {
288            final List<String> values;
289            if (headers.containsKey(header.getName())) {
290                values = headers.get(header.getName());
291            } else {
292                values = new ArrayList<>();
293                headers.put(header.getName(), values);
294            }
295            values.add(header.getValue());
296        }
297        return headers;
298    }
299
300    /**
301     * Builds an FcrepoClient
302     *
303     * @author bbpennel
304     */
305    public static class FcrepoClientBuilder {
306
307        private String authUser;
308
309        private String authPassword;
310
311        private String authHost;
312
313        private boolean throwExceptionOnFailure;
314
315        /**
316         * Add basic authentication credentials to this client
317         *
318         * @param username username for authentication
319         * @param password password for authentication
320         * @return the client builder
321         */
322        public FcrepoClientBuilder credentials(final String username, final String password) {
323            this.authUser = username;
324            this.authPassword = password;
325            return this;
326        }
327
328        /**
329         * Add an authentication scope to this client
330         *
331         * @param authHost authentication scope value
332         * @return this builder
333         */
334        public FcrepoClientBuilder authScope(final String authHost) {
335            this.authHost = authHost;
336
337            return this;
338        }
339
340        /**
341         * Client should throw exceptions when failures occur
342         *
343         * @return this builder
344         */
345        public FcrepoClientBuilder throwExceptionOnFailure() {
346            this.throwExceptionOnFailure = true;
347            return this;
348        }
349
350        /**
351         * Get the client
352         *
353         * @return the client constructed by this builder
354         */
355        public FcrepoClient build() {
356            final FcrepoHttpClientBuilder httpClient = new FcrepoHttpClientBuilder(authUser, authPassword, authHost);
357            return new FcrepoClient(httpClient.build(), throwExceptionOnFailure);
358        }
359    }
360}