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}