You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by gs...@apache.org on 2013/07/09 05:10:13 UTC

svn commit: r1501051 - in /lucene/dev/branches/branch_4x: ./ solr/ solr/core/ solr/core/src/java/org/apache/solr/rest/schema/ solr/core/src/java/org/apache/solr/schema/ solr/core/src/test/org/apache/solr/rest/schema/

Author: gsingers
Date: Tue Jul  9 03:10:11 2013
New Revision: 1501051

URL: http://svn.apache.org/r1501051
Log:
SOLR-5010: merge from trunk

Modified:
    lucene/dev/branches/branch_4x/   (props changed)
    lucene/dev/branches/branch_4x/solr/   (props changed)
    lucene/dev/branches/branch_4x/solr/core/   (props changed)
    lucene/dev/branches/branch_4x/solr/core/src/java/org/apache/solr/rest/schema/CopyFieldCollectionResource.java
    lucene/dev/branches/branch_4x/solr/core/src/java/org/apache/solr/rest/schema/FieldCollectionResource.java
    lucene/dev/branches/branch_4x/solr/core/src/java/org/apache/solr/rest/schema/FieldResource.java
    lucene/dev/branches/branch_4x/solr/core/src/java/org/apache/solr/schema/IndexSchema.java
    lucene/dev/branches/branch_4x/solr/core/src/java/org/apache/solr/schema/ManagedIndexSchema.java
    lucene/dev/branches/branch_4x/solr/core/src/test/org/apache/solr/rest/schema/TestManagedSchemaFieldResource.java

Modified: lucene/dev/branches/branch_4x/solr/core/src/java/org/apache/solr/rest/schema/CopyFieldCollectionResource.java
URL: http://svn.apache.org/viewvc/lucene/dev/branches/branch_4x/solr/core/src/java/org/apache/solr/rest/schema/CopyFieldCollectionResource.java?rev=1501051&r1=1501050&r2=1501051&view=diff
==============================================================================
--- lucene/dev/branches/branch_4x/solr/core/src/java/org/apache/solr/rest/schema/CopyFieldCollectionResource.java (original)
+++ lucene/dev/branches/branch_4x/solr/core/src/java/org/apache/solr/rest/schema/CopyFieldCollectionResource.java Tue Jul  9 03:10:11 2013
@@ -17,37 +17,46 @@ package org.apache.solr.rest.schema;
  */
 
 
+import org.apache.solr.common.SolrException;
 import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.rest.GETable;
+import org.apache.solr.rest.POSTable;
 import org.apache.solr.schema.IndexSchema;
+import org.apache.solr.schema.ManagedIndexSchema;
+import org.noggit.ObjectBuilder;
+import org.restlet.data.MediaType;
 import org.restlet.representation.Representation;
 import org.restlet.resource.ResourceException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
  * This class responds to requests at /solr/(corename)/schema/copyfields
- * 
  * <p/>
- * 
+ *
  * To restrict the set of copyFields in the response, specify one or both
  * of the following as query parameters, with values as space and/or comma
  * separated dynamic or explicit field names:
- * 
+ *
  * <ul>
  *   <li>dest.fl: include copyFields that have one of these as a destination</li>
  *   <li>source.fl: include copyFields that have one of these as a source</li>
  * </ul>
- * 
+ *
  * If both dest.fl and source.fl are given as query parameters, the copyfields
  * in the response will be restricted to those that match any of the destinations
  * in dest.fl and also match any of the sources in source.fl.
  */
