lucene-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From sar...@apache.org
Subject svn commit: r1453161 [2/3] - in /lucene/dev/trunk: dev-tools/maven/ dev-tools/maven/solr/ dev-tools/maven/solr/core/src/java/ lucene/ lucene/analysis/common/src/java/org/apache/lucene/analysis/util/ solr/ solr/core/ solr/core/src/java/org/apache/solr/c...
Date Wed, 06 Mar 2013 04:50:35 GMT
Modified: lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/FieldType.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/FieldType.java?rev=1453161&r1=1453160&r2=1453161&view=diff
==============================================================================
--- lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/FieldType.java (original)
+++ lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/FieldType.java Wed Mar  6 04:50:33 2013
@@ -19,17 +19,23 @@ package org.apache.solr.schema;
 
 import java.io.IOException;
 import java.io.Reader;
+import java.util.ArrayList;
 import java.util.Collections;
 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.lucene.analysis.Analyzer;
 import org.apache.lucene.analysis.Tokenizer;
 import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
 import org.apache.lucene.analysis.tokenattributes.OffsetAttribute;
+import org.apache.lucene.analysis.util.CharFilterFactory;
+import org.apache.lucene.analysis.util.TokenFilterFactory;
+import org.apache.lucene.analysis.util.TokenizerFactory;
 import org.apache.lucene.document.Field;
-import org.apache.lucene.index.FieldInfo.DocValuesType;
 import org.apache.lucene.index.FieldInfo.IndexOptions;
 import org.apache.lucene.index.StorableField;
 import org.apache.lucene.index.Term;
@@ -49,8 +55,12 @@ import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.CharsRef;
 import org.apache.lucene.util.UnicodeUtil;
 import org.apache.solr.analysis.SolrAnalyzer;
+import org.apache.solr.analysis.TokenizerChain;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrException.ErrorCode;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.common.util.StrUtils;
 import org.apache.solr.response.TextResponseWriter;
 import org.apache.solr.search.QParser;
 import org.apache.solr.search.Sorting;
@@ -82,6 +92,8 @@ public abstract class FieldType extends 
   /** properties explicitly set to false */
   protected int falseProperties;
   protected int properties;
+  private boolean isExplicitQueryAnalyzer;
+  private boolean isExplicitAnalyzer;
 
 
   /** Returns true if fields of this type should be tokenized */
@@ -138,7 +150,7 @@ public abstract class FieldType extends 
       args.remove("compressThreshold");
     }
 
-    this.args=args;
+    this.args = Collections.unmodifiableMap(args);
     Map<String,String> initArgs = new HashMap<String,String>(args);
 
     trueProperties = FieldProperties.parseProperties(initArgs,true);
@@ -151,25 +163,25 @@ public abstract class FieldType extends 
 
     init(schema, initArgs);
 
-    String positionInc = initArgs.get("positionIncrementGap");
+    String positionInc = initArgs.get(POSITION_INCREMENT_GAP);
     if (positionInc != null) {
       Analyzer analyzer = getAnalyzer();
       if (analyzer instanceof SolrAnalyzer) {
         ((SolrAnalyzer)analyzer).setPositionIncrementGap(Integer.parseInt(positionInc));
       } else {
-        throw new RuntimeException("Can't set positionIncrementGap on custom analyzer " + analyzer.getClass());
+        throw new RuntimeException("Can't set " + POSITION_INCREMENT_GAP + " on custom analyzer " + analyzer.getClass());
       }
       analyzer = getQueryAnalyzer();
       if (analyzer instanceof SolrAnalyzer) {
         ((SolrAnalyzer)analyzer).setPositionIncrementGap(Integer.parseInt(positionInc));
       } else {
-        throw new RuntimeException("Can't set positionIncrementGap on custom analyzer " + analyzer.getClass());
+        throw new RuntimeException("Can't set " + POSITION_INCREMENT_GAP + " on custom analyzer " + analyzer.getClass());
       }
-      initArgs.remove("positionIncrementGap");
+      initArgs.remove(POSITION_INCREMENT_GAP);
     }
 
-    this.postingsFormat = initArgs.remove("postingsFormat");
-    this.docValuesFormat = initArgs.remove("docValuesFormat");
+    this.postingsFormat = initArgs.remove(POSTINGS_FORMAT);
+    this.docValuesFormat = initArgs.remove(DOC_VALUES_FORMAT);
 
     if (initArgs.size() > 0) {
       throw new RuntimeException("schema fieldtype " + typeName
@@ -385,7 +397,23 @@ public abstract class FieldType extends 
     UnicodeUtil.UTF16toUTF8(internal, 0, internal.length(), result);
   }
 
-  /**
+  public void setIsExplicitQueryAnalyzer(boolean isExplicitQueryAnalyzer) {
+    this.isExplicitQueryAnalyzer = isExplicitQueryAnalyzer;
+  }
+
+  public boolean isExplicitQueryAnalyzer() {
+    return isExplicitQueryAnalyzer;
+  }
+
+  public void setIsExplicitAnalyzer(boolean explicitAnalyzer) {
+    isExplicitAnalyzer = explicitAnalyzer;
+  }
+
+  public boolean isExplicitAnalyzer() {
+    return isExplicitAnalyzer;
+  }
+
+    /**
    * Default analyzer for types that only produce 1 verbatim token...
    * A maximum size of chars to be read must be specified
    */
@@ -500,8 +528,11 @@ public abstract class FieldType extends 
   }
 
   /** @lucene.internal */
+  protected SimilarityFactory similarityFactory;
+
+  /** @lucene.internal */
   protected Similarity similarity;
