Skip to content

Commit

Permalink
feat(search-instances): implement endpoint for consolidate holdings a…
Browse files Browse the repository at this point in the history
…ccess in consortium (#535)

* feat(search-instances): implement endpoint for consolidate holdings access in consortium

Closes: MSEARCH-692
Signed-off-by: psmagin <[email protected]>
  • Loading branch information
psmagin authored Mar 13, 2024
1 parent cd2563c commit 36a0aee
Show file tree
Hide file tree
Showing 34 changed files with 1,457 additions and 477 deletions.
4 changes: 3 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### New APIs versions
* Requires `classification-types v1.2`
* Provides `browse-config v1.0`
* Provides `consortium-search v1.0`

### Features
* Update module descriptor with environment variables ([MSEARCH-635](https://issues.folio.org/browse/MSEARCH-635))
Expand All @@ -19,7 +20,8 @@
* Implement endpoint to browse by classifications ([MSEARCH-665](https://issues.folio.org/browse/MSEARCH-665))
* Synchronize browse config with classification types changes ([MSEARCH-683](https://issues.folio.org/browse/MSEARCH-683))
* Authority search: Modify query search option to search authorities by normalized LCCN ([MSEARCH-663](https://issues.folio.org/browse/MSEARCH-663))
* Add ability to case insensitive search ISSNs with trailing roman numerals ([MSEARCH-672](https://folio-org.atlassian.net/browse/MSEARCH-672))
* Add ability to case-insensitive search ISSNs with trailing roman numerals ([MSEARCH-672](https://folio-org.atlassian.net/browse/MSEARCH-672))
* implement endpoint for consolidate holdings access in consortium ([MSEARCH-692](https://folio-org.atlassian.net/browse/MSEARCH-692))

### Bug fixes
* Fix secure setup of system users by default ([MSEARCH-608](https://issues.folio.org/browse/MSEARCH-608))
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,13 @@ When the job is COMPLETED, it is possible to retrieve ids by job id
After retrieving ids, job should change status to "DEPRECATED". If there are no completed job with prepared ids, client
can't receive ids by query.

### Consortium Search API
Special API that provide consolidated access to records in consortium environment. Works only for central tenant.

| METHOD | URL | DESCRIPTION |
|:-------|:------------------------------|:------------------------------|
| GET | `/search/consortium/holdings` | Returns consolidated holdings |

## Additional Information

### Issue tracker
Expand Down
23 changes: 23 additions & 0 deletions descriptors/ModuleDescriptor-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,24 @@
}
]
},
{
"id": "consortium-search",
"version": "1.0",
"handlers": [
{
"methods": [
"GET"
],
"pathPattern": "/search/consortium/holdings",
"permissionsRequired": [
"consortium-search.holdings.collection.get"
],
"modulePermissions": [
"user-tenants.collection.get"
]
}
]
},
{
"id": "resource-ids-streaming",
"version": "0.3",
Expand Down Expand Up @@ -589,6 +607,11 @@
"permissionName": "browse.config.item.put",
"displayName": "Browse - updates configuration entry for browse type",
"description": "Updates configuration entry for browse type"
},
{
"permissionName": "consortium-search.holdings.collection.get",
"displayName": "Consortium Search - fetch holdings records",
"description": "Returns holdings records in consortium"
}
],
"launchDescriptor": {
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/org/folio/search/configuration/WebConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.folio.search.domain.dto.BrowseType;
import org.folio.search.domain.dto.CallNumberType;
import org.folio.search.domain.dto.RecordType;
import org.folio.search.domain.dto.SortOrder;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.format.FormatterRegistry;
Expand All @@ -18,6 +19,7 @@ public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToCallNumberTypeEnumConverter());
registry.addConverter(new StringToBrowseTypeConverter());
registry.addConverter(new StringToBrowseOptionTypeConverter());
registry.addConverter(new StringToSortOrderTypeConverter());
}

private static final class StringToRecordTypeEnumConverter implements Converter<String, RecordType> {
Expand Down Expand Up @@ -47,4 +49,11 @@ public BrowseOptionType convert(String source) {
return BrowseOptionType.fromValue(source.toLowerCase());
}
}

private static final class StringToSortOrderTypeConverter implements Converter<String, SortOrder> {
@Override
public SortOrder convert(String source) {
return SortOrder.fromValue(source.toLowerCase());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.folio.search.controller;

import lombok.RequiredArgsConstructor;
import org.folio.search.domain.dto.ConsortiumHoldingCollection;
import org.folio.search.domain.dto.SortOrder;
import org.folio.search.exception.RequestValidationException;
import org.folio.search.model.service.ConsortiumSearchContext;
import org.folio.search.model.types.ResourceType;
import org.folio.search.rest.resource.SearchConsortiumApi;
import org.folio.search.service.consortium.ConsortiumInstanceService;
import org.folio.search.service.consortium.ConsortiumTenantService;
import org.folio.spring.integration.XOkapiHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Validated
@RestController
@RequiredArgsConstructor
@RequestMapping("/")
public class SearchConsortiumController implements SearchConsortiumApi {

static final String REQUEST_NOT_ALLOWED_MSG =
"The request allowed only for central tenant of consortium environment";

private final ConsortiumTenantService consortiumTenantService;
private final ConsortiumInstanceService instanceService;

@Override
public ResponseEntity<ConsortiumHoldingCollection> getConsortiumHoldings(String tenantHeader, String instanceId,
String tenantId, Integer limit,
Integer offset, String sortBy,
SortOrder sortOrder) {
checkAllowance(tenantHeader);
var context = ConsortiumSearchContext.builderFor(ResourceType.HOLDINGS)
.filter("instanceId", instanceId)
.filter("tenantId", tenantId)
.limit(limit)
.offset(offset)
.sortBy(sortBy)
.sortOrder(sortOrder)
.build();
return ResponseEntity.ok(instanceService.fetchHoldings(context));
}

private void checkAllowance(String tenantHeader) {
var centralTenant = consortiumTenantService.getCentralTenant(tenantHeader);
if (centralTenant.isEmpty() || !centralTenant.get().equals(tenantHeader)) {
throw new RequestValidationException(REQUEST_NOT_ALLOWED_MSG, XOkapiHeaders.TENANT, tenantHeader);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package org.folio.search.model.service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import org.folio.search.domain.dto.SortOrder;
import org.folio.search.exception.RequestValidationException;
import org.folio.search.model.Pair;
import org.folio.search.model.types.ResourceType;

@Getter
public class ConsortiumSearchContext {

static final String SORT_NOT_ALLOWED_MSG = "Not allowed sort field for %s";
static final String FILTER_REQUIRED_MSG = "At least one filter criteria required";

private static final Map<ResourceType, List<String>> ALLOWED_SORT_FIELDS = Map.of(
ResourceType.HOLDINGS, List.of("id", "hrid", "tenantId", "instanceId",
"callNumberPrefix", "callNumber", "copyNumber", "permanentLocationId")
);

private static final Map<ResourceType, String> DEFAULT_SORT_FIELD = Map.of(
ResourceType.HOLDINGS, "id"
);

private final ResourceType resourceType;
private final List<Pair<String, String>> filters;
private final Integer limit;
private final Integer offset;
private final String sortBy;
private final SortOrder sortOrder;

ConsortiumSearchContext(ResourceType resourceType, List<Pair<String, String>> filters, Integer limit, Integer offset,
String sortBy, SortOrder sortOrder) {
this.resourceType = resourceType;
this.filters = filters;
if (sortBy != null && !ALLOWED_SORT_FIELDS.get(resourceType).contains(sortBy)) {
throw new RequestValidationException(SORT_NOT_ALLOWED_MSG.formatted(resourceType.getValue()), "sortBy", sortBy);
}
if (sortBy != null && (filters.isEmpty())) {
throw new RequestValidationException(FILTER_REQUIRED_MSG, null, null);
}
this.limit = limit;
this.offset = offset;
this.sortBy = sortBy;
this.sortOrder = sortOrder;
}

public static ConsortiumSearchContextBuilder builderFor(ResourceType resourceType) {
return new ConsortiumSearchContextBuilder(resourceType);
}

public static class ConsortiumSearchContextBuilder {
private final ResourceType resourceType;
private List<Pair<String, String>> filters = new ArrayList<>();
private Integer limit;
private Integer offset;
private String sortBy;
private SortOrder sortOrder;

ConsortiumSearchContextBuilder(ResourceType resourceType) {
this.resourceType = resourceType;
}

public ConsortiumSearchContextBuilder filter(String name, String value) {
if (StringUtils.isNotBlank(name) && StringUtils.isNotBlank(value)) {
this.filters.add(Pair.pair(name, value));
}
return this;
}

public ConsortiumSearchContextBuilder limit(Integer limit) {
this.limit = limit;
return this;
}

public ConsortiumSearchContextBuilder offset(Integer offset) {
this.offset = offset;
return this;
}

public ConsortiumSearchContextBuilder sortBy(String sortBy) {
this.sortBy = sortBy;
return this;
}

public ConsortiumSearchContextBuilder sortOrder(SortOrder sortOrder) {
this.sortOrder = sortOrder;
return this;
}

public ConsortiumSearchContext build() {
return new ConsortiumSearchContext(this.resourceType, this.filters, this.limit, this.offset,
this.sortBy, this.sortOrder);
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
public enum ResourceType {

INSTANCE("instance"),
HOLDINGS("holdings"),
AUTHORITY("authority"),
CLASSIFICATION_TYPE("classification-type");

private final String value;

ResourceType(String value) {

this.value = value;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.folio.search.service.consortium;

import static org.folio.search.service.consortium.ConsortiumSearchQueryBuilder.CONSORTIUM_TABLES;
import static org.folio.search.utils.JdbcUtils.getFullTableName;
import static org.folio.search.utils.JdbcUtils.getParamPlaceholder;

Expand All @@ -13,6 +14,8 @@
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.folio.search.domain.dto.ConsortiumHolding;
import org.folio.search.model.types.ResourceType;
import org.folio.spring.FolioExecutionContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
Expand All @@ -22,7 +25,6 @@
@RequiredArgsConstructor
public class ConsortiumInstanceRepository {

static final String CONSORTIUM_INSTANCE_TABLE_NAME = "consortium_instance";
private static final String SELECT_BY_ID_SQL = "SELECT * FROM %s WHERE instance_id IN (%s)";
private static final String DELETE_BY_TENANT_AND_ID_SQL = "DELETE FROM %s WHERE tenant_id = ? AND instance_id = ?;";
private static final String UPSERT_SQL = """
Expand Down Expand Up @@ -74,12 +76,28 @@ public void delete(Set<ConsortiumInstanceId> instanceIds) {
);
}

public List<ConsortiumHolding> fetchHoldings(ConsortiumSearchQueryBuilder searchQueryBuilder) {
return jdbcTemplate.query(searchQueryBuilder.buildSelectQuery(context),
(rs, rowNum) -> new ConsortiumHolding()
.id(rs.getString("id"))
.hrid(rs.getString("hrid"))
.tenantId(rs.getString("tenantId"))
.instanceId(rs.getString("instanceId"))
.callNumberPrefix(rs.getString("callNumberPrefix"))
.callNumber(rs.getString("callNumber"))
.copyNumber(rs.getString("copyNumber"))
.permanentLocationId(rs.getString("permanentLocationId"))
.discoverySuppress(rs.getBoolean("discoverySuppress")),
searchQueryBuilder.getQueryArguments()
);
}

private ConsortiumInstance toConsortiumInstance(ResultSet rs) throws SQLException {
var id = new ConsortiumInstanceId(rs.getString(TENANT_ID_COLUMN), rs.getString(INSTANCE_ID_COLUMN));
return new ConsortiumInstance(id, rs.getString(JSON_COLUMN));
}

private String getTableName() {
return getFullTableName(context, CONSORTIUM_INSTANCE_TABLE_NAME);
return getFullTableName(context, CONSORTIUM_TABLES.get(ResourceType.INSTANCE));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@
import lombok.extern.log4j.Log4j2;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.ListUtils;
import org.folio.search.domain.dto.ConsortiumHolding;
import org.folio.search.domain.dto.ConsortiumHoldingCollection;
import org.folio.search.domain.dto.ResourceEvent;
import org.folio.search.domain.dto.ResourceEventType;
import org.folio.search.model.event.ConsortiumInstanceEvent;
import org.folio.search.model.service.ConsortiumSearchContext;
import org.folio.search.utils.JsonConverter;
import org.folio.search.utils.SearchConverterUtils;
import org.folio.spring.FolioExecutionContext;
Expand Down Expand Up @@ -147,6 +150,11 @@ public List<ResourceEvent> fetchInstances(Iterable<String> instanceIds) {
return resourceEvents;
}

public ConsortiumHoldingCollection fetchHoldings(ConsortiumSearchContext context) {
List<ConsortiumHolding> holdingList = repository.fetchHoldings(new ConsortiumSearchQueryBuilder(context));
return new ConsortiumHoldingCollection().holdings(holdingList).totalRecords(holdingList.size());
}

@SuppressWarnings("unchecked")
private void addListItems(List<Map<String, Object>> mergedList, Map<String, Object> instanceMap, String key) {
var items = instanceMap.get(key);
Expand Down
Loading

0 comments on commit 36a0aee

Please sign in to comment.