SOLR-2444 -- refactor to support pseudo fields

git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/branches/pseudo@1088812 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/solr/src/common/org/apache/solr/common/params/CommonParams.java b/solr/src/common/org/apache/solr/common/params/CommonParams.java
index 97e613f..90765e2 100755
--- a/solr/src/common/org/apache/solr/common/params/CommonParams.java
+++ b/solr/src/common/org/apache/solr/common/params/CommonParams.java
@@ -55,7 +55,11 @@
   /** query and init param for field list */
   public static final String FL = "fl";
 
-  /** Pseudo fields */
+  /** Pseudo fields -- 
+   *  To enable pseudo fields, use fl.pseudo=true
+   *  then send a pseudo field in for each field that should be remapped.
+   *  For example: fl.pseudo.name=foo(x,y)
+   */
   public static final String PSEUDO_FL = "fl.pseudo";
   
   /** default query field */
diff --git a/solr/src/java/org/apache/solr/response/transform/DocTransformers.java b/solr/src/java/org/apache/solr/response/transform/DocTransformers.java
index 3aef76e..dfccb56 100644
--- a/solr/src/java/org/apache/solr/response/transform/DocTransformers.java
+++ b/solr/src/java/org/apache/solr/response/transform/DocTransformers.java
@@ -48,6 +48,10 @@
     str.append( "]" );
     return str.toString();
   }
