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.mint; 017 018import static org.slf4j.LoggerFactory.getLogger; 019import static org.apache.commons.lang.StringUtils.isBlank; 020import static com.google.common.base.Preconditions.checkArgument; 021 022import org.slf4j.Logger; 023 024import com.codahale.metrics.annotation.Timed; 025 026import java.io.ByteArrayInputStream; 027import java.io.IOException; 028import java.net.URI; 029 030import org.w3c.dom.Document; 031 032import javax.xml.parsers.DocumentBuilder; 033import javax.xml.parsers.DocumentBuilderFactory; 034import javax.xml.xpath.XPathException; 035import javax.xml.xpath.XPathExpression; 036import javax.xml.xpath.XPathFactory; 037 038import org.apache.http.HttpResponse; 039import org.apache.http.client.HttpClient; 040import org.apache.http.client.methods.HttpGet; 041import org.apache.http.client.methods.HttpPost; 042import org.apache.http.client.methods.HttpPut; 043import org.apache.http.client.methods.HttpUriRequest; 044import org.apache.http.impl.client.HttpClientBuilder; 045import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; 046import org.apache.http.util.EntityUtils; 047import org.apache.http.auth.AuthScope; 048import org.apache.http.auth.UsernamePasswordCredentials; 049import org.apache.http.client.CredentialsProvider; 050import org.apache.http.impl.client.BasicCredentialsProvider; 051import org.fcrepo.kernel.identifiers.PidMinter; 052 053 054/** 055 * PID minter that uses an external REST service to mint PIDs. 056 * 057 * @author escowles 058 * @since 04/28/2014 059 */ 060public class HttpPidMinter implements PidMinter { 061 062 private static final Logger log = getLogger(HttpPidMinter.class); 063 protected final String url; 064 protected final String method; 065 protected final String username; 066 protected final String password; 067 private final String regex; 068 private XPathExpression xpath; 069 070 protected HttpClient client; 071 072 /** 073 * Create a new HttpPidMinter. 074 * @param url The URL for the minter service. This is the only required argument -- all 075 * other parameters can be blank. 076 * @param method The HTTP method (POST, PUT or GET) used to generate a new PID (POST will 077 * be used if the method is blank. 078 * @param username If not blank, use this username to connect to the minter service. 079 * @param password If not blank, use this password used to connect to the minter service. 080 * @param regex If not blank, use this regular expression used to remove unwanted text from the 081 * minter service response. For example, if the response text is "/foo/bar:baz" and the 082 * desired identifier is "baz", then the regex would be ".*:". 083 * @param xpath If not blank, use this XPath expression used to extract the desired identifier 084 * from an XML minter response. 085 **/ 086 public HttpPidMinter( final String url, final String method, final String username, 087 final String password, final String regex, final String xpath ) { 088 089 checkArgument( !isBlank(url), "Minter URL must be specified!" ); 090 091 this.url = url; 092 this.method = (method == null ? "post" : method); 093 this.username = username; 094 this.password = password; 095 this.regex = regex; 096 if ( !isBlank(xpath) ) { 097 try { 098 this.xpath = XPathFactory.newInstance().newXPath().compile(xpath); 099 } catch ( XPathException ex ) { 100 log.warn("Error parsing xpath ({}): {}", xpath, ex ); 101 throw new IllegalArgumentException("Error parsing xpath" + xpath, ex); 102 } 103 } 104 this.client = buildClient(); 105 } 106 107 /** 108 * Setup authentication in httpclient. 109 **/ 110 protected HttpClient buildClient() { 111 HttpClientBuilder builder = HttpClientBuilder.create().useSystemProperties().setConnectionManager( 112 new PoolingHttpClientConnectionManager()); 113 if (!isBlank(username) && !isBlank(password)) { 114 final URI uri = URI.create(url); 115 final CredentialsProvider credsProvider = new BasicCredentialsProvider(); 116 credsProvider.setCredentials(new AuthScope(uri.getHost(), uri.getPort()), 117 new UsernamePasswordCredentials(username, password)); 118 builder = builder.setDefaultCredentialsProvider(credsProvider); 119 } 120 return builder.build(); 121 } 122 123 /** 124 * Instantiate a request object based on the method variable. 125 **/ 126 private HttpUriRequest minterRequest() { 127 switch (method) { 128 case "GET": case "get": 129 return new HttpGet(url); 130 case "PUT": case "put": 131 return new HttpPut(url); 132 default: 133 return new HttpPost(url); 134 } 135 } 136 137 /** 138 * Remove unwanted text from the minter service response to produce the desired identifer. 139 * Override this method for processing more complex than a simple regex replacement. 140 **/ 141 protected String responseToPid( final String responseText ) throws Exception { 142 log.debug("responseToPid({})", responseText); 143 if ( !isBlank(regex) ) { 144 return responseText.replaceFirst(regex,""); 145 } else if ( xpath != null ) { 146 return xpath( responseText, xpath ); 147 } else { 148 return responseText; 149 } 150 } 151 152 /** 153 * Extract the desired identifier value from an XML response using XPath 154 **/ 155 private static String xpath( final String xml, final XPathExpression xpath ) throws Exception { 156 final DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); 157 final Document doc = builder.parse(new ByteArrayInputStream(xml.getBytes())); 158 return xpath.evaluate(doc); 159 } 160 161 /** 162 * Mint a unique identifier using an external HTTP API. 163 * @return The generated identifier. 164 */ 165 @Timed 166 @Override 167 public String mintPid() { 168 try { 169 log.debug("mintPid()"); 170 final HttpResponse resp = client.execute( minterRequest() ); 171 return responseToPid( EntityUtils.toString(resp.getEntity()) ); 172 } catch ( IOException ex ) { 173 log.warn("Error minting pid from {}: {}", url, ex); 174 throw new RuntimeException("Error minting pid", ex); 175 } catch ( Exception ex ) { 176 log.warn("Error processing minter response", ex); 177 throw new RuntimeException("Error processing minter response", ex); 178 } 179 } 180}