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