/** * Copyright 2008 The University of North Carolina at Chapel Hill * * 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 edu.unc.lib.dl.ui.service; import static edu.unc.lib.dl.util.ContentModelHelper.CDRProperty.invalidTerm; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.response.FacetField; import org.apache.solr.client.solrj.response.FacetField.Count; import org.apache.solr.client.solrj.response.QueryResponse; import org.apache.solr.common.SolrDocument; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import edu.unc.lib.dl.acl.exception.AccessRestrictionException; import edu.unc.lib.dl.acl.util.AccessGroupConstants; import edu.unc.lib.dl.acl.util.AccessGroupSet; import edu.unc.lib.dl.acl.util.GroupsThreadStore; import edu.unc.lib.dl.acl.util.UserRole; import edu.unc.lib.dl.fedora.PID; import edu.unc.lib.dl.search.solr.model.AbstractHierarchicalFacet; import edu.unc.lib.dl.search.solr.model.BriefObjectMetadata; import edu.unc.lib.dl.search.solr.model.BriefObjectMetadataBean; import edu.unc.lib.dl.search.solr.model.CaseInsensitiveFacet; import edu.unc.lib.dl.search.solr.model.CutoffFacet; import edu.unc.lib.dl.search.solr.model.FacetFieldObject; import edu.unc.lib.dl.search.solr.model.GenericFacet; import edu.unc.lib.dl.search.solr.model.HierarchicalBrowseRequest; import edu.unc.lib.dl.search.solr.model.HierarchicalBrowseResultResponse; import edu.unc.lib.dl.search.solr.model.HierarchicalBrowseResultResponse.ResultNode; import edu.unc.lib.dl.search.solr.model.HierarchicalFacetNode; import edu.unc.lib.dl.search.solr.model.SearchRequest; import edu.unc.lib.dl.search.solr.model.SearchResultResponse; import edu.unc.lib.dl.search.solr.model.SearchState; import edu.unc.lib.dl.search.solr.model.SimpleIdRequest; import edu.unc.lib.dl.search.solr.service.ObjectPathFactory; import edu.unc.lib.dl.search.solr.service.SearchStateFactory; import edu.unc.lib.dl.search.solr.service.SolrSearchService; import edu.unc.lib.dl.search.solr.util.SearchFieldKeys; import edu.unc.lib.dl.search.solr.util.SolrSettings; import edu.unc.lib.dl.ui.util.AccessUtil; import edu.unc.lib.dl.util.ContentModelHelper; import edu.unc.lib.dl.util.ResourceType; /** * Solr query construction layer. Constructs search states specific to common tasks before passing them on to lower * level classes to retrieve the results. * * @author bbpennel */ public class SolrQueryLayerService extends SolrSearchService { private static final Logger LOG = LoggerFactory.getLogger(SolrQueryLayerService.class); protected SearchStateFactory searchStateFactory; protected PID collectionsPid; protected ObjectPathFactory pathFactory; private static int NEIGHBOR_SEEK_PAGE_SIZE = 500; /** * Returns a list of the most recently added items in the collection * * @param accessGroups * @return Result response, where items only contain title and id. */ public SearchResultResponse getNewlyAdded(AccessGroupSet accessGroups) { SearchRequest searchRequest = new SearchRequest(); searchRequest.setAccessGroups(accessGroups); SearchState searchState = searchStateFactory.createTitleListSearchState(); List<String> resourceTypes = new ArrayList<String>(); resourceTypes.add(searchSettings.resourceTypeCollection); searchState.setResourceTypes(resourceTypes); searchState.setRowsPerPage(searchSettings.defaultListResultsPerPage); searchState.setSortType("dateAdded"); searchRequest.setSearchState(searchState); return getSearchResults(searchRequest); } /** * Returns a list of collections * * @param accessGroups * @return */ public SearchResultResponse getCollectionList(AccessGroupSet accessGroups) { SearchRequest searchRequest = new SearchRequest(); searchRequest.setAccessGroups(accessGroups); SearchState searchState = searchStateFactory.createSearchState(); searchState.setResourceTypes(searchSettings.defaultCollectionResourceTypes); searchState.setRowsPerPage(50); searchState.setFacetsToRetrieve(null); ArrayList<String> resultFields = new ArrayList<String>(); resultFields.add(SearchFieldKeys.ANCESTOR_PATH.name()); resultFields.add(SearchFieldKeys.TITLE.name()); resultFields.add(SearchFieldKeys.ID.name()); searchState.setResultFields(resultFields); searchRequest.setSearchState(searchState); return getSearchResults(searchRequest); } public SearchResultResponse getDepartmentList(AccessGroupSet accessGroups, String pid) { SearchState searchState; Boolean has_pid = (pid != null) ? true : false; searchState = searchStateFactory.createFacetSearchState(SearchFieldKeys.DEPARTMENT.name(), "index", Integer.MAX_VALUE); SearchRequest searchRequest = new SearchRequest(searchState, accessGroups, true); searchRequest.setRootPid(pid); BriefObjectMetadata selectedContainer = null; if (has_pid) { selectedContainer = addSelectedContainer(searchRequest.getRootPid(), searchState, false); } SearchResultResponse results = getSearchResults(searchRequest); if (has_pid) { results.setSelectedContainer(selectedContainer); } if (results.getFacetFields() != null && results.getFacetFields().size() > 0) { FacetFieldObject deptField = results.getFacetFields().get(0); if (deptField != null) { CaseInsensitiveFacet.deduplicateCaseInsensitiveValues(deptField); } } return results; } /** * Retrieves the facet list for the search defined by searchState. The facet results optionally can ignore * hierarchical cutoffs. * * @param searchState * @param facetsToRetrieve * @param applyCutoffs * @return */ public SearchResultResponse getFacetList(SearchRequest searchRequest) { SearchState searchState = (SearchState) searchRequest.getSearchState().clone(); LOG.debug("Retrieving facet list"); BriefObjectMetadata selectedContainer = null; if (searchRequest.getRootPid() != null) { selectedContainer = addSelectedContainer(searchRequest.getRootPid(), searchState, searchRequest.isApplyCutoffs()); } else { CutoffFacet ancestorPath; if (!searchState.getFacets().containsKey(SearchFieldKeys.ANCESTOR_PATH.name())) { ancestorPath = new CutoffFacet(SearchFieldKeys.ANCESTOR_PATH.name(), "2,*"); ancestorPath.setFacetCutoff(3); searchState.getFacets().put(SearchFieldKeys.ANCESTOR_PATH.name(), ancestorPath); } else { ancestorPath = (CutoffFacet) searchState.getFacets().get(SearchFieldKeys.ANCESTOR_PATH.name()); if (ancestorPath.getFacetCutoff() == null) ancestorPath.setFacetCutoff(ancestorPath.getHighestTier() + 1); } if (!searchRequest.isApplyCutoffs()) { ancestorPath.setCutoff(null); } } // Turning off rollup because it is really slow searchState.setRollup(false); SearchRequest facetRequest = new SearchRequest(searchState, true); searchState.setRowsPerPage(0); searchState.setResourceTypes(null); SearchResultResponse resultResponse = getSearchResults(facetRequest); resultResponse.setSelectedContainer(selectedContainer); // If this facet list contains parent collections, then retrieve display names for them if (resultResponse.getFacetFields() != null && (searchState.getFacetsToRetrieve() == null || searchState.getFacetsToRetrieve().contains(SearchFieldKeys.PARENT_COLLECTION.name()))) { FacetFieldObject parentCollectionFacet = resultResponse.getFacetFields().get( SearchFieldKeys.PARENT_COLLECTION.name()); if (parentCollectionFacet != null) { for (GenericFacet pidFacet : parentCollectionFacet.getValues()) { String parentName = pathFactory.getName(pidFacet.getSearchValue()); if (parentName != null) { pidFacet.setFieldName(SearchFieldKeys.ANCESTOR_PATH.name()); pidFacet.setDisplayValue(parentName); } } } } return resultResponse; } /** * Retrieves metadata fields for the parent collection pids contained by the supplied facet object. * * @param parentCollectionFacet * Facet object containing parent collection ids to lookup * @param accessGroups * @return */ public List<BriefObjectMetadataBean> getParentCollectionValues(FacetFieldObject parentCollectionFacet) { if (parentCollectionFacet == null || parentCollectionFacet.getValues() == null || parentCollectionFacet.getValues().size() == 0) { return null; } QueryResponse queryResponse = null; SolrQuery solrQuery = new SolrQuery(); StringBuilder query = new StringBuilder(); boolean first = true; query.append('('); for (GenericFacet pidFacet : parentCollectionFacet.getValues()) { if (pidFacet.getSearchValue() != null && pidFacet.getSearchValue().length() > 0) { if (first) { first = false; } else { query.append(" OR "); } query.append(solrSettings.getFieldName(SearchFieldKeys.ID.name())).append(':') .append(SolrSettings.sanitize(pidFacet.getSearchValue())); } } query.append(')'); // If no pids were added to the query, then there's nothing to look up if (first) { return null; } try { // Add access restrictions to query addAccessRestrictions(query); } catch (AccessRestrictionException e) { // If the user doesn't have any access groups, they don't have access to anything, return null. LOG.error("No access groups", e); return null; } solrQuery.setQuery(query.toString()); solrQuery.setFacet(true); solrQuery.setFields(solrSettings.getFieldName(SearchFieldKeys.ID.name()), solrSettings.getFieldName(SearchFieldKeys.ANCESTOR_PATH.name()), solrSettings.getFieldName(SearchFieldKeys.TITLE.name())); solrQuery.setRows(parentCollectionFacet.getValues().size()); try { queryResponse = this.executeQuery(solrQuery); return queryResponse.getBeans(BriefObjectMetadataBean.class); } catch (SolrServerException e) { LOG.error("Failed to execute query " + solrQuery.toString(), e); } return null; } /** * Retrieves a list of the closest windowSize neighbors within the parent container of the specified object, * using the default sort order. The first windowSize / 2 - 1 neighbors are retrieved to each side * of the item, and trimmed so that there are always windowSize - 1 neighbors surrounding the item if possible. * * @param metadata * Record which the window pivots around. * @param windowSize * max number of items in the window. This includes the pivot, so odd numbers are recommended. * @param accessGroups * Access groups of the user making this request. * @return */ public List<BriefObjectMetadataBean> getNeighboringItems(BriefObjectMetadataBean metadata, int windowSize, AccessGroupSet accessGroups) { // Get the common access restriction clause (starts with "AND ...") StringBuilder accessRestrictionClause = new StringBuilder(); try { addAccessRestrictions(accessRestrictionClause, accessGroups); } catch (AccessRestrictionException e) { // If the user doesn't have any access groups, they don't have access to anything, return null. LOG.error("Attempted to get neighboring items without creditentials", e); return null; } // Restrict query to files/aggregates and objects within the same parent SolrQuery solrQuery = new SolrQuery(); solrQuery.setQuery("*:*" + accessRestrictionClause); solrQuery.setFacet(true); solrQuery.addFilterQuery(solrSettings.getFieldName(SearchFieldKeys.RESOURCE_TYPE.name()) + ":File " + solrSettings.getFieldName(SearchFieldKeys.RESOURCE_TYPE.name()) + ":Aggregate"); CutoffFacet ancestorPath = null; if (metadata.getResourceType().equals(searchSettings.resourceTypeFile) || metadata.getResourceType().equals(searchSettings.resourceTypeAggregate)) { ancestorPath = metadata.getAncestorPathFacet(); } else { ancestorPath = metadata.getPath(); } if (ancestorPath != null) { // We want only objects at the same level of the hierarchy ancestorPath.setCutoff(ancestorPath.getHighestTier() + 1); facetFieldUtil.addToSolrQuery(ancestorPath, solrQuery); } // Sort neighbors using the default sort addSort(solrQuery, "default", true); // Query for ids in this container in groups of NEIGHBOR_SEEK_PAGE_SIZE until we find the offset of the object solrQuery.setRows(NEIGHBOR_SEEK_PAGE_SIZE); solrQuery.setFields("id"); long total = -1; int start = 0; pageLoop: do { try { solrQuery.setStart(start); QueryResponse queryResponse = this.executeQuery(solrQuery); total = queryResponse.getResults().getNumFound(); for (SolrDocument doc : queryResponse.getResults()) { if (metadata.getId().equals(doc.getFieldValue("id"))) { break pageLoop; } start++; } } catch (SolrServerException e) { LOG.error("Error retrieving Neighboring items: " + e); return null; } } while (start < total); // Wasn't found, no neighbors shall be forthcoming if (start >= total) { return null; } // Calculate the starting index for the window, so that object is as close to the middle as possible long left = start - (windowSize / 2); long right = start + (windowSize / 2); if (left < 0) { right -= left; left = 0; } if (right >= total) { left -= (right - total) + 1; if (left < 0) { left = 0; } } // Query for the windowSize of objects solrQuery.setFields(new String[0]); solrQuery.setRows(windowSize); solrQuery.setStart((int) left); try { QueryResponse queryResponse = this.executeQuery(solrQuery); return queryResponse.getBeans(BriefObjectMetadataBean.class); } catch (SolrServerException e) { LOG.error("Error retrieving Neighboring items: " + e); return null; } } /** * Returns the number of children plus a facet list for the parent defined by ancestorPath. * * @param ancestorPath * @param accessGroups * @return */ public SearchResultResponse getFullRecordSupplementalData(CutoffFacet ancestorPath, AccessGroupSet accessGroups, List<String> facetsToRetrieve) { SearchState searchState = searchStateFactory.createSearchState(); searchState.getFacets().put(SearchFieldKeys.ANCESTOR_PATH.name(), ancestorPath); searchState.setRowsPerPage(0); searchState.setFacetsToRetrieve(facetsToRetrieve); return getFacetList(new SearchRequest(searchState, true)); } public long getChildrenCount(BriefObjectMetadataBean metadataObject, AccessGroupSet accessGroups) { QueryResponse queryResponse = null; SolrQuery solrQuery = new SolrQuery(); StringBuilder query = new StringBuilder("*:* "); try { // Add access restrictions to query addAccessRestrictions(query, accessGroups); } catch (AccessRestrictionException e) { // If the user doesn't have any access groups, they don't have access to anything, return null. LOG.error(e.getMessage()); return -1; } solrQuery.setStart(0); solrQuery.setRows(0); solrQuery.setQuery(query.toString()); query = new StringBuilder(); query.append(solrSettings.getFieldName(SearchFieldKeys.ANCESTOR_PATH.name())).append(':') .append(SolrSettings.sanitize(metadataObject.getPath().getSearchValue())); solrQuery.setFacet(true); solrQuery.addFilterQuery(query.toString()); try { queryResponse = this.executeQuery(solrQuery); return queryResponse.getResults().getNumFound(); } catch (SolrServerException e) { LOG.error("Error retrieving Solr search result request", e); } return -1; } /** * Populates the child count attributes of all metadata objects in the given search result response by querying for * all non-folder objects which have the metadata object's highest ancestor path tier somewhere in its ancestor path. * * Items in resultList must have their ancestorPaths populated. * * @param resultList * @param accessGroups */ public void getChildrenCounts(List<BriefObjectMetadata> resultList, AccessGroupSet accessGroups) { this.getChildrenCounts(resultList, accessGroups, "child", null, null); } public void getChildrenCounts(List<BriefObjectMetadata> resultList, SearchRequest searchRequest) { this.getChildrenCounts(resultList, searchRequest.getAccessGroups(), "child", null, this.generateSearch(searchRequest)); } public void getChildrenCounts(List<BriefObjectMetadata> resultList, AccessGroupSet accessGroups, String countName, String queryAddendum, SolrQuery baseQuery) { long startTime = System.currentTimeMillis(); if (resultList == null || resultList.size() == 0) return; String ancestorPathField = solrSettings.getFieldName(SearchFieldKeys.ANCESTOR_PATH.name()); SolrQuery solrQuery; if (baseQuery == null) { // Create a base query since we didn't receive one solrQuery = new SolrQuery(); StringBuilder query = new StringBuilder("*:*"); try { // Add access restrictions to query addAccessRestrictions(query, accessGroups); } catch (AccessRestrictionException e) { // If the user doesn't have any access groups, they don't have access to anything, return null. LOG.error(e.getMessage()); return; } solrQuery.setStart(0); solrQuery.setRows(0); solrQuery.setQuery(query.toString()); } else { // Starting from a base query solrQuery = baseQuery.getCopy(); // Make sure we aren't returning any normal results solrQuery.setRows(0); // Remove all facet fields so we are only getting ancestor path if (solrQuery.getFacetFields() != null) { for (String facetField : solrQuery.getFacetFields()) { solrQuery.removeFacetField(facetField); } } } if (queryAddendum != null) { solrQuery.setQuery(solrQuery.getQuery() + " AND " + queryAddendum); } solrQuery.setFacet(true); solrQuery.setFacetMinCount(1); solrQuery.addFacetField(ancestorPathField); solrQuery.add("f." + ancestorPathField + ".facet.limit", Integer.toString(Integer.MAX_VALUE)); // Sort by value rather than count so that earlier tiers will come first in case the result gets cut off solrQuery.setFacetSort("index"); try { startTime = System.currentTimeMillis(); QueryResponse queryResponse = this.executeQuery(solrQuery); LOG.info("Query executed in " + (System.currentTimeMillis() - startTime)); assignChildrenCounts(queryResponse.getFacetField(ancestorPathField), resultList, countName); } catch (SolrServerException e) { LOG.error("Error retrieving Solr search result request", e); } } /** * Assigns children counts to container objects from ancestor path facet results based on matching search values * * @param facetField * @param containerObjects * @param countName */ protected void assignChildrenCounts(FacetField facetField, List<BriefObjectMetadata> containerObjects, String countName) { if (facetField.getValues() != null) { boolean binarySearch = facetField.getValues().size() > 64; for (BriefObjectMetadata container : containerObjects) { // Find the facet count for this container, either using a binary or linear search String searchValue = container.getPath().getSearchValue(); int matchIndex = -1; if (binarySearch) { matchIndex = Collections.binarySearch(facetField.getValues(), searchValue, new Comparator<Object>() { @Override public int compare(Object currentFacetValueObject, Object searchValueObject) { if (searchValueObject == null) throw new NullPointerException(); String searchValue = (String) searchValueObject; Count facetValue = (Count) currentFacetValueObject; return facetValue.getName().indexOf(searchValue) == 0 ? 0 : facetValue.getName().compareTo( searchValue); } }); } else { for (int i = 0; i < facetField.getValues().size(); i++) { Count facetValue = facetField.getValues().get(i); if (facetValue.getName().indexOf(searchValue) == 0) { matchIndex = i; break; } } } if (matchIndex > -1) { container.getCountMap().put(countName, facetField.getValues().get(matchIndex).getCount()); } } } } /** * Retrieves the tree of partially expanded containers from the Collections object up to the rootPID in the request. * Only the containers directly in the ancestor path of the starting pid will be expanded * * @param browseRequest * @return */ public HierarchicalBrowseResultResponse getExpandedStructurePath(HierarchicalBrowseRequest browseRequest) { HierarchicalBrowseResultResponse browseResponse = null; HierarchicalBrowseResultResponse previousResponse = null; CutoffFacet path = this.getAncestorPath(browseRequest.getRootPid(), browseRequest.getAccessGroups()); String currentPID = browseRequest.getRootPid(); // Retrieve structure results for each tier in the path, in reverse order List<HierarchicalFacetNode> nodes = path != null ? path.getFacetNodes() : null; int cnt = nodes != null ? nodes.size() : 0; while (cnt >= 0) { // Get the list of objects for the current tier HierarchicalBrowseRequest stepRequest = new HierarchicalBrowseRequest(browseRequest.getSearchState(), 1, browseRequest.getAccessGroups()); stepRequest.setRootPid(currentPID); browseResponse = getHierarchicalBrowseResults(stepRequest); if (previousResponse != null) { // Store the previous root node as a child in the current tier ResultNode previousRoot = previousResponse.getRootNode(); int childNodeIndex = browseResponse.getChildNodeIndex(previousRoot.getMetadata().getId()); if (childNodeIndex != -1) { browseResponse.getRootNode().getChildren().set(childNodeIndex, previousRoot); } else { LOG.warn("Could not locate child entry {} inside of results for {}", previousRoot.getMetadata().getId(), browseResponse.getRootNode().getMetadata().getId()); } } cnt--; if (cnt > -1) { HierarchicalFacetNode node = nodes.get(cnt); currentPID = node.getSearchKey(); previousResponse = browseResponse; } } return browseResponse; } /** * Retrieves results for populating a hierarchical browse view. Supports all the regular navigation available to * searches. Results contain child counts for each item (all items returned are containers), and a map containing the * number of nested subcontainers per container. Children counts are retrieved based on facet counts. * * @param browseRequest * @return */ public HierarchicalBrowseResultResponse getHierarchicalBrowseResults(HierarchicalBrowseRequest browseRequest) { AccessGroupSet accessGroups = browseRequest.getAccessGroups(); SearchState browseState = (SearchState) browseRequest.getSearchState().clone(); HierarchicalBrowseResultResponse browseResults = new HierarchicalBrowseResultResponse(); CutoffFacet rootPath = null; BriefObjectMetadataBean rootNode = null; if (browseRequest.getRootPid() != null) { rootNode = getObjectById(new SimpleIdRequest(browseRequest.getRootPid(), browseRequest.getAccessGroups())); if (rootNode != null) { rootPath = rootNode.getPath(); browseState.getFacets().put(SearchFieldKeys.ANCESTOR_PATH.name(), rootPath); browseResults.setSelectedContainer(rootNode); } } // Default the ancestor path to the collections object so we always have a root if (rootNode == null) { rootPath = (CutoffFacet) browseState.getFacets().get(SearchFieldKeys.ANCESTOR_PATH.name()); if (rootPath == null) { rootPath = new CutoffFacet(SearchFieldKeys.ANCESTOR_PATH.name(), "1," + this.collectionsPid.getPid()); browseState.getFacets().put(SearchFieldKeys.ANCESTOR_PATH.name(), rootPath); } rootNode = getObjectById(new SimpleIdRequest(rootPath.getSearchKey(), browseRequest.getAccessGroups())); } boolean rootIsAStub = rootNode == null; if (rootIsAStub) { // Parent is not found, but children are, so make a stub for the parent. rootNode = new BriefObjectMetadataBean(); rootNode.setId(rootPath.getSearchKey()); rootNode.setAncestorPathFacet(rootPath); } SearchState hierarchyState = searchStateFactory.createHierarchyListSearchState(); // Use the ancestor path facet from the state where we will have set a default value hierarchyState.getFacets().put(SearchFieldKeys.ANCESTOR_PATH.name(), rootPath); hierarchyState.setRowsPerPage(0); SearchRequest hierarchyRequest = new SearchRequest(hierarchyState, accessGroups, false); SolrQuery baseQuery = this.generateSearch(hierarchyRequest); // Get the set of all applicable containers SolrQuery hierarchyQuery = baseQuery.getCopy(); hierarchyQuery.setRows(new Integer(searchSettings.getProperty("search.results.maxBrowsePerPage"))); // Reusable query segment for limiting the results to just the depth asked for StringBuilder cutoffQuery = new StringBuilder(); cutoffQuery.append('!').append(solrSettings.getFieldName(SearchFieldKeys.ANCESTOR_PATH.name())).append(":"); cutoffQuery.append(rootPath.getHighestTier() + browseRequest.getRetrievalDepth()); cutoffQuery.append(searchSettings.facetSubfieldDelimiter).append('*'); hierarchyQuery.addFilterQuery(cutoffQuery.toString()); SearchResultResponse results; try { results = this.executeSearch(hierarchyQuery, hierarchyState, false, false); browseResults.setSearchResultResponse(results); } catch (SolrServerException e) { LOG.error("Error while getting container results for hierarchical browse results", e); return null; } // Add the root node into the result set browseResults.getResultList().add(0, rootNode); if (browseRequest.isRetrieveFacets() && browseRequest.getSearchState().getFacetsToRetrieve() != null) { SearchState facetState = (SearchState) browseState.clone(); facetState.setRowsPerPage(0); SearchRequest facetRequest = new SearchRequest(facetState, browseRequest.getAccessGroups(), true); SolrQuery facetQuery = this.generateSearch(facetRequest); try { SearchResultResponse facetResponse = this.executeSearch(facetQuery, facetState, true, false); browseResults.setFacetFields(facetResponse.getFacetFields()); } catch (SolrServerException e) { LOG.warn("Failed to retrieve facet results for " + facetQuery.toString(), e); } } // Don't need to manipulate the container list any further unless either the root is a real record or there are // subcontainers if (!rootIsAStub || results.getResultCount() > 0) { // Get the children counts per container SearchRequest filteredChildrenRequest = new SearchRequest(browseState, browseRequest.getAccessGroups(), true); this.getChildrenCounts(results.getResultList(), accessGroups, "child", null, this.generateSearch(filteredChildrenRequest)); this.getChildrenCounts(results.getResultList(), accessGroups, "containers", "resourceType:" + "(" + ResourceType.Collection.toString() + " OR " + ResourceType.Folder.toString() + ")", this.generateSearch(filteredChildrenRequest)); try { // If anything that constituted a search is in the request then trim out possible empty folders if (browseState.isPopulatedSearch()) { // Get the list of any direct matches for the current query browseResults.setMatchingContainerPids(this.getDirectContainerMatches(browseState, accessGroups)); browseResults.getMatchingContainerPids().add(browseRequest.getRootPid()); // Remove all containers that are not direct matches for the user's query and have 0 children browseResults.removeContainersWithoutContents(); } } catch (SolrServerException e) { LOG.error("Error while getting children counts for hierarchical browse", e); return null; } } // Retrieve normal item search results, which are restricted to a max number per page if (browseRequest.isIncludeFiles() && browseState.getRowsPerPage() > 0) { browseState.getResourceTypes().add(searchSettings.resourceTypeFile); SearchState fileSearchState = new SearchState(browseState); List<String> resourceTypes = new ArrayList<String>(); resourceTypes.add(searchSettings.resourceTypeFile); fileSearchState.setResourceTypes(resourceTypes); CutoffFacet ancestorPath = (CutoffFacet) fileSearchState.getFacets().get(SearchFieldKeys.ANCESTOR_PATH.name()); ancestorPath.setCutoff(rootPath.getHighestTier() + 1); fileSearchState.setFacetsToRetrieve(null); SearchRequest fileSearchRequest = new SearchRequest(fileSearchState, browseRequest.getAccessGroups()); SearchResultResponse fileResults = this.getSearchResults(fileSearchRequest); browseResults.populateItemResults(fileResults.getResultList()); } browseResults.generateResultTree(); return browseResults; } /** * Returns a set of object IDs for containers that directly matched the restrictions from the base query. * * @param baseState * @param accessGroups * @return * @throws SolrServerException */ private Set<String> getDirectContainerMatches(SearchState baseState, AccessGroupSet accessGroups) throws SolrServerException { SearchState directMatchState = (SearchState) baseState.clone(); directMatchState.setResourceTypes(null); directMatchState.setResultFields(Arrays.asList(SearchFieldKeys.ID.name())); directMatchState.getFacets().put(SearchFieldKeys.CONTENT_MODEL.name(), ContentModelHelper.Model.CONTAINER.toString()); directMatchState.setRowsPerPage(new Integer(searchSettings.getProperty("search.results.maxBrowsePerPage"))); SearchRequest directMatchRequest = new SearchRequest(directMatchState, accessGroups, false); SolrQuery directMatchQuery = this.generateSearch(directMatchRequest); QueryResponse directMatchResponse = this.executeQuery(directMatchQuery); String idField = solrSettings.getFieldName(SearchFieldKeys.ID.name()); Set<String> directMatchIds = new HashSet<String>(directMatchResponse.getResults().size()); for (SolrDocument document : directMatchResponse.getResults()) { directMatchIds.add((String) document.getFirstValue(idField)); } return directMatchIds; } public HierarchicalBrowseResultResponse getStructureTier(SearchRequest browseRequest) { SearchState fileSearchState = new SearchState(browseRequest.getSearchState()); CutoffFacet ancestorPath = (CutoffFacet) fileSearchState.getFacets().get(SearchFieldKeys.ANCESTOR_PATH.name()); if (ancestorPath != null) { ancestorPath.setCutoff(ancestorPath.getHighestTier() + 1); } else { ancestorPath = new CutoffFacet(SearchFieldKeys.ANCESTOR_PATH.name(), "1,*"); fileSearchState.getFacets().put(SearchFieldKeys.ANCESTOR_PATH.name(), ancestorPath); } fileSearchState.setFacetsToRetrieve(null); SearchRequest fileSearchRequest = new SearchRequest(fileSearchState, browseRequest.getAccessGroups()); SearchResultResponse fileResults = this.getSearchResults(fileSearchRequest); HierarchicalBrowseResultResponse response = new HierarchicalBrowseResultResponse(); response.setResultList(fileResults.getResultList()); // Add in a stub root node to top the tree BriefObjectMetadataBean rootNode = new BriefObjectMetadataBean(); rootNode.setId(ancestorPath.getSearchKey()); rootNode.setAncestorPathFacet(ancestorPath); response.getResultList().add(0, rootNode); response.generateResultTree(); return response; } /** * Matches hierarchical facets in the search state with those in the facet list. If a match is found, then the search * state hierarchical facet is overwritten with the result facet in order to give it a display value. * * @param searchState * @param resultResponse */ public void lookupHierarchicalDisplayValues(SearchState searchState, AccessGroupSet accessGroups) { if (searchState.getFacets() == null) return; Iterator<String> facetIt = searchState.getFacets().keySet().iterator(); while (facetIt.hasNext()) { String facetKey = facetIt.next(); Object facetValue = searchState.getFacets().get(facetKey); if (facetValue instanceof AbstractHierarchicalFacet) { FacetFieldObject resultFacet = getHierarchicalFacet((AbstractHierarchicalFacet) facetValue, accessGroups); if (resultFacet != null) { GenericFacet facet = resultFacet.getValues().get(resultFacet.getValues().size() - 1); searchState.getFacets().put(facetKey, facet); if (facetValue instanceof CutoffFacet) { ((CutoffFacet) facet).setCutoff(((CutoffFacet) facetValue).getCutoff()); } } } } } /** * Checks if an item is accessible given the specified access restrictions * * @param idRequest * @param accessType * @return */ public boolean isAccessible(SimpleIdRequest idRequest) { QueryResponse queryResponse = null; SolrQuery solrQuery = new SolrQuery(); StringBuilder query = new StringBuilder(); PID pid = new PID(idRequest.getId()); String id = pid.getPid(); String[] idParts = id.split("/"); String datastream = null; if (idParts.length > 1) { id = idParts[0]; datastream = idParts[1]; solrQuery.addField(solrSettings.getFieldName(SearchFieldKeys.ROLE_GROUP.name())); } query.append(solrSettings.getFieldName(SearchFieldKeys.ID.name())).append(':').append(SolrSettings.sanitize(id)); try { // Add access restrictions to query addAccessRestrictions(query, idRequest.getAccessGroups()); } catch (AccessRestrictionException e) { // If the user doesn't have any access groups, they don't have access to anything, return null. LOG.error(e.getMessage()); return false; } solrQuery.setQuery(query.toString()); if (datastream == null) solrQuery.setRows(0); else solrQuery.setRows(1); solrQuery.addField(solrSettings.getFieldName(SearchFieldKeys.ID.name())); LOG.debug("getObjectById query: " + solrQuery.toString()); try { queryResponse = this.executeQuery(solrQuery); if (queryResponse.getResults().getNumFound() == 0) return false; if (datastream == null) return true; List<BriefObjectMetadataBean> results = queryResponse.getBeans(BriefObjectMetadataBean.class); BriefObjectMetadataBean metadata = results.get(0); return AccessUtil.permitDatastreamAccess(idRequest.getAccessGroups(), datastream, metadata); } catch (SolrServerException e) { LOG.error("Error retrieving Solr object request: " + e); } return false; } /** * Determines if the user has adminRole permissions on any items * * @param accessGroups * @return */ public boolean hasAdminViewPermission(AccessGroupSet accessGroups) { if (accessGroups.contains(AccessGroupConstants.ADMIN_GROUP)) { return true; } StringBuilder query = new StringBuilder(); String joinedGroups = accessGroups.joinAccessGroups(" OR ", null, true); query.append("adminGroup:(").append(joinedGroups).append(')'); SolrQuery solrQuery = new SolrQuery(); solrQuery.setQuery(query.toString()); solrQuery.setRows(0); try { QueryResponse queryResponse = this.executeQuery(solrQuery); return queryResponse.getResults().getNumFound() > 0; } catch (SolrServerException e) { LOG.error("Error retrieving Solr object request: " + e); } return false; } public boolean hasRole(AccessGroupSet accessGroups, UserRole userRole) { StringBuilder query = new StringBuilder(); String joinedGroups = accessGroups.joinAccessGroups(" OR ", userRole.getPredicate() + "|", true); query.append("roleGroup:(").append(joinedGroups).append(')'); SolrQuery solrQuery = new SolrQuery(); solrQuery.setQuery(query.toString()); solrQuery.setRows(0); try { QueryResponse queryResponse = this.executeQuery(solrQuery); return queryResponse.getResults().getNumFound() > 0; } catch (SolrServerException e) { LOG.error("Error retrieving Solr object request: " + e); } return false; } public SearchResultResponse performSearch(SearchRequest searchRequest) { SearchState searchState = (SearchState) searchRequest.getSearchState().clone(); SearchState originalState = searchRequest.getSearchState(); searchRequest.setSearchState(searchState); searchState.setFacetsToRetrieve(null); // Adjust the number of results to retrieve if (searchState.getRowsPerPage() == null || searchState.getRowsPerPage() < 0) { searchState.setRowsPerPage(searchSettings.defaultPerPage); } else if (searchState.getRowsPerPage() > searchSettings.getMaxPerPage()) { searchState.setRowsPerPage(searchSettings.getMaxPerPage()); } Boolean rollup = searchState.getRollup(); BriefObjectMetadata selectedContainer = null; // Get the record for the currently selected container if one is selected. if (searchRequest.getRootPid() != null) { selectedContainer = addSelectedContainer(searchRequest.getRootPid(), searchState, searchRequest.isApplyCutoffs()); } else if (rollup == null) { LOG.debug("No container and no rollup, defaulting rollup to true"); searchState.setRollup(true); } // Retrieve search results SearchResultResponse resultResponse = getSearchResults(searchRequest); if (resultResponse.getResultCount() == 0 && searchRequest.isApplyCutoffs() && searchState.getFacets().containsKey(SearchFieldKeys.ANCESTOR_PATH.name())) { ((CutoffFacet) searchState.getFacets().get(SearchFieldKeys.ANCESTOR_PATH.name())).setCutoff(null); resultResponse = getSearchResults(searchRequest); } resultResponse.setSelectedContainer(selectedContainer); // Get the children counts for container entries. getChildrenCounts(resultResponse.getResultList(), searchRequest.getAccessGroups()); searchRequest.setSearchState(originalState); return resultResponse; } public void populateBreadcrumbs(SearchRequest searchRequest, SearchResultResponse resultResponse) { SearchState searchState = searchRequest.getSearchState(); if (searchState.getFacets().containsKey(SearchFieldKeys.CONTENT_TYPE.name())) { if (resultResponse.getResultCount() == 0 || searchState.getResultFields() == null || !searchState.getResultFields().contains(SearchFieldKeys.CONTENT_TYPE.name())) { SearchState contentTypeSearchState = new SearchState(); contentTypeSearchState.setRowsPerPage(1); contentTypeSearchState.getFacets().put(SearchFieldKeys.CONTENT_TYPE.name(), searchState.getFacets().get(SearchFieldKeys.CONTENT_TYPE.name())); contentTypeSearchState.setResultFields(Arrays.asList(SearchFieldKeys.CONTENT_TYPE.name())); SearchRequest contentTypeRequest = new SearchRequest(contentTypeSearchState, GroupsThreadStore.getGroups()); SearchResultResponse contentTypeResponse = getSearchResults(contentTypeRequest); if (contentTypeResponse.getResultCount() > 0) resultResponse.extractCrumbDisplayValueFromRepresentative(contentTypeResponse.getResultList().get(0)); } else { resultResponse.extractCrumbDisplayValueFromRepresentative(resultResponse.getResultList().get(0)); } } } public void setSearchStateFactory(SearchStateFactory searchStateFactory) { this.searchStateFactory = searchStateFactory; } public void setCollectionsPid(PID collectionsPid) { this.collectionsPid = collectionsPid; } /** * Get the number of departments represented in the collection * * @return the count, or -1 if there was an error retrieving the count */ public int getDepartmentsCount() { SolrQuery query; QueryResponse response; query = new SolrQuery(); query.setQuery("*:*"); query.setRows(0); query.addFacetField("department"); query.setFacetLimit(-1); try { response = this.executeQuery(query); return response.getFacetField("department").getValueCount(); } catch (SolrServerException e) { LOG.error("Error retrieving Solr object request: " + e); } return -1; } public long getInvalidVocabularyCount(SearchRequest searchRequest) { if (searchRequest.getRootPid() != null) { addSelectedContainer(searchRequest.getRootPid(), searchRequest.getSearchState(), searchRequest.isApplyCutoffs()); } SolrQuery query = generateSearch(searchRequest); query.setQuery(query.getQuery() + " AND " + solrSettings.getFieldName(SearchFieldKeys.RELATIONS.name()) + ":" + invalidTerm.getPredicate() + "*"); query.setRows(0); try { QueryResponse response = this.executeQuery(query); return response.getResults().getNumFound(); } catch (SolrServerException e) { LOG.error("Error retrieving Solr object request: " + e); } return -1; } public SearchResultResponse getRelationSet(SearchRequest searchRequest, String relationName) { SolrQuery query = generateSearch(searchRequest); query.setQuery(query.getQuery() + " AND " + solrSettings.getFieldName(SearchFieldKeys.RELATIONS.name()) + ":" + SolrSettings.sanitize(relationName) + "|*"); query.setRows(1000); try { return executeSearch(query, searchRequest.getSearchState(), false, false); } catch (SolrServerException e) { LOG.error("Error retrieving Solr object request: " + e); } return null; } /** * Get the total number of collections * * @return the count, or -1 if there was an error retrieving the count */ public long getCollectionsCount() { SolrQuery query; QueryResponse response; query = new SolrQuery(); query.setQuery("resourceType:Collection"); query.setRows(0); query.setFacetLimit(-1); try { response = this.executeQuery(query); return response.getResults().getNumFound(); } catch (SolrServerException e) { LOG.error("Error retrieving Solr object request: " + e); } return -1; } /** * Get the number of objects present in the collection for various formats * * @return a map from format name to count */ public Map<String, Long> getFormatCounts() { SolrQuery query; QueryResponse response; query = new SolrQuery(); query.setQuery("*:*"); query.setRows(0); query.addFacetField("contentType"); query.setFacetLimit(-1); HashMap<String, Long> counts = new HashMap<String, Long>(); try { response = this.executeQuery(query); FacetField facetField = response.getFacetField("contentType"); for (Count count : facetField.getValues()) { if (count.getName().startsWith("^text")) counts.put("text", count.getCount()); else if (count.getName().startsWith("^image")) counts.put("image", count.getCount()); else if (count.getName().startsWith("^dataset")) counts.put("dataset", count.getCount()); else if (count.getName().startsWith("^audio")) counts.put("audio", count.getCount()); else if (count.getName().startsWith("^video")) counts.put("video", count.getCount()); } } catch (SolrServerException e) { LOG.error("Error retrieving Solr object request: " + e); } return counts; } public static String getWriteRoleFilter(AccessGroupSet groups) { StringBuilder roleString = new StringBuilder(); roleString.append('('); for (String group : groups) { String saneGroup = SolrSettings.sanitize(group); roleString.append(UserRole.processor.getPredicate()).append('|').append(saneGroup).append(' '); roleString.append(UserRole.curator.getPredicate()).append('|').append(saneGroup).append(' '); roleString.append(UserRole.administrator.getPredicate()).append('|').append(saneGroup).append(' '); } roleString.append(')'); return roleString.toString(); } public void setPathFactory(ObjectPathFactory pathFactory) { this.pathFactory = pathFactory; } }