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.fcrepo.kernel.exception.RepositoryRuntimeException;
023import org.slf4j.Logger;
024
025import com.codahale.metrics.annotation.Timed;
026
027import java.io.ByteArrayInputStream;
028import java.io.IOException;
029import java.net.URI;
030
031import org.w3c.dom.Document;
032
033import javax.xml.parsers.DocumentBuilder;
034import javax.xml.parsers.DocumentBuilderFactory;
035import javax.xml.parsers.ParserConfigurationException;
036import javax.xml.xpath.XPathException;
037import javax.xml.xpath.XPathExpression;
038import javax.xml.xpath.XPathExpressionException;
039import javax.xml.xpath.XPathFactory;
040
041import org.apache.http.HttpResponse;
042import org.apache.http.client.HttpClient;
043import org.apache.http.client.methods.HttpGet;
044import org.apache.http.client.methods.HttpPost;
045import org.apache.http.client.methods.HttpPut;
046import org.apache.http.client.methods.HttpUriRequest;
047import org.apache.http.impl.client.HttpClientBuilder;
048import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
049import org.apache.http.util.EntityUtils;
050import org.apache.http.auth.AuthScope;
051import org.apache.http.auth.UsernamePasswordCredentials;
052import org.apache.http.client.CredentialsProvider;
053import org.apache.http.impl.client.BasicCredentialsProvider;
054import org.fcrepo.kernel.identifiers.PidMinter;
055import org.xml.sax.SAXException;
056
057
058/**
059 * PID minter that uses an external REST service to mint PIDs.
060 *
061 * @author escowles
062 * @since 04/28/2014
063 */
064public class HttpPidMinter implements PidMinter {
065
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
076    /**
077     * Create a new HttpPidMinter.
078     * @param url The URL for the minter service.  This is the only required argument -- all
079     *    other parameters can be blank.
080     * @param method The HTTP method (POST, PUT or GET) used to generate a new PID (POST will
081     *    be used if the method is blank.
082     * @param username If not blank, use this username to connect to the minter service.
083     * @param password If not blank, use this password used to connect to the minter service.
084     * @param regex If not blank, use this regular expression used to remove unwanted text from the
085     *    minter service response.  For example, if the response text is "/foo/bar:baz" and the
086     *    desired identifier is "baz", then the regex would be ".*:".
087     * @param xpath If not blank, use this XPath expression used to extract the desired identifier
088     *    from an XML minter response.
089    **/
090    public HttpPidMinter( final String url, final String method, final String username,
091        final String password, final String regex, final String xpath ) {
092
093        checkArgument( !isBlank(url), "Minter URL must be specified!" );
094
095        this.url = url;
096        this.method = (method == null ? "post" : method);
097        this.username = username;
098        this.password = password;
099        this.regex = regex;
100        if ( !isBlank(xpath) ) {
101            try {
102                this.xpath = XPathFactory.newInstance().newXPath().compile(xpath);
103            } catch ( XPathException ex ) {
104                LOGGER.warn("Error parsing xpath ({}): {}", xpath, ex );
105                throw new IllegalArgumentException("Error parsing xpath" + xpath, ex);
106            }
107        }
108        this.client = buildClient();
109    }
110
111    /**
112     * Setup authentication in httpclient.
113     * @return the setup of authentication
114    **/
115    protected HttpClient buildClient() {
116        HttpClientBuilder builder = HttpClientBuilder.create().useSystemProperties().setConnectionManager(
117            new PoolingHttpClientConnectionManager());
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) {
133            case "GET": case "get":
134                return new HttpGet(url);
135            case "PUT": 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 | IOException | 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 = DocumentBuilderFactory.newInstance().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 mintPid() {
181        try {
182            LOGGER.debug("mintPid()");
183            final HttpResponse resp = client.execute( minterRequest() );
184            return responseToPid( EntityUtils.toString(resp.getEntity()) );
185        } catch ( IOException ex ) {
186            LOGGER.warn("Error minting pid from {}: {}", url, ex);
187            throw new RepositoryRuntimeException("Error minting pid", ex);
188        } catch ( Exception ex ) {
189            LOGGER.warn("Error processing minter response", ex);
190            throw new RepositoryRuntimeException("Error processing minter response", ex);
191        }
192    }
193}