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