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