Skip to content

Commit

Permalink
Merge e5b11ca into fb55fee
Browse files Browse the repository at this point in the history
  • Loading branch information
wwelling committed Mar 4, 2021
2 parents fb55fee + e5b11ca commit 4360974
Show file tree
Hide file tree
Showing 12 changed files with 312 additions and 40 deletions.
24 changes: 24 additions & 0 deletions service/src/main/java/edu/tamu/catalog/properties/Credentials.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package edu.tamu.catalog.properties;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

import lombok.Data;

@Data
public class Credentials {

private String username;

private String password;

@JsonCreator
public Credentials(
@JsonProperty(value = "username", required = true) String username,
@JsonProperty(value = "password", required = true) String password
) {
setUsername(username);
setPassword(password);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ public class FolioProperties extends AbstractCatalogServiceProperties {

private String tenant;

private String username;

private String password;
private Credentials credentials;

private String edgeApiKey;

Expand All @@ -35,8 +33,7 @@ public FolioProperties(
@JsonProperty(value = "baseOkapiUrl", required = true) String baseOkapiUrl,
@JsonProperty(value = "baseEdgeUrl", required = true) String baseEdgeUrl,
@JsonProperty(value = "tenant", required = true) String tenant,
@JsonProperty(value = "username", required = true) String username,
@JsonProperty(value = "password", required = true) String password,
@JsonProperty(value = "credentials", required = true) Credentials credentials,
@JsonProperty(value = "edgeApiKey", required = true) String edgeApiKey,
@JsonProperty(value = "repositoryBaseUrl", required = true) String repositoryBaseUrl
) {
Expand All @@ -45,8 +42,7 @@ public FolioProperties(
setBaseOkapiUrl(baseOkapiUrl);
setBaseEdgeUrl(baseEdgeUrl);
setTenant(tenant);
setUsername(username);
setPassword(password);
setCredentials(credentials);
setEdgeApiKey(edgeApiKey);
setRepositoryBaseUrl(repositoryBaseUrl);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TimeZone;

import javax.xml.parsers.DocumentBuilder;
Expand All @@ -39,6 +41,14 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.client.RestTemplate;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
Expand All @@ -52,8 +62,10 @@
import edu.tamu.catalog.domain.model.HoldingsRecord;
import edu.tamu.catalog.domain.model.LoanItem;
import edu.tamu.catalog.properties.CatalogServiceProperties;
import edu.tamu.catalog.properties.Credentials;
import edu.tamu.catalog.properties.FolioProperties;
import edu.tamu.catalog.utility.Marc21Xml;
import edu.tamu.catalog.utility.TokenUtility;

public class FolioCatalogService implements CatalogService {

Expand All @@ -73,6 +85,9 @@ public class FolioCatalogService implements CatalogService {
private static final String NODE_OAI = "oai";
private static final String NODE_RECORD = "record";

private static final String OKAPI_TENANT_HEADER = "X-Okapi-Tenant";
private static final String OKAPI_TOKEN_HEADER = "X-Okapi-Token";

@Autowired
private RestTemplate restTemplate;

Expand Down Expand Up @@ -192,6 +207,40 @@ public List<LoanItem> getLoanItems(String uin) throws ParseException {
return list;
}

/**
* Okapi request method not requiring a request body. i.e. HEAD, GET, DELETE
*
* @param <T> generic class for response body type
* @param url String
* @param method HttpMethod
* @param responseType Class<T>
* @param uriVariables Object... uri variables to be expanded into url
* @return response entity with response type as body
*/
<T> ResponseEntity<T> okapiRequest(String url, HttpMethod method, Class<T> responseType, Object... uriVariables) {
HttpEntity<?> requestEntity = new HttpEntity<>(headers(properties.getTenant(), getToken()));

return okapiRequest(1, url, method, requestEntity, responseType, uriVariables);
}

/**
* Okapi request method requiring a request body. i.e. PUT, POST
*
* @param <B> generic class for request body type
* @param <T> generic class for response body type
* @param url String
* @param method HttpMethod
* @param body B request body
* @param responseType Class<T>
* @param uriVariables Object... uri variables to be expanded into url
* @return response entity with response type as body
*/
<B,T> ResponseEntity<T> okapiRequest(String url, HttpMethod method, B body, Class<T> responseType, Object... uriVariables) {
HttpEntity<B> requestEntity = new HttpEntity<>(body, headers(properties.getTenant(), getToken()));

return okapiRequest(1, url, method, requestEntity, responseType, uriVariables);
}

private List<HoldingsRecord> requestHoldings(String instanceId, String holdingId) {
List<HoldingsRecord> holdings = new ArrayList<>();

Expand Down Expand Up @@ -399,4 +448,65 @@ private Date folioDateToDate(String folioDate) throws ParseException {
return Date.from(formatter.parse(folioDate).toInstant());
}

/**
* Okapi request method to attempt one token refresh and retry if request unauthorized.
*
* @param <T> generic class for response body type
* @param attempt int
* @param url String
* @param method HttpMethod
* @param requestEntity HttpEntity<T>
* @param responseType Class<T>
* @param uriVariables Object... uri variables to be expanded into url
* @return response entity with response type as body
*/
private <T> ResponseEntity<T> okapiRequest(int attempt, String url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables) {
try {
return restTemplate.exchange(url, method, requestEntity, responseType, uriVariables);
} catch(RestClientResponseException e) {
if (e.getRawStatusCode() == HttpStatus.UNAUTHORIZED.value() && attempt == 1) {
requestEntity = new HttpEntity<>(requestEntity.getBody(), headers(properties.getTenant(), okapiLogin()));
return okapiRequest(++attempt, url, method, requestEntity, responseType, uriVariables);
}
throw e;
}
}

private String getToken() {
Optional<String> token = TokenUtility.getToken(getName());
if (token.isPresent()) {
return token.get();
}
return okapiLogin();
}

private String okapiLogin() {
String url = properties.getBaseOkapiUrl() + "/authn/login";
HttpEntity<Credentials> entity = new HttpEntity<>(properties.getCredentials(), headers(properties.getTenant()));
ResponseEntity<?> response = restTemplate.postForObject(url, entity, ResponseEntity.class);
if (response.getStatusCode().equals(HttpStatus.CREATED)) {
String token = response.getHeaders().getFirst(OKAPI_TOKEN_HEADER);
TokenUtility.setToken(getName(), token);
return token;
} else {
logger.error("Failed to login {}: {}", response.getStatusCodeValue(), response.getBody());
throw new HttpServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR, "Catalog service failed to login into Okapi!");
}
}

private HttpHeaders headers(String tenant, String token) {
HttpHeaders headers = headers(tenant);
headers.set(OKAPI_TOKEN_HEADER, token);
return headers;
}

// NOTE: assuming all accept and content type will be application/json
private HttpHeaders headers(String tenant) {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN));
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set(OKAPI_TENANT_HEADER, tenant);
return headers;
}

}
23 changes: 23 additions & 0 deletions service/src/main/java/edu/tamu/catalog/utility/TokenUtility.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package edu.tamu.catalog.utility;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

public class TokenUtility {

private static final Map<String, String> tokens = new HashMap<>();

private TokenUtility() {

}

public static synchronized void setToken(String catalog, String token) {
tokens.put(catalog, token);
}

public static synchronized Optional<String> getToken(String catalog) {
return Optional.ofNullable(tokens.get(catalog));
}

}
6 changes: 4 additions & 2 deletions service/src/main/resources/catalogs/folio.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
"baseOkapiUrl": "https://folio-okapi-q3.library.tamu.edu/",
"baseEdgeUrl": "https://folio-edge.library.tamu.edu/",
"tenant": "tamu",
"username": "",
"password": "",
"credentials": {
"username": "",
"password": ""
},
"edgeApiKey": "",
"repositoryBaseUrl": "folio-edge.library.tamu.edu"
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public void testExpectedNumberOfCatalogServices() {
@Test
public void testAutowiredEvansVoyagerCatalogService() throws NoSuchFieldException, SecurityException,
IllegalArgumentException, IllegalAccessException {
assertNotNull(mslCatalogService);
assertNotNull(evansCatalogService);
assertEquals("evans", evansCatalogService.getName());
Field field = VoyagerCatalogService.class.getDeclaredField("properties");
field.setAccessible(true);
Expand Down Expand Up @@ -86,8 +86,9 @@ public void testAutowiredFolioVoyagerCatalogService() throws NoSuchFieldExceptio
assertEquals("http://localhost:9130", properties.getBaseOkapiUrl());
assertEquals("http://localhost:8080", properties.getBaseEdgeUrl());
assertEquals("diku", properties.getTenant());
assertEquals("diku_admin", properties.getUsername());
assertEquals("admin", properties.getPassword());
assertNotNull(properties.getCredentials());
assertEquals("diku_admin", properties.getCredentials().getUsername());
assertEquals("admin", properties.getCredentials().getPassword());
assertEquals("mock_api_key", properties.getEdgeApiKey());
assertEquals("localhost", properties.getRepositoryBaseUrl());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package edu.tamu.catalog.service;

import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.when;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.client.RestTemplate;

import edu.tamu.catalog.properties.Credentials;
import edu.tamu.catalog.properties.FolioProperties;

@RunWith(SpringRunner.class)
public class FolioCatalogServiceTest {

private ObjectMapper objectMapper = new ObjectMapper();

private Credentials credentials = new Credentials("diku_admin", "admin");

private FolioProperties properties = new FolioProperties(
"folio",
"folio",
"http://localhost:9130",
"http://localhost:8080",
"diku",
credentials,
"mock_api_key",
"localhost"
);

@InjectMocks
private FolioCatalogService folioCatalogService = new FolioCatalogService(properties);

@Mock
private RestTemplate restTemplate;

@Before
public void setup() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("X-Okapi-Token", "token");
when(restTemplate.postForObject(eq("http://localhost:9130/authn/login"), any(HttpEntity.class), eq(ResponseEntity.class)))
.thenReturn(new ResponseEntity<String>("{ \"username\": \"diku_admin\", \"password\": \"admin\" }", headers, HttpStatus.CREATED));
}

@Test
public void testOkapiHeadRequest() {
testOkapiRequest("http://localhost:9130/locations", HttpMethod.HEAD, HttpStatus.OK);
}

@Test
public void testOkapiGetRequest() {
testOkapiRequest("http://localhost:9130/locations", HttpMethod.GET, HttpStatus.OK);
}

@Test
public void testOkapiPostRequest() {
JsonNode requestBody = objectMapper.createObjectNode();
testOkapiRequest("http://localhost:9130/locations", HttpMethod.POST, requestBody, HttpStatus.CREATED);
}

@Test
public void testOkapiPutRequest() {
JsonNode requestBody = objectMapper.createObjectNode();
testOkapiRequest("http://localhost:9130/locations/uuid", HttpMethod.PUT, requestBody, HttpStatus.OK);
}

@Test
public void testOkapiDeleteRequest() {
testOkapiRequest("http://localhost:9130/locations/uuid", HttpMethod.DELETE, HttpStatus.OK);
}

private void testOkapiRequest(String url, HttpMethod method, HttpStatus status) {
JsonNode responseBody = objectMapper.createObjectNode();
mockExchange(url, method, responseBody, status);
ResponseEntity<JsonNode> response = folioCatalogService.okapiRequest(url, method, JsonNode.class);
assertEquals(status, response.getStatusCode());
}

private void testOkapiRequest(String url, HttpMethod method, JsonNode requestBody, HttpStatus status) {
JsonNode responseBody = objectMapper.createObjectNode();
mockExchange(url, method, responseBody, status);
ResponseEntity<JsonNode> response = folioCatalogService.okapiRequest(url, method, requestBody, JsonNode.class);
assertEquals(status, response.getStatusCode());
}

private void mockExchange(String url, HttpMethod method, JsonNode responseBody, HttpStatus status) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("X-Okapi-Tenant", "diku");
when(restTemplate.exchange(eq(url), eq(method), any(HttpEntity.class), eq(JsonNode.class)))
.thenReturn(new ResponseEntity<JsonNode>(responseBody, headers, status));
}

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package edu.tamu.catalog.util;
package edu.tamu.catalog.utility;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
Expand Down

0 comments on commit 4360974

Please sign in to comment.