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