Skip to content

Commit

Permalink
Add HTTP-based PID minter
Browse files Browse the repository at this point in the history
- Add support for authenticated PID minter services
- Makd master.xml spring config file to bundle config files and avoid wildcard include

Resolves: https://www.pivotaltracker.com/story/show/70678050
  • Loading branch information
escowles authored and Andrew Woods committed May 15, 2014
1 parent 05a9522 commit 4959897
Show file tree
Hide file tree
Showing 6 changed files with 293 additions and 6 deletions.
@@ -0,0 +1,176 @@
/**
* Copyright 2014 DuraSpace, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.fcrepo.kernel.identifiers;

import static org.slf4j.LoggerFactory.getLogger;
import static org.apache.commons.lang.StringUtils.isBlank;
import static com.google.common.base.Preconditions.checkArgument;

import org.slf4j.Logger;
import com.codahale.metrics.annotation.Timed;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;

import org.w3c.dom.Document;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPathException;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;

import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;


/**
* PID minter that uses an external REST service to mint PIDs.
*
* @author escowles
* @date 04/28/2014
*/
public class HttpPidMinter extends BasePidMinter {

private static final Logger log = getLogger(HttpPidMinter.class);
protected final String url;
protected final String method;
protected final String username;
protected final String password;
private final String regex;
private XPathExpression xpath;

protected HttpClient client;

/**
* Create a new HttpPidMinter.
* @param url The URL for the minter service. This is the only required argument -- all
* other parameters can be blank.
* @param method The HTTP method (POST, PUT or GET) used to generate a new PID (POST will
* be used if the method is blank.
* @param username If not blank, use this username to connect to the minter service.
* @param password If not blank, use this password used to connect to the minter service.
* @param regex If not blank, use this regular expression used to remove unwanted text from the
* minter service response. For example, if the response text is "/foo/bar:baz" and the
* desired identifier is "baz", then the regex would be ".*:".
* @param xpath If not blank, use this XPath expression used to extract the desired identifier
* from an XML minter response.
**/
public HttpPidMinter( final String url, final String method, final String username,
final String password, final String regex, final String xpath ) {

checkArgument( !isBlank(url), "Minter URL must be specified!" );

this.url = url;
this.method = method;
this.username = username;
this.password = password;
this.regex = regex;
if ( xpath != null ) {
try {
this.xpath = XPathFactory.newInstance().newXPath().compile(xpath);
} catch ( XPathException ex ) {
log.warn("Error parsing xpath ({}): {}", xpath, ex );
}
}
this.client = buildClient();
}

/**
* Setup authentication in httpclient.
**/
protected HttpClient buildClient() {
HttpClientBuilder builder = HttpClientBuilder.create().useSystemProperties().setConnectionManager(
new PoolingHttpClientConnectionManager());
if (!isBlank(username) && !isBlank(password)) {
final URI uri = URI.create(url);
final CredentialsProvider credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(new AuthScope(uri.getHost(), uri.getPort()),
new UsernamePasswordCredentials(username, password));
builder = builder.setDefaultCredentialsProvider(credsProvider);
}
return builder.build();
}

/**
* Instantiate a request object based on the method variable.
**/
private HttpUriRequest minterRequest() {
if ( method != null && method.equalsIgnoreCase("GET") ) {
return new HttpGet(url);
} else if ( method != null && method.equalsIgnoreCase("PUT") ) {
return new HttpPut(url);
} else {
return new HttpPost(url);
}
}

/**
* Remove unwanted text from the minter service response to produce the desired identifer.
* Override this method for processing more complex than a simple regex replacement.
**/
protected String responseToPid( final String responseText ) throws Exception {
log.debug("responseToPid({})", responseText);
if ( !isBlank(regex) ) {
return responseText.replaceFirst(regex,"");
} else if ( xpath != null ) {
return xpath( responseText, xpath );
} else {
return responseText;
}
}

/**
* Extract the desired identifier value from an XML response using XPath
**/
private static String xpath( String xml, XPathExpression xpath ) throws Exception {
final DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
final Document doc = builder.parse(new ByteArrayInputStream(xml.getBytes()));
return xpath.evaluate(doc);
}

/**
* Mint a unique identifier using an external HTTP API.
* @return The generated identifier.
*/
@Timed
@Override
public String mintPid() {
try {
log.debug("mintPid()");
final HttpResponse resp = client.execute( minterRequest() );
return responseToPid( EntityUtils.toString(resp.getEntity()) );
} catch ( IOException ex ) {
log.warn("Error minting pid from {}: {}", url, ex);
throw new RuntimeException("Error minting pid", ex);
} catch ( Exception ex ) {
log.warn("Error processing minter response", ex);
throw new RuntimeException("Error processing minter response", ex);
}
}
}
@@ -0,0 +1,78 @@
/**
* Copyright 2014 DuraSpace, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.fcrepo.kernel.identifiers;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;

import org.junit.Test;

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.client.methods.HttpUriRequest;

public class HttpPidMinterTest {

@Test
public void testMintPid() throws Exception {
final HttpPidMinter testMinter = new HttpPidMinter(
"http://localhost/minter","POST", "", "", ".*/", "");