-public class CopyFieldCollectionResource extends BaseFieldResource implements GETable {
+public class CopyFieldCollectionResource extends BaseFieldResource implements GETable, POSTable {
   private static final Logger log = LoggerFactory.getLogger(CopyFieldCollectionResource.class);
   private static final String SOURCE_FIELD_LIST = IndexSchema.SOURCE + "." + CommonParams.FL;
   private static final String DESTINATION_FIELD_LIST = IndexSchema.DESTINATION + "." + CommonParams.FL;
@@ -94,4 +103,78 @@ public class CopyFieldCollectionResource
 
     return new SolrOutputRepresentation();
   }
+
+  @Override
+  public Representation post(Representation entity) throws ResourceException {
+    try {
+      if (!getSchema().isMutable()) {
+        final String message = "This IndexSchema is not mutable.";
+        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, message);
+      } else {
+        if (!entity.getMediaType().equals(MediaType.APPLICATION_JSON, true)) {
+          String message = "Only media type " + MediaType.APPLICATION_JSON.toString() + " is accepted."
+              + "  Request has media type " + entity.getMediaType().toString() + ".";
+          log.error(message);
+          throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, message);
+        } else {
+          Object object = ObjectBuilder.fromJSON(entity.getText());
+
+          if (!(object instanceof List)) {
+            String message = "Invalid JSON type " + object.getClass().getName() + ", expected List of the form"
+                + " (ignore the backslashes): [{\"source\":\"foo\",\"dest\":\"comma-separated list of targets\"}, {...}, ...]";
+            log.error(message);
+            throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, message);
+          } else {
+            List<Map<String, Object>> list = (List<Map<String, Object>>) object;
+            Map<String, Collection<String>> fieldsToCopy = new HashMap<String, Collection<String>>();
+            ManagedIndexSchema oldSchema = (ManagedIndexSchema) getSchema();
+            Set<String> malformed = new HashSet<String>();
+            for (Map<String,Object> map : list) {
+              String fieldName = (String)map.get(IndexSchema.SOURCE);
+              if (null == fieldName) {
+                String message = "Missing '" + IndexSchema.SOURCE + "' mapping.";
+                log.error(message);
+                throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, message);
+              }
+              String destinations = (String)map.get(IndexSchema.DESTINATION);
+              if (destinations == null) {
+                String message = "Missing '" + IndexSchema.DESTINATION + "' mapping.";
+                log.error(message);
+                throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, message);
+              }
+              String [] splits = destinations.split(",");
+              Set<String> destinationSet = new HashSet<String>();
+              if (splits != null && splits.length > 0){
+                for (int i = 0; i < splits.length; i++) {
+                  destinationSet.add(splits[i].trim());
+                }
+                fieldsToCopy.put(fieldName, destinationSet);
+              } else {
+                malformed.add(fieldName);
+              }
+            }
+            if (malformed.size() > 0){
+              StringBuilder message = new StringBuilder("Malformed destination(s) for: ");
+              for (String s : malformed) {
+                message.append(s).append(", ");
+              }
+              if (message.length() > 2) {
+                message.setLength(message.length() - 2);//drop the last ,
+              }
+              log.error(message.toString().trim());
+              throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, message.toString().trim());
+            }
+            IndexSchema newSchema = oldSchema.addCopyFields(fieldsToCopy);
+            if (newSchema != null) {
+              getSolrCore().setLatestSchema(newSchema);
+            }
+          }
+        }
+      }
+    } catch (Exception e) {
+      getSolrResponse().setException(e);
+    }
+    handlePostExecution(log);
+    return new SolrOutputRepresentation();
+  }
 }

Modified: lucene/dev/branches/branch_4x/solr/core/src/java/org/apache/solr/rest/schema/FieldCollectionResource.java
URL: http://svn.apache.org/viewvc/lucene/dev/branches/branch_4x/solr/core/src/java/org/apache/solr/rest/schema/FieldCollectionResource.java?rev=1501051&r1=1501050&r2=1501051&view=diff
==============================================================================
--- lucene/dev/branches/branch_4x/solr/core/src/java/org/apache/solr/rest/schema/FieldCollectionResource.java (original)
+++ lucene/dev/branches/branch_4x/solr/core/src/java/org/apache/solr/rest/schema/FieldCollectionResource.java Tue Jul  9 03:10:11 2013
@@ -33,9 +33,12 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.SortedSet;
 import java.util.TreeSet;
 
@@ -44,22 +47,22 @@ import java.util.TreeSet;
  * <p/>
  * Two query parameters are supported:
  * <ul>
- *   <li>
- *     "fl": a comma- and/or space-separated list of fields to send properties
- *     for in the response, rather than the default: all of them.
- *   </li>
- *   <li>
- *     "includeDynamic": if the "fl" parameter is specified, matching dynamic
- *     fields are included in the response and identified with the "dynamicBase"
- *     property.  If the "fl" parameter is not specified, the "includeDynamic"
- *     query parameter is ignored.
- *   </li>
+ * <li>
+ * "fl": a comma- and/or space-separated list of fields to send properties
+ * for in the response, rather than the default: all of them.
+ * </li>
+ * <li>
+ * "includeDynamic": if the "fl" parameter is specified, matching dynamic
+ * fields are included in the response and identified with the "dynamicBase"
+ * property.  If the "fl" parameter is not specified, the "includeDynamic"
+ * query parameter is ignored.
+ * </li>
  * </ul>
  */