-  
+
   /**
    * Gets the Similarity used when scoring fields of this type
    * 
@@ -516,6 +547,21 @@ public abstract class FieldType extends 
     return similarity;
   }
 
+  /**
+   * Gets the factory for the Similarity used when scoring fields of this type
+   *
+   * <p>
+   * The default implementation returns null, which means this type
+   * has no custom similarity factory associated with it.
+   * </p>
+   *
+   * @lucene.internal
+   */
+  public SimilarityFactory getSimilarityFactory() {
+    return similarityFactory;
+  }
+
+
   /** Return the numeric type of this field, or null if this field is not a
    *  numeric field. */
   public org.apache.lucene.document.FieldType.NumericType getNumericType() {
@@ -526,8 +572,9 @@ public abstract class FieldType extends 
    * Sets the Similarity used when scoring fields of this type
    * @lucene.internal
    */
-  public void setSimilarity(Similarity similarity) {
-    this.similarity = similarity;
+  public void setSimilarity(SimilarityFactory similarityFactory) {
+    this.similarityFactory = similarityFactory;
+    this.similarity = similarityFactory.getSimilarity();
   }
   
   /**
@@ -675,4 +722,188 @@ public abstract class FieldType extends 
     }
   }
 
+  private static final String TYPE_NAME = "name";
+  private static final String CLASS_NAME = "class";
+  private static final String ANALYZER = "analyzer";
+  private static final String INDEX_ANALYZER = "indexAnalyzer";
+  private static final String QUERY_ANALYZER = "queryAnalyzer";
+  private static final String MULTI_TERM_ANALYZER = "multiTermAnalyzer";
+  private static final String SIMILARITY = "similarity";
+  private static final String POSTINGS_FORMAT = "postingsFormat";
+  private static final String DOC_VALUES_FORMAT = "docValuesFormat";
+  private static final String AUTO_GENERATE_PHRASE_QUERIES = "autoGeneratePhraseQueries";
+  private static final String ARGS = "args";
+  private static final String CHAR_FILTERS = "charFilters";
+  private static final String TOKENIZER = "tokenizer";
+  private static final String FILTERS = "filters";
+  private static final String POSITION_INCREMENT_GAP = "positionIncrementGap";
+
+  /**
+   * Get a map of property name -> value for this field type. 
+   * @param showDefaults if true, include default properties.
+   */
+  public SimpleOrderedMap<Object> getNamedPropertyValues(boolean showDefaults) {
+    SimpleOrderedMap<Object> namedPropertyValues = new SimpleOrderedMap<Object>();
+    namedPropertyValues.add(TYPE_NAME, getTypeName());
+    namedPropertyValues.add(CLASS_NAME, normalizeSPIname(getClass().getName()));
+    if (showDefaults) {
+      Map<String,String> fieldTypeArgs = getNonFieldPropertyArgs();
+      if (null != fieldTypeArgs) {
+        for (String key : fieldTypeArgs.keySet()) {
+          namedPropertyValues.add(key, fieldTypeArgs.get(key));
+        }
+      }
+      if (this instanceof TextField) {
+        namedPropertyValues.add(AUTO_GENERATE_PHRASE_QUERIES, ((TextField) this).getAutoGeneratePhraseQueries());
+      }
+      namedPropertyValues.add(getPropertyName(INDEXED), hasProperty(INDEXED));
+      namedPropertyValues.add(getPropertyName(STORED), hasProperty(STORED));
+      namedPropertyValues.add(getPropertyName(DOC_VALUES), hasProperty(DOC_VALUES));
+      namedPropertyValues.add(getPropertyName(STORE_TERMVECTORS), hasProperty(STORE_TERMVECTORS));
+      namedPropertyValues.add(getPropertyName(STORE_TERMPOSITIONS), hasProperty(STORE_TERMPOSITIONS));
+      namedPropertyValues.add(getPropertyName(STORE_TERMOFFSETS), hasProperty(STORE_TERMOFFSETS));
+      namedPropertyValues.add(getPropertyName(OMIT_NORMS), hasProperty(OMIT_NORMS));
+      namedPropertyValues.add(getPropertyName(OMIT_TF_POSITIONS), hasProperty(OMIT_TF_POSITIONS));
+      namedPropertyValues.add(getPropertyName(OMIT_POSITIONS), hasProperty(OMIT_POSITIONS));
+      namedPropertyValues.add(getPropertyName(STORE_OFFSETS), hasProperty(STORE_OFFSETS));
+      namedPropertyValues.add(getPropertyName(MULTIVALUED), hasProperty(MULTIVALUED));
+      if (hasProperty(SORT_MISSING_FIRST)) {
+        namedPropertyValues.add(getPropertyName(SORT_MISSING_FIRST), true);
+      } else if (hasProperty(SORT_MISSING_LAST)) {
+        namedPropertyValues.add(getPropertyName(SORT_MISSING_LAST), true);
+      }
+      namedPropertyValues.add(getPropertyName(TOKENIZED), isTokenized());
+      // The BINARY property is always false
+      // namedPropertyValues.add(getPropertyName(BINARY), hasProperty(BINARY));
+    } else { // Don't show defaults
+      Set<String> fieldProperties = new HashSet<String>();
+      for (String propertyName : FieldProperties.propertyNames) {
+        fieldProperties.add(propertyName);
+      }
+      for (String key : args.keySet()) {
+        if (fieldProperties.contains(key)) {
+          namedPropertyValues.add(key, StrUtils.parseBool(args.get(key)));
+        } else {
+          namedPropertyValues.add(key, args.get(key));
+        }
+      }
+    }
+    
+    if (isExplicitAnalyzer()) {
+      String analyzerProperty = isExplicitQueryAnalyzer() ? INDEX_ANALYZER : ANALYZER;
+      namedPropertyValues.add(analyzerProperty, getAnalyzerProperties(getAnalyzer()));
+    } 
+    if (isExplicitQueryAnalyzer()) {
+      String analyzerProperty = isExplicitAnalyzer() ? QUERY_ANALYZER : ANALYZER;
+      namedPropertyValues.add(analyzerProperty, getAnalyzerProperties(getQueryAnalyzer()));
+    }
+    if (this instanceof TextField) {
+      if (((TextField)this).isExplicitMultiTermAnalyzer()) {
+        namedPropertyValues.add(MULTI_TERM_ANALYZER, getAnalyzerProperties(((TextField) this).getMultiTermAnalyzer()));
+      }
+    }
+    if (null != getSimilarity()) {
+      namedPropertyValues.add(SIMILARITY, getSimilarityProperties());
+    }
+    if (null != getPostingsFormat()) {
+      namedPropertyValues.add(POSTINGS_FORMAT, getPostingsFormat());
+    }
+    if (null != getDocValuesFormat()) {
+      namedPropertyValues.add(DOC_VALUES_FORMAT, getDocValuesFormat());
+    }
+    return namedPropertyValues;
+  }
+
+  /** Returns args to this field type that aren't standard field properties */
+  protected Map<String,String> getNonFieldPropertyArgs() {
+    Map<String,String> initArgs =  new HashMap<String,String>(args);
+    for (String prop : FieldProperties.propertyNames) {
+      initArgs.remove(prop);
+    }
+    return initArgs;
+  }
+
+  /** 
+   * Returns a description of the given analyzer, by either reporting the Analyzer name
+   * if it's not a TokenizerChain, or if it is, querying each analysis factory for its
+   * name and args.
+   */
+  protected static SimpleOrderedMap<Object> getAnalyzerProperties(Analyzer analyzer) {
+    SimpleOrderedMap<Object> analyzerProps = new SimpleOrderedMap<Object>();
+    analyzerProps.add(CLASS_NAME, normalizeSPIname(analyzer.getClass().getName()));
+    
+    if (analyzer instanceof TokenizerChain) {
+      Map<String,String> factoryArgs;
+      TokenizerChain tokenizerChain = (TokenizerChain)analyzer;
+      CharFilterFactory[] charFilterFactories = tokenizerChain.getCharFilterFactories();
+      if (null != charFilterFactories && charFilterFactories.length > 0) {
+        List<SimpleOrderedMap<Object>> charFilterProps = new ArrayList<SimpleOrderedMap<Object>>();
+        for (CharFilterFactory charFilterFactory : charFilterFactories) {
+          SimpleOrderedMap<Object> props = new SimpleOrderedMap<Object>();
+          props.add(CLASS_NAME, normalizeSPIname(charFilterFactory.getClass().getName()));
+          factoryArgs = charFilterFactory.getOriginalArgs();
+          if (null != factoryArgs) {
+            for (String key : factoryArgs.keySet()) {
+              props.add(key, factoryArgs.get(key));
+            }
+          }
+          charFilterProps.add(props);
+        }
+        analyzerProps.add(CHAR_FILTERS, charFilterProps);
+      }
+
+      SimpleOrderedMap<Object> tokenizerProps = new SimpleOrderedMap<Object>();
+      TokenizerFactory tokenizerFactory = tokenizerChain.getTokenizerFactory();
+      tokenizerProps.add(CLASS_NAME, normalizeSPIname(tokenizerFactory.getClass().getName()));
+      factoryArgs = tokenizerFactory.getOriginalArgs();
+      if (null != factoryArgs) {
+        for (String key : factoryArgs.keySet()) {
+          tokenizerProps.add(key, factoryArgs.get(key));
+        }
+      }
+      analyzerProps.add(TOKENIZER, tokenizerProps);
+
+      TokenFilterFactory[] filterFactories = tokenizerChain.getTokenFilterFactories();
+      if (null != filterFactories && filterFactories.length > 0) {
+        List<SimpleOrderedMap<Object>> filterProps = new ArrayList<SimpleOrderedMap<Object>>();
+        for (TokenFilterFactory filterFactory : filterFactories) {
+          SimpleOrderedMap<Object> props = new SimpleOrderedMap<Object>();
+          props.add(CLASS_NAME, normalizeSPIname(filterFactory.getClass().getName()));
+          factoryArgs = filterFactory.getOriginalArgs();
+          if (null != factoryArgs) {
+            for (String key : factoryArgs.keySet()) {
+              props.add(key, factoryArgs.get(key));
+            }
+          }
+          filterProps.add(props);
+        }
+        analyzerProps.add(FILTERS, filterProps);
+      }
+    }
+    return analyzerProps;
+  }
+
+  private static String normalizeSPIname(String fullyQualifiedName) {
+    if (fullyQualifiedName.startsWith("org.apache.lucene.") || fullyQualifiedName.startsWith("org.apache.solr.")) {
+      return "solr" + fullyQualifiedName.substring(fullyQualifiedName.lastIndexOf('.')); 
+    }
+    return fullyQualifiedName;
+  }
+
+  /** Returns a description of this field's similarity, if any */
+  protected SimpleOrderedMap<Object> getSimilarityProperties() {
+    SimpleOrderedMap<Object> props = new SimpleOrderedMap<Object>();
+    if (similarity != null) {
+      props.add(CLASS_NAME, normalizeSPIname(similarity.getClass().getName()));
+      SolrParams factoryParams = similarityFactory.getParams();
+      if (null != factoryParams) {
+        Iterator<String> iter = factoryParams.getParameterNamesIterator();
+        while (iter.hasNext()) {
+          String key = iter.next();
+          props.add(key, factoryParams.get(key));
+        }
+      }
+    }
+    return props;
+  }
 }

Modified: lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/FieldTypePluginLoader.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/FieldTypePluginLoader.java?rev=1453161&r1=1453160&r2=1453161&view=diff
==============================================================================
--- lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/FieldTypePluginLoader.java (original)
+++ lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/FieldTypePluginLoader.java Wed Mar  6 04:50:33 2013
@@ -98,20 +98,35 @@ public final class FieldTypePluginLoader
     expression = "./similarity";
     anode = (Node)xpath.evaluate(expression, node, XPathConstants.NODE);
     SimilarityFactory simFactory = IndexSchema.readSimilarity(loader, anode);
+    if (null != simFactory) {
+      ft.setSimilarity(simFactory);
+    }
     
-    if (queryAnalyzer==null) queryAnalyzer=analyzer;
-    if (analyzer==null) analyzer=queryAnalyzer;
-    if (multiAnalyzer == null) {
-      multiAnalyzer = constructMultiTermAnalyzer(queryAnalyzer);
+    if (null == queryAnalyzer) {
+      queryAnalyzer = analyzer;
+      ft.setIsExplicitQueryAnalyzer(false);
+    } else {
+      ft.setIsExplicitQueryAnalyzer(true);
+    }
+    if (null == analyzer) {
+      analyzer = queryAnalyzer;
+      ft.setIsExplicitAnalyzer(false);
+    } else {
+      ft.setIsExplicitAnalyzer(true);
     }
-    if (analyzer!=null) {
+
+    if (null != analyzer) {
       ft.setAnalyzer(analyzer);
       ft.setQueryAnalyzer(queryAnalyzer);
-      if (ft instanceof TextField)
+      if (ft instanceof TextField) {
+        if (null == multiAnalyzer) {
+          multiAnalyzer = constructMultiTermAnalyzer(queryAnalyzer);
+          ((TextField)ft).setIsExplicitMultiTermAnalyzer(false);
+        } else {
+          ((TextField)ft).setIsExplicitMultiTermAnalyzer(true);
+        }
         ((TextField)ft).setMultiTermAnalyzer(multiAnalyzer);
-    }
-    if (simFactory!=null) {
-      ft.setSimilarity(simFactory.getSimilarity());
+      }
     }
     if (ft instanceof SchemaAware){
       schemaAware.add((SchemaAware) ft);

Modified: lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/IndexSchema.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/IndexSchema.java?rev=1453161&r1=1453160&r2=1453161&view=diff
==============================================================================
--- lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/IndexSchema.java (original)
+++ lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/IndexSchema.java Wed Mar  6 04:50:33 2013
@@ -24,8 +24,8 @@ import org.apache.lucene.index.StorableF
 import org.apache.lucene.index.StoredDocument;
 import org.apache.lucene.search.similarities.Similarity;
 import org.apache.lucene.util.Version;
-import org.apache.lucene.analysis.util.ResourceLoader;
 import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrException.ErrorCode;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.util.DOMUtil;
 import org.apache.solr.util.SystemIdResolver;
@@ -34,13 +34,17 @@ import org.apache.solr.core.Config;
 import org.apache.solr.core.SolrResourceLoader;
 import org.apache.solr.search.similarities.DefaultSimilarityFactory;
 import org.apache.solr.util.plugin.SolrCoreAware;
-import org.w3c.dom.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
 import org.xml.sax.InputSource;
 
 import javax.xml.xpath.XPath;
 import javax.xml.xpath.XPathConstants;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -77,6 +81,7 @@ public final class IndexSchema {
   private final List<SchemaField> fieldsWithDefaultValue = new ArrayList<SchemaField>();
   private final Collection<SchemaField> requiredFields = new HashSet<SchemaField>();
   private DynamicField[] dynamicFields;
+  public DynamicField[] getDynamicFields() { return dynamicFields; }
 
   private Analyzer analyzer;
   private Analyzer queryAnalyzer;
@@ -86,13 +91,16 @@ public final class IndexSchema {
 
 
   private final Map<String, List<CopyField>> copyFieldsMap = new HashMap<String, List<CopyField>>();
+  public Map<String,List<CopyField>> getCopyFieldsMap() { return Collections.unmodifiableMap(copyFieldsMap); }
+  
   private DynamicCopy[] dynamicCopyFields;
+  public DynamicCopy[] getDynamicCopyFields() { return dynamicCopyFields; }
+
   /**
    * keys are all fields copied to, count is num of copyField
    * directives that target them.
    */
-  private Map<SchemaField, Integer> copyFieldTargetCounts
-    = new HashMap<SchemaField, Integer>();
+  private Map<SchemaField, Integer> copyFieldTargetCounts = new HashMap<SchemaField, Integer>();
 
     /**
    * Constructs a schema using the specified resource name and stream.
@@ -122,8 +130,7 @@ public final class IndexSchema {
   /**
    * @since solr 1.4
    */
-  public SolrResourceLoader getResourceLoader()
-  {
+  public SolrResourceLoader getResourceLoader() {
     return loader;
   }
   
@@ -209,8 +216,6 @@ public final class IndexSchema {
    */
   public Analyzer getAnalyzer() { return analyzer; }
 
-
-
   /**
    * Returns the Analyzer used when searching this index
    *
@@ -287,8 +292,7 @@ public final class IndexSchema {
    * 
    * @since solr 1.3
    */
-  public void refreshAnalyzers()
-  {
+  public void refreshAnalyzers() {
     analyzer = new SolrIndexAnalyzer();
     queryAnalyzer = new SolrQueryAnalyzer();
   }
@@ -389,7 +393,7 @@ public final class IndexSchema {
 
         FieldType ft = fieldTypes.get(type);
         if (ft==null) {
-          throw new SolrException( SolrException.ErrorCode.BAD_REQUEST,"Unknown fieldtype '" + type + "' specified on field " + name);
+          throw new SolrException(ErrorCode.BAD_REQUEST,"Unknown fieldtype '" + type + "' specified on field " + name);
         }
 
         Map<String,String> args = DOMUtil.toMapExcept(attrs, "name", "type");
@@ -404,7 +408,7 @@ public final class IndexSchema {
           if( old != null ) {
             String msg = "[schema.xml] Duplicate field definition for '"
               + f.getName() + "' [[["+old.toString()+"]]] and [[["+f.toString()+"]]]";
-            throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, msg );
+            throw new SolrException(ErrorCode.SERVER_ERROR, msg );
           }
           log.debug("field defined: " + f);
           if( f.getDefaultValue() != null ) {
@@ -416,8 +420,14 @@ public final class IndexSchema {
             requiredFields.add(f);
           }
         } else if (node.getNodeName().equals("dynamicField")) {
-          // make sure nothing else has the same path
-          addDynamicField(dFields, f);
+          if (isValidDynamicFieldName(name)) {
+            // make sure nothing else has the same path
+            addDynamicField(dFields, f);
+          } else {
+            String msg = "Dynamic field name '" + name 
+                + "' should have either a leading or a trailing asterisk, and no others.";
+            throw new SolrException(ErrorCode.SERVER_ERROR, msg);
+          }
         } else {
           // we should never get here
           throw new RuntimeException("Unknown field type");
@@ -454,7 +464,7 @@ public final class IndexSchema {
         if (null != ft.getSimilarity()) {
           String msg = "FieldType '" + ft.getTypeName() + "' is configured with a similarity, but the global similarity does not support it: " + simFactory.getClass();
           log.error(msg);
-          throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, msg);
+          throw new SolrException(ErrorCode.SERVER_ERROR, msg);
         }
       }
     }
@@ -470,7 +480,7 @@ public final class IndexSchema {
         SchemaField defaultSearchField = getFields().get(defaultSearchFieldName);
         if ((defaultSearchField == null) || !defaultSearchField.indexed()) {
           String msg =  "default search field '" + defaultSearchFieldName + "' not defined or not indexed" ;
-          throw new SolrException( SolrException.ErrorCode.SERVER_ERROR, msg );
+          throw new SolrException(ErrorCode.SERVER_ERROR, msg);
         }
       }
       log.info("default search field in schema is "+defaultSearchFieldName);
@@ -494,7 +504,7 @@ public final class IndexSchema {
           ") can not be configured with a default value ("+
           uniqueKeyField.getDefaultValue()+")";
         log.error(msg);
-        throw new SolrException( SolrException.ErrorCode.SERVER_ERROR, msg );
+        throw new SolrException(ErrorCode.SERVER_ERROR, msg);
       }
 
       if (!uniqueKeyField.stored()) {
@@ -504,7 +514,7 @@ public final class IndexSchema {
         String msg = "uniqueKey field ("+uniqueKeyFieldName+
           ") can not be configured to be multivalued";
         log.error(msg);
-        throw new SolrException( SolrException.ErrorCode.SERVER_ERROR, msg );
+        throw new SolrException(ErrorCode.SERVER_ERROR, msg);
       }
       uniqueKeyFieldName=uniqueKeyField.getName();
       uniqueKeyFieldType=uniqueKeyField.getType();
@@ -546,7 +556,7 @@ public final class IndexSchema {
           String msg = "uniqueKey field ("+uniqueKeyFieldName+
             ") can not be the dest of a copyField (src="+source+")";
           log.error(msg);
-          throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, msg);
+          throw new SolrException(ErrorCode.SERVER_ERROR, msg);
           
         }
 
@@ -570,38 +580,44 @@ public final class IndexSchema {
       throw e;
     } catch(Exception e) {
       // unexpected exception...
-      throw new SolrException( SolrException.ErrorCode.SERVER_ERROR,"Schema Parsing Failed: " + e.getMessage(), e);
+      throw new SolrException(ErrorCode.SERVER_ERROR, "Schema Parsing Failed: " + e.getMessage(), e);
     }
 
     // create the field analyzers
     refreshAnalyzers();
-
   }
 
+  /** Returns true if the given name has exactly one asterisk either at the start or end of the name */
+  private boolean isValidDynamicFieldName(String name) {
+    if (name.startsWith("*") || name.endsWith("*")) {
+      int count = 0;
+      for (int pos = 0 ; pos < name.length() && -1 != (pos = name.indexOf('*', pos)) ; ++pos) ++count;
+      if (1 == count) return true;
+    }
+    return false;
+  }
+  
   private void addDynamicField(List<DynamicField> dFields, SchemaField f) {
-    boolean dup = isDuplicateDynField(dFields, f);
-    if( !dup ) {
-      addDynamicFieldNoDupCheck(dFields, f);
+    if (isDuplicateDynField(dFields, f)) {
+      String msg = "[schema.xml] Duplicate DynamicField definition for '" + f.getName() + "'";
+      throw new SolrException(ErrorCode.SERVER_ERROR, msg);
     } else {
-      String msg = "[schema.xml] Duplicate DynamicField definition for '"
-              + f.getName() + "'";
-
-      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, msg);
+      addDynamicFieldNoDupCheck(dFields, f);
     }
   }
 
   /**
-   * Register one or more new Dynamic Field with the Schema.
-   * @param f The {@link org.apache.solr.schema.SchemaField}
+   * Register one or more new Dynamic Fields with the Schema.
+   * @param fields The sequence of {@link org.apache.solr.schema.SchemaField}
    */
-  public void registerDynamicField(SchemaField ... f) {
+  public void registerDynamicFields(SchemaField... fields) {
     List<DynamicField> dynFields = new ArrayList<DynamicField>(Arrays.asList(dynamicFields));
-    for (SchemaField field : f) {
-      if (isDuplicateDynField(dynFields, field) == false) {
+    for (SchemaField field : fields) {
+      if (isDuplicateDynField(dynFields, field)) {
+        log.debug("dynamic field already exists: dynamic field: [" + field.getName() + "]");
+      } else {
         log.debug("dynamic field creation for schema field: " + field.getName());
         addDynamicFieldNoDupCheck(dynFields, field);
-      } else {
-        log.debug("dynamic field already exists: dynamic field: [" + field.getName() + "]");
       }
     }
     Collections.sort(dynFields);
@@ -614,14 +630,13 @@ public final class IndexSchema {
   }
 
   private boolean isDuplicateDynField(List<DynamicField> dFields, SchemaField f) {
-    for( DynamicField df : dFields ) {
-      if( df.regex.equals( f.name ) ) return true;
+    for (DynamicField df : dFields) {
+      if (df.getRegex().equals(f.name)) return true;
     }
     return false;
   }
 
-  public void registerCopyField( String source, String dest )
-  {
+  public void registerCopyField( String source, String dest ) {
     registerCopyField(source, dest, CopyField.UNLIMITED);
   }
 
@@ -634,56 +649,87 @@ public final class IndexSchema {
    * 
    * @see SolrCoreAware
    */
-  public void registerCopyField( String source, String dest, int maxChars )
-  {
-    boolean sourceIsPattern = isWildCard(source);
-    boolean destIsPattern   = isWildCard(dest);
-
-    log.debug("copyField source='"+source+"' dest='"+dest+"' maxChars='"+maxChars);
-    SchemaField d = getFieldOrNull(dest);
-    if(d == null){
-      throw new SolrException( SolrException.ErrorCode.SERVER_ERROR, "copyField destination :'"+dest+"' does not exist" );
-    }
-
-    if(sourceIsPattern) {
-      if( destIsPattern ) {
-        DynamicField df = null;
-        for( DynamicField dd : dynamicFields ) {
-          if( dd.regex.equals( dest ) ) {
-            df = dd;
-            break;
+  public void registerCopyField(String source, String dest, int maxChars) {
+    log.debug("copyField source='" + source + "' dest='" + dest + "' maxChars=" + maxChars);
+
+    DynamicField destDynamicField = null;
+    SchemaField destSchemaField = fields.get(dest);
+    SchemaField sourceSchemaField = fields.get(source);
+    
+    DynamicField sourceDynamicBase = null;
+    DynamicField destDynamicBase = null;
+    
+    boolean sourceIsDynamicFieldReference = false;
+    
+    if (null == destSchemaField || null == sourceSchemaField) {
+      // Go through dynamicFields array only once, collecting info for both source and dest fields, if needed
+      for (DynamicField dynamicField : dynamicFields) {
+        if (null == sourceSchemaField && ! sourceIsDynamicFieldReference) {
+          if (dynamicField.matches(source)) {
+            sourceIsDynamicFieldReference = true;
+            if ( ! source.equals(dynamicField.getRegex())) {
+              sourceDynamicBase = dynamicField;
+            }
           }
         }
-        if( df == null ) {
-          throw new SolrException( SolrException.ErrorCode.SERVER_ERROR, "copyField dynamic destination must match a dynamicField." );
+        if (null == destSchemaField) {
+          if (dest.equals(dynamicField.getRegex())) {
+            destDynamicField = dynamicField;
+            destSchemaField = dynamicField.prototype;
+          } else if (dynamicField.matches(dest)) {
+            destSchemaField = dynamicField.makeSchemaField(dest);
+            destDynamicField = new DynamicField(destSchemaField);
+            destDynamicBase = dynamicField;
+          }
         }
-        registerDynamicCopyField(new DynamicDestCopy(source, df, maxChars ));
+        if (null != destSchemaField && (null != sourceSchemaField || sourceIsDynamicFieldReference)) break;
       }
-      else {
-        registerDynamicCopyField(new DynamicCopy(source, d, maxChars));
-      }
-    } 
-    else if( destIsPattern ) {
-      String msg =  "copyField only supports a dynamic destination if the source is also dynamic" ;
-      throw new SolrException( SolrException.ErrorCode.SERVER_ERROR, msg );
     }
-    else {
-      // retrieve the field to force an exception if it doesn't exist
-      SchemaField f = getField(source);
-
-      List<CopyField> copyFieldList = copyFieldsMap.get(source);
-      if (copyFieldList == null) {
-        copyFieldList = new ArrayList<CopyField>();
-        copyFieldsMap.put(source, copyFieldList);
+    if (null == sourceSchemaField && ! sourceIsDynamicFieldReference) {
+      String msg = "copyField source :'" + source + "' is not an explicit field and doesn't match a dynamicField.";
+      throw new SolrException(ErrorCode.SERVER_ERROR, msg);
+    }
+    if (null == destSchemaField) {
+      String msg = "copyField dest :'" + dest + "' is not an explicit field and doesn't match a dynamicField.";
+      throw new SolrException(ErrorCode.SERVER_ERROR, msg);
+    }
+    if (sourceIsDynamicFieldReference) {
+      if (null != destDynamicField) { // source & dest: dynamic field references
+        registerDynamicCopyField(new DynamicCopy(source, destDynamicField, maxChars, sourceDynamicBase, destDynamicBase));
+        incrementCopyFieldTargetCount(destSchemaField);
+      } else {                        // source: dynamic field reference; dest: explicit field
+        destDynamicField = new DynamicField(destSchemaField);
+        registerDynamicCopyField(new DynamicCopy(source, destDynamicField, maxChars, sourceDynamicBase, null));
+        incrementCopyFieldTargetCount(destSchemaField);
+      }
+    } else {                          
+      if (null != destDynamicField) { // source: explicit field; dest: dynamic field reference
+        if (destDynamicField.pattern instanceof DynamicReplacement.DynamicPattern.NameEquals) {
+          // Dynamic dest with no asterisk is acceptable
+          registerDynamicCopyField(new DynamicCopy(source, destDynamicField, maxChars, sourceDynamicBase, destDynamicBase));
+          incrementCopyFieldTargetCount(destSchemaField);
+        } else {
+          String msg = "copyField only supports a dynamic destination with an asterisk "
+                     + "if the source is also dynamic with an asterisk";
+          throw new SolrException(ErrorCode.SERVER_ERROR, msg);
+        }
+      } else {                        // source & dest: explicit fields 
+        List<CopyField> copyFieldList = copyFieldsMap.get(source);
+        if (copyFieldList == null) {
+          copyFieldList = new ArrayList<CopyField>();
+          copyFieldsMap.put(source, copyFieldList);
+        }
+        copyFieldList.add(new CopyField(sourceSchemaField, destSchemaField, maxChars));
+        incrementCopyFieldTargetCount(destSchemaField);
       }
-      copyFieldList.add(new CopyField(f, d, maxChars));
-
-      copyFieldTargetCounts.put(d, (copyFieldTargetCounts.containsKey(d) ? copyFieldTargetCounts.get(d) + 1 : 1));
     }
   }
   
-  private void registerDynamicCopyField( DynamicCopy dcopy )
-  {
+  private void incrementCopyFieldTargetCount(SchemaField dest) {
+    copyFieldTargetCounts.put(dest, copyFieldTargetCounts.containsKey(dest) ? copyFieldTargetCounts.get(dest) + 1 : 1);
+  }
+  
+  private void registerDynamicCopyField( DynamicCopy dcopy ) {
     if( dynamicCopyFields == null ) {
       dynamicCopyFields = new DynamicCopy[] {dcopy};
     }
@@ -693,14 +739,7 @@ public final class IndexSchema {
       temp[temp.length -1] = dcopy;
       dynamicCopyFields = temp;
     }
-    log.trace("Dynamic Copy Field:" + dcopy );
-  }
-
-  private static Object[] append(Object[] orig, Object item) {
-    Object[] newArr = (Object[])java.lang.reflect.Array.newInstance(orig.getClass().getComponentType(), orig.length+1);
-    System.arraycopy(orig, 0, newArr, 0, orig.length);
-    newArr[orig.length] = item;
-    return newArr;
+    log.trace("Dynamic Copy Field:" + dcopy);
   }
 
   static SimilarityFactory readSimilarity(SolrResourceLoader loader, Node node) {
@@ -728,34 +767,58 @@ public final class IndexSchema {
   }
 
 
-  static abstract class DynamicReplacement implements Comparable<DynamicReplacement> {
-    final static int STARTS_WITH=1;
-    final static int ENDS_WITH=2;
+  public static abstract class DynamicReplacement implements Comparable<DynamicReplacement> {
+    abstract protected static class DynamicPattern {
+      protected final String regex;
+      protected final String fixedStr;
+
+      protected DynamicPattern(String regex, String fixedStr) { this.regex = regex; this.fixedStr = fixedStr; }
+
+      static DynamicPattern createPattern(String regex) {
+        if (regex.startsWith("*")) { return new NameEndsWith(regex); }
+        else if (regex.endsWith("*")) { return new NameStartsWith(regex); }
+        else { return new NameEquals(regex);
+        }
+      }
+      
+      /** Returns true if the given name matches this pattern */
+      abstract boolean matches(String name);
 
-    final String regex;
-    final int type;
+      /** Returns the remainder of the given name after removing this pattern's fixed string component */
+      abstract String remainder(String name);
 
-    final String str;
+      /** Returns the result of combining this pattern's fixed string component with the given replacement */
+      abstract String subst(String replacement);
+      
+      /** Returns the length of the original regex, including the asterisk, if any. */
+      public int length() { return regex.length(); }
 
-    protected DynamicReplacement(String regex) {
-      this.regex = regex;
-      if (regex.startsWith("*")) {
-        type=ENDS_WITH;
-        str=regex.substring(1);
+      private static class NameStartsWith extends DynamicPattern {
+        NameStartsWith(String regex) { super(regex, regex.substring(0, regex.length() - 1)); }
+        boolean matches(String name) { return name.startsWith(fixedStr); }
+        String remainder(String name) { return name.substring(fixedStr.length()); }
+        String subst(String replacement) { return fixedStr + replacement; }
       }
-      else if (regex.endsWith("*")) {
-        type=STARTS_WITH;
-        str=regex.substring(0,regex.length()-1);
+      private static class NameEndsWith extends DynamicPattern {
+        NameEndsWith(String regex) { super(regex, regex.substring(1)); }
+        boolean matches(String name) { return name.endsWith(fixedStr); }
+        String remainder(String name) { return name.substring(0, name.length() - fixedStr.length()); }
+        String subst(String replacement) { return replacement + fixedStr; }
       }
-      else {
-        throw new RuntimeException("dynamic field name must start or end with *");
+      private static class NameEquals extends DynamicPattern {
+        NameEquals(String regex) { super(regex, regex); }
+        boolean matches(String name) { return regex.equals(name); }
+        String remainder(String name) { return ""; }
+        String subst(String replacement) { return fixedStr; }
       }
     }
 
-    public boolean matches(String name) {
-      if (type==STARTS_WITH && name.startsWith(str)) return true;
-      else if (type==ENDS_WITH && name.endsWith(str)) return true;
-      else return false;
+    protected DynamicPattern pattern;
+
+    public boolean matches(String name) { return pattern.matches(name); }
+
+    protected DynamicReplacement(String regex) {
+      pattern = DynamicPattern.createPattern(regex);
     }
 
     /**
@@ -767,18 +830,17 @@ public final class IndexSchema {
      */
     @Override
     public int compareTo(DynamicReplacement other) {
-      return other.regex.length() - regex.length();
+      return other.pattern.length() - pattern.length();
+    }
+    
+    /** Returns the regex used to create this instance's pattern */
+    public String getRegex() {
+      return pattern.regex;
     }
   }
 
 
-  //
-  // Instead of storing a type, this could be implemented as a hierarchy
-  // with a virtual matches().
-  // Given how often a search will be done, however, speed is the overriding
-  // concern and I'm not sure which is faster.
-  //
-  final static class DynamicField extends DynamicReplacement {
+  public final static class DynamicField extends DynamicReplacement {
     final SchemaField prototype;
 
     DynamicField(SchemaField prototype) {
@@ -801,78 +863,47 @@ public final class IndexSchema {
     }
   }
 
-  static class DynamicCopy extends DynamicReplacement {
-    final SchemaField targetField;
-    final int maxChars;
+  public static class DynamicCopy extends DynamicReplacement {
+    private final DynamicField destination;
+    
+    private final int maxChars;
+    public int getMaxChars() { return maxChars; }
 
-    DynamicCopy(String regex, SchemaField targetField) {
-      this(regex, targetField, CopyField.UNLIMITED);
-    }
+    final DynamicField sourceDynamicBase;
+    public DynamicField getSourceDynamicBase() { return sourceDynamicBase; }
 
-    DynamicCopy(String regex, SchemaField targetField, int maxChars) {
-      super(regex);
-      this.targetField = targetField;
+    final DynamicField destDynamicBase;
+    public DynamicField getDestDynamicBase() { return destDynamicBase; }
+
+    DynamicCopy(String sourceRegex, DynamicField destination, int maxChars, 
+                DynamicField sourceDynamicBase, DynamicField destDynamicBase) {
+      super(sourceRegex);
+      this.destination = destination;
       this.maxChars = maxChars;
-    }
-    
-    public SchemaField getTargetField( String sourceField )
-    {
-      return targetField;
+      this.sourceDynamicBase = sourceDynamicBase;
+      this.destDynamicBase = destDynamicBase;
     }
 
-    @Override
-    public String toString() {
-      return targetField.toString();
-    }
-  }
+    public String getDestFieldName() { return destination.getRegex(); }
 
-  static class DynamicDestCopy extends DynamicCopy 
-  {
-    final DynamicField dynamic;
-    
-    final int dtype;
-    final String dstr;
-    
-    DynamicDestCopy(String source, DynamicField dynamic) {
-      this(source, dynamic, CopyField.UNLIMITED);
-    }
-      
-    DynamicDestCopy(String source, DynamicField dynamic, int maxChars) {
-      super(source, dynamic.prototype, maxChars);
-      this.dynamic = dynamic;
-      
-      String dest = dynamic.regex;
-      if (dest.startsWith("*")) {
-        dtype=ENDS_WITH;
-        dstr=dest.substring(1);
-      }
-      else if (dest.endsWith("*")) {
-        dtype=STARTS_WITH;
-        dstr=dest.substring(0,dest.length()-1);
-      }
-      else {
-        throw new RuntimeException("dynamic copyField destination name must start or end with *");
-      }
-    }
-    
-    @Override
-    public SchemaField getTargetField( String sourceField )
-    {
-      String dyn = ( type==STARTS_WITH ) 
-        ? sourceField.substring( str.length() )
-        : sourceField.substring( 0, sourceField.length()-str.length() );
-      
-      String name = (dtype==STARTS_WITH) ? (dstr+dyn) : (dyn+dstr);
-      return dynamic.makeSchemaField( name );
+    /**
+     *  Generates a destination field name based on this source pattern,
+     *  by substituting the remainder of this source pattern into the
+     *  the given destination pattern.
+     */
+    public SchemaField getTargetField(String sourceField) {
+      String remainder = pattern.remainder(sourceField);
+      String targetFieldName = destination.pattern.subst(remainder);
+      return destination.makeSchemaField(targetFieldName);
     }
 
+    
     @Override
     public String toString() {
-      return targetField.toString();
+      return destination.prototype.toString();
     }
   }
 
-
   public SchemaField[] getDynamicFieldPrototypes() {
     SchemaField[] df = new SchemaField[dynamicFields.length];
     for (int i=0;i<dynamicFields.length;i++) {
@@ -883,25 +914,24 @@ public final class IndexSchema {
 
   public String getDynamicPattern(String fieldName) {
    for (DynamicField df : dynamicFields) {
-     if (df.matches(fieldName)) return df.regex;
+     if (df.matches(fieldName)) return df.getRegex();
    }
    return  null; 
   }
   
   /**
-   * Does the schema have the specified field defined explicitly, i.e.
-   * not as a result of a copyField declaration with a wildcard?  We
-   * consider it explicitly defined if it matches a field or dynamicField
-   * declaration.
+   * Does the schema explicitly define the specified field, i.e. not as a result
+   * of a copyField declaration?  We consider it explicitly defined if it matches
+   * a field name or a dynamicField name.
    * @return true if explicitly declared in the schema.
    */
   public boolean hasExplicitField(String fieldName) {
-    if(fields.containsKey(fieldName)) {
+    if (fields.containsKey(fieldName)) {
       return true;
     }
 
     for (DynamicField df : dynamicFields) {
-      if (df.matches(fieldName)) return true;
+      if (fieldName.equals(df.getRegex())) return true;
     }
 
     return false;
@@ -964,7 +994,7 @@ public final class IndexSchema {
     /***  REMOVED -YCS
     if (defaultFieldType != null) return new SchemaField(fieldName,defaultFieldType);
     ***/
-    throw new SolrException( SolrException.ErrorCode.BAD_REQUEST,"undefined field: \""+fieldName+"\"");
+    throw new SolrException(ErrorCode.BAD_REQUEST,"undefined field: \""+fieldName+"\"");
   }
 
   /**
@@ -976,7 +1006,7 @@ public final class IndexSchema {
    * </p>
    *
    * @param fieldName may be an explicitly created field, or a name that
-   * excercies a dynamic field.
+   *  excercises a dynamic field.
    * @throws SolrException if no such field exists
    * @see #getField(String)
    * @see #getFieldTypeNoEx
@@ -1007,7 +1037,7 @@ public final class IndexSchema {
    * </p>
    *
    * @param fieldName may be an explicitly created field, or a name that
-   * excercies a dynamic field.
+   * exercises a dynamic field.
    * @return null if field is not defined.
    * @see #getField(String)
    * @see #getFieldTypeNoEx
@@ -1024,7 +1054,7 @@ public final class IndexSchema {
    * the specified field name
    *
    * @param fieldName may be an explicitly created field, or a name that
-   * excercies a dynamic field.
+   * exercises a dynamic field.
    * @throws SolrException if no such field exists
    * @see #getField(String)
    * @see #getFieldTypeNoEx
@@ -1033,7 +1063,7 @@ public final class IndexSchema {
      for (DynamicField df : dynamicFields) {
       if (df.matches(fieldName)) return df.prototype.getType();
     }
-    throw new SolrException( SolrException.ErrorCode.BAD_REQUEST,"undefined field "+fieldName);
+    throw new SolrException(ErrorCode.BAD_REQUEST,"undefined field "+fieldName);
   }
 
   private FieldType dynFieldType(String fieldName) {
@@ -1062,6 +1092,11 @@ public final class IndexSchema {
         }
       }
     }
+    for (DynamicCopy dynamicCopy : dynamicCopyFields) {
+      if (dynamicCopy.getDestFieldName().equals(destField)) {
+        sf.add(getField(dynamicCopy.getRegex()));
+      }
+    }
     return sf.toArray(new SchemaField[sf.size()]);
   }
 
@@ -1080,8 +1115,7 @@ public final class IndexSchema {
       }
     }
     List<CopyField> fixedCopyFields = copyFieldsMap.get(sourceField);
-    if (fixedCopyFields != null)
-    {
+    if (null != fixedCopyFields) {
       result.addAll(fixedCopyFields);
     }
 
@@ -1093,17 +1127,7 @@ public final class IndexSchema {
    * 
    * @since solr 1.3
    */
-  public boolean isCopyFieldTarget( SchemaField f )
-  {
+  public boolean isCopyFieldTarget( SchemaField f ) {
     return copyFieldTargetCounts.containsKey( f );
   }
-
-  /**
-   * Is the given field name a wildcard?  I.e. does it begin or end with *?
-   * @return true/false
-   */
-  private static boolean isWildCard(String name) {
-    return  name.startsWith("*") || name.endsWith("*");
-  }
-
 }

Modified: lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/SchemaField.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/SchemaField.java?rev=1453161&r1=1453160&r2=1453161&view=diff
==============================================================================
--- lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/SchemaField.java (original)
+++ lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/SchemaField.java Wed Mar  6 04:50:33 2013
@@ -20,10 +20,14 @@ package org.apache.solr.schema;
 import org.apache.solr.common.SolrException;
 import org.apache.lucene.index.StorableField;
 import org.apache.lucene.search.SortField;
+import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.common.util.StrUtils;
 import org.apache.solr.search.QParser;
 
 import org.apache.solr.response.TextResponseWriter;
 
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.io.IOException;
@@ -34,11 +38,18 @@ import java.io.IOException;
  *
  */
 public final class SchemaField extends FieldProperties {
+  private static final String FIELD_NAME = "name";
+  private static final String TYPE_NAME = "type";
+  private static final String DEFAULT_VALUE = "default";
+
   final String name;
   final FieldType type;
   final int properties;
   final String defaultValue;
   boolean required = false;  // this can't be final since it may be changed dynamically
+  
+  /** Declared field property overrides */
+  Map<String,String> args = Collections.emptyMap();
 
 
   /** Create a new SchemaField with the given name and type,
@@ -52,7 +63,8 @@ public final class SchemaField extends F
    * of the properties of the prototype except the field name.
    */
   public SchemaField(SchemaField prototype, String name) {
-    this(name, prototype.type, prototype.properties, prototype.defaultValue );
+    this(name, prototype.type, prototype.properties, prototype.defaultValue);
+    args = prototype.args;
   }
 
  /** Create a new SchemaField with the given name and type,
@@ -186,10 +198,12 @@ public final class SchemaField extends F
   static SchemaField create(String name, FieldType ft, Map<String,String> props) {
 
     String defaultValue = null;
-    if( props.containsKey( "default" ) ) {
-      defaultValue = props.get( "default" );
+    if (props.containsKey(DEFAULT_VALUE)) {
+      defaultValue = props.get(DEFAULT_VALUE);
     }
-    return new SchemaField(name, ft, calcProps(name, ft, props), defaultValue );
+    SchemaField field = new SchemaField(name, ft, calcProps(name, ft, props), defaultValue);
+    field.args = new HashMap<String,String>(props);
+    return field;
   }
 
   /**
@@ -285,10 +299,51 @@ public final class SchemaField extends F
   public boolean equals(Object obj) {
     return(obj instanceof SchemaField) && name.equals(((SchemaField)obj).name);
   }
-}
-
-
-
-
-
 
+  /**
+   * Get a map of property name -> value for this field.  If showDefaults is true,
+   * include default properties (those inherited from the declared property type and
+   * not overridden in the field declaration).
+   */
+  public SimpleOrderedMap<Object> getNamedPropertyValues(boolean showDefaults) {
+    SimpleOrderedMap<Object> properties = new SimpleOrderedMap<Object>();
+    properties.add(FIELD_NAME, getName());
+    properties.add(TYPE_NAME, getType().getTypeName());
+    if (showDefaults) {
+      if (null != getDefaultValue()) {
+        properties.add(DEFAULT_VALUE, getDefaultValue());
+      }
+      properties.add(getPropertyName(INDEXED), indexed());
+      properties.add(getPropertyName(STORED), stored());
+      properties.add(getPropertyName(DOC_VALUES), hasDocValues());
+      properties.add(getPropertyName(STORE_TERMVECTORS), storeTermVector());
+      properties.add(getPropertyName(STORE_TERMPOSITIONS), storeTermPositions());
+      properties.add(getPropertyName(STORE_TERMOFFSETS), storeTermOffsets());
+      properties.add(getPropertyName(OMIT_NORMS), omitNorms());
+      properties.add(getPropertyName(OMIT_TF_POSITIONS), omitTermFreqAndPositions());
+      properties.add(getPropertyName(OMIT_POSITIONS), omitPositions());
+      properties.add(getPropertyName(STORE_OFFSETS), storeOffsetsWithPositions());
+      properties.add(getPropertyName(MULTIVALUED), multiValued());
+      if (sortMissingFirst()) {
+        properties.add(getPropertyName(SORT_MISSING_FIRST), sortMissingFirst());
+      } else if (sortMissingLast()) {
+        properties.add(getPropertyName(SORT_MISSING_LAST), sortMissingLast());
+      }
+      properties.add(getPropertyName(REQUIRED), isRequired());
+      properties.add(getPropertyName(TOKENIZED), isTokenized());
+      // The BINARY property is always false
+      // properties.add(getPropertyName(BINARY), isBinary());
+    } else {
+      for (Map.Entry<String,String> arg : args.entrySet()) {
+        String key = arg.getKey();
+        String value = arg.getValue();
+        if (key.equals(DEFAULT_VALUE)) {
+          properties.add(key, value);
+        } else {
+          properties.add(key, StrUtils.parseBool(value, false));
+        }
+      }
+    }
+    return properties;
+  }
+}

Modified: lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/TextField.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/TextField.java?rev=1453161&r1=1453160&r2=1453161&view=diff
==============================================================================
--- lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/TextField.java (original)
+++ lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/TextField.java Wed Mar  6 04:50:33 2013
@@ -19,8 +19,6 @@ package org.apache.solr.schema;
 
 import org.apache.lucene.analysis.tokenattributes.TermToBytesRefAttribute;
 import org.apache.lucene.search.*;
-import org.apache.lucene.index.GeneralField;
-import org.apache.lucene.index.IndexableField;
 import org.apache.lucene.index.StorableField;
 import org.apache.lucene.index.Term;
 import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute;
@@ -58,6 +56,7 @@ public class TextField extends FieldType
    * @see #setMultiTermAnalyzer
    */
   protected Analyzer multiTermAnalyzer=null;
+  private boolean isExplicitMultiTermAnalyzer = false;
 
   @Override
   protected void init(IndexSchema schema, Map<String,String> args) {
@@ -331,4 +330,11 @@ public class TextField extends FieldType
 
   }
 
+  public void setIsExplicitMultiTermAnalyzer(boolean isExplicitMultiTermAnalyzer) {
+    this.isExplicitMultiTermAnalyzer = isExplicitMultiTermAnalyzer;
+  }
+
+  public boolean isExplicitMultiTermAnalyzer() {
+    return isExplicitMultiTermAnalyzer;
+  }
 }

Added: lucene/dev/trunk/solr/core/src/java/org/apache/solr/servlet/ResponseUtils.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/java/org/apache/solr/servlet/ResponseUtils.java?rev=1453161&view=auto
==============================================================================
--- lucene/dev/trunk/solr/core/src/java/org/apache/solr/servlet/ResponseUtils.java (added)
+++ lucene/dev/trunk/solr/core/src/java/org/apache/solr/servlet/ResponseUtils.java Wed Mar  6 04:50:33 2013
@@ -0,0 +1,71 @@
+package org.apache.solr.servlet;
+/*
+ * 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.
+ */
+
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.util.NamedList;
+import org.slf4j.Logger;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+/**
+ * Response helper methods.
+ */
+public class ResponseUtils {
+  private ResponseUtils() {}
+
+  /**
+   * Adds the given Throwable's message to the given NamedList.
+   * <p/>
+   * If the response code is not a regular code, the Throwable's
+   * stack trace is both logged and added to the given NamedList.
+   * <p/>
+   * Status codes less than 100 are adjusted to be 500.
+   */
+  public static int getErrorInfo(Throwable ex, NamedList info, Logger log) {
+    int code = 500;
+    if (ex instanceof SolrException) {
+      code = ((SolrException)ex).code();
+    }
+    
+    for (Throwable th = ex; th != null; th = th.getCause()) {
+      String msg = th.getMessage();
+      if (msg != null) {
+        info.add("msg", msg);
+        break;
+      }
+    }
+    
+    // For any regular code, don't include the stack trace
+    if (code == 500 || code < 100) {
+      StringWriter sw = new StringWriter();
+      ex.printStackTrace(new PrintWriter(sw));
+      SolrException.log(log, null, ex);
+      info.add("trace", sw.toString());
+
+      // non standard codes have undefined results with various servers
+      if (code < 100) {
+        log.warn("invalid return code: " + code);
+        code = 500;
+      }
+    }
+    
+    info.add("code", code);
+    return code;
+  }
+}

Modified: lucene/dev/trunk/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java?rev=1453161&r1=1453160&r2=1453161&view=diff
==============================================================================
--- lucene/dev/trunk/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java (original)
+++ lucene/dev/trunk/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java Wed Mar  6 04:50:33 2013
@@ -23,8 +23,6 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
-import java.io.StringWriter;
 import java.io.Writer;
 import java.net.HttpURLConnection;
 import java.net.URL;
@@ -248,6 +246,20 @@ public class SolrDispatchFilter implemen
             parsers.put(config, parser );
           }
 
+          // Handle /schema/* paths via Restlet
+          if( path.startsWith("/schema") ) {
+            solrReq = parser.parse(core, path, req);
+            SolrRequestInfo.setRequestInfo(new SolrRequestInfo(solrReq, new SolrQueryResponse()));
+            if( path.equals(req.getServletPath()) ) {
+              // avoid endless loop - pass through to Restlet via webapp
+              chain.doFilter(request, response);
+            } else {
+              // forward rewritten URI (without path prefix and core/collection name) to Restlet
+              req.getRequestDispatcher(path).forward(request, response);
+            }
+            return;
+          }
+
           // Determine the handler from the url path if not set
           // (we might already have selected the cores handler)
           if( handler == null && path.length() > 1 ) { // don't match "" or "/" as valid path
@@ -353,13 +365,17 @@ public class SolrDispatchFilter implemen
       try {
         con.connect();
 
-        InputStream is = req.getInputStream();
-        OutputStream os = con.getOutputStream();
-        try {
-          IOUtils.copyLarge(is, os);
-        } finally {
-          IOUtils.closeQuietly(os);
-          IOUtils.closeQuietly(is);  // TODO: I thought we weren't supposed to explicitly close servlet streams
+        InputStream is;
+        OutputStream os;
+        if ("POST".equals(req.getMethod())) {
+          is = req.getInputStream();
+          os = con.getOutputStream(); // side effect: method is switched to POST
+          try {
+            IOUtils.copyLarge(is, os);
+          } finally {
+            IOUtils.closeQuietly(os);
+            IOUtils.closeQuietly(is);  // TODO: I thought we weren't supposed to explicitly close servlet streams
+          }
         }
         
         resp.setStatus(con.getResponseCode());
@@ -491,19 +507,11 @@ public class SolrDispatchFilter implemen
   private void handleAdminRequest(HttpServletRequest req, ServletResponse response, SolrRequestHandler handler,
                                   SolrQueryRequest solrReq) throws IOException {
     SolrQueryResponse solrResp = new SolrQueryResponse();
-    final NamedList<Object> responseHeader = new SimpleOrderedMap<Object>();
-    solrResp.add("responseHeader", responseHeader);
-    NamedList toLog = solrResp.getToLog();
-    toLog.add("webapp", req.getContextPath());
-    toLog.add("path", solrReq.getContext().get("path"));
-    toLog.add("params", "{" + solrReq.getParamString() + "}");
+    SolrCore.preDecorateResponse(solrReq, solrResp);
     handler.handleRequest(solrReq, solrResp);
-    SolrCore.setResponseHeaderValues(handler, solrReq, solrResp);
-    StringBuilder sb = new StringBuilder();
-    for (int i = 0; i < toLog.size(); i++) {
-      String name = toLog.getName(i);
-      Object val = toLog.getVal(i);
-      sb.append(name).append("=").append(val).append(" ");
+    SolrCore.postDecorateResponse(handler, solrReq, solrResp);
+    if (log.isInfoEnabled() && solrResp.getToLog().size() > 0) {
+      log.info(solrResp.getToLogAsString("[admin] "));
     }
     QueryResponseWriter respWriter = SolrCore.DEFAULT_RESPONSE_WRITERS.get(solrReq.getParams().get(CommonParams.WT));
     if (respWriter == null) respWriter = SolrCore.DEFAULT_RESPONSE_WRITERS.get("standard");
@@ -521,7 +529,7 @@ public class SolrDispatchFilter implemen
 
     if (solrRsp.getException() != null) {
       NamedList info = new SimpleOrderedMap();
-      int code = getErrorInfo(solrRsp.getException(),info);
+      int code = ResponseUtils.getErrorInfo(solrRsp.getException(), info, log);
       solrRsp.add("error", info);
       ((HttpServletResponse) response).setStatus(code);
     }
@@ -543,38 +551,6 @@ public class SolrDispatchFilter implemen
     //else http HEAD request, nothing to write out, waited this long just to get ContentType
   }
   
-  protected int getErrorInfo(Throwable ex, NamedList info) {
-    int code=500;
-    if( ex instanceof SolrException ) {
-      code = ((SolrException)ex).code();
-    }
-
-    String msg = null;
-    for (Throwable th = ex; th != null; th = th.getCause()) {
-      msg = th.getMessage();
-      if (msg != null) break;
-    }
-    if(msg != null) {
-      info.add("msg", msg);
-    }
-    
-    // For any regular code, don't include the stack trace
-    if( code == 500 || code < 100 ) {
-      StringWriter sw = new StringWriter();
-      ex.printStackTrace(new PrintWriter(sw));
-      SolrException.log(log, null, ex);
-      info.add("trace", sw.toString());
-
-      // non standard codes have undefined results with various servers
-      if( code < 100 ) {
-        log.warn( "invalid return code: "+code );
-        code = 500;
-      }
-    }
-    info.add("code", new Integer(code));
-    return code;
-  }
-
   protected void execute( HttpServletRequest req, SolrRequestHandler handler, SolrQueryRequest sreq, SolrQueryResponse rsp) {
     // a custom filter could add more stuff to the request before passing it on.
     // for example: sreq.getContext().put( "HttpServletRequest", req );
@@ -615,7 +591,7 @@ public class SolrDispatchFilter implemen
     }
     catch( Throwable t ) { // This error really does not matter
       SimpleOrderedMap info = new SimpleOrderedMap();
-      int code=getErrorInfo(ex, info);
+      int code = ResponseUtils.getErrorInfo(ex, info, log);
       response.sendError( code, info.toString() );
     }
   }

Modified: lucene/dev/trunk/solr/core/src/java/org/apache/solr/update/processor/LogUpdateProcessorFactory.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/java/org/apache/solr/update/processor/LogUpdateProcessorFactory.java?rev=1453161&r1=1453160&r2=1453161&view=diff
==============================================================================
--- lucene/dev/trunk/solr/core/src/java/org/apache/solr/update/processor/LogUpdateProcessorFactory.java (original)
+++ lucene/dev/trunk/solr/core/src/java/org/apache/solr/update/processor/LogUpdateProcessorFactory.java Wed Mar  6 04:50:33 2013
@@ -179,35 +179,24 @@ class LogUpdateProcessor extends UpdateR
     if (next != null) next.finish();
 
     // LOG A SUMMARY WHEN ALL DONE (INFO LEVEL)
-    
 
+    if (log.isInfoEnabled()) {
+      StringBuilder sb = new StringBuilder(rsp.getToLogAsString(req.getCore().getLogId()));
 
-    NamedList<Object> stdLog = rsp.getToLog();
+      rsp.getToLog().clear();   // make it so SolrCore.exec won't log this again
 
-    StringBuilder sb = new StringBuilder(req.getCore().getLogId());
-
-    for (int i=0; i<stdLog.size(); i++) {
-      String name = stdLog.getName(i);
-      Object val = stdLog.getVal(i);
-      if (name != null) {
-        sb.append(name).append('=');
+      // if id lists were truncated, show how many more there were
+      if (adds != null && numAdds > maxNumToLog) {
+        adds.add("... (" + numAdds + " adds)");
       }
-      sb.append(val).append(' ');
-    }
-
-    stdLog.clear();   // make it so SolrCore.exec won't log this again
+      if (deletes != null && numDeletes > maxNumToLog) {
+        deletes.add("... (" + numDeletes + " deletes)");
+      }
+      long elapsed = rsp.getEndTime() - req.getStartTime();
 
-    // if id lists were truncated, show how many more there were
-    if (adds != null && numAdds > maxNumToLog) {
-      adds.add("... (" + numAdds + " adds)");
-    }
-    if (deletes != null && numDeletes > maxNumToLog) {
-      deletes.add("... (" + numDeletes + " deletes)");
+      sb.append(toLog).append(" 0 ").append(elapsed);
+      log.info(sb.toString());
     }
-    long elapsed = rsp.getEndTime() - req.getStartTime();
-
-    sb.append(toLog).append(" 0 ").append(elapsed);
-    log.info(sb.toString());
   }
 }
 

Copied: lucene/dev/trunk/solr/core/src/test-files/solr/collection1/conf/schema-rest.xml (from r1450954, lucene/dev/trunk/solr/core/src/test-files/solr/collection1/conf/schema15.xml)
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/test-files/solr/collection1/conf/schema-rest.xml?p2=lucene/dev/trunk/solr/core/src/test-files/solr/collection1/conf/schema-rest.xml&p1=lucene/dev/trunk/solr/core/src/test-files/solr/collection1/conf/schema15.xml&r1=1450954&r2=1453161&rev=1453161&view=diff
==============================================================================
--- lucene/dev/trunk/solr/core/src/test-files/solr/collection1/conf/schema15.xml (original)
+++ lucene/dev/trunk/solr/core/src/test-files/solr/collection1/conf/schema-rest.xml Wed Mar  6 04:50:33 2013
@@ -18,7 +18,7 @@
 
 <!-- The Solr schema file, version 1.5  -->
 
-<schema name="test" version="1.5">
+<schema name="test-rest" version="1.5">
   <!-- attribute "name" is the name of this schema and is only used for display purposes.
        Applications should change this to reflect the nature of the search collection.
        version="x.y" is Solr's version number for the schema syntax and semantics.  It should
@@ -591,13 +591,29 @@
  <defaultSearchField>text</defaultSearchField>
  <uniqueKey>id</uniqueKey>
 
-   <copyField source="title" dest="title_stemmed"/>
+   <copyField source="title" dest="title_stemmed" maxChars="200"/>
    <copyField source="title" dest="title_lettertok"/>
 
    <copyField source="title" dest="text"/>
    <copyField source="subject" dest="text"/>
 
    <copyField source="copyfield_source" dest="text"/>
-   <copyField source="copyfield_source" dest="copyfield_dest_ss"/>  <!-- copyField into another stored copyField - not best practice --> 
- 
+   <copyField source="copyfield_source" dest="copyfield_dest_ss"/>  <!-- copyField into another stored copyField - not best practice -->
+
+   <copyField source="title" dest="dest_sub_no_ast_s"/>
+
+   <copyField source="*_i" dest="title"/>
+   <copyField source="*_i" dest="*_s"/>
+   <copyField source="*_i" dest="*_dest_sub_s"/>
+   <copyField source="*_i" dest="dest_sub_no_ast_s"/>
+
+   <copyField source="*_src_sub_i" dest="title"/>
+   <copyField source="*_src_sub_i" dest="*_s"/>
+   <copyField source="*_src_sub_i" dest="*_dest_sub_s"/>
+   <copyField source="*_src_sub_i" dest="dest_sub_no_ast_s"/>
+
+   <copyField source="src_sub_no_ast_i" dest="title"/>
+   <copyField source="src_sub_no_ast_i" dest="*_s"/>
+   <copyField source="src_sub_no_ast_i" dest="*_dest_sub_s"/>
+   <copyField source="src_sub_no_ast_i" dest="dest_sub_no_ast_s"/>
 </schema>

Modified: lucene/dev/trunk/solr/core/src/test/org/apache/solr/handler/admin/LukeRequestHandlerTest.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/test/org/apache/solr/handler/admin/LukeRequestHandlerTest.java?rev=1453161&r1=1453160&r2=1453161&view=diff
==============================================================================
--- lucene/dev/trunk/solr/core/src/test/org/apache/solr/handler/admin/LukeRequestHandlerTest.java (original)
+++ lucene/dev/trunk/solr/core/src/test/org/apache/solr/handler/admin/LukeRequestHandlerTest.java Wed Mar  6 04:50:33 2013
@@ -191,8 +191,7 @@ public class LukeRequestHandlerTest exte
        field("text") + "/arr[@name='copySources']/str[.='subject']",
        field("title") + "/arr[@name='copyDests']/str[.='text']",
        field("title") + "/arr[@name='copyDests']/str[.='title_stemmed']",
-       // :TODO: SOLR-3798
-       //dynfield("bar_copydest_*") + "/arr[@name='copySource']/str[.='foo_copysource_*']",
+       dynfield("bar_copydest_*") + "/arr[@name='copySources']/str[.='foo_copysource_*']",
        dynfield("foo_copysource_*") + "/arr[@name='copyDests']/str[.='bar_copydest_*']");
     assertEquals(xml, null, r);
   }

Added: lucene/dev/trunk/solr/core/src/test/org/apache/solr/rest/SchemaRestletTestBase.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/test/org/apache/solr/rest/SchemaRestletTestBase.java?rev=1453161&view=auto
==============================================================================
--- lucene/dev/trunk/solr/core/src/test/org/apache/solr/rest/SchemaRestletTestBase.java (added)
+++ lucene/dev/trunk/solr/core/src/test/org/apache/solr/rest/SchemaRestletTestBase.java Wed Mar  6 04:50:33 2013
@@ -0,0 +1,37 @@
+package org.apache.solr.rest;
+/*
+ * 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.
+ */
+
+import org.apache.solr.util.RestTestBase;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.junit.BeforeClass;
+import org.restlet.ext.servlet.ServerServlet;
+
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+abstract public class SchemaRestletTestBase extends RestTestBase {
+  @BeforeClass
+  public static void init() throws Exception {
+    final SortedMap<ServletHolder,String> extraServlets = new TreeMap<ServletHolder,String>();
+    final ServletHolder schemaRestApi = new ServletHolder("SchemaRestApi", ServerServlet.class);
+    schemaRestApi.setInitParameter("org.restlet.application", "org.apache.solr.rest.SchemaRestApi");
+    extraServlets.put(schemaRestApi, "/schema/*");
+
+    createJettyAndHarness(TEST_HOME(), "solrconfig.xml", "schema-rest.xml", "/solr", true, extraServlets);
+  }
+}

Added: lucene/dev/trunk/solr/core/src/test/org/apache/solr/rest/TestCopyFieldCollectionResource.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/test/org/apache/solr/rest/TestCopyFieldCollectionResource.java?rev=1453161&view=auto
==============================================================================
--- lucene/dev/trunk/solr/core/src/test/org/apache/solr/rest/TestCopyFieldCollectionResource.java (added)
+++ lucene/dev/trunk/solr/core/src/test/org/apache/solr/rest/TestCopyFieldCollectionResource.java Wed Mar  6 04:50:33 2013
@@ -0,0 +1,140 @@
+package org.apache.solr.rest;
+/*
+ * 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.
+ */
+
+import org.junit.Test;
+
+public class TestCopyFieldCollectionResource extends SchemaRestletTestBase {
+  @Test
+  public void testGetAllCopyFields() throws Exception {
+    assertQ("/schema/copyfields?indent=on&wt=xml",
+            "/response/arr[@name='copyfields']/lst[    str[@name='source'][.='title']"
+           +"                                      and str[@name='dest'][.='title_stemmed']"
+           +"                                      and int[@name='maxChars'][.='200']]",
+
+            "/response/arr[@name='copyfields']/lst[    str[@name='source'][.='title']"
+           +"                                      and str[@name='dest'][.='dest_sub_no_ast_s']"
+           +"                                      and str[@name='destDynamicBase'][.='*_s']]",
+
+            "/response/arr[@name='copyfields']/lst[    str[@name='source'][.='*_i']"
+           +"                                      and str[@name='dest'][.='title']]",
+
+            "/response/arr[@name='copyfields']/lst[    str[@name='source'][.='*_i']"
+           +"                                      and str[@name='dest'][.='*_s']]",
+
+            "/response/arr[@name='copyfields']/lst[    str[@name='source'][.='*_i']"
+           +"                                      and str[@name='dest'][.='*_dest_sub_s']"
+           +"                                      and str[@name='destDynamicBase'][.='*_s']]",
+
+            "/response/arr[@name='copyfields']/lst[    str[@name='source'][.='*_i']"
+           +"                                      and str[@name='dest'][.='dest_sub_no_ast_s']"
+           +"                                      and str[@name='destDynamicBase'][.='*_s']]",
+
+            "/response/arr[@name='copyfields']/lst[    str[@name='source'][.='*_src_sub_i']"
+           +"                                      and str[@name='sourceDynamicBase'][.='*_i']"
+           +"                                      and str[@name='dest'][.='title']]",
+
+            "/response/arr[@name='copyfields']/lst[    str[@name='source'][.='*_src_sub_i']"
+           +"                                      and str[@name='sourceDynamicBase'][.='*_i']"
+           +"                                      and str[@name='dest'][.='*_s']]",
+
+            "/response/arr[@name='copyfields']/lst[    str[@name='source'][.='*_src_sub_i']"
+           +"                                      and str[@name='sourceDynamicBase'][.='*_i']"
+           +"                                      and str[@name='dest'][.='*_dest_sub_s']"
+           +"                                      and str[@name='destDynamicBase'][.='*_s']]",
+
+            "/response/arr[@name='copyfields']/lst[    str[@name='source'][.='*_src_sub_i']"
+           +"                                      and str[@name='sourceDynamicBase'][.='*_i']"
+           +"                                      and str[@name='dest'][.='dest_sub_no_ast_s']"
+           +"                                      and str[@name='destDynamicBase'][.='*_s']]",
+
+            "/response/arr[@name='copyfields']/lst[    str[@name='source'][.='src_sub_no_ast_i']"
+           +"                                      and str[@name='sourceDynamicBase'][.='*_i']"
+           +"                                      and str[@name='dest'][.='title']]",
+
+            "/response/arr[@name='copyfields']/lst[    str[@name='source'][.='src_sub_no_ast_i']"
+           +"                                      and str[@name='sourceDynamicBase'][.='*_i']"
+           +"                                      and str[@name='dest'][.='*_s']]",
+
+            "/response/arr[@name='copyfields']/lst[    str[@name='source'][.='src_sub_no_ast_i']"
+           +"                                      and str[@name='sourceDynamicBase'][.='*_i']"
+           +"                                      and str[@name='dest'][.='*_dest_sub_s']"
+           +"                                      and str[@name='destDynamicBase'][.='*_s']]",
+
+            "/response/arr[@name='copyfields']/lst[    str[@name='source'][.='src_sub_no_ast_i']"
+           +"                                      and str[@name='sourceDynamicBase'][.='*_i']"
+           +"                                      and str[@name='dest'][.='dest_sub_no_ast_s']"
+           +"                                      and str[@name='destDynamicBase'][.='*_s']]");
+  }
+
+  @Test
+  public void testJsonGetAllCopyFields() throws Exception {
+    assertJQ("/schema/copyfields?indent=on&wt=json",
+             "/copyfields/[6]=={'source':'title','dest':'dest_sub_no_ast_s','destDynamicBase':'*_s'}",
+
+             "/copyfields/[7]=={'source':'*_i','dest':'title'}",
+             "/copyfields/[8]=={'source':'*_i','dest':'*_s'}",
+             "/copyfields/[9]=={'source':'*_i','dest':'*_dest_sub_s','destDynamicBase':'*_s'}",
+             "/copyfields/[10]=={'source':'*_i','dest':'dest_sub_no_ast_s','destDynamicBase':'*_s'}",
+
+             "/copyfields/[11]=={'source':'*_src_sub_i','sourceDynamicBase':'*_i','dest':'title'}",
+             "/copyfields/[12]=={'source':'*_src_sub_i','sourceDynamicBase':'*_i','dest':'*_s'}",
+             "/copyfields/[13]=={'source':'*_src_sub_i','sourceDynamicBase':'*_i','dest':'*_dest_sub_s','destDynamicBase':'*_s'}",
+             "/copyfields/[14]=={'source':'*_src_sub_i','sourceDynamicBase':'*_i','dest':'dest_sub_no_ast_s','destDynamicBase':'*_s'}",
+
+             "/copyfields/[15]=={'source':'src_sub_no_ast_i','sourceDynamicBase':'*_i','dest':'title'}",
+             "/copyfields/[16]=={'source':'src_sub_no_ast_i','sourceDynamicBase':'*_i','dest':'*_s'}",
+             "/copyfields/[17]=={'source':'src_sub_no_ast_i','sourceDynamicBase':'*_i','dest':'*_dest_sub_s','destDynamicBase':'*_s'}",
+             "/copyfields/[18]=={'source':'src_sub_no_ast_i','sourceDynamicBase':'*_i','dest':'dest_sub_no_ast_s','destDynamicBase':'*_s'}");
+
+  }
+
+  @Test
+  public void testRestrictSource() throws Exception {
+    assertQ("/schema/copyfields/?indent=on&wt=xml&source.fl=title,*_i,*_src_sub_i,src_sub_no_ast_i",
+            "count(/response/arr[@name='copyfields']/lst) = 16", // 4 + 4 + 4 + 4
+            "count(/response/arr[@name='copyfields']/lst/str[@name='source'][.='title']) = 4",
+            "count(/response/arr[@name='copyfields']/lst/str[@name='source'][.='*_i']) = 4",
+            "count(/response/arr[@name='copyfields']/lst/str[@name='source'][.='*_src_sub_i']) = 4",
+            "count(/response/arr[@name='copyfields']/lst/str[@name='source'][.='src_sub_no_ast_i']) = 4");
+  }
+
+  @Test
+  public void testRestrictDest() throws Exception {
+    assertQ("/schema/copyfields/?indent=on&wt=xml&dest.fl=title,*_s,*_dest_sub_s,dest_sub_no_ast_s",
+            "count(/response/arr[@name='copyfields']/lst) = 13", // 3 + 3 + 3 + 4
+            "count(/response/arr[@name='copyfields']/lst/str[@name='dest'][.='title']) = 3",
+            "count(/response/arr[@name='copyfields']/lst/str[@name='dest'][.='*_s']) = 3",
+            "count(/response/arr[@name='copyfields']/lst/str[@name='dest'][.='*_dest_sub_s']) = 3",
+            "count(/response/arr[@name='copyfields']/lst/str[@name='dest'][.='dest_sub_no_ast_s']) = 4");
+  }
+
+  @Test
+  public void testRestrictSourceAndDest() throws Exception {
+    assertQ("/schema/copyfields/?indent=on&wt=xml&source.fl=title,*_i&dest.fl=title,dest_sub_no_ast_s",
+            "count(/response/arr[@name='copyfields']/lst) = 3",
+
+            "/response/arr[@name='copyfields']/lst[    str[@name='source'][.='title']"
+           +"                                      and str[@name='dest'][.='dest_sub_no_ast_s']]",
+
+            "/response/arr[@name='copyfields']/lst[    str[@name='source'][.='*_i']"
+           +"                                      and str[@name='dest'][.='title']]",
+
+            "/response/arr[@name='copyfields']/lst[    str[@name='source'][.='*_i']"
+           +"                                      and str[@name='dest'][.='dest_sub_no_ast_s']]");
+  }
+}

Added: lucene/dev/trunk/solr/core/src/test/org/apache/solr/rest/TestDynamicFieldCollectionResource.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/test/org/apache/solr/rest/TestDynamicFieldCollectionResource.java?rev=1453161&view=auto
==============================================================================
--- lucene/dev/trunk/solr/core/src/test/org/apache/solr/rest/TestDynamicFieldCollectionResource.java (added)
+++ lucene/dev/trunk/solr/core/src/test/org/apache/solr/rest/TestDynamicFieldCollectionResource.java Wed Mar  6 04:50:33 2013
@@ -0,0 +1,62 @@
+package org.apache.solr.rest;
+/*
+ * 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.
+ */
+
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class TestDynamicFieldCollectionResource extends SchemaRestletTestBase {
+  @Test
+  public void testGetAllDynamicFields() throws Exception {
+    assertQ("/schema/dynamicfields?indent=on&wt=xml",
+        "(/response/arr[@name='dynamicfields']/lst/str[@name='name'])[1] = '*_coordinate'",
+        "(/response/arr[@name='dynamicfields']/lst/str[@name='name'])[2] = 'ignored_*'",
+        "(/response/arr[@name='dynamicfields']/lst/str[@name='name'])[3] = '*_mfacet'",
+        "count(//copySources/str)=count(//copyDests/str)");
+  }
+
+  @Test
+  public void testGetTwoDynamicFields() throws IOException {
+    assertQ("/schema/dynamicfields?indent=on&wt=xml&fl=*_i,*_s",
+            "count(/response/arr[@name='dynamicfields']/lst/str[@name='name']) = 2",
+            "(/response/arr[@name='dynamicfields']/lst/str[@name='name'])[1] = '*_i'",
+            "(/response/arr[@name='dynamicfields']/lst/str[@name='name'])[2] = '*_s'");
+  }
+
+  @Test
+  public void testNotFoundDynamicFields() throws IOException {
+    assertQ("/schema/dynamicfields?indent=on&wt=xml&fl=*_not_in_there,this_one_isnt_either_*",
+        "count(/response/arr[@name='dynamicfields']) = 1",
+        "count(/response/arr[@name='dynamicfields']/lst/str[@name='name']) = 0");
+  }
+
+  @Test
+  public void testJsonGetAllDynamicFields() throws Exception {
+    assertJQ("/schema/dynamicfields?indent=on",
+             "/dynamicfields/[0]/name=='*_coordinate'",
+             "/dynamicfields/[1]/name=='ignored_*'",
+             "/dynamicfields/[2]/name=='*_mfacet'");
+  }
+  
+  @Test
+  public void testJsonGetTwoDynamicFields() throws Exception {
+    assertJQ("/schema/dynamicfields?indent=on&fl=*_i,*_s&wt=xml", // assertJQ will fix the wt param to be json
+             "/dynamicfields/[0]/name=='*_i'",
+             "/dynamicfields/[1]/name=='*_s'");
+  }
+}

Added: lucene/dev/trunk/solr/core/src/test/org/apache/solr/rest/TestDynamicFieldResource.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/test/org/apache/solr/rest/TestDynamicFieldResource.java?rev=1453161&view=auto
==============================================================================
--- lucene/dev/trunk/solr/core/src/test/org/apache/solr/rest/TestDynamicFieldResource.java (added)
+++ lucene/dev/trunk/solr/core/src/test/org/apache/solr/rest/TestDynamicFieldResource.java Wed Mar  6 04:50:33 2013
@@ -0,0 +1,70 @@
+package org.apache.solr.rest;
+/*
+ * 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.
+ */
+
+import org.junit.Test;
+
+public class TestDynamicFieldResource extends SchemaRestletTestBase {
+  @Test
+  public void testGetDynamicField() throws Exception {
+    assertQ("/schema/dynamicfields/*_i?indent=on&wt=xml&showDefaults=on",
+            "count(/response/lst[@name='dynamicfield']) = 1",
+            "/response/lst[@name='dynamicfield']/str[@name='name'] = '*_i'",
+            "/response/lst[@name='dynamicfield']/str[@name='type'] = 'int'",
+            "/response/lst[@name='dynamicfield']/bool[@name='indexed'] = 'true'",
+            "/response/lst[@name='dynamicfield']/bool[@name='stored'] = 'true'",
+            "/response/lst[@name='dynamicfield']/bool[@name='docValues'] = 'false'",
+            "/response/lst[@name='dynamicfield']/bool[@name='termVectors'] = 'false'",
+            "/response/lst[@name='dynamicfield']/bool[@name='termPositions'] = 'false'",
+            "/response/lst[@name='dynamicfield']/bool[@name='termOffsets'] = 'false'",
+            "/response/lst[@name='dynamicfield']/bool[@name='omitNorms'] = 'true'",
+            "/response/lst[@name='dynamicfield']/bool[@name='omitTermFreqAndPositions'] = 'true'",
+            "/response/lst[@name='dynamicfield']/bool[@name='omitPositions'] = 'false'",
+            "/response/lst[@name='dynamicfield']/bool[@name='storeOffsetsWithPositions'] = 'false'",
+            "/response/lst[@name='dynamicfield']/bool[@name='multiValued'] = 'false'",
+            "/response/lst[@name='dynamicfield']/bool[@name='required'] = 'false'",
+            "/response/lst[@name='dynamicfield']/bool[@name='tokenized'] = 'false'");
+  }
+
+  @Test
+  public void testGetNotFoundDynamicField() throws Exception {
+    assertQ("/schema/dynamicfields/*not_in_there?indent=on&wt=xml",
+            "count(/response/lst[@name='dynamicfield']) = 0",
+            "/response/lst[@name='responseHeader']/int[@name='status'] = '404'",
+            "/response/lst[@name='error']/int[@name='code'] = '404'");
+  } 
+  
+  @Test
+  public void testJsonGetDynamicField() throws Exception {
+    assertJQ("/schema/dynamicfields/*_i?indent=on&showDefaults=on",
+             "/dynamicfield/name=='*_i'",
+             "/dynamicfield/type=='int'",
+             "/dynamicfield/indexed==true",
+             "/dynamicfield/stored==true",
+             "/dynamicfield/docValues==false",
+             "/dynamicfield/termVectors==false",
+             "/dynamicfield/termPositions==false",
+             "/dynamicfield/termOffsets==false",
+             "/dynamicfield/omitNorms==true",
+             "/dynamicfield/omitTermFreqAndPositions==true",
+             "/dynamicfield/omitPositions==false",
+             "/dynamicfield/storeOffsetsWithPositions==false",
+             "/dynamicfield/multiValued==false",
+             "/dynamicfield/required==false",
+             "/dynamicfield/tokenized==false");
+  }
+}



Mime
View raw message