+  
+  public DocTransformer get( int idx ) {
+    return children.get( idx );
+  }
 
   public void addTransformer( DocTransformer a ) {
     children.add( a );
diff --git a/solr/src/java/org/apache/solr/response/transform/ValueAugmenterFactory.java b/solr/src/java/org/apache/solr/response/transform/ValueAugmenterFactory.java
index c2903b0..9c8c4e4 100644
--- a/solr/src/java/org/apache/solr/response/transform/ValueAugmenterFactory.java
+++ b/solr/src/java/org/apache/solr/response/transform/ValueAugmenterFactory.java
@@ -42,7 +42,7 @@
 
   public static Object getObjectFrom( String str )
   {
-    int idx = str.indexOf( ':' );
+    int idx = str.indexOf( ' ' );
     if( idx > 0 ) {
       String type = str.substring(0,idx);
       String val = str.substring(idx+1);
diff --git a/solr/src/java/org/apache/solr/search/ReturnFields.java b/solr/src/java/org/apache/solr/search/ReturnFields.java
index c571b0a..117c545 100644
--- a/solr/src/java/org/apache/solr/search/ReturnFields.java
+++ b/solr/src/java/org/apache/solr/search/ReturnFields.java
@@ -42,6 +42,7 @@
 import org.apache.solr.search.function.FunctionQuery;
 import org.apache.solr.search.function.QueryValueSource;
 import org.apache.solr.search.function.ValueSource;
+import org.apache.solr.util.SolrPluginUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -71,10 +72,10 @@
   }
 
   public ReturnFields(SolrQueryRequest req) {
-    this( req.getParams().getParams(CommonParams.FL), 
-      req.getParams().getParams(CommonParams.PSEUDO_FL), req );
+    this( req.getParams().getParams(CommonParams.FL), req );
   }
   
+  // This is only used in testing?
   public ReturnFields(String fl, SolrQueryRequest req) {
     if( fl == null ) {
       parseFieldList((String[])null, req);
@@ -94,10 +95,7 @@
     }
   }
 
-  public ReturnFields(String[] fl, String[] pseudo, SolrQueryRequest req) {
-    if( pseudo != null && fl != null && fl.length > 0 ) {
-      parsePseudoFields(pseudo);
-    }
+  public ReturnFields(String[] fl, SolrQueryRequest req) {
     parseFieldList(fl, req);
   }
 
@@ -109,10 +107,11 @@
       return;
     }
 
+    boolean checkPseudoFields = req.getParams().getBool(CommonParams.PSEUDO_FL, false);
     NamedList<String> rename = new NamedList<String>();
     DocTransformers augmenters = new DocTransformers();
     for (String fieldList : fl) {
-      add(fieldList,rename,augmenters,req);
+      addFieldParam(fieldList,checkPseudoFields,rename,augmenters,req);
     }
     if( rename.size() > 0 ) {
       for( int i=0; i<rename.size(); i++ ) {
@@ -122,7 +121,7 @@
     }
 
     // Legacy behavior? "score" == "*,score"  Distributed tests for this
-    if( fields.size() == 1 && _wantsScore ) {
+    if( _wantsScore && fields.size() == 1 && augmenters.size() == 1 ) {
       _wantsAllFields = true;
     }
 
@@ -144,137 +143,127 @@
       transformer = augmenters;
     }
   }
-  
-  public Map<String,String> pseudo = null;
-  
-  private void parsePseudoFields( String[] fields ) {
-    if( fields != null ) {
-      pseudo = new HashMap<String, String>();
-      for( String f : fields ) {
-        int idx = f.indexOf( ':' );
+
+  /**
+   * TODO, this should be a fancy parser that splits on ',' but respects quoted things
+   */
+  private void addFieldParam(String fls, boolean checkPseudoFields, NamedList<String> rename, DocTransformers augmenters, SolrQueryRequest req) {
+    if( fls != null ) {
+      String[] fields = fls.split(",");
+      for( String fl : fields ) {
+        String as = null;
+        int idx = fl.indexOf( ':' );
         if( idx > 0 ) {
-          String p = f.substring(0,idx);
-          String r = f.substring(idx+1);
-          pseudo.put(p, r);
+          as = fl.substring(0,idx).trim();
+          fl = fl.substring(idx+1).trim();
         }
         else {
-          throw new SolrException( ErrorCode.BAD_REQUEST, "Pseudo fields must be in the form ?fl.pseudo=hello:replace" );
+          fl = fl.trim();
         }
+        
+        if( checkPseudoFields ) {
+          String pseudo = req.getParams().get( CommonParams.PSEUDO_FL+'.'+fl );
+          if( pseudo != null ) {
+            if( as == null ) {
+              as = fl;  // use the original
+            }
+            
+            // Just replace the input text
+            okFieldNames.add( fl );
+            fl = pseudo;
+          }
+        }
+        
+        addField( fl, as, rename, augmenters, req );
       }
     }
   }
-
-  private void add(String fls, NamedList<String> rename, DocTransformers augmenters, SolrQueryRequest req) {
-    // commas deliminate fields?  is this true?
-    StringTokenizer st = new StringTokenizer( fls, "," );
-    while( st.hasMoreTokens() ) {
-      String as = null;
-      String fl = st.nextToken().trim();
-      int idx = fl.lastIndexOf( " AS " );
+  
+  private void addField(String fl, String display, NamedList<String> rename, DocTransformers augmenters, SolrQueryRequest req) {
+    // Maybe it is everything
+    if( "*".equals( fl ) ) {
+      if( display != null ) {
+        throw new SolrException( ErrorCode.BAD_REQUEST, "* can not use an 'AS' request" );
+      }
+      _wantsAllFields = true;
+      return;
+    }
+    
+    // maybe it is a Transformer (starts and ends with [])
+    if( fl.charAt( 0 ) == '[' && fl.charAt( fl.length()-1 ) == ']' ) {
+      String name = null;
+      String args = null;
+      int idx = fl.indexOf( ' ' );
       if( idx > 0 ) {
-        as = fl.substring( idx+4 ).trim();
-        fl = fl.substring(0,idx).trim();
-      }
-      
-      // check if the fl is a pseudo field
-      if( pseudo != null ) {
-        String p = pseudo.get( fl );
-        if( p != null ) {
-          if( as == null ) {
-            as = fl;  // use the original
-          }
-          
-          // Just replace the input text
-          okFieldNames.add( fl );
-          fl = p;
-        }
-      }
-      
-      // Maybe it is everything
-      if( "*".equals( fl ) ) {
-        if( as != null ) {
-          throw new SolrException( ErrorCode.BAD_REQUEST, "* can not use an 'AS' request" );
-        }
-        _wantsAllFields = true;
-        continue;
-      }
-      
-      // maybe it is a Transformer (starts and ends with [])
-      if( fl.charAt( 0 ) == '[' && fl.charAt( fl.length()-1 ) == ']' ) {
-        String name = null;
-        String args = null;
-        idx = fl.indexOf( ':' );
-        if( idx > 0 ) {
-          name = fl.substring(1,idx);
-          args = fl.substring(idx+1,fl.length()-1);
-        }
-        else {
-          name = fl.substring(1,fl.length()-1 );
-        }
-
-        TransformerFactory factory = req.getCore().getTransformerFactory( name );
-        if( factory != null ) {
-          augmenters.addTransformer( factory.create(as==null?fl:as, args, req) );
-          continue;
-        }
-        else {
-          // unknown field?  field that starts with [ and ends with ]?
-        }
-      }
-      
-      // If it has a ( it may be a FunctionQuery
-      else if( StringUtils.contains(fl, '(' ) ) {
-        try {
-          QParser parser = QParser.getParser(fl, FunctionQParserPlugin.NAME, req);
-          Query q = null;
-          ValueSource vs = null;
-
-          if (parser instanceof FunctionQParser) {
-            FunctionQParser fparser = (FunctionQParser)parser;
-            fparser.setParseMultipleSources(false);
-            fparser.setParseToEnd(false);
-
-            q = fparser.getQuery();
-          } else {
-            // A QParser that's not for function queries.
-            // It must have been specified via local params.
-            q = parser.getQuery();
-            assert parser.getLocalParams() != null;
-          }
-
-          if (q instanceof FunctionQuery) {
-            vs = ((FunctionQuery)q).getValueSource();
-          } else {
-            vs = new QueryValueSource(q, 0.0f);
-          }
-          
-          okFieldNames.add( fl );
-          okFieldNames.add( as );
-          augmenters.addTransformer( new ValueSourceAugmenter( as==null?fl:as, parser, vs ) );
-          continue;
-        }
-        catch (Exception e) {
-          // Its OK... could just be a wierd field name
-        }
-      }
-      
-      // TODO? support fancy globs?
-      else if( fl.endsWith( "*" ) || fl.startsWith( "*" ) ) {
-        globs.add( fl );
-        continue;
-      }
-
-      fields.add( fl ); // need to put in the map to maintain order for things like CSVResponseWriter
-      okFieldNames.add( fl );
-      okFieldNames.add( as );
-      
-      if( SCORE.equals(fl)) {
-        _wantsScore = true;
-        augmenters.addTransformer( new ScoreAugmenter( as==null?fl:as ) );
+        name = fl.substring(1,idx);
+        args = fl.substring(idx+1,fl.length()-1);
       }
       else {
-        // it is a normal field
+        name = fl.substring(1,fl.length()-1 );
       }
+
+      TransformerFactory factory = req.getCore().getTransformerFactory( name );
+      if( factory != null ) {
+        augmenters.addTransformer( factory.create(display==null?fl:display, args, req) );
+        return;
+      }
+      else {
+        // unknown field?  field that starts with [ and ends with ]?
+      }
+    }
+    
+    // If it has a ( it may be a FunctionQuery
+    else if( StringUtils.contains(fl, '(' ) ) {
+      try {
+        QParser parser = QParser.getParser(fl, FunctionQParserPlugin.NAME, req);
+        Query q = null;
+        ValueSource vs = null;
+
+        if (parser instanceof FunctionQParser) {
+          FunctionQParser fparser = (FunctionQParser)parser;
+          fparser.setParseMultipleSources(false);
+          fparser.setParseToEnd(false);
+
+          q = fparser.getQuery();
+        } else {
+          // A QParser that's not for function queries.
+          // It must have been specified via local params.
+          q = parser.getQuery();
+          assert parser.getLocalParams() != null;
+        }
+
+        if (q instanceof FunctionQuery) {
+          vs = ((FunctionQuery)q).getValueSource();
+        } else {
+          vs = new QueryValueSource(q, 0.0f);
+        }
+        
+        okFieldNames.add( fl );
+        okFieldNames.add( display );
+        augmenters.addTransformer( new ValueSourceAugmenter( display==null?fl:display, parser, vs ) );
+        return;
+      }
+      catch (Exception e) {
+        // Its OK... could just be a wierd field name
+      }
+    }
+    
+    // TODO? support fancy globs?
+    else if( fl.endsWith( "*" ) || fl.startsWith( "*" ) ) {
+      globs.add( fl );
+      return;
+    }
+
+    fields.add( fl ); // need to put in the map to maintain order for things like CSVResponseWriter
+    okFieldNames.add( fl );
+    okFieldNames.add( display );
+    
+    if( SCORE.equals(fl)) {
+      _wantsScore = true;
+      augmenters.addTransformer( new ScoreAugmenter( display==null?fl:display ) );
+    }
+    else {
+      // it is a normal field
     }
   }
  
diff --git a/solr/src/test/org/apache/solr/ConvertedLegacyTest.java b/solr/src/test/org/apache/solr/ConvertedLegacyTest.java
index 92cdcfc..5f731fc 100644
--- a/solr/src/test/org/apache/solr/ConvertedLegacyTest.java
+++ b/solr/src/test/org/apache/solr/ConvertedLegacyTest.java
@@ -1114,7 +1114,8 @@
     assertQ(req("id:44")
             );
     args = new HashMap<String,String>();
-    args.put("fl","fname_s,arr_f  ");
+    args.put("fl","fname_s,arr_f  "); /// space should include score?
+    args.put("fl","fname_s,arr_f,score"); // really?  is this what we want in 4.0?
     req = new LocalSolrQueryRequest(h.getCore(), "id:44",
                                     "standard", 0, 10, args);
     assertQ(req
@@ -1133,7 +1134,7 @@
     // test addition of score field
 
     args = new HashMap<String,String>();
-    args.put("fl","score ");
+    args.put("fl","score "); 
     req = new LocalSolrQueryRequest(h.getCore(), "id:44",
                                     "standard", 0, 10, args);
     assertQ(req
diff --git a/solr/src/test/org/apache/solr/client/solrj/SolrExampleTests.java b/solr/src/test/org/apache/solr/client/solrj/SolrExampleTests.java
index fe04936..5d3b01b 100644
--- a/solr/src/test/org/apache/solr/client/solrj/SolrExampleTests.java
+++ b/solr/src/test/org/apache/solr/client/solrj/SolrExampleTests.java
@@ -405,7 +405,7 @@
     
     SolrQuery query = new SolrQuery();
     query.setQuery( "*:*" );
-    query.set( CommonParams.FL, "id,price,[docid],[explain:nl],score,[value:aaa] AS aaa,[value:int:10] AS ten" );
+    query.set( CommonParams.FL, "id,price,[docid],[explain nl],score,aaa:[value aaa],ten:[value int 10]" );
     query.addSortField( "price", SolrQuery.ORDER.asc );
     QueryResponse rsp = server.query( query );
     
@@ -423,8 +423,10 @@
     int id2 = (Integer)out2.getFieldValue( "[docid]" );
     assertTrue( "should be bigger ["+id1+","+id2+"]", id2 > id1 );
     
+    System.out.println( out1 );
+    
     // The score from explain should be the same as the score
-    NamedList explain = (NamedList)out1.getFieldValue( "[explain:nl]" );
+    NamedList explain = (NamedList)out1.getFieldValue( "[explain nl]" );
     assertEquals( out1.get( "score"), explain.get( "value" ) );
     
     // Augmented _value_ with alias
diff --git a/solr/src/test/org/apache/solr/search/TestSolrQueryParser.java b/solr/src/test/org/apache/solr/search/TestSolrQueryParser.java
index dec3319..9f3b605 100644
--- a/solr/src/test/org/apache/solr/search/TestSolrQueryParser.java
+++ b/solr/src/test/org/apache/solr/search/TestSolrQueryParser.java
@@ -17,7 +17,9 @@
 package org.apache.solr.search;
 
 import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.response.transform.DocTransformers;
 import org.apache.solr.response.transform.ScoreAugmenter;
+import org.apache.solr.response.transform.ValueAugmenterFactory;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
@@ -64,11 +66,18 @@
     assertTrue( rf.wantsField( "xxx" ) );
     assertTrue( rf.wantsAllFields() );
     assertNull( rf.getTransformer() );
+
+    // legacy, score is *,score
+    rf = new ReturnFields( req("fl", "score ") );
+    assertTrue( rf.wantsScore() );
+    assertTrue( rf.wantsAllFields() );
+    assertTrue( rf.wantsField( "score" ) );
     
-    rf = new ReturnFields( req("fl", "[explain]") );
-    assertFalse( rf.wantsScore() );
+    rf = new ReturnFields( req("fl", "[explain],score") );
+    assertTrue( rf.wantsScore() );
     assertFalse( rf.wantsField( "id" ) );
-    assertEquals( "[explain]", rf.getTransformer().getName() );
+    assertEquals( "Transformers[[explain],score]", rf.getTransformer().getName() );
+    assertTrue( rf.wantsScore() );
 
     // Check that we want wildcards
     rf = new ReturnFields( req("fl", "id,aaa*,*bbb") );
@@ -79,14 +88,22 @@
     assertFalse( rf.wantsField( "bb" ) );
     
     // Check pseudo fiels are replaced
-    rf = new ReturnFields( req("fl", "price", "fl.pseudo", "price:[value:10]" ) );
+    rf = new ReturnFields( req("fl", "price", "fl.pseudo.price", "[value 10]", "fl.pseudo", "true" ) );
     assertTrue( rf.wantsField( "price" ) );
     assertEquals( "price", rf.getTransformer().getName() );
+    assertTrue( rf.getTransformer().getClass().getName().indexOf( "ValueAugmenter" ) > 0 );
 
-    rf = new ReturnFields( req("fl", "price AS xxx,name AS yyy", "fl.pseudo", "price:[value:10]" ) );
+    rf = new ReturnFields( req("fl", "xxx:price,yyy:name", "fl.pseudo.price", "[value 10]", "fl.pseudo", "true" ) );
     assertTrue( rf.wantsField( "price" ) );
     assertTrue( rf.wantsField( "yyy" ) );
     assertTrue( rf.getLuceneFieldNames().contains("name") );
     assertEquals( "xxx", rf.getTransformer().getName() );
+    assertTrue( rf.getTransformer().getClass().getName().indexOf( "ValueAugmenter" ) > 0 );
+
+    // multiple transformers
+    rf = new ReturnFields( req("fl", "[value hello],[explain]" ) );
+    DocTransformers tx = (DocTransformers)rf.getTransformer(); // will throw exception
+    assertTrue( tx.get(0).getClass().getName().indexOf( "ValueAugmenter" ) > 0 );
+    assertTrue( tx.get(1).getClass().getName().indexOf( "ExplainAugmenter" ) > 0 );
   }
 }