-public class FieldCollectionResource extends BaseFieldResource implements GETable,POSTable {
+public class FieldCollectionResource extends BaseFieldResource implements GETable, POSTable {
   private static final Logger log = LoggerFactory.getLogger(FieldCollectionResource.class);
   private boolean includeDynamic;
-  
+
   public FieldCollectionResource() {
     super();
   }
@@ -109,65 +112,78 @@ public class FieldCollectionResource ext
 
     return new SolrOutputRepresentation();
   }
-  
+
   @Override
   public Representation post(Representation entity) {
     try {
-      if ( ! getSchema().isMutable()) {
+      if (!getSchema().isMutable()) {
         final String message = "This IndexSchema is not mutable.";
         throw new SolrException(ErrorCode.BAD_REQUEST, message);
       } else {
         if (null == entity.getMediaType()) {
           entity.setMediaType(MediaType.APPLICATION_JSON);
         }
-        if ( ! entity.getMediaType().equals(MediaType.APPLICATION_JSON, true)) {
+        if (!entity.getMediaType().equals(MediaType.APPLICATION_JSON, true)) {
           String message = "Only media type " + MediaType.APPLICATION_JSON.toString() + " is accepted."
               + "  Request has media type " + entity.getMediaType().toString() + ".";
           log.error(message);
           throw new SolrException(ErrorCode.BAD_REQUEST, message);
         } else {
           Object object = ObjectBuilder.fromJSON(entity.getText());
-          Map<String, String> copyFields = new HashMap<String, String>();
           if ( ! (object instanceof List)) {
             String message = "Invalid JSON type " + object.getClass().getName() + ", expected List of the form"
                 + " (ignore the backslashes): [{\"name\":\"foo\",\"type\":\"text_general\", ...}, {...}, ...]";
             log.error(message);
             throw new SolrException(ErrorCode.BAD_REQUEST, message);
           } else {
-            List<Map<String,Object>> list = (List<Map<String,Object>>)object;
+            List<Map<String, Object>> list = (List<Map<String, Object>>) object;
             List<SchemaField> newFields = new ArrayList<SchemaField>();
             IndexSchema oldSchema = getSchema();
-            for (Map<String,Object> map : list) {
-              String fieldName = (String)map.remove(IndexSchema.NAME);
+            Map<String, Collection<String>> copyFields = new HashMap<String, Collection<String>>();
+            Set<String> malformed = new HashSet<String>();
+            for (Map<String, Object> map : list) {
+              String fieldName = (String) map.remove(IndexSchema.NAME);
               if (null == fieldName) {
                 String message = "Missing '" + IndexSchema.NAME + "' mapping.";
                 log.error(message);
                 throw new SolrException(ErrorCode.BAD_REQUEST, message);
               }
-              String fieldType = (String)map.remove(IndexSchema.TYPE);
+              String fieldType = (String) map.remove(IndexSchema.TYPE);
               if (null == fieldType) {
                 String message = "Missing '" + IndexSchema.TYPE + "' mapping.";
                 log.error(message);
                 throw new SolrException(ErrorCode.BAD_REQUEST, message);
               }
               // copyFields:"comma separated list of destination fields"
-              String copyTo = (String)map.get(IndexSchema.COPY_FIELDS);
-              if (copyTo != null){
+              String copyTo = (String) map.get(IndexSchema.COPY_FIELDS);
+              if (copyTo != null) {
                 map.remove(IndexSchema.COPY_FIELDS);
-                copyFields.put(fieldName, copyTo);
+                String[] splits = copyTo.split(",");
+                Set<String> destinations = new HashSet<String>();
+                if (splits != null && splits.length > 0) {
+                  for (int i = 0; i < splits.length; i++) {
+                    destinations.add(splits[i].trim());
+                  }
+                  copyFields.put(fieldName, destinations);
+                } else{
+                  malformed.add(fieldName);
+                }
               }
               newFields.add(oldSchema.newField(fieldName, fieldType, map));
             }
-            IndexSchema newSchema = oldSchema.addFields(newFields);
-            for (Map.Entry<String, String> entry : copyFields.entrySet()) {
-              //key is the source, value is a comma separated list of targets
-              String [] splits = entry.getValue().split(",");
-              if (splits != null && splits.length > 0){
-                for (int i = 0; i < splits.length; i++) {
-                  newSchema.registerCopyField(entry.getKey(), splits[i].trim());
-                }
+            if (malformed.size() > 0){
+              StringBuilder message = new StringBuilder("Malformed destination(s) for: ");
+              for (String s : malformed) {
+                message.append(s).append(", ");
+              }
+              if (message.length() > 2) {
+                message.setLength(message.length() - 2);//drop the last ,
               }
+              log.error(message.toString().trim());
+              throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, message.toString().trim());
             }
+            IndexSchema newSchema = oldSchema.addFields(newFields, copyFields);
+
             getSolrCore().setLatestSchema(newSchema);
           }
         }

Modified: lucene/dev/branches/branch_4x/solr/core/src/java/org/apache/solr/rest/schema/FieldResource.java
URL: http://svn.apache.org/viewvc/lucene/dev/branches/branch_4x/solr/core/src/java/org/apache/solr/rest/schema/FieldResource.java?rev=1501051&r1=1501050&r2=1501051&view=diff
==============================================================================
--- lucene/dev/branches/branch_4x/solr/core/src/java/org/apache/solr/rest/schema/FieldResource.java (original)
+++ lucene/dev/branches/branch_4x/solr/core/src/java/org/apache/solr/rest/schema/FieldResource.java Tue Jul  9 03:10:11 2013
@@ -150,8 +150,17 @@ public class FieldResource extends BaseF
                 if (copyTo != null) {
                   map.remove(IndexSchema.COPY_FIELDS);
                   String [] tmp = copyTo.split(",");
-                  copyFieldNames = new HashSet<String>(tmp.length);
-                  Collections.addAll(copyFieldNames, tmp);
+                  if (tmp != null && tmp.length > 0) {
+                    copyFieldNames = new HashSet<String>(tmp.length);
+                    for (int i = 0; i < tmp.length; i++) {
+                      copyFieldNames.add(tmp[i].trim());
+                    }
+                  } else {
+                    //the user specified copy fields, but then passed in something invalid
+                    String msg = "Invalid " + IndexSchema.COPY_FIELDS + " for field: " + fieldName;
+                    log.error(msg);
+                    throw new SolrException(ErrorCode.BAD_REQUEST, msg);
+                  }
                 }
                 SchemaField newField = oldSchema.newField(fieldName, fieldType, map);
                 IndexSchema newSchema = oldSchema.addField(newField, copyFieldNames);

Modified: lucene/dev/branches/branch_4x/solr/core/src/java/org/apache/solr/schema/IndexSchema.java
URL: http://svn.apache.org/viewvc/lucene/dev/branches/branch_4x/solr/core/src/java/org/apache/solr/schema/IndexSchema.java?rev=1501051&r1=1501050&r2=1501051&view=diff
==============================================================================
--- lucene/dev/branches/branch_4x/solr/core/src/java/org/apache/solr/schema/IndexSchema.java (original)
+++ lucene/dev/branches/branch_4x/solr/core/src/java/org/apache/solr/schema/IndexSchema.java Tue Jul  9 03:10:11 2013
@@ -1438,7 +1438,7 @@ public class IndexSchema {
    * Copies this schema, adds the given field to the copy, then persists the new schema.
    *
    * @param newField the SchemaField to add
-   * @param copyFieldNames 0 or more names of targets to copy this field to
+   * @param copyFieldNames 0 or more names of targets to copy this field to.  The targets must already exist.
    * @return a new IndexSchema based on this schema with newField added
    * @see #newField(String, String, Map)
    */
@@ -1465,7 +1465,7 @@ public class IndexSchema {
    * Copies this schema, adds the given fields to the copy, then persists the new schema.
    *
    * @param newFields the SchemaFields to add
-   * @param copyFieldNames 0 or more names of targets to copy this field to
+   * @param copyFieldNames 0 or more names of targets to copy this field to.  The target fields must already exist.
    * @return a new IndexSchema based on this schema with newFields added
    * @see #newField(String, String, Map)
    */
@@ -1476,6 +1476,17 @@ public class IndexSchema {
   }
 
   /**
+   * Copies this schema and adds the new copy fields to the copy, then persists the new schema
+   * @param copyFields Key is the name of the source field name, value is a collection of target field names.  Fields must exist.
+   * @return The new Schema with the copy fields added
+   */
+  public IndexSchema addCopyFields(Map<String, Collection<String>> copyFields){
+    String msg = "This IndexSchema is not mutable.";
+    log.error(msg);
+    throw new SolrException(ErrorCode.SERVER_ERROR, msg);
+  }
+
+  /**
    * Returns a SchemaField if the given fieldName does not already 
    * exist in this schema, and does not match any dynamic fields 
    * in this schema.  The resulting SchemaField can be used in a call

Modified: lucene/dev/branches/branch_4x/solr/core/src/java/org/apache/solr/schema/ManagedIndexSchema.java
URL: http://svn.apache.org/viewvc/lucene/dev/branches/branch_4x/solr/core/src/java/org/apache/solr/schema/ManagedIndexSchema.java?rev=1501051&r1=1501050&r2=1501051&view=diff
==============================================================================
--- lucene/dev/branches/branch_4x/solr/core/src/java/org/apache/solr/schema/ManagedIndexSchema.java (original)
+++ lucene/dev/branches/branch_4x/solr/core/src/java/org/apache/solr/schema/ManagedIndexSchema.java Tue Jul  9 03:10:11 2013
@@ -166,12 +166,12 @@ public final class ManagedIndexSchema ex
   }
 
   @Override
-  public IndexSchema addField(SchemaField newField) {
+  public ManagedIndexSchema addField(SchemaField newField) {
     return addFields(Arrays.asList(newField));
   }
 
   @Override
-  public IndexSchema addField(SchemaField newField, Collection<String> copyFieldNames) {
+  public ManagedIndexSchema addField(SchemaField newField, Collection<String> copyFieldNames) {
     return addFields(Arrays.asList(newField), Collections.singletonMap(newField.getName(), copyFieldNames));
   }
 
@@ -182,12 +182,12 @@ public final class ManagedIndexSchema ex
   }
 
   @Override
-  public IndexSchema addFields(Collection<SchemaField> newFields) {
+  public ManagedIndexSchema addFields(Collection<SchemaField> newFields) {
     return addFields(newFields, Collections.<String, Collection<String>>emptyMap());
   }
 
   @Override
-  public IndexSchema addFields(Collection<SchemaField> newFields, Map<String, Collection<String>> copyFieldNames) {
+  public ManagedIndexSchema addFields(Collection<SchemaField> newFields, Map<String, Collection<String>> copyFieldNames) {
     ManagedIndexSchema newSchema = null;
     if (isMutable) {
       boolean success = false;
@@ -243,6 +243,39 @@ public final class ManagedIndexSchema ex
   }
 
   @Override
+  public ManagedIndexSchema addCopyFields(Map<String, Collection<String>> copyFields) {
+    ManagedIndexSchema newSchema = null;
+    if (isMutable) {
+      boolean success = false;
+      while (!success) { // optimistic concurrency
+        // even though fields is volatile, we need to synchronize to avoid two addCopyFields
+        // happening concurrently (and ending up missing one of them)
+        synchronized (getSchemaUpdateLock()) {
+          newSchema = shallowCopy(true);
+          for (Map.Entry<String, Collection<String>> entry : copyFields.entrySet()) {
+            //Key is the name of the field, values are the destinations
+
+            for (String destination : entry.getValue()) {
+              newSchema.registerCopyField(entry.getKey(), destination);
+            }
+          }
+          //TODO: move this common stuff out to shared methods
+           // Run the callbacks on SchemaAware now that everything else is done
+          for (SchemaAware aware : newSchema.schemaAware) {
+            aware.inform(newSchema);
+          }
+          newSchema.refreshAnalyzers();
+          success = newSchema.persistManagedSchema(false); // don't just create - update it if it already exists
+          if (success) {
+            log.debug("Added copy fields for {} sources", copyFields.size());
+          }
+        }
+      }
+    }
+    return newSchema;
+  }
+
+  @Override
   public SchemaField newField(String fieldName, String fieldType, Map<String,?> options) {
     SchemaField sf; 
     if (isMutable) {

Modified: lucene/dev/branches/branch_4x/solr/core/src/test/org/apache/solr/rest/schema/TestManagedSchemaFieldResource.java
URL: http://svn.apache.org/viewvc/lucene/dev/branches/branch_4x/solr/core/src/test/org/apache/solr/rest/schema/TestManagedSchemaFieldResource.java?rev=1501051&r1=1501050&r2=1501051&view=diff
==============================================================================
--- lucene/dev/branches/branch_4x/solr/core/src/test/org/apache/solr/rest/schema/TestManagedSchemaFieldResource.java (original)
+++ lucene/dev/branches/branch_4x/solr/core/src/test/org/apache/solr/rest/schema/TestManagedSchemaFieldResource.java Tue Jul  9 03:10:11 2013
@@ -122,7 +122,13 @@ public class TestManagedSchemaFieldResou
     assertQ("/schema/copyfields/?indent=on&wt=xml&source.fl=fieldB",
         "count(/response/arr[@name='copyFields']/lst) = 1"
     );
-
+    //some bad usages
+    assertJPut("/schema/fields/fieldB",
+        "{\"type\":\"text\",\"stored\":\"false\", \"copyFields\":\",,,\"}",
+        "/error/msg==\"Invalid copyFields for field: fieldB\"");
+    assertJPut("/schema/fields/fieldC",
+        "{\"type\":\"text\",\"stored\":\"false\", \"copyFields\":\"some_nonexistent_field\"}",
+        "/error/msg==\"copyField dest :\\'some_nonexistent_field\\' is not an explicit field and doesn\\'t match a dynamicField.\"");
   }
 
   @Test
@@ -192,7 +198,37 @@ public class TestManagedSchemaFieldResou
     assertQ("/schema/copyfields/?indent=on&wt=xml&source.fl=fieldF",
         "count(/response/arr[@name='copyFields']/lst) = 2"
     );
+    //some bad usages
+    assertJPost("/schema/fields",
+              "[{\"name\":\"fieldX\",\"type\":\"text\",\"stored\":\"false\"},"
+               + "{\"name\":\"fieldY\",\"type\":\"text\",\"stored\":\"false\"},"
+               + " {\"name\":\"fieldZ\",\"type\":\"text\",\"stored\":\"false\", \"copyFields\":\",,,\"}]",
+                "/error/msg==\"Malformed destination(s) for: fieldZ\"");
+
+    assertJPost("/schema/fields",
+              "[{\"name\":\"fieldX\",\"type\":\"text\",\"stored\":\"false\"},"
+               + "{\"name\":\"fieldY\",\"type\":\"text\",\"stored\":\"false\"},"
+               + " {\"name\":\"fieldZ\",\"type\":\"text\",\"stored\":\"false\", \"copyFields\":\"some_nonexistent_field\"}]",
+                "/error/msg==\"copyField dest :\\'some_nonexistent_field\\' is not an explicit field and doesn\\'t match a dynamicField.\"");
+  }
 
+  @Test
+  public void testPostCopyFields() throws Exception {
+    assertJPost("/schema/fields",
+              "[{\"name\":\"fieldA\",\"type\":\"text\",\"stored\":\"false\"},"
+               + "{\"name\":\"fieldB\",\"type\":\"text\",\"stored\":\"false\"},"
+               + "{\"name\":\"fieldC\",\"type\":\"text\",\"stored\":\"false\"},"
+                  + "{\"name\":\"fieldD\",\"type\":\"text\",\"stored\":\"false\"},"
+               + " {\"name\":\"fieldE\",\"type\":\"text\",\"stored\":\"false\"}]",
+                "/responseHeader/status==0");
+    assertJPost("/schema/copyfields", "[{\"source\":\"fieldA\", \"dest\":\"fieldB\"},{\"source\":\"fieldD\", \"dest\":\"fieldC,   fieldE\"}]", "/responseHeader/status==0");
+    assertQ("/schema/copyfields/?indent=on&wt=xml&source.fl=fieldA",
+        "count(/response/arr[@name='copyFields']/lst) = 1");
+    assertQ("/schema/copyfields/?indent=on&wt=xml&source.fl=fieldD",
+        "count(/response/arr[@name='copyFields']/lst) = 2");
+    assertJPost("/schema/copyfields", "[{\"source\":\"fieldD\", \"dest\":\",,,\"}]", "/error/msg==\"Malformed destination(s) for: fieldD\"");
+    assertJPost("/schema/copyfields", "[{\"source\":\"some_nonexistent_field\", \"dest\":\"fieldA\"}]", "/error/msg==\"copyField source :\\'some_nonexistent_field\\' is not a glob and doesn\\'t match any explicit field or dynamicField.\"");
+    assertJPost("/schema/copyfields", "[{\"source\":\"fieldD\", \"dest\":\"some_nonexistent_field\"}]", "/error/msg==\"copyField dest :\\'some_nonexistent_field\\' is not an explicit field and doesn\\'t match a dynamicField.\"");
   }
 
 }