final HttpClient mockClient = mock(HttpClient.class);
final HttpResponse mockResponse = mock(HttpResponse.class);
final ByteArrayEntity entity = new ByteArrayEntity("/foo/bar/baz".getBytes());
testMinter.client = mockClient;

when(mockClient.execute(isA(HttpUriRequest.class))).thenReturn(mockResponse);
when(mockResponse.getEntity()).thenReturn(entity);

final String pid = testMinter.mintPid();
verify(mockClient).execute(isA(HttpUriRequest.class));
assertEquals( pid, "baz" );
}

@Test
public void testMintPidXPath() throws Exception {
final HttpPidMinter testMinter = new HttpPidMinter(
"http://localhost/minter","POST", "", "", "", "/test/id");

final HttpClient mockClient = mock(HttpClient.class);
final HttpResponse mockResponse = mock(HttpResponse.class);
final ByteArrayEntity entity = new ByteArrayEntity("<test><id>baz</id></test>".getBytes());
testMinter.client = mockClient;

when(mockClient.execute(isA(HttpUriRequest.class))).thenReturn(mockResponse);
when(mockResponse.getEntity()).thenReturn(entity);

final String pid = testMinter.mintPid();
verify(mockClient).execute(isA(HttpUriRequest.class));
assertEquals( pid, "baz" );
}

@Test
public void testHttpClient() throws Exception {
final HttpPidMinter testMinter = new HttpPidMinter(
"http://localhost/minter","POST", "user", "pass", "", "");
final HttpClient client = testMinter.buildClient();
assertNotNull(client);
}

}
17 changes: 17 additions & 0 deletions fcrepo-webapp/src/main/resources/spring/master.xml
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

<!-- Master context for fcrepo4. -->

<import resource="classpath:/spring/repo.xml"/>
<import resource="classpath:/spring/rest.xml"/>
<import resource="${fcrepo.minter.config:classpath:/spring/minter.xml}"/>
<import resource="classpath:/spring/eventing.xml"/>
<import resource="classpath:/spring/jms.xml"/>
<import resource="classpath:/spring/generator.xml"/>
<import resource="classpath:/spring/transactions.xml"/>

</beans>
21 changes: 21 additions & 0 deletions fcrepo-webapp/src/main/resources/spring/minter.xml
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

<!-- Mints PIDs using random UUIDs -->
<bean class="org.fcrepo.kernel.identifiers.UUIDPathMinter"
c:length="${fcrepo.uuid.path.length:2}"
c:count="${fcrepo.uuid.path.count:4}"/>

<!-- Mints PIDs using external REST service
<bean class="org.fcrepo.kernel.identifiers.HttpPidMinter"
c:url="http://localhost/my/minter" c:method="POST"
c:username="${fcrepo.minter.username:minterUser}"
c:password="${fcrepo.minter.password:minterPass}"
c:regex="" c:xpath="/response/ids/value"/>
-->


</beans>
5 changes: 0 additions & 5 deletions fcrepo-webapp/src/main/resources/spring/rest.xml
Expand Up @@ -16,11 +16,6 @@

<context:annotation-config/>

<!-- Mints PIDs-->
<bean class="org.fcrepo.kernel.identifiers.UUIDPathMinter"
c:length="${fcrepo.uuid.path.length:2}"
c:count="${fcrepo.uuid.path.count:4}"/>

<bean class="org.fcrepo.http.commons.session.SessionFactory"/>

<!-- Identifier translation chain -->
Expand Down
2 changes: 1 addition & 1 deletion fcrepo-webapp/src/main/webapp/WEB-INF/web.xml
Expand Up @@ -9,7 +9,7 @@

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>WEB-INF/classes/spring/*.xml</param-value>
<param-value>WEB-INF/classes/spring/master.xml</param-value>
</context-param>

<listener>
Expand Down

0 comments on commit 4959897

Please sign in to comment.