001/* 002 * Copyright 2017 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.web.frontoffice; 017 018import java.util.ArrayList; 019import java.util.Collection; 020import java.util.Collections; 021import java.util.HashMap; 022import java.util.HashSet; 023import java.util.List; 024import java.util.Map; 025import java.util.Map.Entry; 026import java.util.Optional; 027import java.util.stream.Collectors; 028 029import org.apache.solr.client.solrj.SolrQuery; 030import org.apache.solr.client.solrj.response.QueryResponse; 031import org.slf4j.Logger; 032 033import org.ametys.cms.search.SearchField; 034import org.ametys.cms.search.SearchResult; 035import org.ametys.cms.search.SearchResults; 036import org.ametys.cms.search.SearchResultsIterable; 037import org.ametys.cms.search.solr.SearcherFactory; 038import org.ametys.cms.search.solr.SolrSearchResults; 039import org.ametys.plugins.repository.AmetysObject; 040import org.ametys.plugins.repository.AmetysObjectIterable; 041 042/** 043 * {@link SearcherFactory} for creating a searcher for {@link AbstractSearchGenerator} 044 */ 045public class FrontOfficeSearcherFactory extends SearcherFactory 046{ 047 /** The component role. */ 048 @SuppressWarnings("hiding") 049 public static final String ROLE = FrontOfficeSearcherFactory.class.getName(); 050 051 @Override 052 public FrontOfficeSearcher create() 053 { 054 return new FrontOfficeSearcher(getLogger()); 055 } 056 057 /** 058 * Searcher for {@link AbstractSearchGenerator} 059 */ 060 public class FrontOfficeSearcher extends Searcher 061 { 062 private Collection<QueryFacet> _queryFacets; 063 private Collection<String> _queryFacetValues; 064 065 /** 066 * Build a Searcher with default values. 067 * @param logger The logger. 068 */ 069 protected FrontOfficeSearcher(Logger logger) 070 { 071 super(logger); 072 _queryFacets = new ArrayList<>(); 073 _queryFacetValues = new HashSet<>(); 074 } 075 076 /** 077 * Sets the query facets (facet.query parameters) 078 * @param queryFacets The facets 079 * @return The Searcher object itself. 080 */ 081 public FrontOfficeSearcher withQueryFacets(Collection<QueryFacet> queryFacets) 082 { 083 _queryFacets = new ArrayList<>(queryFacets); 084 return this; 085 } 086 087 /** 088 * Set the facet.query values 089 * @param queryFacetValues The values 090 * @return The Searcher object itself. 091 */ 092 public FrontOfficeSearcher withQueryFacetValues(Collection<String> queryFacetValues) 093 { 094 _queryFacetValues = new HashSet<>(queryFacetValues); 095 return this; 096 } 097 098 @Override 099 protected void modifySolrQuery(SolrQuery query) 100 { 101 if (_queryFacets.size() > 0) 102 { 103 // unlimited facet values 104 query.setFacetLimit(-1); 105 query.setFacet(true); 106 } 107 108 _queryFacets.forEach(facet -> 109 { 110 String keyName = facet.getKeyName(); 111 String excludedTagName = facet.getExcludedTagName(); 112 String facetQuery = facet.getFacetQuery(); 113 boolean queryFacetValue = _queryFacetValues.contains(keyName); 114 115 if (queryFacetValue) 116 { 117 StringBuilder fq = new StringBuilder(); 118 fq.append("{!tag=").append(excludedTagName) 119 .append("}(") 120 .append(facetQuery) 121 .append(')'); 122 query.addFilterQuery(fq.toString()); 123 } 124 125 query.addFacetQuery("{!ex=" + excludedTagName + " key=" + keyName + "}" + facetQuery); 126 }); 127 } 128 129 @Override 130 public <A extends AmetysObject> SearchResults<A> _buildResults(QueryResponse response, List<SearchField> facets) throws Exception 131 { 132 SearchResults<A> wrappedRes = super._buildResults(response, facets); 133 Collection<String> queryFacetKeys = _queryFacets.stream().map(QueryFacet::getKeyName).collect(Collectors.toList()); 134 Map<String, Integer> facetQueryResults = Optional.ofNullable(response.getFacetQuery()) 135 .orElseGet(HashMap::new) 136 .entrySet() 137 .stream() 138 .filter(e -> queryFacetKeys.contains(e.getKey())) 139 .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); 140 return new FrontOfficeSolrSearchResults<>(wrappedRes, response, facetQueryResults); 141 } 142 } 143 144 /** 145 * Wraps a 'facet.query' parameter 146 */ 147 public static class QueryFacet 148 { 149 private final String _keyName; 150 private final String _excludedTagName; 151 private final String _facetQuery; 152 153 /** 154 * Creates a QueryFacet 155 * @param keyName The name of the query facet 156 * @param excludedTagName The tag for exclusion for facet counting 157 * @param facetQuery The value of the query 158 */ 159 public QueryFacet(String keyName, String excludedTagName, String facetQuery) 160 { 161 _keyName = keyName; 162 _excludedTagName = excludedTagName; 163 _facetQuery = facetQuery; 164 } 165 166 /** 167 * Gets the keyName 168 * @return the keyName 169 */ 170 public String getKeyName() 171 { 172 return _keyName; 173 } 174 175 /** 176 * Gets the excludedTagName 177 * @return the excludedTagName 178 */ 179 public String getExcludedTagName() 180 { 181 return _excludedTagName; 182 } 183 184 /** 185 * Gets the facetQuery 186 * @return the _acetQuery 187 */ 188 public String getFacetQuery() 189 { 190 return _facetQuery; 191 } 192 } 193 194 /** 195 * Wrapper of a {@link SolrSearchResults} for {@link AbstractSearchGenerator} (add the possibility to add facet.queries) 196 * @param <A> the actual type of {@link AmetysObject}s. 197 */ 198 public static class FrontOfficeSolrSearchResults<A extends AmetysObject>/* extends SolrSearchResults<A>*/implements SearchResults<A> 199 { 200 private Map<String, Integer> _facetQueryResults; 201 private SearchResults<A> _searchResults; 202 203 /** 204 * Build a FrontOfficeSolrSearchResults object. 205 * @param searchResults The wrapped {@link SearchResults} object 206 * @param response the solr search response. 207 * @param facetQueryResults The facet.query results. 208 */ 209 public FrontOfficeSolrSearchResults(SearchResults<A> searchResults, 210 QueryResponse response, 211 Map<String, Integer> facetQueryResults) 212 { 213 _searchResults = searchResults; 214 _facetQueryResults = facetQueryResults; 215 } 216 217 @Override 218 public SearchResultsIterable<SearchResult<A>> getResults() 219 { 220 return _searchResults.getResults(); 221 } 222 223 @Override 224 public AmetysObjectIterable<A> getObjects() 225 { 226 return _searchResults.getObjects(); 227 } 228 229 @Override 230 public Iterable<String> getObjectIds() 231 { 232 return _searchResults.getObjectIds(); 233 } 234 235 @Override 236 public Map<String, Map<String, Integer>> getFacetResults() 237 { 238 return _searchResults.getFacetResults(); 239 } 240 241 /** 242 * Gets the facet.query results as a Map 243 * @return the facet.query results 244 */ 245 public Map<String, Integer> getFacetQueryResults() 246 { 247 return Collections.unmodifiableMap(_facetQueryResults); 248 } 249 250 @Override 251 public long getTotalCount() 252 { 253 return _searchResults.getTotalCount(); 254 } 255 256 @Override 257 public float getMaxScore() 258 { 259 return _searchResults.getMaxScore(); 260 } 261 262 @Override 263 public Optional<Map<String, Object>> getDebugMap() 264 { 265 return _searchResults.getDebugMap(); 266 } 267 } 268}