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.utils; 017 018import static java.lang.Integer.MAX_VALUE; 019 020import static org.apache.http.HttpStatus.SC_BAD_REQUEST; 021import static org.apache.http.HttpStatus.SC_FORBIDDEN; 022import static org.apache.http.HttpStatus.SC_NOT_FOUND; 023import static org.apache.http.HttpStatus.SC_OK; 024 025import static org.apache.jena.riot.WebContent.contentTypeSPARQLUpdate; 026 027import static org.apache.commons.lang3.StringUtils.isBlank; 028import static org.slf4j.LoggerFactory.getLogger; 029 030import java.io.IOException; 031import java.io.InputStream; 032import java.io.UnsupportedEncodingException; 033import java.net.URI; 034import java.net.URLEncoder; 035import java.util.Iterator; 036import java.util.List; 037import java.util.Map; 038 039import org.fcrepo.client.BadRequestException; 040import org.fcrepo.client.ForbiddenException; 041import org.fcrepo.client.NotFoundException; 042 043import com.hp.hpl.jena.graph.Node; 044 045import org.apache.http.Header; 046import org.apache.http.HttpEntity; 047import org.apache.http.HttpResponse; 048import org.apache.http.StatusLine; 049import org.apache.http.auth.AuthScope; 050import org.apache.http.auth.UsernamePasswordCredentials; 051import org.apache.http.client.CredentialsProvider; 052import org.apache.http.client.HttpClient; 053import org.apache.http.client.methods.HttpDelete; 054import org.apache.http.client.methods.HttpGet; 055import org.apache.http.client.methods.HttpHead; 056import org.apache.http.client.methods.HttpPatch; 057import org.apache.http.client.methods.HttpPost; 058import org.apache.http.client.methods.HttpPut; 059import org.apache.http.client.methods.HttpUriRequest; 060import org.apache.http.entity.ByteArrayEntity; 061import org.apache.http.entity.InputStreamEntity; 062import org.apache.http.impl.client.BasicCredentialsProvider; 063import org.apache.http.impl.client.DefaultHttpClient; 064import org.apache.http.impl.client.DefaultRedirectStrategy; 065import org.apache.http.impl.client.StandardHttpRequestRetryHandler; 066import org.apache.http.impl.conn.PoolingClientConnectionManager; 067 068import org.apache.jena.riot.Lang; 069import org.apache.jena.riot.RDFLanguages; 070import org.apache.jena.riot.RiotReader; 071import org.apache.jena.riot.lang.CollectorStreamTriples; 072 073import org.fcrepo.client.FedoraContent; 074import org.fcrepo.client.FedoraException; 075import org.fcrepo.client.FedoraObject; 076import org.fcrepo.client.ReadOnlyException; 077import org.fcrepo.client.impl.FedoraResourceImpl; 078 079import org.slf4j.Logger; 080 081 082/** 083 * HTTP utilities 084 * @author escowles 085 * @since 2014-08-26 086**/ 087public class HttpHelper { 088 private static final Logger LOGGER = getLogger(HttpHelper.class); 089 090 private final String repositoryURL; 091 private final HttpClient httpClient; 092 private final boolean readOnly; 093 094 /** 095 * Create an HTTP helper with a pre-configured HttpClient instance. 096 * @param repositoryURL Fedora base URL. 097 * @param httpClient Pre-configured HttpClient instance. 098 * @param readOnly If true, throw an exception when an update is attempted. 099 **/ 100 public HttpHelper(final String repositoryURL, final HttpClient httpClient, final boolean readOnly) { 101 this.repositoryURL = repositoryURL; 102 this.httpClient = httpClient; 103 this.readOnly = readOnly; 104 } 105 106 /** 107 * Create an HTTP helper for the specified repository. If fedoraUsername and fedoraPassword are not null, then 108 * they will be used to connect to the repository. 109 * @param repositoryURL Fedora base URL. 110 * @param fedoraUsername Fedora username 111 * @param fedoraPassword Fedora password 112 * @param readOnly If true, throw an exception when an update is attempted. 113 **/ 114 public HttpHelper(final String repositoryURL, final String fedoraUsername, final String fedoraPassword, 115 final boolean readOnly) { 116 this.repositoryURL = repositoryURL; 117 this.readOnly = readOnly; 118 119 final PoolingClientConnectionManager connMann = new PoolingClientConnectionManager(); 120 connMann.setMaxTotal(MAX_VALUE); 121 connMann.setDefaultMaxPerRoute(MAX_VALUE); 122 123 final DefaultHttpClient httpClient = new DefaultHttpClient(connMann); 124 httpClient.setRedirectStrategy(new DefaultRedirectStrategy()); 125 httpClient.setHttpRequestRetryHandler(new StandardHttpRequestRetryHandler(0, false)); 126 127 // If the Fedora instance requires authentication, set it up here 128 if (!isBlank(fedoraUsername) && !isBlank(fedoraPassword)) { 129 LOGGER.debug("Adding BASIC credentials to client for repo requests."); 130 131 final URI fedoraUri = URI.create(repositoryURL); 132 final CredentialsProvider credsProvider = new BasicCredentialsProvider(); 133 credsProvider.setCredentials(new AuthScope(fedoraUri.getHost(), fedoraUri.getPort()), 134 new UsernamePasswordCredentials(fedoraUsername, fedoraPassword)); 135 136 httpClient.setCredentialsProvider(credsProvider); 137 } 138 139 this.httpClient = httpClient; 140 } 141 142 /** 143 * Execute a request for a subclass. 144 * 145 * @param request request to be executed 146 * @return response containing response to request 147 * @throws IOException 148 * @throws ReadOnlyException 149 **/ 150 public HttpResponse execute( final HttpUriRequest request ) throws IOException, ReadOnlyException { 151 if ( readOnly ) { 152 switch ( request.getMethod().toLowerCase() ) { 153 case "copy": case "delete": case "move": case "patch": case "post": case "put": 154 throw new ReadOnlyException(); 155 default: 156 break; 157 } 158 } 159 160 return httpClient.execute(request); 161 } 162 163 /** 164 * Encode URL parameters as a query string. 165 * @param params Query parameters 166 **/ 167 private static String queryString( final Map<String, List<String>> params ) { 168 final StringBuilder builder = new StringBuilder(); 169 if (params != null && params.size() > 0) { 170 for (final Iterator<String> it = params.keySet().iterator(); it.hasNext(); ) { 171 final String key = it.next(); 172 final List<String> values = params.get(key); 173 for (final String value : values) { 174 try { 175 builder.append(key + "=" + URLEncoder.encode(value, "UTF-8") + "&"); 176 } catch (final UnsupportedEncodingException e) { 177 builder.append(key + "=" + value + "&"); 178 } 179 } 180 } 181 return builder.length() > 0 ? "?" + builder.substring(0, builder.length() - 1) : ""; 182 } 183 return ""; 184 } 185 186 /** 187 * Create HEAD method 188 * @param path Resource path, relative to repository baseURL 189 * @return HEAD method 190 **/ 191 public HttpHead createHeadMethod(final String path) { 192 return new HttpHead(repositoryURL + path); 193 } 194 195 /** 196 * Create GET method with list of parameters 197 * @param path Resource path, relative to repository baseURL 198 * @param params Query parameters 199 * @return GET method 200 **/ 201 public HttpGet createGetMethod(final String path, final Map<String, List<String>> params) { 202 return new HttpGet(repositoryURL + path + queryString(params)); 203 } 204 205 /** 206 * Create DELETE method 207 * @param path Resource path, relative to repository baseURL 208 * @return DELETE method 209 **/ 210 public HttpDelete createDeleteMethod(final String path) { 211 return new HttpDelete(repositoryURL + path); 212 } 213 214 /** 215 * Create a request to update triples with SPARQL Update. 216 * @param path The datastream path. 217 * @param sparqlUpdate SPARQL Update command. 218 * @return created patch based on parameters 219 * @throws FedoraException 220 **/ 221 public HttpPatch createPatchMethod(final String path, final String sparqlUpdate) throws FedoraException { 222 if ( isBlank(sparqlUpdate) ) { 223 throw new FedoraException("SPARQL Update command must not be blank"); 224 } 225 226 final HttpPatch patch = new HttpPatch(repositoryURL + path); 227 patch.setEntity( new ByteArrayEntity(sparqlUpdate.getBytes()) ); 228 patch.setHeader("Content-Type", contentTypeSPARQLUpdate); 229 return patch; 230 } 231 232 /** 233 * Create POST method with list of parameters 234 * @param path Resource path, relative to repository baseURL 235 * @param params Query parameters 236 * @return PUT method 237 **/ 238 public HttpPost createPostMethod(final String path, final Map<String, List<String>> params) { 239 return new HttpPost(repositoryURL + path + queryString(params)); 240 } 241 242 /** 243 * Create PUT method with list of parameters 244 * @param path Resource path, relative to repository baseURL 245 * @param params Query parameters 246 * @return PUT method 247 **/ 248 public HttpPut createPutMethod(final String path, final Map<String, List<String>> params) { 249 return new HttpPut(repositoryURL + path + queryString(params)); 250 } 251 252 /** 253 * Create a request to create/update content. 254 * @param path The datastream path. 255 * @param params Mapping of parameters for the PUT request 256 * @param content Content parameters. 257 * @return PUT method 258 **/ 259 public HttpPut createContentPutMethod(final String path, final Map<String, List<String>> params, 260 final FedoraContent content ) { 261 String contentPath = path; 262 if ( content != null && content.getChecksum() != null ) { 263 contentPath += "?checksum=" + content.getChecksum(); 264 } 265 266 final HttpPut put = createPutMethod( contentPath, params ); 267 268 // content stream 269 if ( content != null ) { 270 put.setEntity( new InputStreamEntity(content.getContent()) ); 271 } 272 273 // filename 274 if ( content != null && content.getFilename() != null ) { 275 put.setHeader("Content-Disposition", "attachment; filename=\"" + content.getFilename() + "\"" ); 276 } 277 278 // content type 279 if ( content != null && content.getContentType() != null ) { 280 put.setHeader("Content-Type", content.getContentType() ); 281 } 282 283 return put; 284 } 285 286 /** 287 * Create a request to update triples. 288 * @param path The datastream path. 289 * @param updatedProperties InputStream containing RDF. 290 * @param contentType Content type of the RDF in updatedProperties (e.g., "text/rdf+n3" or 291 * "application/rdf+xml"). 292 * @return PUT method 293 * @throws FedoraException 294 **/ 295 public HttpPut createTriplesPutMethod(final String path, final InputStream updatedProperties, 296 final String contentType) throws FedoraException { 297 if ( updatedProperties == null ) { 298 throw new FedoraException("updatedProperties must not be null"); 299 } else if ( isBlank(contentType) ) { 300 throw new FedoraException("contentType must not be blank"); 301 } 302 303 final HttpPut put = new HttpPut(repositoryURL + path); 304 put.setEntity( new InputStreamEntity(updatedProperties) ); 305 put.setHeader("Content-Type", contentType); 306 return put; 307 } 308 309 /** 310 * Retrieve RDF from the repository and update the properties of a resource 311 * @param resource The resource to update 312 * @return the updated resource 313 * @throws FedoraException 314 **/ 315 public FedoraResourceImpl loadProperties( final FedoraResourceImpl resource ) throws FedoraException { 316 final String path = resource.getPropertiesPath(); 317 final HttpGet get = createGetMethod(path, null); 318 if (resource instanceof FedoraObject) { 319 get.addHeader("Prefer", "return=representation; " 320 + "include=\"http://fedora.info/definitions/v4/repository#EmbedResources\""); 321 } 322 323 try { 324 get.setHeader("accept", "application/rdf+xml"); 325 final HttpResponse response = execute(get); 326 327 final String uri = get.getURI().toString(); 328 final StatusLine status = response.getStatusLine(); 329 330 if (status.getStatusCode() == SC_OK) { 331 LOGGER.debug("Updated properties for resource {}", uri); 332 333 // header processing 334 final Header[] etagHeader = response.getHeaders("ETag"); 335 if (etagHeader != null && etagHeader.length > 0) { 336 resource.setEtagValue( etagHeader[0].getValue() ); 337 } 338 339 // StreamRdf 340 final HttpEntity entity = response.getEntity(); 341 final Lang lang = RDFLanguages.contentTypeToLang(entity.getContentType().getValue().split(":")[0]); 342 final CollectorStreamTriples streamTriples = new CollectorStreamTriples(); 343 RiotReader.parse(entity.getContent(), lang, uri, streamTriples); 344 resource.setGraph( RDFSinkFilter.filterTriples(streamTriples.getCollected().iterator(), Node.ANY) ); 345 return resource; 346 } else if (status.getStatusCode() == SC_FORBIDDEN) { 347 LOGGER.info("request for resource {} is not authorized.", uri); 348 throw new ForbiddenException("request for resource " + uri + " is not authorized."); 349 } else if (status.getStatusCode() == SC_BAD_REQUEST) { 350 LOGGER.info("server does not support metadata type application/rdf+xml for resource {} " + 351 " cannot retrieve", uri); 352 throw new BadRequestException("server does not support the request metadata type for resource " + uri); 353 } else if (status.getStatusCode() == SC_NOT_FOUND) { 354 LOGGER.info("resource {} does not exist, cannot retrieve", uri); 355 throw new NotFoundException("resource " + uri + " does not exist, cannot retrieve"); 356 } else { 357 LOGGER.info("unexpected status code ({}) when retrieving resource {}", status.getStatusCode(), uri); 358 throw new FedoraException("error retrieving resource " + uri + ": " + status.getStatusCode() + " " 359 + status.getReasonPhrase()); 360 } 361 } catch (final FedoraException e) { 362 throw e; 363 } catch (final Exception e) { 364 e.printStackTrace(); 365 LOGGER.info("could not encode URI parameter", e); 366 throw new FedoraException(e); 367 } finally { 368 get.releaseConnection(); 369 } 370 } 371 372 /** 373 * Create COPY method 374 * @param sourcePath Source path, relative to repository baseURL 375 * @param destinationPath Destination path, relative to repository baseURL 376 * @return COPY method 377 **/ 378 public HttpCopy createCopyMethod(final String sourcePath, final String destinationPath) { 379 return new HttpCopy(repositoryURL + sourcePath, repositoryURL + destinationPath); 380 } 381 382 /** 383 * Create MOVE method 384 * @param sourcePath Source path, relative to repository baseURL 385 * @param destinationPath Destination path, relative to repository baseURL 386 * @return MOVE method 387 **/ 388 public HttpMove createMoveMethod(final String sourcePath, final String destinationPath) { 389 return new HttpMove(repositoryURL + sourcePath, repositoryURL + destinationPath); 390 } 391 392}