/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.apache.solr.search; import org.apache.commons.io.FilenameUtils; import org.apache.lucene.queries.function.FunctionQuery; import org.apache.lucene.queries.function.ValueSource; import org.apache.lucene.queries.function.valuesource.QueryValueSource; import org.apache.lucene.search.Query; import org.apache.solr.common.SolrException; import org.apache.solr.common.params.CommonParams; import org.apache.solr.common.params.ModifiableSolrParams; import org.apache.solr.common.params.SolrParams; import org.apache.solr.common.util.NamedList; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.response.transform.DocTransformer; import org.apache.solr.response.transform.DocTransformers; import org.apache.solr.response.transform.RenameFieldTransformer; import org.apache.solr.response.transform.ScoreAugmenter; import org.apache.solr.response.transform.TransformerFactory; import org.apache.solr.response.transform.ValueSourceAugmenter; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * The default implementation of return fields parsing for Solr. */ public class SolrReturnFields extends ReturnFields { // Special Field Keys public static final String SCORE = "score"; private final List<String> globs = new ArrayList<>(1); // The lucene field names to request from the SolrIndexSearcher // This *may* include fields that will not be in the final response private final Set<String> fields = new HashSet<>(); // Field names that are OK to include in the response. // This will include pseudo fields, lucene fields, and matching globs private Set<String> okFieldNames = new HashSet<>(); // The list of explicitly requested fields // Order is important for CSVResponseWriter private Set<String> reqFieldNames = null; protected DocTransformer transformer; protected boolean _wantsScore = false; protected boolean _wantsAllFields = false; protected Map<String,String> renameFields = Collections.emptyMap(); public SolrReturnFields() { _wantsAllFields = true; } public SolrReturnFields(SolrQueryRequest req) { this( req.getParams().getParams(CommonParams.FL), req ); } public SolrReturnFields(String fl, SolrQueryRequest req) { // this( (fl==null)?null:SolrPluginUtils.split(fl), req ); if( fl == null ) { parseFieldList((String[])null, req); } else { if( fl.trim().length() == 0 ) { // legacy thing to support fl=' ' => fl=*,score! // maybe time to drop support for this? // See ConvertedLegacyTest _wantsScore = true; _wantsAllFields = true; transformer = new ScoreAugmenter(SCORE); } else { parseFieldList( new String[]{fl}, req); } } } public SolrReturnFields(String[] fl, SolrQueryRequest req) { parseFieldList(fl, req); } private void parseFieldList(String[] fl, SolrQueryRequest req) { _wantsScore = false; _wantsAllFields = false; if (fl == null || fl.length == 0 || fl.length == 1 && fl[0].length()==0) { _wantsAllFields = true; return; } NamedList<String> rename = new NamedList<>(); DocTransformers augmenters = new DocTransformers(); for (String fieldList : fl) { add(fieldList,rename,augmenters,req); } for( int i=0; i<rename.size(); i++ ) { String from = rename.getName(i); String to = rename.getVal(i); okFieldNames.add( to ); boolean copy = (reqFieldNames!=null && reqFieldNames.contains(from)); if(!copy) { // Check that subsequent copy/rename requests have the field they need to copy for(int j=i+1; j<rename.size(); j++) { if(from.equals(rename.getName(j))) { rename.setName(j, to); // copy from the current target if(reqFieldNames==null) { reqFieldNames = new LinkedHashSet<>(); } reqFieldNames.add(to); // don't rename our current target } } } augmenters.addTransformer( new RenameFieldTransformer( from, to, copy ) ); } if (rename.size() > 0 ) { renameFields = rename.asShallowMap(); } if( !_wantsAllFields && !globs.isEmpty() ) { // TODO??? need to fill up the fields with matching field names in the index // and add them to okFieldNames? // maybe just get all fields? // this would disable field selection optimization... i think that is OK fields.clear(); // this will get all fields, and use wantsField to limit } if( augmenters.size() == 1 ) { transformer = augmenters.getTransformer(0); } else if( augmenters.size() > 1 ) { transformer = augmenters; } } @Override public Map<String,String> getFieldRenames() { return renameFields; } // like getId, but also accepts dashes for legacy fields public static String getFieldName(StrParser sp) { sp.eatws(); int id_start = sp.pos; char ch; if (sp.pos < sp.end && (ch = sp.val.charAt(sp.pos)) != '$' && Character.isJavaIdentifierStart(ch)) { sp.pos++; while (sp.pos < sp.end) { ch = sp.val.charAt(sp.pos); if (!Character.isJavaIdentifierPart(ch) && ch != '.' && ch != '-') { break; } sp.pos++; } return sp.val.substring(id_start, sp.pos); } return null; } private void add(String fl, NamedList<String> rename, DocTransformers augmenters, SolrQueryRequest req) { if( fl == null ) { return; } try { StrParser sp = new StrParser(fl); for(;;) { sp.opt(','); sp.eatws(); if (sp.pos >= sp.end) break; int start = sp.pos; // short circuit test for a really simple field name String key = null; String field = getFieldName(sp); char ch = sp.ch(); if (field != null) { if (sp.opt(':')) { // this was a key, not a field name key = field; field = null; sp.eatws(); start = sp.pos; } else { if (Character.isWhitespace(ch) || ch == ',' || ch==0) { addField(field, key, augmenters, false); continue; } // an invalid field name... reset the position pointer to retry sp.pos = start; field = null; } } if (key != null) { // we read "key : " field = sp.getId(null); ch = sp.ch(); if (field != null && (Character.isWhitespace(ch) || ch == ',' || ch==0)) { rename.add(field, key); addField(field, key, augmenters, false); continue; } // an invalid field name... reset the position pointer to retry sp.pos = start; field = null; } if (field == null) { // We didn't find a simple name, so let's see if it's a globbed field name. // Globbing only works with field names of the recommended form (roughly like java identifiers) field = sp.getGlobbedId(null); ch = sp.ch(); if (field != null && (Character.isWhitespace(ch) || ch == ',' || ch==0)) { // "*" looks and acts like a glob, but we give it special treatment if ("*".equals(field)) { _wantsAllFields = true; } else { globs.add(field); } continue; } // an invalid glob sp.pos = start; } String funcStr = sp.val.substring(start); // Is it an augmenter of the form [augmenter_name foo=1 bar=myfield]? // This is identical to localParams syntax except it uses [] instead of {!} if (funcStr.startsWith("[")) { ModifiableSolrParams augmenterParams = new ModifiableSolrParams(); int end = QueryParsing.parseLocalParams(funcStr, 0, augmenterParams, req.getParams(), "[", ']'); sp.pos += end; // [foo] is short for [type=foo] in localParams syntax String augmenterName = augmenterParams.get("type"); augmenterParams.remove("type"); String disp = key; if( disp == null ) { disp = '['+augmenterName+']'; } TransformerFactory factory = req.getCore().getTransformerFactory( augmenterName ); if( factory != null ) { DocTransformer t = factory.create(disp, augmenterParams, req); if(t!=null) { if(!_wantsAllFields) { String[] extra = t.getExtraRequestFields(); if(extra!=null) { for(String f : extra) { fields.add(f); // also request this field from IndexSearcher } } } augmenters.addTransformer( t ); } } else { //throw new SolrException(ErrorCode.BAD_REQUEST, "Unknown DocTransformer: "+augmenterName); } addField(field, disp, augmenters, true); continue; } // let's try it as a function instead QParser parser = QParser.getParser(funcStr, FunctionQParserPlugin.NAME, req); Query q = null; ValueSource vs = null; try { if (parser instanceof FunctionQParser) { FunctionQParser fparser = (FunctionQParser)parser; fparser.setParseMultipleSources(false); fparser.setParseToEnd(false); q = fparser.getQuery(); if (fparser.localParams != null) { if (fparser.valFollowedParams) { // need to find the end of the function query via the string parser int leftOver = fparser.sp.end - fparser.sp.pos; sp.pos = sp.end - leftOver; // reset our parser to the same amount of leftover } else { // the value was via the "v" param in localParams, so we need to find // the end of the local params themselves to pick up where we left off sp.pos = start + fparser.localParamsEnd; } } else { // need to find the end of the function query via the string parser int leftOver = fparser.sp.end - fparser.sp.pos; sp.pos = sp.end - leftOver; // reset our parser to the same amount of leftover } } else { // A QParser that's not for function queries. // It must have been specified via local params. q = parser.getQuery(); assert parser.getLocalParams() != null; sp.pos = start + parser.localParamsEnd; } funcStr = sp.val.substring(start, sp.pos); if (q instanceof FunctionQuery) { vs = ((FunctionQuery)q).getValueSource(); } else { vs = new QueryValueSource(q, 0.0f); } if (key==null) { SolrParams localParams = parser.getLocalParams(); if (localParams != null) { key = localParams.get("key"); } } if (key==null) { key = funcStr; } addField(funcStr, key, augmenters, true); augmenters.addTransformer( new ValueSourceAugmenter( key, parser, vs ) ); } catch (SyntaxError e) { // try again, simple rules for a field name with no whitespace sp.pos = start; field = sp.getSimpleString(); if (req.getSchema().getFieldOrNull(field) != null) { // OK, it was an oddly named field addField(field, key, augmenters, false); if( key != null ) { rename.add(field, key); } } else { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Error parsing fieldname: " + e.getMessage(), e); } } // end try as function } // end for(;;) } catch (SyntaxError e) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Error parsing fieldname", e); } } private void addField(String field, String key, DocTransformers augmenters, boolean isPseudoField) { if(reqFieldNames==null) { reqFieldNames = new LinkedHashSet<>(); } if(key==null) { reqFieldNames.add(field); } else { reqFieldNames.add(key); } if ( ! isPseudoField) { // fields is returned by getLuceneFieldNames(), to be used to select which real fields // to return, so pseudo-fields should not be added fields.add(field); } okFieldNames.add( field ); okFieldNames.add( key ); // a valid field name if(SCORE.equals(field)) { _wantsScore = true; String disp = (key==null) ? field : key; augmenters.addTransformer( new ScoreAugmenter( disp ) ); } } @Override public Set<String> getLuceneFieldNames() { return getLuceneFieldNames(false); } @Override public Set<String> getLuceneFieldNames(boolean ignoreWantsAll) { if (ignoreWantsAll) return fields; else return (_wantsAllFields || fields.isEmpty()) ? null : fields; } @Override public Set<String> getRequestedFieldNames() { if(_wantsAllFields || reqFieldNames==null || reqFieldNames.isEmpty()) { return null; } return reqFieldNames; } @Override public boolean hasPatternMatching() { return !globs.isEmpty(); } @Override public boolean wantsField(String name) { if( _wantsAllFields || okFieldNames.contains( name ) ){ return true; } for( String s : globs ) { // TODO something better? if( FilenameUtils.wildcardMatch(name, s) ) { okFieldNames.add(name); // Don't calculate it again return true; } } return false; } @Override public boolean wantsAllFields() { return _wantsAllFields; } @Override public boolean wantsScore() { return _wantsScore; } @Override public DocTransformer getTransformer() { return transformer; } @Override public String toString() { final StringBuilder sb = new StringBuilder("SolrReturnFields=("); sb.append("globs="); sb.append(globs); sb.append(",fields="); sb.append(fields); sb.append(",okFieldNames="); sb.append(okFieldNames); sb.append(",reqFieldNames="); sb.append(reqFieldNames); sb.append(",transformer="); sb.append(transformer); sb.append(",wantsScore="); sb.append(_wantsScore); sb.append(",wantsAllFields="); sb.append(_wantsAllFields); sb.append(')'); return sb.toString(); } }