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 COPY request to copy a resource (and its subtree) to a new location.
170     *
171     * @param source url of the resource to copy
172     * @param destination url of the location for the copy
173     * @return a copy request builder object
174     * @deprecated the COPY method is not supported by the Fedora 1.0 specification
175     */
176    @Deprecated
177    public CopyBuilder copy(final URI source, final URI destination) {
178        return new CopyBuilder(source, destination, this);
179    }
180
181    /**
182     * Make a MOVE request to move a resource (and its subtree) to a new location.
183     *
184     * @param source url of the resource to move
185     * @param destination url of the new location for the resource
186     * @return a move request builder object
187     * @deprecated the MOVE method is not supported by the Fedora 1.0 specification
188     */
189    @Deprecated
190    public MoveBuilder move(final URI source, final URI destination) {
191        return new MoveBuilder(source, destination, this);
192    }
193
194    /**
195     * Make a GET request to retrieve the content of a resource
196     *
197     * @param url the URL of the resource to which to GET
198     * @return a get request builder object
199     */
200    public GetBuilder get(final URI url) {
201        return new GetBuilder(url, this);
202    }
203
204    /**
205     * Make a HEAD request to retrieve resource headers.
206     *
207     * @param url the URL of the resource to make the HEAD request on.
208     * @return a HEAD request builder object
209     */
210    public HeadBuilder head(final URI url) {
211        return new HeadBuilder(url, this);
212    }
213
214    /**
215     * Make a OPTIONS request to output information about the supported HTTP methods, etc.
216     *
217     * @param url the URL of the resource to make the OPTIONS request on.
218     * @return a OPTIONS request builder object
219     */
220    public OptionsBuilder options(final URI url) {
221        return new OptionsBuilder(url, this);
222    }
223
224    /**
225     * Execute a HTTP request
226     *
227     * @param url URI the request is made to
228     * @param request the request
229     * @return the repository response
230     * @throws FcrepoOperationFailedException when the underlying HTTP request results in an error
231     */
232    public FcrepoResponse executeRequest(final URI url, final HttpRequestBase request)
233            throws FcrepoOperationFailedException {
234        LOGGER.debug("Fcrepo {} request to resource {}", request.getMethod(), url);
235        final CloseableHttpResponse response = executeRequest(request);
236
237        return fcrepoGenericResponse(url, response, throwExceptionOnFailure);
238    }
239
240    /**
241     * Execute the HTTP request
242     */
243    private CloseableHttpResponse executeRequest(final HttpRequestBase request)
244            throws FcrepoOperationFailedException {
245        try {
246            return httpclient.execute(request);
247        } catch (final IOException ex) {
248            LOGGER.debug("HTTP Operation failed: ", ex);
249            throw new FcrepoOperationFailedException(request.getURI(), -1, ex.getMessage());
250        }
251    }
252
253    /**
254     * Handle the general case with responses.
255     */
256    private FcrepoResponse fcrepoGenericResponse(final URI url, final CloseableHttpResponse response,
257            final Boolean throwExceptionOnFailure) throws FcrepoOperationFailedException {
258        final int status = response.getStatusLine().getStatusCode();
259        final Map<String, List<String>> headers = getHeaders(response);
260
261        if ((status >= HttpStatus.SC_OK && status < HttpStatus.SC_BAD_REQUEST) || !throwExceptionOnFailure) {
262            return new FcrepoResponse(url, status, headers, getEntityContent(response));
263        } else {
264            free(response);
265            throw new FcrepoOperationFailedException(url, status,
266                    response.getStatusLine().getReasonPhrase());
267        }
268    }
269
270    /**
271     * Frees resources associated with the HTTP response. Specifically, closing the {@code response} frees the
272     * connection of the {@link org.apache.http.conn.HttpClientConnectionManager} underlying this {@link #httpclient}.
273     *
274     * @param response the response object to close
275     */
276    private void free(final CloseableHttpResponse response) {
277        // Free resources associated with the response.
278        try {
279            response.close();
280        } catch (final IOException e) {
281            LOGGER.warn("Unable to close HTTP response.", e);
282        }
283    }
284
285    /**
286     * Extract the response body as an input stream
287     */
288    private static InputStream getEntityContent(final HttpResponse response) {
289        try {
290            final HttpEntity entity = response.getEntity();
291            if (entity == null) {
292                return null;
293            } else {
294                return entity.getContent();
295            }
296        } catch (final IOException ex) {
297            LOGGER.debug("Unable to extract HttpEntity response into an InputStream: ", ex);
298            return null;
299        }
300    }
301
302    /**
303     * Retrieve all header values
304     *
305     * @param response response from request
306     * @return Map of all values for all response headers
307     */
308    private static Map<String, List<String>> getHeaders(final HttpResponse response) {
309        final Map<String, List<String>> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
310
311        for (final Header header : response.getAllHeaders()) {
312            final List<String> values;
313            if (headers.containsKey(header.getName())) {
314                values = headers.get(header.getName());
315            } else {
316                values = new ArrayList<>();
317                headers.put(header.getName(), values);
318            }
319            values.add(header.getValue());
320        }
321        return headers;
322    }
323
324    /**
325     * Builds an FcrepoClient
326     *
327     * @author bbpennel
328     */
329    public static class FcrepoClientBuilder {
330
331        private String authUser;
332
333        private String authPassword;
334
335        private String authHost;
336
337        private boolean throwExceptionOnFailure;
338
339        /**
340         * Add basic authentication credentials to this client
341         *
342         * @param username username for authentication
343         * @param password password for authentication
344         * @return the client builder
345         */
346        public FcrepoClientBuilder credentials(final String username, final String password) {
347            this.authUser = username;
348            this.authPassword = password;
349            return this;
350        }
351
352        /**
353         * Add an authentication scope to this client
354         *
355         * @param authHost authentication scope value
356         * @return this builder
357         */
358        public FcrepoClientBuilder authScope(final String authHost) {
359            this.authHost = authHost;
360
361            return this;
362        }
363
364        /**
365         * Client should throw exceptions when failures occur
366         *
367         * @return this builder
368         */
369        public FcrepoClientBuilder throwExceptionOnFailure() {
370            this.throwExceptionOnFailure = true;
371            return this;
372        }
373
374        /**
375         * Get the client
376         *
377         * @return the client constructed by this builder
378         */
379        public FcrepoClient build() {
380            return new FcrepoClient(authUser, authPassword, authHost, throwExceptionOnFailure);
381        }
382    }
383}