001/** 002 * Copyright 2014 Tampere University of Technology, Pori Department 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 core.tut.pori.dao; 017 018import java.util.ArrayList; 019import java.util.HashSet; 020import java.util.Iterator; 021import java.util.List; 022import java.util.Map; 023import java.util.Set; 024 025import org.apache.commons.lang3.StringUtils; 026import org.apache.log4j.Logger; 027import org.apache.solr.client.solrj.SolrQuery; 028import org.apache.solr.client.solrj.SolrQuery.ORDER; 029import org.apache.solr.client.solrj.util.ClientUtils; 030import org.apache.solr.common.params.CommonParams; 031 032import core.tut.pori.dao.SQLSelectBuilder.OrderDirection; 033import core.tut.pori.dao.filter.AbstractQueryFilter; 034import core.tut.pori.http.parameters.Limits; 035import core.tut.pori.http.parameters.QueryParameter; 036import core.tut.pori.http.parameters.SortOptions; 037import core.tut.pori.http.parameters.SortOptions.Option; 038 039/** 040 * Helper class, which can be used to build a SOLR query. 041 * 042 */ 043public class SolrQueryBuilder { 044 /** Separator for solr fields and values */ 045 public static final char SEPARATOR_SOLR_FIELD_VALUE = ':'; 046 /** Solr query selector for all items */ 047 public static final String QUERY_ALL = "*:*"; 048 /** Solr wild card */ 049 public static final char SOLR_WILD_CARD = '*'; 050 private static final Limits DEFAULT_MAX_LIMITS = new Limits(0, SolrDAO.MAX_DOCUMENT_COUNT); 051 private static final Logger LOGGER = Logger.getLogger(SolrQueryBuilder.class); 052 private static final String SOLR_SELECT = "/select"; 053 private static final String SOLR_QUERY = "/query"; 054 private static final String SOLR_SUGGEST = "/suggest"; 055 private List<AbstractQueryFilter> _customFilters = null; 056 private Map<String, String> _elementFieldMap = null; 057 private Limits _limits = null; 058 private QueryParameter _queryParameter = null; 059 private SortOptions _sortOptions = null; 060 private boolean _useExtendedDismax = false; 061 private Set<String> _fieldList = null; 062 063 /** 064 * Type of the request handler. 065 */ 066 public enum RequestHandlerType{ 067 /** select */ 068 SELECT(SOLR_SELECT), 069 /** query */ 070 QUERY(SOLR_QUERY), 071 /** suggest */ 072 SUGGEST(SOLR_SUGGEST); 073 074 private String _handler; 075 076 /** 077 * 078 * @param handler 079 */ 080 private RequestHandlerType(String handler){ 081 _handler = handler; 082 } 083 084 /** 085 * 086 * @return the type as a string 087 */ 088 public String toHandlerString(){ 089 return _handler; 090 } 091 } // enum RequestHandlerType 092 093 /** 094 * Note: the given map is ASSUMED to be escaped and to contain only valid element names and field names 095 * 096 * @param elementFieldMap map of element-solr field relations 097 */ 098 public SolrQueryBuilder(Map<String,String> elementFieldMap){ 099 _elementFieldMap = elementFieldMap; 100 } 101 102 /** 103 * same as calling the overloaded constructor with parameter null (without relation map) 104 */ 105 public SolrQueryBuilder(){ 106 // nothing needed 107 } 108 109 /** 110 * @param limits the limits to set 111 */ 112 public void setLimits(Limits limits) { 113 _limits = limits; 114 } 115 116 /** 117 * 118 * @param option 119 */ 120 public void addSortOption(Option option){ 121 if(_sortOptions == null){ 122 _sortOptions = new SortOptions(); 123 } 124 _sortOptions.addSortOption(option); 125 } 126 127 /** 128 * Add field to be selected in the response. 129 * 130 * @param field 131 */ 132 public void addField(String field){ 133 if(_fieldList == null){ 134 _fieldList = new HashSet<>(); 135 } 136 _fieldList.add(ClientUtils.escapeQueryChars(field)); 137 } 138 139 /** 140 * Add a list of fields to be selected in the response 141 * 142 * @param fields 143 */ 144 public void addFields(String ...fields){ 145 for(int i=0;i<fields.length; ++i){ 146 addField(fields[i]); 147 } 148 } 149 150 /** 151 * @param queryParameter the query to set 152 */ 153 public void setQueryParameter(QueryParameter queryParameter) { 154 _queryParameter = queryParameter; 155 } 156 157 /** 158 * @param sortOptions the sortOptions to set 159 */ 160 public void setSortOptions(SortOptions sortOptions) { 161 _sortOptions = sortOptions; 162 } 163 164 /** 165 * 166 * @return true if extended dismax is enabled 167 */ 168 public boolean isUseExtendedDismax() { 169 return _useExtendedDismax; 170 } 171 172 /** 173 * Setter for the use of enabling Extended Query Parser (edismax). Setting this true will be using the more user friendly query parser. 174 * @param useExtendedDismax 175 */ 176 public void setUseExtendedDismax(boolean useExtendedDismax) { 177 _useExtendedDismax = useExtendedDismax; 178 } 179 180 /** 181 * Add new filter query. 182 * 183 * @param filter 184 */ 185 public void addCustomFilter(AbstractQueryFilter filter){ 186 if(filter == null){ 187 LOGGER.warn("Ignored null filter."); 188 return; 189 } 190 if(_customFilters == null){ 191 _customFilters = new ArrayList<>(); 192 } 193 _customFilters.add(filter); 194 } 195 196 /** 197 * clear the list of currently set custom filters 198 */ 199 public void clearCustomFilters(){ 200 _customFilters = null; 201 } 202 203 /** 204 * Get the query using a specified type parameter for sort and limit operations. 205 * 206 * @param type the type or null 207 * @return new query created with the given parameters using type information 208 * @throws IllegalArgumentException on bad query value 209 */ 210 public SolrQuery toSolrQuery(String type) throws IllegalArgumentException{ 211 SolrQuery query = new SolrQuery(); 212 213 if(_customFilters != null){ 214 Iterator<AbstractQueryFilter> pIter = _customFilters.iterator(); 215 StringBuilder filter = new StringBuilder(); 216 pIter.next().toFilterString(filter); 217 while(pIter.hasNext()){ 218 AbstractQueryFilter fq = pIter.next(); 219 filter.append(fq.getQueryType().toTypeString()); 220 fq.toFilterString(filter); 221 } 222 query.addFilterQuery(filter.toString()); 223 } 224 225 boolean querySet = false; 226 Set<String> queryParams = QueryParameter.getValues(_queryParameter, type); 227 if(queryParams != null){ 228 int paramCount = queryParams.size(); 229 if(paramCount != 1){ 230 throw new IllegalArgumentException("Only a single query parameter is accepted, found: "+paramCount); 231 } 232 querySet = true; 233 234 String q = queryParams.iterator().next(); 235 if(_useExtendedDismax){ 236 LOGGER.debug("Using edismax query parser and raw query"); 237 query.set("defType", "edismax"); //activate edismax query parser 238 query.set("q.alt", QUERY_ALL); //setter for alternative query to retrieve everything if the regular query is not given. 239 query.setQuery(q); 240 }else if(_elementFieldMap == null){ 241 LOGGER.debug("No element-field map provided, using the raw query."); 242 query.setQuery(q); 243 }else{ 244 StringBuilder solr = new StringBuilder(q.length()); // estimate size to minimize re-allocations 245 boolean inParenthesis = false; 246 boolean inElement = false; 247 StringBuilder elementName = new StringBuilder(); 248 for(int i = q.length()-1;i>=0;--i){ // go in reverse 249 char c = q.charAt(i); 250 if(c == '"'){ 251 inParenthesis = !inParenthesis; 252 }else if(!inParenthesis && c == SEPARATOR_SOLR_FIELD_VALUE){ // not in parenthesis, and this is the start of a new element name 253 if(inElement){ // an error, cannot start a new element and already be inside an element 254 throw new IllegalArgumentException("Bad query string, duplicate field separator at: "+i); 255 } 256 inElement = true; 257 }else if(inElement){ 258 if(c == SOLR_WILD_CARD){ 259 if(elementName.length() > 0){ // there is a wild card in the element name 260 LOGGER.debug("Wild card detected inside element name. Skipping element-field mapping..."); 261 solr.append(elementName); // dump whatever is in the element name in back into the query string 262 elementName.setLength(0); // clear the element 263 }else{ // probably *:SOMETHING 264 inElement = false; 265 } 266 }else if(c < 'A' || c > 'z'){ // end of element 267 solr.append(getReversedFieldName(elementName)); 268 elementName.setLength(0); 269 inElement = false; 270 }else{ 271 elementName.append(c); 272 continue; 273 } 274 } 275 solr.append(c); // append in the query string 276 } // for 277 if(inElement){ 278 solr.append(getReversedFieldName(elementName)); 279 } 280 query.setQuery(solr.reverse().toString()); // the query was processed from end to beginning, we need to reverse this first 281 } // else 282 } 283 284 if(!querySet){ 285 if(_useExtendedDismax){ 286 LOGGER.debug("Extended Dismax enabled, query parameter can be null"); 287 query.set("defType", "edismax"); //activate edismax query parser 288 query.set("q.alt", QUERY_ALL); //setter for alternative query to retrieve everything if the regular query is not given. 289 }else{ 290 query.setQuery(QUERY_ALL); 291 } 292 } 293 294 if(_limits == null){ 295 query.setStart(DEFAULT_MAX_LIMITS.getStartItem()); 296 query.setRows(DEFAULT_MAX_LIMITS.getMaxItems()); 297 }else{ 298 int start = _limits.getStartItem(type); 299 query.setStart(_limits.getStartItem(type)); 300 int rows = _limits.getMaxItems(type); 301 long total = (long)start+(long)rows; 302 if(total > DEFAULT_MAX_LIMITS.getMaxItems()){ // solr server throws array out of bounds exception if total row count (start+rows) exceeds INTEGER.MAX 303 rows = DEFAULT_MAX_LIMITS.getMaxItems()-start; 304 LOGGER.debug("Max items exceeds the maximum document count, capping to: "+rows); 305 } 306 query.setRows(rows); 307 } 308 309 Set<Option> options = SortOptions.getSortOptions(_sortOptions, type); 310 if(options != null){ 311 for(Option o : options){ 312 if(_elementFieldMap == null){ 313 LOGGER.debug("No element-field map provided, using the element name directly."); 314 query.addSort(o.getElementName(), fromOrderDirection(o.getOrderDirection())); 315 }else{ 316 String field = _elementFieldMap.get(o.getElementName()); 317 if(field == null){ 318 LOGGER.warn("Ignored unknown sort element: "+o.getElementName()); 319 }else{ 320 query.addSort(field, fromOrderDirection(o.getOrderDirection())); 321 } 322 } 323 } 324 } 325 if(_fieldList != null){ 326 query.setFields(_fieldList.toArray(new String[_fieldList.size()])); 327 } 328 329 return query; 330 } 331 332 /** 333 * Helper method to reverse element name 334 * @param reversedElementName 335 * @return reversed field name mapped to the given reversed element name 336 */ 337 private String getReversedFieldName(StringBuilder reversedElementName){ 338 String eName = reversedElementName.reverse().toString(); // the query is processed from end-to-beginning, so we need to reverse the element name 339 String fieldName = _elementFieldMap.get(eName); // check if it matches a known field name 340 if(fieldName == null){ 341 throw new IllegalArgumentException("Bad element name: "+eName); 342 } 343 return StringUtils.reverse(fieldName); 344 } 345 346 /** 347 * get an executable SolrQuery from the builder 348 * 349 * @return new query created with the given parameters 350 */ 351 public SolrQuery toSolrQuery(){ 352 return toSolrQuery(null); 353 } 354 355 /** 356 * Can be used to convert SQL OrderDirection to SOLR ORDER clause. 357 * 358 * @param direction 359 * @return the given sql order direction converted to solr order direction 360 * @throws UnsupportedOperationException 361 */ 362 public static ORDER fromOrderDirection(OrderDirection direction) throws UnsupportedOperationException{ 363 switch(direction){ 364 case ASCENDING: 365 return ORDER.asc; 366 case DESCENDING: 367 return ORDER.desc; 368 default: 369 throw new UnsupportedOperationException("Unhandeled direction: "+direction.name()); 370 } 371 } 372 373 /** 374 * Helper method to set the correct path for different kinds of request handlers (mainly for suggest-handler) 375 * @param query 376 * @param type 377 * @return the passed query 378 */ 379 public static SolrQuery setRequestHandler(SolrQuery query, RequestHandlerType type) { 380 if(query != null){ 381 switch(type){ 382 case SUGGEST: 383 query.setParam(CommonParams.QT, type.toHandlerString()); 384 break; 385 case QUERY: // default is OK 386 case SELECT: // default is OK 387 LOGGER.debug("Using default query handler."); 388 break; 389 default: 390 throw new UnsupportedOperationException("Unhandeled "+RequestHandlerType.class.toString()+" : "+type.name()); 391 } 392 } 393 return query; 394 } 395}