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