001/**
002 * Copyright 2014 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}