diff --git a/src/main/java/org/folio/search/cql/CqlSearchQueryConverter.java b/src/main/java/org/folio/search/cql/CqlSearchQueryConverter.java index bc33611c3..2ee3a06f6 100644 --- a/src/main/java/org/folio/search/cql/CqlSearchQueryConverter.java +++ b/src/main/java/org/folio/search/cql/CqlSearchQueryConverter.java @@ -1,5 +1,6 @@ package org.folio.search.cql; +import static org.folio.search.service.browse.CallNumberBrowseService.CALL_NUMBER_FIELD; import static org.folio.search.utils.SearchQueryUtils.isBoolQuery; import static org.folio.search.utils.SearchQueryUtils.isDisjunctionFilterQuery; import static org.folio.search.utils.SearchQueryUtils.isFilterQuery; @@ -108,6 +109,9 @@ private QueryBuilder convertToQuery(CQLNode node, String resource) { cqlNode = cqlSortNode.getSubtree(); } if (cqlNode instanceof CQLTermNode cqlTermNode) { + if (CALL_NUMBER_FIELD.equals(cqlTermNode.getIndex())) { + cqlTermQueryConverter.getCallNumberQuery(cqlTermNode, resource, CALL_NUMBER_FIELD); + } return cqlTermQueryConverter.getQuery(cqlTermNode, resource); } if (cqlNode instanceof CQLBooleanNode cqlBooleanNode) { diff --git a/src/main/java/org/folio/search/cql/CqlTermQueryConverter.java b/src/main/java/org/folio/search/cql/CqlTermQueryConverter.java index ce10c62cf..1f2cc6c3c 100644 --- a/src/main/java/org/folio/search/cql/CqlTermQueryConverter.java +++ b/src/main/java/org/folio/search/cql/CqlTermQueryConverter.java @@ -4,6 +4,7 @@ import static java.util.stream.Collectors.joining; import static org.apache.commons.lang3.StringUtils.lowerCase; import static org.folio.search.utils.SearchUtils.ASTERISKS_SIGN; +import static org.opensearch.index.query.QueryBuilders.boolQuery; import static org.opensearch.index.query.QueryBuilders.matchAllQuery; import java.time.format.DateTimeFormatter; @@ -21,6 +22,7 @@ import org.folio.search.model.metadata.PlainFieldDescription; import org.folio.search.service.metadata.LocalSearchFieldProvider; import org.folio.search.service.metadata.SearchFieldProvider; +import org.opensearch.index.query.BoolQueryBuilder; import org.opensearch.index.query.QueryBuilder; import org.springframework.stereotype.Component; import org.springframework.util.Assert; @@ -81,20 +83,13 @@ public QueryBuilder getQuery(CQLTermNode termNode, String resource) { var fieldName = fieldsList.size() == 1 ? fieldsList.get(0) : fieldIndex; var optionalPlainFieldByPath = searchFieldProvider.getPlainFieldByPath(resource, fieldName); var searchTerm = getSearchTerm(termNode.getTerm(), optionalPlainFieldByPath); - var comparator = isWildcardQuery(searchTerm) ? WILDCARD_OPERATOR : lowerCase(termNode.getRelation().getBase()); - - var termQueryBuilder = termQueryBuilders.get(comparator); - if (termQueryBuilder == null) { - throw new UnsupportedOperationException(String.format( - "Failed to parse CQL query. Comparator '%s' is not supported.", comparator)); - } + var termQueryBuilder = getTermQueryBuilder(termNode, searchTerm); if (CollectionUtils.isNotEmpty(fieldsList)) { return termQueryBuilder.getQuery(searchTerm, resource, fieldsList.toArray(String[]::new)); } - var plainFieldByPath = optionalPlainFieldByPath.orElseThrow(() -> new RequestValidationException( - "Invalid search field provided in the CQL query", "field", fieldName)); + var plainFieldByPath = getPlainFieldByPath(optionalPlainFieldByPath, fieldName); var index = plainFieldByPath.getIndex(); validateIndexFormat(index, termNode); @@ -107,6 +102,46 @@ public QueryBuilder getQuery(CQLTermNode termNode, String resource) { : termQueryBuilder.getTermLevelQuery(searchTerm, fieldName, resource, index); } + /** + * Provides Elasticsearch {@link BoolQueryBuilder} object for the given termNode and resource name. + * + * @param termNode - CQL term node as {@link CQLTermNode} object + * @param resource - resource name as {@link String} value + * @param fieldName - resource name as {@link String} fieldName value + * @return created Elasticsearch {@link BoolQueryBuilder} object + */ + public BoolQueryBuilder getCallNumberQuery(CQLTermNode termNode, String resource, String fieldName) { + + var optionalPlainFieldByPath = searchFieldProvider.getPlainFieldByPath(resource, fieldName); + List searchTerms = (List) getSearchTerm(termNode.getTerm(), optionalPlainFieldByPath); + + var plainFieldByPath = getPlainFieldByPath(optionalPlainFieldByPath, fieldName); + var index = plainFieldByPath.getIndex(); + validateIndexFormat(index, termNode); + + var boolQuery = boolQuery(); + var conditions = boolQuery.should(); + + for (String searchTerm : searchTerms) { + var termQueryBuilder = getTermQueryBuilder(termNode, searchTerm); + + conditions.add(termQueryBuilder.getTermLevelQuery(searchTerm, fieldName, resource, index)); + } + return boolQuery; + } + + + private TermQueryBuilder getTermQueryBuilder(CQLTermNode termNode, Object searchTerm) { + var comparator = isWildcardQuery(searchTerm) ? WILDCARD_OPERATOR : lowerCase(termNode.getRelation().getBase()); + var termQueryBuilder = termQueryBuilders.get(comparator); + + if (termQueryBuilder == null) { + throw new UnsupportedOperationException(String.format( + "Failed to parse CQL query. Comparator '%s' is not supported.", comparator)); + } + return termQueryBuilder; + } + private Object getSearchTerm(String term, Optional plainFieldDescription) { return plainFieldDescription .map(PlainFieldDescription::getSearchTermProcessor) @@ -115,6 +150,12 @@ private Object getSearchTerm(String term, Optional plainF .orElse(term); } + private PlainFieldDescription getPlainFieldByPath(Optional plainFieldDescription, + String fieldName) { + return plainFieldDescription.orElseThrow(() -> new RequestValidationException( + "Invalid search field provided in the CQL query", "field", fieldName)); + } + private static boolean isWildcardQuery(Object query) { return query instanceof String string && string.contains(ASTERISKS_SIGN); } diff --git a/src/main/java/org/folio/search/cql/EffectiveShelvingOrderTermProcessor.java b/src/main/java/org/folio/search/cql/EffectiveShelvingOrderTermProcessor.java index 148878670..9a3c05b37 100644 --- a/src/main/java/org/folio/search/cql/EffectiveShelvingOrderTermProcessor.java +++ b/src/main/java/org/folio/search/cql/EffectiveShelvingOrderTermProcessor.java @@ -2,6 +2,8 @@ import static org.folio.search.utils.CallNumberUtils.normalizeEffectiveShelvingOrder; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import org.marc4j.callnum.CallNumber; import org.marc4j.callnum.DeweyCallNumber; @@ -13,18 +15,28 @@ public class EffectiveShelvingOrderTermProcessor implements SearchTermProcessor { @Override - public String getSearchTerm(String inputTerm) { - return getValidShelfKey(new SuDocCallNumber(inputTerm)) - .or(() -> getValidShelfKey(new NlmCallNumber(inputTerm))) - .or(() -> getValidShelfKey(new LCCallNumber(inputTerm))) - .or(() -> getValidShelfKey(new DeweyCallNumber(inputTerm))) - .orElse(normalizeEffectiveShelvingOrder(inputTerm)) - .trim(); + public List getSearchTerm(String inputTerm) { + + var searchTerms = new ArrayList(); + + getValidShelfKey(searchTerms, new SuDocCallNumber(inputTerm)); + getValidShelfKey(searchTerms, new NlmCallNumber(inputTerm)); + getValidShelfKey(searchTerms, new LCCallNumber(inputTerm)); + getValidShelfKey(searchTerms, new DeweyCallNumber(inputTerm)); + + if (searchTerms.isEmpty()) { + searchTerms.add(normalizeEffectiveShelvingOrder(inputTerm).trim()); + } + + return searchTerms; } - private static Optional getValidShelfKey(CallNumber value) { - return Optional.of(value) + private static void getValidShelfKey(List searchTerms, CallNumber value) { + var term = Optional.of(value) .filter(CallNumber::isValid) - .map(CallNumber::getShelfKey); + .map(CallNumber::getShelfKey) + .map(String::trim); + + term.ifPresent(searchTerms::add); } } diff --git a/src/main/java/org/folio/search/model/service/BrowseContext.java b/src/main/java/org/folio/search/model/service/BrowseContext.java index 19c80ca24..6feb284a3 100644 --- a/src/main/java/org/folio/search/model/service/BrowseContext.java +++ b/src/main/java/org/folio/search/model/service/BrowseContext.java @@ -1,5 +1,6 @@ package org.folio.search.model.service; +import java.util.Arrays; import java.util.Collections; import java.util.List; import lombok.Builder; @@ -68,4 +69,21 @@ public boolean isAnchorIncluded() { public int getLimit(boolean isForward) { return isForward ? this.succeedingLimit : this.precedingLimit; } + + /** + * Checks if multiple anchors are present. + * + * @return {@code true} if multiple anchors are present, {@code false} - otherwise + */ + public boolean isMultiAnchor() { + return Arrays.stream(anchor.split(",")).count() > 1; + } + /** + * Checks if multiple anchors are present. + * + * @return {@code true} if multiple anchors are present, {@code false} - otherwise + */ + public List getAnchorsList() { + return Arrays.asList(anchor.split(",")); + } } diff --git a/src/main/java/org/folio/search/service/browse/BrowseContextProvider.java b/src/main/java/org/folio/search/service/browse/BrowseContextProvider.java index 77afc3fa7..e3eb44865 100644 --- a/src/main/java/org/folio/search/service/browse/BrowseContextProvider.java +++ b/src/main/java/org/folio/search/service/browse/BrowseContextProvider.java @@ -160,7 +160,18 @@ private static boolean isBoolQueryWithFilters(BoolQueryBuilder boolQuery) { } private static String getAnchor(RangeQueryBuilder rangeQuery) { - return rangeQuery.from() != null ? (String) rangeQuery.from() : (String) rangeQuery.to(); + var anchor = rangeQuery.from() != null ? rangeQuery.from() : rangeQuery.to(); + if (anchor instanceof List) { + return getMultipleAnchors((List) anchor); + } + return (String) anchor; + } + + private static String getMultipleAnchors(List anchors) { + if (anchors.size() == 1) { + return anchors.get(0); + } + return String.join(",", anchors); } private static String validateAndGetAnchorForBrowsingAround(BrowseRequest request, List shouldClauses) { diff --git a/src/main/java/org/folio/search/service/browse/CallNumberBrowseService.java b/src/main/java/org/folio/search/service/browse/CallNumberBrowseService.java index 1a38897ee..63346d413 100644 --- a/src/main/java/org/folio/search/service/browse/CallNumberBrowseService.java +++ b/src/main/java/org/folio/search/service/browse/CallNumberBrowseService.java @@ -4,6 +4,7 @@ import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.apache.commons.collections.CollectionUtils.isEmpty; +import static org.apache.commons.collections.CollectionUtils.isNotEmpty; import static org.folio.search.utils.CollectionUtils.mergeSafelyToList; import java.util.ArrayList; @@ -17,6 +18,8 @@ import org.folio.search.model.service.BrowseContext; import org.folio.search.model.service.BrowseRequest; import org.folio.search.repository.SearchRepository; +import org.opensearch.action.search.MultiSearchResponse; +import org.opensearch.action.search.SearchResponse; import org.opensearch.search.builder.SearchSourceBuilder; import org.springframework.stereotype.Service; @@ -25,6 +28,7 @@ @RequiredArgsConstructor public class CallNumberBrowseService extends AbstractBrowseService { + public static final String CALL_NUMBER_FIELD = "callNumber"; private static final int ADDITIONAL_REQUEST_SIZE = 100; private static final int ADDITIONAL_REQUEST_SIZE_MAX = 500; private final SearchRepository searchRepository; @@ -37,8 +41,21 @@ protected BrowseResult browseInOneDirection(BrowseRequest log.debug("browseInOneDirection:: by: [request: {}]", request); var isBrowsingForward = context.isBrowsingForward(); - var searchSource = callNumberBrowseQueryProvider.get(request, context, isBrowsingForward); - var searchResponse = searchRepository.search(request, searchSource); + SearchResponse searchResponse = null; + + if (context.isMultiAnchor()) { + var anchors = context.getAnchorsList(); + for (String anchor : anchors){ + context = buildBrowseContext(context, anchor); + var searchSource = callNumberBrowseQueryProvider.get(request, context, isBrowsingForward); + searchResponse = searchRepository.search(request, searchSource); + if (isAnchorPresent(searchResponse, context)) + break; + } + } else { + var searchSource = callNumberBrowseQueryProvider.get(request, context, isBrowsingForward); + searchResponse = searchRepository.search(request, searchSource); + } var browseResult = callNumberBrowseResultConverter.convert(searchResponse, context, isBrowsingForward); var records = browseResult.getRecords(); return new BrowseResult() @@ -51,12 +68,23 @@ protected BrowseResult browseInOneDirection(BrowseRequest @Override protected BrowseResult browseAround(BrowseRequest request, BrowseContext context) { log.debug("browseAround:: by: [request: {}]", request); + MultiSearchResponse.Item[] responses = {}; + var precedingQuery = callNumberBrowseQueryProvider.get(request, context,false); + + if (context.isMultiAnchor()) { + var anchors = context.getAnchorsList(); + for (String anchor : anchors){ + context = buildBrowseContext(context, anchor); + precedingQuery = callNumberBrowseQueryProvider.get(request, context, false); + + responses = getBrowseAround(request, context, precedingQuery); + if (isAnchorPresent(responses[0].getResponse(), context)) + break; + } + } else { + responses = getBrowseAround(request, context, precedingQuery); + } - var precedingQuery = callNumberBrowseQueryProvider.get(request, context, false); - var succeedingQuery = callNumberBrowseQueryProvider.get(request, context, true); - var multiSearchResponse = searchRepository.msearch(request, List.of(precedingQuery, succeedingQuery)); - - var responses = multiSearchResponse.getResponses(); var precedingResult = callNumberBrowseResultConverter.convert(responses[0].getResponse(), context, false); var succeedingResult = callNumberBrowseResultConverter.convert(responses[1].getResponse(), context, true); var backwardSucceedingResult = callNumberBrowseResultConverter.convert(responses[1].getResponse(), context, false); @@ -95,6 +123,14 @@ protected String getValueForBrowsing(CallNumberBrowseItem browseItem) { return browseItem.getShelfKey(); } + private MultiSearchResponse.Item[] getBrowseAround(BrowseRequest request, BrowseContext context, + SearchSourceBuilder precedingQuery) { + var succeedingQuery = callNumberBrowseQueryProvider.get(request, context, true); + var multiSearchResponse = searchRepository.msearch(request, List.of(precedingQuery, succeedingQuery)); + + return multiSearchResponse.getResponses(); + } + private List additionalPrecedingRequests(BrowseRequest request, BrowseContext context, SearchSourceBuilder precedingQuery) { @@ -122,6 +158,24 @@ private List additionalPrecedingRequests(BrowseRequest req return additionalPrecedingRecords; } + private boolean isAnchorPresent(SearchResponse searchResponse, BrowseContext context) { + var items = callNumberBrowseResultConverter. + convert(searchResponse, context, true).getRecords(); + + return isNotEmpty(items) && StringUtils.equals(items.get(0).getShelfKey(), context.getAnchor()); + } + + private BrowseContext buildBrowseContext(BrowseContext context, String anchor) { + return BrowseContext.builder() + .precedingQuery(context.getPrecedingQuery()) + .succeedingQuery(context.getSucceedingQuery()) + .filters(context.getFilters()) + .anchor(anchor) + .precedingLimit(context.getPrecedingLimit()) + .succeedingLimit(context.getSucceedingLimit()) + .build(); + } + private static void highlightMatchingCallNumber(BrowseContext ctx, String callNumber, BrowseResult result) { diff --git a/src/test/java/org/folio/search/controller/BrowseAroundCallNumberIT.java b/src/test/java/org/folio/search/controller/BrowseAroundCallNumberIT.java index c28f5963a..03ce90464 100644 --- a/src/test/java/org/folio/search/controller/BrowseAroundCallNumberIT.java +++ b/src/test/java/org/folio/search/controller/BrowseAroundCallNumberIT.java @@ -137,7 +137,7 @@ private static Instance instance(List data) { .id(randomId()) .discoverySuppress(false) .effectiveCallNumberComponents(new ItemEffectiveCallNumberComponents().callNumber(callNumber)) - .effectiveShelvingOrder(getShelfKeyFromCallNumber(callNumber))) + .effectiveShelvingOrder(getShelfKeyFromCallNumber(callNumber).get(0))) .toList(); return new Instance() diff --git a/src/test/java/org/folio/search/controller/BrowseCallNumberIT.java b/src/test/java/org/folio/search/controller/BrowseCallNumberIT.java index 692ce2cba..c0c3685a3 100644 --- a/src/test/java/org/folio/search/controller/BrowseCallNumberIT.java +++ b/src/test/java/org/folio/search/controller/BrowseCallNumberIT.java @@ -101,6 +101,24 @@ void browseByCallNumber_browsingVeryFirstCallNumberWithNoException() { ))); } + @Test + void browseByCallNumber_browsingAroundWhenMultipleAnchors() { + var request = get(instanceCallNumberBrowsePath()) + .param("query", prepareQuery("callNumber < {value} or callNumber >= {value}", "\"J29.2\"")) + .param("limit", "5") + .param("expandAll", "true") + .param("precedingRecordsCount", "4"); + var actual = parseResponse(doGet(request), CallNumberBrowseResult.class); + assertThat(actual).isEqualTo(new CallNumberBrowseResult() + .totalRecords(41).prev("GA 216 D64 541548A").next("J 229 12").items(List.of( + cnBrowseItem(instance("instance #39"), "GA 16 D64 41548A"), + cnBrowseItem(instance("instance #30"), "GA 16 G32 41557 V1"), + cnBrowseItem(instance("instance #30"), "GA 16 G32 41557 V2"), + cnBrowseItem(instance("instance #30"), "GA 16 G32 41557 V3"), + cnBrowseItem(instance("instance #47"), "J29.2", true) + ))); + } + @Test void browseByCallNumber_browsingAroundWithoutHighlightMatch() { var request = get(instanceCallNumberBrowsePath()) @@ -373,7 +391,7 @@ private static Instance instance(List data) { .id(randomId()) .discoverySuppress(false) .effectiveCallNumberComponents(new ItemEffectiveCallNumberComponents().callNumber(callNumber)) - .effectiveShelvingOrder(getShelfKeyFromCallNumber(callNumber))) + .effectiveShelvingOrder(getShelfKeyFromCallNumber(callNumber).get(0))) .toList(); return new Instance() @@ -439,6 +457,7 @@ private static List> callNumberBrowseInstanceData() { List.of("instance #43", List.of("FA 42010 3546 256")), List.of("instance #44", List.of("CE 16 B6713 X 41993")), List.of("instance #45", List.of("CE 16 B6724 41993")), + List.of("instance #47", List.of("J29.2")), List.of("instance #46", List.of("F PR1866.S63 V.1 C.1")) ); } diff --git a/src/test/java/org/folio/search/controller/BrowseCallNumberOtherIT.java b/src/test/java/org/folio/search/controller/BrowseCallNumberOtherIT.java index 3937e43f4..5a044ec95 100644 --- a/src/test/java/org/folio/search/controller/BrowseCallNumberOtherIT.java +++ b/src/test/java/org/folio/search/controller/BrowseCallNumberOtherIT.java @@ -109,7 +109,7 @@ private static Instance instance(List data) { .id(randomId()) .discoverySuppress(false) .effectiveCallNumberComponents(new ItemEffectiveCallNumberComponents().callNumber(callNumber)) - .effectiveShelvingOrder(getShelfKeyFromCallNumber(callNumber))) + .effectiveShelvingOrder(getShelfKeyFromCallNumber(callNumber).get(0))) .toList(); return new Instance() diff --git a/src/test/java/org/folio/search/controller/BrowseTypedCallNumberIT.java b/src/test/java/org/folio/search/controller/BrowseTypedCallNumberIT.java index f0c32cc60..8ff096633 100644 --- a/src/test/java/org/folio/search/controller/BrowseTypedCallNumberIT.java +++ b/src/test/java/org/folio/search/controller/BrowseTypedCallNumberIT.java @@ -175,7 +175,7 @@ private static Instance instance(List data) { .discoverySuppress(false) .effectiveCallNumberComponents(new ItemEffectiveCallNumberComponents() .callNumber(callNumber).typeId(data.get(2).toString())) - .effectiveShelvingOrder(getShelfKeyFromCallNumber(callNumber))) + .effectiveShelvingOrder(getShelfKeyFromCallNumber(callNumber).get(0))) .toList(); return new Instance() diff --git a/src/test/java/org/folio/search/cql/EffectiveShelvingOrderTermProcessorTest.java b/src/test/java/org/folio/search/cql/EffectiveShelvingOrderTermProcessorTest.java index eec79e419..16bf81b82 100644 --- a/src/test/java/org/folio/search/cql/EffectiveShelvingOrderTermProcessorTest.java +++ b/src/test/java/org/folio/search/cql/EffectiveShelvingOrderTermProcessorTest.java @@ -19,6 +19,7 @@ class EffectiveShelvingOrderTermProcessorTest { @ParameterizedTest @ValueSource(strings = { + "T22.19:M54/990", "A 11", "DA 3880", "A 210", "ZA 3123", "DA 3880 O6", "DA 3880 O6 J72", "E 211 N52 VOL 14", "F 43733 L370 41992", "E 12.11 I12 288 D", "CE 16 B6713 X 41993", diff --git a/src/test/java/org/folio/search/service/browse/CallNumberBrowseResultConverterTest.java b/src/test/java/org/folio/search/service/browse/CallNumberBrowseResultConverterTest.java index f3955471f..d9095de85 100644 --- a/src/test/java/org/folio/search/service/browse/CallNumberBrowseResultConverterTest.java +++ b/src/test/java/org/folio/search/service/browse/CallNumberBrowseResultConverterTest.java @@ -255,7 +255,7 @@ private static List browseItems(String... shelfKeys) { private static Instance instance(String... callNumbers) { var items = stream(callNumbers) .map(callNumber -> new Item() - .effectiveShelvingOrder(getShelfKeyFromCallNumber(callNumber)) + .effectiveShelvingOrder(getShelfKeyFromCallNumber(callNumber).get(0)) .effectiveCallNumberComponents(new ItemEffectiveCallNumberComponents().callNumber(callNumber))) .toList(); return new Instance().items(items); diff --git a/src/test/java/org/folio/search/service/browse/CallNumberBrowseServiceTest.java b/src/test/java/org/folio/search/service/browse/CallNumberBrowseServiceTest.java index 1c9d38fd2..8a5905e88 100644 --- a/src/test/java/org/folio/search/service/browse/CallNumberBrowseServiceTest.java +++ b/src/test/java/org/folio/search/service/browse/CallNumberBrowseServiceTest.java @@ -327,7 +327,7 @@ private static MultiSearchResponse msearchResponse(SearchResponse... responses) private static Instance instance(String... callNumbers) { var items = stream(callNumbers) .map(callNumber -> new Item() - .effectiveShelvingOrder(getShelfKeyFromCallNumber(callNumber)) + .effectiveShelvingOrder(getShelfKeyFromCallNumber(callNumber).get(0)) .effectiveCallNumberComponents(new ItemEffectiveCallNumberComponents().callNumber(callNumber))) .toList(); return new Instance().items(items); @@ -357,7 +357,7 @@ private static BrowseRequest request(String query, boolean highlightMatch) { private static CallNumberBrowseItem browseItem(String callNumber) { return new CallNumberBrowseItem() .fullCallNumber(callNumber) - .shelfKey(getShelfKeyFromCallNumber(callNumber)) + .shelfKey(getShelfKeyFromCallNumber(callNumber).get(0)) .instance(instance(callNumber)) .totalRecords(1); } diff --git a/src/test/java/org/folio/search/utils/TestUtils.java b/src/test/java/org/folio/search/utils/TestUtils.java index 39935c6bd..79dd31b62 100644 --- a/src/test/java/org/folio/search/utils/TestUtils.java +++ b/src/test/java/org/folio/search/utils/TestUtils.java @@ -170,7 +170,7 @@ public static CallNumberBrowseResult cnBrowseResult(int total, List getShelfKeyFromCallNumber(String callNumber) { return SHELVING_ORDER_TERM_PROCESSOR.getSearchTerm(callNumber); }