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 */
016package org.fcrepo.client;
017
018import static org.slf4j.LoggerFactory.getLogger;
019
020import java.io.IOException;
021import java.io.InputStream;
022import java.net.URI;
023import java.util.ArrayList;
024import java.util.List;
025
026import org.apache.http.Header;
027import org.apache.http.HttpEntity;
028import org.apache.http.HttpResponse;
029import org.apache.http.HttpStatus;
030import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
031import org.apache.http.client.methods.HttpRequestBase;
032import org.apache.http.entity.InputStreamEntity;
033import org.apache.http.impl.client.CloseableHttpClient;
034import org.slf4j.Logger;
035
036/**
037 * Represents a client to interact with Fedora's HTTP API.
038 *
039 * @author Aaron Coburn
040 * @since October 20, 2014
041 */
042public class FcrepoClient {
043
044    private static final String DESCRIBED_BY = "describedby";
045
046    private static final String CONTENT_TYPE = "Content-Type";
047
048    private static final String LOCATION = "Location";
049
050    private CloseableHttpClient httpclient;
051
052    private Boolean throwExceptionOnFailure = true;
053
054    private static final Logger LOGGER = getLogger(FcrepoClient.class);
055
056    /**
057     * Create a FcrepoClient with a set of authentication values.
058     * @param username the username for the repository
059     * @param password the password for the repository
060     * @param host the authentication hostname (realm) for the repository
061     * @param throwExceptionOnFailure whether to throw an exception on any non-2xx or 3xx HTTP responses
062     */
063    public FcrepoClient(final String username, final String password, final String host,
064            final Boolean throwExceptionOnFailure) {
065
066        final FcrepoHttpClientBuilder client = new FcrepoHttpClientBuilder(username, password, host);
067
068        this.throwExceptionOnFailure = throwExceptionOnFailure;
069        this.httpclient = client.build();
070    }
071
072    /**
073     * Make a HEAD response
074     * @param url the URL of the resource to check
075     * @return the repository response
076     * @throws FcrepoOperationFailedException when the underlying HTTP request results in an error
077     */
078    public FcrepoResponse head(final URI url)
079            throws FcrepoOperationFailedException {
080
081        final HttpRequestBase request = HttpMethods.HEAD.createRequest(url);
082        final HttpResponse response = executeRequest(request);
083        final int status = response.getStatusLine().getStatusCode();
084        final String contentType = getContentTypeHeader(response);
085
086        LOGGER.debug("Fcrepo HEAD request returned status [{}]", status);
087
088        if ((status >= HttpStatus.SC_OK && status < HttpStatus.SC_BAD_REQUEST) || !this.throwExceptionOnFailure) {
089            URI describedBy = null;
090            final List<URI> links = getLinkHeaders(response, DESCRIBED_BY);
091            if (links.size() == 1) {
092                describedBy = links.get(0);
093            }
094            return new FcrepoResponse(url, status, contentType, describedBy, null);
095        } else {
096            throw new FcrepoOperationFailedException(url, status,
097                    response.getStatusLine().getReasonPhrase());
098        }
099    }
100
101    /**
102     * Make a PUT request
103     * @param url the URL of the resource to PUT
104     * @param body the contents of the resource to send
105     * @param contentType the MIMEType of the resource
106     * @return the repository response
107     * @throws FcrepoOperationFailedException when the underlying HTTP request results in an error
108     */
109    public FcrepoResponse put(final URI url, final InputStream body, final String contentType)
110            throws FcrepoOperationFailedException {
111
112        final HttpMethods method = HttpMethods.PUT;
113        final HttpEntityEnclosingRequestBase request = (HttpEntityEnclosingRequestBase)method.createRequest(url);
114
115        if (contentType != null) {
116            request.addHeader(CONTENT_TYPE, contentType);
117        }
118        if (body != null) {
119            request.setEntity(new InputStreamEntity(body));
120        }
121
122        LOGGER.debug("Fcrepo PUT request headers: {}", request.getAllHeaders());
123
124        final HttpResponse response = executeRequest(request);
125
126        LOGGER.debug("Fcrepo PUT request returned status [{}]", response.getStatusLine().getStatusCode());
127
128        return fcrepoGenericResponse(url, response, throwExceptionOnFailure);
129    }
130
131    /**
132     * Make a PATCH request
133     * Please note: the body should have an application/sparql-update content-type
134     * @param url the URL of the resource to PATCH
135     * @param body the body to be sent to the repository
136     * @return the repository response
137     * @throws FcrepoOperationFailedException when the underlying HTTP request results in an error
138     */
139    public FcrepoResponse patch(final URI url, final InputStream body)
140            throws FcrepoOperationFailedException {
141
142        final HttpMethods method = HttpMethods.PATCH;
143        final HttpEntityEnclosingRequestBase request = (HttpEntityEnclosingRequestBase)method.createRequest(url);
144
145        request.addHeader(CONTENT_TYPE, "application/sparql-update");
146        request.setEntity(new InputStreamEntity(body));
147
148        LOGGER.debug("Fcrepo PATCH request headers: {}", request.getAllHeaders());
149
150        final HttpResponse response = executeRequest(request);
151
152        LOGGER.debug("Fcrepo PATCH request returned status [{}]", response.getStatusLine().getStatusCode());
153
154        return fcrepoGenericResponse(url, response, throwExceptionOnFailure);
155    }
156
157    /**
158     * Make a POST request
159     * @param url the URL of the resource to which to POST
160     * @param body the content to be sent to the server
161     * @param contentType the Content-Type of the body
162     * @return the repository response
163     * @throws FcrepoOperationFailedException when the underlying HTTP request results in an error
164     */
165    public FcrepoResponse post(final URI url, final InputStream body, final String contentType)
166            throws FcrepoOperationFailedException {
167
168        final HttpMethods method = HttpMethods.POST;
169        final HttpEntityEnclosingRequestBase request = (HttpEntityEnclosingRequestBase)method.createRequest(url);
170
171        if (contentType != null) {
172            request.addHeader(CONTENT_TYPE, contentType);
173        }
174        if (body != null) {
175            request.setEntity(new InputStreamEntity(body));
176        }
177
178        LOGGER.debug("Fcrepo POST request headers: {}", request.getAllHeaders());
179
180        final HttpResponse response = executeRequest(request);
181
182        LOGGER.debug("Fcrepo POST request returned status [{}]", response.getStatusLine().getStatusCode());
183
184        return fcrepoGenericResponse(url, response, throwExceptionOnFailure);
185    }
186
187    /**
188     * Make a DELETE request
189     * @param url the URL of the resource to delete
190     * @return the repository response
191     * @throws FcrepoOperationFailedException when the underlying HTTP request results in an error
192     */
193    public FcrepoResponse delete(final URI url)
194            throws FcrepoOperationFailedException {
195
196        final HttpRequestBase request = HttpMethods.DELETE.createRequest(url);
197        final HttpResponse response = executeRequest(request);
198
199        LOGGER.debug("Fcrepo DELETE request returned status [{}]", response.getStatusLine().getStatusCode());
200
201        return fcrepoGenericResponse(url, response, throwExceptionOnFailure);
202    }
203
204    /**
205     * Make a GET request
206     * @param url the URL of the resource to fetch
207     * @param accept the requested MIMEType of the resource to be retrieved
208     * @param prefer the value for a prefer header sent in the request
209     * @return the repository response
210     * @throws FcrepoOperationFailedException when the underlying HTTP request results in an error
211     */
212    public FcrepoResponse get(final URI url, final String accept, final String prefer)
213            throws FcrepoOperationFailedException {
214
215        final HttpRequestBase request = HttpMethods.GET.createRequest(url);
216
217        if (accept != null) {
218            request.setHeader("Accept", accept);
219        }
220
221        if (prefer != null) {
222            request.setHeader("Prefer", prefer);
223        }
224
225        LOGGER.debug("Fcrepo GET request headers: {}", request.getAllHeaders());
226
227        final HttpResponse response = executeRequest(request);
228        final int status = response.getStatusLine().getStatusCode();
229        final String contentType = getContentTypeHeader(response);
230
231        LOGGER.debug("Fcrepo GET request returned status [{}]", status);
232
233        if ((status >= HttpStatus.SC_OK && status < HttpStatus.SC_BAD_REQUEST) || !this.throwExceptionOnFailure) {
234            URI describedBy = null;
235            final List<URI> links = getLinkHeaders(response, DESCRIBED_BY);
236            if (links.size() == 1) {
237                describedBy = links.get(0);
238            }
239            return new FcrepoResponse(url, status, contentType, describedBy,
240                    getEntityContent(response));
241        } else {
242            throw new FcrepoOperationFailedException(url, status,
243                    response.getStatusLine().getReasonPhrase());
244        }
245    }
246
247    /**
248     * Execute the HTTP request
249     */
250    private HttpResponse executeRequest(final HttpRequestBase request) throws FcrepoOperationFailedException {
251        try {
252            return httpclient.execute(request);
253        } catch (IOException ex) {
254            LOGGER.debug("HTTP Operation failed: ", ex);
255            throw new FcrepoOperationFailedException(request.getURI(), -1, ex.getMessage());
256        }
257    }
258
259    /**
260     * Handle the general case with responses.
261     */
262    private FcrepoResponse fcrepoGenericResponse(final URI url, final HttpResponse response,
263            final Boolean throwExceptionOnFailure) throws FcrepoOperationFailedException {
264        final int status = response.getStatusLine().getStatusCode();
265        final URI locationHeader = getLocationHeader(response);
266        final String contentTypeHeader = getContentTypeHeader(response);
267
268        if ((status >= HttpStatus.SC_OK && status < HttpStatus.SC_BAD_REQUEST) || !throwExceptionOnFailure) {
269            return new FcrepoResponse(url, status, contentTypeHeader, locationHeader, getEntityContent(response));
270        } else {
271            throw new FcrepoOperationFailedException(url, status,
272                    response.getStatusLine().getReasonPhrase());
273        }
274    }
275
276
277    /**
278     * Extract the response body as an input stream
279     */
280    private static InputStream getEntityContent(final HttpResponse response) {
281        try {
282            final HttpEntity entity = response.getEntity();
283            if (entity == null) {
284                return null;
285            } else {
286                return entity.getContent();
287            }
288        } catch (IOException ex) {
289            LOGGER.debug("Unable to extract HttpEntity response into an InputStream: ", ex);
290            return null;
291        }
292    }
293
294    /**
295     * Extract the location header value
296     */
297    private static URI getLocationHeader(final HttpResponse response) {
298        final Header location = response.getFirstHeader(LOCATION);
299        if (location != null) {
300            return URI.create(location.getValue());
301        } else {
302            return null;
303        }
304    }
305
306    /**
307     * Extract the content-type header value
308     */
309    private static String getContentTypeHeader(final HttpResponse response) {
310        final Header contentType = response.getFirstHeader(CONTENT_TYPE);
311        if (contentType != null) {
312            return contentType.getValue();
313        } else {
314            return null;
315        }
316    }
317
318    /**
319     * Extract any Link headers
320     */
321    private static List<URI> getLinkHeaders(final HttpResponse response, final String relationship) {
322        final List<URI> uris = new ArrayList<URI>();
323        final Header[] links = response.getHeaders("Link");
324        for (Header header: links) {
325            final FcrepoLink link = new FcrepoLink(header.getValue());
326            if (link.getRel().equals(relationship)) {
327                uris.add(link.getUri());
328            }
329        }
330        return uris;
331    }
332 }