This is an automated email from the ASF dual-hosted git repository.

magibney pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/main by this push:
     new 6ff8131  SOLR-9376: [xml] and [json] RawValue DocTransformers should 
work in cloud mode (#513)
6ff8131 is described below

commit 6ff81312607dd5d33f87dc52aed9d52938dc6883
Author: Michael Gibney <[email protected]>
AuthorDate: Tue Jan 25 14:44:41 2022 -0500

    SOLR-9376: [xml] and [json] RawValue DocTransformers should work in cloud 
mode (#513)
---
 solr/CHANGES.txt                                   |   3 +
 .../solr/response/GeoJSONResponseWriter.java       |   7 +-
 .../apache/solr/response/JSONResponseWriter.java   |  14 +-
 .../java/org/apache/solr/response/JSONWriter.java  |   2 +-
 .../apache/solr/response/PHPResponseWriter.java    |   4 +-
 .../solr/response/PHPSerializedResponseWriter.java |   8 +-
 .../solr/response/RawShimTextResponseWriter.java   | 111 +++++++
 .../org/apache/solr/response/SchemaXmlWriter.java  |   8 +-
 .../solr/response/TabularResponseWriter.java       |   2 +-
 .../apache/solr/response/TextResponseWriter.java   |  47 ++-
 .../java/org/apache/solr/response/XMLWriter.java   |  23 +-
 .../solr/response/transform/DocTransformer.java    |  18 ++
 .../solr/response/transform/DocTransformers.java   |   9 +
 .../response/transform/GeoTransformerFactory.java  | 148 +++++----
 .../transform/RawValueTransformerFactory.java      |  94 +++---
 .../response/transform/RenameFieldTransformer.java |  11 +-
 .../response/transform/TransformerFactory.java     |  57 ++++
 .../solr/response/transform/WriteableGeoJSON.java  |  55 ----
 .../org/apache/solr/search/SolrReturnFields.java   | 101 ++++--
 .../apache/solr/cloud/TestRandomFlRTGCloud.java    | 351 ++++++++++++++++++---
 .../org/apache/solr/response/JSONWriterTest.java   |   4 +-
 .../apache/solr/response/TestRawTransformer.java   | 184 +++++++++--
 .../handler/extraction/XLSXResponseWriter.java     |   5 +-
 .../src/major-changes-in-solr-9.adoc               |   7 +
 .../solr/client/solrj/impl/XMLResponseParser.java  |  38 +++
 .../apache/solr/common/util/JsonTextWriter.java    |  16 +-
 .../org/apache/solr/common/util/TextWriter.java    |  55 +++-
 .../apache/solr/common/util/WriteableValue.java    |  25 --
 28 files changed, 1060 insertions(+), 347 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index fbef2ff..abbcdbc 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -266,6 +266,9 @@ when told to. The admin UI now tells it to. (Nazerke 
Seidan, David Smiley)
 
 * SOLR-15884: Backup responses now use a map to return information instead of 
a list (Houston Putman, Christine Poerschke)
 
+* SOLR-9376: Raw value DocTransformers (`[xml]`, `[json]`, `[geo w=GeoJSON]`) 
now work
+  in a distributed/SolrCloud context (Michael Gibney)
+
 Build
 ---------------------
 * LUCENE-9077 LUCENE-9433: Support Gradle build, remove Ant support from trunk 
(Dawid Weiss, Erick Erickson, Uwe Schindler et.al.)
diff --git 
a/solr/core/src/java/org/apache/solr/response/GeoJSONResponseWriter.java 
b/solr/core/src/java/org/apache/solr/response/GeoJSONResponseWriter.java
index 89d556b..c16e5b6 100644
--- a/solr/core/src/java/org/apache/solr/response/GeoJSONResponseWriter.java
+++ b/solr/core/src/java/org/apache/solr/response/GeoJSONResponseWriter.java
@@ -30,7 +30,6 @@ import org.apache.solr.common.SolrException.ErrorCode;
 import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.transform.WriteableGeoJSON;
 import org.apache.solr.schema.AbstractSpatialFieldType;
 import org.apache.solr.schema.SchemaField;
 import org.apache.solr.search.ReturnFields;
@@ -259,11 +258,7 @@ class GeoJSONWriter extends JSONWriter {
     }
     else if(geo instanceof IndexableField) {
       str = ((IndexableField)geo).stringValue();
-    }
-    else if(geo instanceof WriteableGeoJSON) {
-      shape = ((WriteableGeoJSON)geo).shape;
-    }
-    else {
+    } else {
       str = geo.toString();
     }
     
diff --git 
a/solr/core/src/java/org/apache/solr/response/JSONResponseWriter.java 
b/solr/core/src/java/org/apache/solr/response/JSONResponseWriter.java
index e83f923..3e5cc8b 100644
--- a/solr/core/src/java/org/apache/solr/response/JSONResponseWriter.java
+++ b/solr/core/src/java/org/apache/solr/response/JSONResponseWriter.java
@@ -204,6 +204,16 @@ class ArrayOfNameTypeValueJSONWriter extends JSONWriter {
   }
 
   @Override
+  public void writeStrRaw(String name, String val) throws IOException {
+    if (writeTypeAndValueKey) {
+      throw new IllegalStateException("NamedList should never be a field 
value");
+      // and thus `writeTypeAndValueKey` should always have been cleared (set 
to false) by the
+      // time `writeStrRaw(...)` is called (at the level of individual 
SolrDocument fields).
+    }
+    super.writeStrRaw(name, val);
+  }
+
+  @Override
   public void writeStr(String name, String val, boolean needsEscaping) throws 
IOException {
     ifNeededWriteTypeAndValueKey("str");
     super.writeStr(name, val, needsEscaping);
@@ -230,9 +240,9 @@ class ArrayOfNameTypeValueJSONWriter extends JSONWriter {
   }
 
   @Override
-  public void writeArray(String name, Iterator<?> val) throws IOException {
+  public void writeArray(String name, Iterator<?> val, boolean raw) throws 
IOException {
     ifNeededWriteTypeAndValueKey("array");
-    super.writeArray(name, val);
+    super.writeArray(name, val, raw);
   }
 
   @Override
diff --git a/solr/core/src/java/org/apache/solr/response/JSONWriter.java 
b/solr/core/src/java/org/apache/solr/response/JSONWriter.java
index 4e17696..d805cf2 100644
--- a/solr/core/src/java/org/apache/solr/response/JSONWriter.java
+++ b/solr/core/src/java/org/apache/solr/response/JSONWriter.java
@@ -103,7 +103,7 @@ public class JSONWriter extends TextResponseWriter 
implements JsonTextWriter {
       indent();
       writeKey(fname, true);
       Object val = doc.getFieldValue(fname);
-      writeVal(fname, val);
+      writeVal(fname, val, shouldWriteRaw(fname, returnFields));
     }
 
     if(doc.hasChildDocuments()) {
diff --git a/solr/core/src/java/org/apache/solr/response/PHPResponseWriter.java 
b/solr/core/src/java/org/apache/solr/response/PHPResponseWriter.java
index b10fb3d..ce2f781 100644
--- a/solr/core/src/java/org/apache/solr/response/PHPResponseWriter.java
+++ b/solr/core/src/java/org/apache/solr/response/PHPResponseWriter.java
@@ -78,8 +78,8 @@ class PHPWriter extends JSONWriter {
   }
 
   @Override
-  public void writeArray(String name, List<?> l) throws IOException {
-    writeArray(name,l.iterator());
+  public void writeArray(String name, List<?> l, boolean raw) throws 
IOException {
+    writeArray(name,l.iterator(), raw);
   }
 
   @Override
diff --git 
a/solr/core/src/java/org/apache/solr/response/PHPSerializedResponseWriter.java 
b/solr/core/src/java/org/apache/solr/response/PHPSerializedResponseWriter.java
index 35b41af..b8c5882 100644
--- 
a/solr/core/src/java/org/apache/solr/response/PHPSerializedResponseWriter.java
+++ 
b/solr/core/src/java/org/apache/solr/response/PHPSerializedResponseWriter.java
@@ -168,7 +168,8 @@ class PHPSerializedWriter extends JSONWriter {
 
   
   @Override
-  public void writeArray(String name, Object[] val) throws IOException {
+  public void writeArray(String name, Object[] val, boolean raw) throws 
IOException {
+    assert !raw;
     writeMapOpener(val.length);
     for(int i=0; i < val.length; i++) {
       writeKey(i, false);
@@ -178,12 +179,13 @@ class PHPSerializedWriter extends JSONWriter {
   }
 
   @Override
-  public void writeArray(String name, Iterator<?> val) throws IOException {
+  public void writeArray(String name, Iterator<?> val, boolean raw) throws 
IOException {
+    assert !raw;
     ArrayList<Object> vals = new ArrayList<>();
     while( val.hasNext() ) {
       vals.add(val.next());
     }
-    writeArray(name, vals.toArray());
+    writeArray(name, vals.toArray(), false);
   }
   
   @Override
diff --git 
a/solr/core/src/java/org/apache/solr/response/RawShimTextResponseWriter.java 
b/solr/core/src/java/org/apache/solr/response/RawShimTextResponseWriter.java
new file mode 100644
index 0000000..dd7b9cc
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/response/RawShimTextResponseWriter.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.response;
+
+import org.apache.solr.common.SolrDocument;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.search.ReturnFields;
+
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * Utility class that delegates to another {@link TextResponseWriter}, but 
converts normal write requests
+ * into "raw" requests that write field values directly to the delegate {@link 
TextResponseWriter}'s backing writer.
+ */
+class RawShimTextResponseWriter extends TextResponseWriter {
+
+  private final TextResponseWriter backing;
+
+  RawShimTextResponseWriter(TextResponseWriter backing) {
+    super(null, false);
+    this.backing = backing;
+  }
+
+  // convert non-raw to raw. These are the reason this class exists (see class 
javadocs)
+  @Override
+  public void writeStr(String name, String val, boolean needsEscaping) throws 
IOException {
+    backing.writeStrRaw(name, val);
+  }
+
+  @Override
+  public void writeArray(String name, Iterator<?> val, boolean raw) throws 
IOException {
+    backing.writeArray(name, val, true);
+  }
+
+  // Other stuff; just no-op delegation
+  @Override
+  public void writeStartDocumentList(String name, long start, int size, long 
numFound, Float maxScore, Boolean numFoundExact) throws IOException {
+    backing.writeStartDocumentList(name, start, size, numFound, maxScore, 
numFoundExact);
+  }
+
+  @Override
+  public void writeSolrDocument(String name, SolrDocument doc, ReturnFields 
fields, int idx) throws IOException {
+    backing.writeSolrDocument(name, doc, fields, idx);
+  }
+
+  @Override
+  public void writeEndDocumentList() throws IOException {
+    backing.writeEndDocumentList();
+  }
+
+  @Override
+  public void writeMap(String name, Map<?, ?> val, boolean excludeOuter, 
boolean isFirstVal) throws IOException {
+    backing.writeMap(name, val, excludeOuter, isFirstVal);
+  }
+
+  @Override
+  public void writeNull(String name) throws IOException {
+    backing.writeNull(name);
+  }
+
+  @Override
+  public void writeInt(String name, String val) throws IOException {
+    backing.writeInt(name, val);
+  }
+
+  @Override
+  public void writeLong(String name, String val) throws IOException {
+    backing.writeLong(name, val);
+  }
+
+  @Override
+  public void writeBool(String name, String val) throws IOException {
+    backing.writeBool(name, val);
+  }
+
+  @Override
+  public void writeFloat(String name, String val) throws IOException {
+    backing.writeFloat(name, val);
+  }
+
+  @Override
+  public void writeDouble(String name, String val) throws IOException {
+    backing.writeDouble(name, val);
+  }
+
+  @Override
+  public void writeDate(String name, String val) throws IOException {
+    backing.writeDate(name, val);
+  }
+
+  @Override
+  public void writeNamedList(String name, NamedList<?> val) throws IOException 
{
+    backing.writeNamedList(name, val);
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/response/SchemaXmlWriter.java 
b/solr/core/src/java/org/apache/solr/response/SchemaXmlWriter.java
index 7ab424d..bdc08c6 100644
--- a/solr/core/src/java/org/apache/solr/response/SchemaXmlWriter.java
+++ b/solr/core/src/java/org/apache/solr/response/SchemaXmlWriter.java
@@ -365,17 +365,17 @@ public class SchemaXmlWriter extends TextResponseWriter {
   }
 
   @Override
-  public void writeArray(String name, Object[] val) throws IOException {
-    writeArray(name, Arrays.asList(val).iterator());
+  public void writeArray(String name, Object[] val, boolean raw) throws 
IOException {
+    writeArray(name, Arrays.asList(val).iterator(), raw);
   }
 
   @Override
-  public void writeArray(String name, Iterator<?> iter) throws IOException {
+  public void writeArray(String name, Iterator<?> iter, boolean raw) throws 
IOException {
     if( iter.hasNext() ) {
       startTag("arr", name, false );
       incLevel();
       while( iter.hasNext() ) {
-        writeVal(null, iter.next());
+        writeVal(null, iter.next(), raw);
       }
       decLevel();
       if (doIndent) indent();
diff --git 
a/solr/core/src/java/org/apache/solr/response/TabularResponseWriter.java 
b/solr/core/src/java/org/apache/solr/response/TabularResponseWriter.java
index 065d115..2148107 100644
--- a/solr/core/src/java/org/apache/solr/response/TabularResponseWriter.java
+++ b/solr/core/src/java/org/apache/solr/response/TabularResponseWriter.java
@@ -139,7 +139,7 @@ public abstract class TabularResponseWriter extends 
TextResponseWriter {
   }
 
   @Override
-  public void writeArray(String name, Iterator<?> val) throws IOException {
+  public void writeArray(String name, Iterator<?> val, boolean raw) throws 
IOException {
   }
 
   @Override
diff --git 
a/solr/core/src/java/org/apache/solr/response/TextResponseWriter.java 
b/solr/core/src/java/org/apache/solr/response/TextResponseWriter.java
index e19906a..9ed8be9 100644
--- a/solr/core/src/java/org/apache/solr/response/TextResponseWriter.java
+++ b/solr/core/src/java/org/apache/solr/response/TextResponseWriter.java
@@ -19,7 +19,11 @@ package org.apache.solr.response;
 import java.io.IOException;
 import java.io.Writer;
 import java.util.Calendar;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.Iterator;
+import java.util.Set;
 
 import org.apache.lucene.document.Document;
 import org.apache.lucene.index.IndexableField;
@@ -31,10 +35,12 @@ import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.util.FastWriter;
 import org.apache.solr.common.util.TextWriter;
 import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.transform.DocTransformer;
 import org.apache.solr.schema.IndexSchema;
 import org.apache.solr.schema.SchemaField;
 import org.apache.solr.search.DocList;
 import org.apache.solr.search.ReturnFields;
+import org.apache.solr.search.SolrReturnFields;
 
 /** Base class for text-oriented response writers.
  *
@@ -55,6 +61,15 @@ public abstract class TextResponseWriter implements 
TextWriter {
 
   protected Calendar cal;  // reusable calendar instance
 
+  /**
+   * A signal object that must be used to differentiate from <code>null</code> 
in strict object equality
+   * checks against {@link #rawReturnFields}, in order to determine the 
appropriate context in which to
+   * write raw field values.
+   */
+  private static final ReturnFields NO_RAW_FIELDS = new SolrReturnFields();
+  private final TextResponseWriter rawShim;
+  private final Set<String> rawFields;
+  private final ReturnFields rawReturnFields;
 
   public TextResponseWriter(Writer writer, SolrQueryRequest req, 
SolrQueryResponse rsp) {
     this.writer = writer == null ? null: FastWriter.wrap(writer);
@@ -67,6 +82,17 @@ public abstract class TextResponseWriter implements 
TextWriter {
     }
     returnFields = rsp.getReturnFields();
     if (req.getParams().getBool(CommonParams.OMIT_HEADER, false)) 
rsp.removeResponseHeader();
+    DocTransformer rootDocTransformer = returnFields.getTransformer();
+    Collection<String> rawFields;
+    if (rootDocTransformer == null || (rawFields = 
rootDocTransformer.getRawFields()).isEmpty()) {
+      this.rawFields = null;
+      this.rawShim = null;
+      this.rawReturnFields = NO_RAW_FIELDS;
+    } else {
+      this.rawFields = rawFields.size() == 1 ? 
Collections.singleton(rawFields.iterator().next()) : new HashSet<>(rawFields);
+      this.rawShim = new RawShimTextResponseWriter(this);
+      this.rawReturnFields = returnFields;
+    }
   }
   //only for test purposes
    TextResponseWriter(Writer writer, boolean indent) {
@@ -76,6 +102,16 @@ public abstract class TextResponseWriter implements 
TextWriter {
     this.rsp = null;
     returnFields = null;
     this.doIndent = indent;
+    this.rawShim = null;
+    this.rawFields = null;
+    this.rawReturnFields = null;
+  }
+
+  /**
+   * NOTE: strict object equality check against {@link #rawReturnFields}; see 
javadocs for {@link #NO_RAW_FIELDS}
+   */
+  protected final boolean shouldWriteRaw(String fname, ReturnFields 
returnFields) {
+    return rawReturnFields == returnFields && rawFields.contains(fname);
   }
 
   /** done with this ResponseWriter... make sure any buffers are flushed to 
writer */
@@ -106,7 +142,7 @@ public abstract class TextResponseWriter implements 
TextWriter {
   }
 
 
-  public final void writeVal(String name, Object val) throws IOException {
+  public final void writeVal(String name, Object val, boolean raw) throws 
IOException {
 
     // if there get to be enough types, perhaps hashing on the type
     // to get a handler might be faster (but types must be exact to do that...)
@@ -122,9 +158,10 @@ public abstract class TextResponseWriter implements 
TextWriter {
       IndexableField f = (IndexableField)val;
       SchemaField sf = schema.getFieldOrNull( f.name() );
       if( sf != null ) {
-        sf.getType().write(this, name, f);
-      }
-      else {
+        sf.getType().write(raw ? rawShim : this, name, f);
+      } else if (raw) {
+        writeStrRaw(name, f.stringValue());
+      } else {
         writeStr(name, f.stringValue(), true);
       }
     } else if (val instanceof Document) {
@@ -150,7 +187,7 @@ public abstract class TextResponseWriter implements 
TextWriter {
       BytesRef arr = (BytesRef)val;
       writeByteArr(name, arr.bytes, arr.offset, arr.length);
     } else {
-      TextWriter.super.writeVal(name, val);
+      TextWriter.super.writeVal(name, val, raw);
     }
   }
   // names are passed when writing primitives like writeInt to allow many 
different
diff --git a/solr/core/src/java/org/apache/solr/response/XMLWriter.java 
b/solr/core/src/java/org/apache/solr/response/XMLWriter.java
index e22cb8c..417938d 100644
--- a/solr/core/src/java/org/apache/solr/response/XMLWriter.java
+++ b/solr/core/src/java/org/apache/solr/response/XMLWriter.java
@@ -207,7 +207,7 @@ public class XMLWriter extends TextResponseWriter {
           log.debug(String.valueOf(val));
         }
       }
-      writeVal(fname, val);
+      writeVal(fname, val, shouldWriteRaw(fname, returnFields));
     }
 
     if(doc.hasChildDocuments()) {
@@ -299,17 +299,17 @@ public class XMLWriter extends TextResponseWriter {
   }
 
   @Override
-  public void writeArray(String name, Object[] val) throws IOException {
-    writeArray(name, Arrays.asList(val).iterator());
+  public void writeArray(String name, Object[] val, boolean raw) throws 
IOException {
+    writeArray(name, Arrays.asList(val).iterator(), raw);
   }
 
   @Override
-  public void writeArray(String name, Iterator<?> iter) throws IOException {
+  public void writeArray(String name, Iterator<?> iter, boolean raw) throws 
IOException {
     if( iter.hasNext() ) {
       startTag("arr", name, false );
       incLevel();
       while( iter.hasNext() ) {
-        writeVal(null, iter.next());
+        writeVal(null, iter.next(), raw);
       }
       decLevel();
       if (doIndent) indent();
@@ -321,7 +321,7 @@ public class XMLWriter extends TextResponseWriter {
   }
 
   @Override
-  public void writeIterator(String name, IteratorWriter val) throws 
IOException {
+  public void writeIterator(String name, IteratorWriter val, boolean raw) 
throws IOException {
     // As the size is not known. So, always both startTag and endTag is written
     // irrespective of number of entries in IteratorWriter
     startTag("arr", name, false );
@@ -330,7 +330,7 @@ public class XMLWriter extends TextResponseWriter {
     val.writeIter(new IteratorWriter.ItemWriter() {
       @Override
       public IteratorWriter.ItemWriter add(Object o) throws IOException {
-        writeVal(null, o);
+        writeVal(null, o, raw);
         return this;
       }
     });
@@ -352,6 +352,15 @@ public class XMLWriter extends TextResponseWriter {
   }
 
   @Override
+  public void writeStrRaw(String name, String val) throws IOException {
+    int contentLen = val == null ? 0 : val.length();
+    startTag("raw", name, contentLen == 0);
+    if (contentLen == 0) return;
+    writer.write(val, 0, contentLen);
+    writer.write("</raw>");
+  }
+
+  @Override
   public void writeStr(String name, String val, boolean escape) throws 
IOException {
     writePrim("str",name,val,escape);
   }
diff --git 
a/solr/core/src/java/org/apache/solr/response/transform/DocTransformer.java 
b/solr/core/src/java/org/apache/solr/response/transform/DocTransformer.java
index f1f6bf3..ce329f0 100644
--- a/solr/core/src/java/org/apache/solr/response/transform/DocTransformer.java
+++ b/solr/core/src/java/org/apache/solr/response/transform/DocTransformer.java
@@ -17,6 +17,8 @@
 package org.apache.solr.response.transform;
 
 import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
 
 import org.apache.solr.common.SolrDocument;
 import org.apache.solr.response.QueryResponseWriter;
@@ -53,6 +55,22 @@ public abstract class DocTransformer {
   }
 
   /**
+   * If this transformer wants to bypass escaping in the {@link 
org.apache.solr.response.TextResponseWriter} and
+   * write content directly to output for certain field(s), the names of any 
such field(s) should be returned
+   *
+   * NOTE: normally this will be conditional on the `wt` param in the request, 
as supplied to the
+   * {@link DocTransformer}'s parent {@link TransformerFactory} at the time of 
transformer creation.
+   *
+   * @return Collection containing field names to be written raw; if no field 
names should
+   * be written raw, an empty collection should be returned. Any collection 
returned collection
+   * need not be externally modifiable -- i.e., {@link 
java.util.Collections#singleton(Object)} is
+   * acceptable.
+   */
+  public Collection<String> getRawFields() {
+    return Collections.emptySet();
+  }
+
+  /**
    * Indicates if this transformer requires access to the underlying index to 
perform it's functions.
    *
    * In some situations (notably RealTimeGet) this method <i>may</i> be called 
before {@link #setContext} 
diff --git 
a/solr/core/src/java/org/apache/solr/response/transform/DocTransformers.java 
b/solr/core/src/java/org/apache/solr/response/transform/DocTransformers.java
index 68a0b9f..1bb7bc3 100644
--- a/solr/core/src/java/org/apache/solr/response/transform/DocTransformers.java
+++ b/solr/core/src/java/org/apache/solr/response/transform/DocTransformers.java
@@ -18,8 +18,10 @@ package org.apache.solr.response.transform;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import org.apache.solr.common.SolrDocument;
 import org.apache.solr.response.ResultContext;
@@ -49,6 +51,13 @@ public class DocTransformers extends DocTransformer
     return str.toString();
   }
 
+  @Override
+  public Collection<String> getRawFields() {
+    return children.stream().map(DocTransformer::getRawFields)
+            .flatMap(Collection::stream)
+            .collect(Collectors.toList());
+  }
+
   public void addTransformer( DocTransformer a ) {
     children.add( a );
   }
diff --git 
a/solr/core/src/java/org/apache/solr/response/transform/GeoTransformerFactory.java
 
b/solr/core/src/java/org/apache/solr/response/transform/GeoTransformerFactory.java
index 790d938..3c4dc53 100644
--- 
a/solr/core/src/java/org/apache/solr/response/transform/GeoTransformerFactory.java
+++ 
b/solr/core/src/java/org/apache/solr/response/transform/GeoTransformerFactory.java
@@ -17,7 +17,11 @@
 package org.apache.solr.response.transform;
 
 import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
 
 import org.apache.lucene.index.IndexableField;
 import org.apache.lucene.index.LeafReaderContext;
@@ -63,10 +67,15 @@ import org.locationtech.spatial4j.shape.Shape;
  * </ul>
  * 
  */
-public class GeoTransformerFactory extends TransformerFactory
-{ 
+public class GeoTransformerFactory extends TransformerFactory implements 
TransformerFactory.FieldRenamer {
+
   @Override
   public DocTransformer create(String display, SolrParams params, 
SolrQueryRequest req) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public DocTransformer create(String display, SolrParams params, 
SolrQueryRequest req, Map<String, String> renamedFields, Set<String> 
reqFieldNames) {
 
     String fname = params.get("f", display);
     if(fname.startsWith("[") && fname.endsWith("]")) {
@@ -124,12 +133,7 @@ public class GeoTransformerFactory extends 
TransformerFactory
 
     // Using ValueSource
     if(shapes!=null) {
-      return new DocTransformer() {
-        @Override
-        public String getName() {
-          return display;
-        }
-
+      return new GeoDocTransformer(updater) {
         @Override
         public void transform(SolrDocument doc, int docid) throws IOException {
           int leafOrd = ReaderUtil.subIndex(docid, 
context.getSearcher().getTopReaderContext().leaves());
@@ -144,88 +148,104 @@ public class GeoTransformerFactory extends 
TransformerFactory
 
     }
     
+    // if source has been renamed, update reference
+    updater.field = renamedFields.getOrDefault(updater.field, updater.field);
+
+    // don't remove fields that were explicitly requested by others
+    final boolean copy = reqFieldNames != null && 
reqFieldNames.contains(updater.field);
+    if (!copy) {
+      renamedFields.put(updater.field, updater.display);
+    }
+
     // Using the raw stored values
-    return new DocTransformer() {
+    return new GeoDocTransformer(updater) {
       
       @Override
       public void transform(SolrDocument doc, int docid) throws IOException {
-        Object val = doc.remove(updater.field);
+        Object val = copy ? doc.get(updater.field) : doc.remove(updater.field);
         if(val!=null) {
           updater.setValue(doc, val);
         }
       }
       
       @Override
-      public String getName() {
-        return updater.display;
-      }
-
-      @Override
       public String[] getExtraRequestFields() {
         return new String[] {updater.field};
       }
     };
   }
 
-}
+  private static abstract class GeoDocTransformer extends DocTransformer {
 
-class GeoFieldUpdater {
-  String field;
-  String display;
-  String display_error;
-  
-  boolean isJSON;
-  ShapeWriter writer;
-  SupportedFormats formats;
-  
-  void addShape(SolrDocument doc, Shape shape) {
-    if(isJSON) {
-      doc.addField(display, new WriteableGeoJSON(shape, writer));
+    private final GeoFieldUpdater updater;
+
+    private GeoDocTransformer(GeoFieldUpdater updater) {
+      this.updater = updater;
     }
-    else {
-      doc.addField(display, writer.toString(shape));
+
+    @Override
+    public String getName() {
+      return updater.display;
     }
-  }
-  
-  void setValue(SolrDocument doc, Object val) {
-    doc.remove(display);
-    if(val != null) {
-      if(val instanceof Iterable) {
-        Iterator<?> iter = ((Iterable<?>)val).iterator();
-        while(iter.hasNext()) {
-          addValue(doc, iter.next());
-        }
-      }
-      else {
-        addValue(doc, val);
-      }
+
+    @Override
+    public Collection<String> getRawFields() {
+      return updater.isJSON ? Collections.singleton(updater.display) : 
Collections.emptySet();
     }
   }
-    
-  void addValue(SolrDocument doc, Object val) {
-    if(val == null) {
-      return;
-    }
-    
-    if(val instanceof Shape) {
-      addShape(doc, (Shape)val);
+
+  private static class GeoFieldUpdater {
+    String field;
+    String display;
+    String display_error;
+
+    boolean isJSON;
+    ShapeWriter writer;
+    SupportedFormats formats;
+
+    void addShape(SolrDocument doc, Shape shape) {
+      doc.addField(display, writer.toString(shape));
     }
-    // Don't explode on 'InvalidShpae'
-    else if( val instanceof Exception) {
-      doc.setField( display_error, ((Exception)val).toString() );
+
+    void setValue(SolrDocument doc, Object val) {
+      doc.remove(display);
+      if(val != null) {
+        if(val instanceof Iterable) {
+          Iterator<?> iter = ((Iterable<?>)val).iterator();
+          while(iter.hasNext()) {
+            addValue(doc, iter.next());
+          }
+        }
+        else {
+          addValue(doc, val);
+        }
+      }
     }
-    else {
-      // Use the stored value
-      if(val instanceof IndexableField) {
-        val = ((IndexableField)val).stringValue();
+
+    void addValue(SolrDocument doc, Object val) {
+      if(val == null) {
+        return;
+      }
+
+      if(val instanceof Shape) {
+        addShape(doc, (Shape)val);
       }
-      try {
-        addShape(doc, formats.read(val.toString()));
+      // Don't explode on 'InvalidShpae'
+      else if( val instanceof Exception) {
+        doc.setField( display_error, ((Exception)val).toString() );
       }
-      catch(Exception ex) {
-        doc.setField( display_error, ex.toString() );
+      else {
+        // Use the stored value
+        if(val instanceof IndexableField) {
+          val = ((IndexableField)val).stringValue();
+        }
+        try {
+          addShape(doc, formats.read(val.toString()));
+        }
+        catch(Exception ex) {
+          doc.setField( display_error, ex.toString() );
+        }
       }
     }
   }
 }
-
diff --git 
a/solr/core/src/java/org/apache/solr/response/transform/RawValueTransformerFactory.java
 
b/solr/core/src/java/org/apache/solr/response/transform/RawValueTransformerFactory.java
index 5838e10..da657bb 100644
--- 
a/solr/core/src/java/org/apache/solr/response/transform/RawValueTransformerFactory.java
+++ 
b/solr/core/src/java/org/apache/solr/response/transform/RawValueTransformerFactory.java
@@ -16,27 +16,23 @@
  */
 package org.apache.solr.response.transform;
 
-import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
 
 import com.google.common.base.Strings;
-import org.apache.lucene.index.IndexableField;
 import org.apache.solr.common.SolrDocument;
 import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.params.SolrParams;
-import org.apache.solr.common.util.JavaBinCodec;
-import org.apache.solr.common.util.JavaBinCodec.ObjectResolver;
 import org.apache.solr.common.util.NamedList;
-import org.apache.solr.common.util.TextWriter;
-import org.apache.solr.common.util.WriteableValue;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.QueryResponseWriter;
 
 /**
  * @since solr 5.2
  */
-public class RawValueTransformerFactory extends TransformerFactory
+public class RawValueTransformerFactory extends TransformerFactory implements 
TransformerFactory.FieldRenamer
 {
   String applyToWT = null;
   
@@ -58,10 +54,28 @@ public class RawValueTransformerFactory extends 
TransformerFactory
   
   @Override
   public DocTransformer create(String display, SolrParams params, 
SolrQueryRequest req) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean mayModifyValue() {
+    // The only thing we may modify is the _serialization_; field values per 
se are guaranteed to be unmodified.
+    return false;
+  }
+
+  @Override
+  public DocTransformer create(String display, SolrParams params, 
SolrQueryRequest req,
+                               Map<String, String> renamedFields, Set<String> 
reqFieldNames) {
     String field = params.get("f");
     if(Strings.isNullOrEmpty(field)) {
       field = display;
     }
+    field = renamedFields.getOrDefault(field, field);
+    final boolean rename = !field.equals(display);
+    final boolean copy = rename && reqFieldNames != null && 
reqFieldNames.contains(field);
+    if (!copy) {
+      renamedFields.put(field, display);
+    }
     // When a 'wt' is specified in the transformer, only apply it to the same 
wt
     boolean apply = true;
     if(applyToWT!=null) {
@@ -79,25 +93,27 @@ public class RawValueTransformerFactory extends 
TransformerFactory
     }
 
     if(apply) {
-      return new RawTransformer( field, display );
+      return new RawTransformer( field, display, copy );
     }
     
-    if (field.equals(display)) {
+    if (!rename) {
       // we have to ensure the field is returned
       return new DocTransformer.NoopFieldTransformer(field);
     }
-    return new RenameFieldTransformer( field, display, false );
+    return new RenameFieldTransformer( field, display, copy );
   }
-  
+
   static class RawTransformer extends DocTransformer
   {
     final String field;
     final String display;
+    final boolean copy;
 
-    public RawTransformer( String field, String display )
+    public RawTransformer( String field, String display, boolean copy )
     {
       this.field = field;
       this.display = display;
+      this.copy = copy;
     }
 
     @Override
@@ -107,57 +123,21 @@ public class RawValueTransformerFactory extends 
TransformerFactory
     }
 
     @Override
-    public void transform(SolrDocument doc, int docid) {
-      Object val = doc.remove(field);
-      if(val==null) {
-        return;
-      }
-      if(val instanceof Collection) {
-        Collection<?> current = (Collection<?>)val;
-        ArrayList<WriteableStringValue> vals = new 
ArrayList<RawValueTransformerFactory.WriteableStringValue>();
-        for(Object v : current) {
-          vals.add(new WriteableStringValue(v));
-        }
-        doc.setField(display, vals);
-      }
-      else {
-        doc.setField(display, new WriteableStringValue(val));
-      }
+    public Collection<String> getRawFields() {
+      return Collections.singleton(display);
     }
 
     @Override
-    public String[] getExtraRequestFields() {
-      return new String[] {this.field};
-    }
-  }
-  
-  public static class WriteableStringValue extends WriteableValue {
-    public final Object val;
-    
-    public WriteableStringValue(Object val) {
-      this.val = val;
-    }
-    
-    @Override
-    public void write(String name, TextWriter writer) throws IOException {
-      String str = null;
-      if(val instanceof IndexableField) { // delays holding it in memory
-        str = ((IndexableField)val).stringValue();
-      }
-      else {
-        str = val.toString();
+    public void transform(SolrDocument doc, int docid) {
+      Object val = copy ? doc.get(field) : doc.remove(field);
+      if(val != null) {
+        doc.setField(display, val);
       }
-      writer.getWriter().write(str);
     }
 
     @Override
-    public Object resolve(Object o, JavaBinCodec codec) throws IOException {
-      ObjectResolver orig = codec.getResolver();
-      if(orig != null) {
-        codec.writeVal(orig.resolve(val, codec));
-        return null;
-      }
-      return val.toString();
+    public String[] getExtraRequestFields() {
+      return new String[] {this.field};
     }
   }
 }
diff --git 
a/solr/core/src/java/org/apache/solr/response/transform/RenameFieldTransformer.java
 
b/solr/core/src/java/org/apache/solr/response/transform/RenameFieldTransformer.java
index 41ac54f..d67ea41 100644
--- 
a/solr/core/src/java/org/apache/solr/response/transform/RenameFieldTransformer.java
+++ 
b/solr/core/src/java/org/apache/solr/response/transform/RenameFieldTransformer.java
@@ -29,12 +29,14 @@ public class RenameFieldTransformer extends DocTransformer
   final String from;
   final String to;
   final boolean copy;
+  final String[] ensureFromFieldPresent;
 
-  public RenameFieldTransformer( String from, String to, boolean copy )
-  {
+  public RenameFieldTransformer( String from, String to, boolean copy ) {
     this.from = from;
     this.to = to;
     this.copy = copy;
+    this.ensureFromFieldPresent = new String[] { from };
+    assert !from.equals(to);
   }
 
   @Override
@@ -50,4 +52,9 @@ public class RenameFieldTransformer extends DocTransformer
       doc.setField(to, v);
     }
   }
+
+  @Override
+  public String[] getExtraRequestFields() {
+    return ensureFromFieldPresent;
+  }
 }
diff --git 
a/solr/core/src/java/org/apache/solr/response/transform/TransformerFactory.java 
b/solr/core/src/java/org/apache/solr/response/transform/TransformerFactory.java
index 97134ea..ab9807f 100644
--- 
a/solr/core/src/java/org/apache/solr/response/transform/TransformerFactory.java
+++ 
b/solr/core/src/java/org/apache/solr/response/transform/TransformerFactory.java
@@ -18,6 +18,7 @@ package org.apache.solr.response.transform;
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Set;
 
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.util.NamedList;
@@ -40,6 +41,62 @@ public abstract class TransformerFactory implements 
NamedListInitializedPlugin
 
   public abstract DocTransformer create(String field, SolrParams params, 
SolrQueryRequest req);
 
+  /**
+   * The {@link FieldRenamer} interface should be implemented by any {@link 
TransformerFactory} capable of generating
+   * transformers that might rename fields, and should implement {@link 
#create(String, SolrParams, SolrQueryRequest, Map, Set)}
+   * in place of {@link #create(String, SolrParams, SolrQueryRequest)} (with 
the latter method
+   * overridden to throw {@link UnsupportedOperationException}).
+   *
+   * {@link DocTransformer}s returned via {@link #create(String, SolrParams, 
SolrQueryRequest, Map, Set)}
+   * will be added in a second pass, allowing simplified logic in {@link 
TransformerFactory#create(String, SolrParams, SolrQueryRequest)}
+   * for non-renaming factories.
+   *
+   * {@link #create(String, SolrParams, SolrQueryRequest, Map, Set)} must 
implement extra logic to be aware of
+   * preceding field renames, and to make subsequent {@link FieldRenamer} 
transformers aware of its own field renames.
+   *
+   * It is harmless for a {@link DocTransformer} that does _not_ in practice 
rename fields to be returned from a
+   * factory that implements this interface (e.g., for conditional renames?); 
but doing so opens the possibility of
+   * {@link #create(String, SolrParams, SolrQueryRequest, Map, Set)} being 
called _after_ fields have been renamed,
+   * so such implementations must still check whether the field with which 
they are concerned has been renamed ...
+   * and if it _has_, must copy the field back to its original name. This 
situation also demonstrates the
+   * motivation for separating the creation of {@link DocTransformer}s into 
two phases: an initial phase involving
+   * no field renames, and a subsequent phase that implement extra logic to 
properly handle field renames.
+   */
+  public interface FieldRenamer {
+    // TODO: Behavior is undefined in the event of a "destination field" 
collision (e.g., a user maps two fields to
+    //  the same "destination field", or maps a field to a top-level requested 
field). In the future, the easiest way
+    //  to detect such a case would be by "failing fast" upon renaming to a 
field that already has an associated value,
+    //  or support for this feature could be expressly added via a hypothetical
+    //  `combined_field:[consolidate fl=field_1,field_2]` transformer.
+    /**
+     * Analogous to {@link TransformerFactory#create(String, SolrParams, 
SolrQueryRequest)}, but to be implemented
+     * by {@link TransformerFactory}s that produce {@link DocTransformer}s 
that may rename fields.
+     *
+     * @param field The destination field
+     * @param params Local params associated with this transformer (e.g., 
source field)
+     * @param req The current request
+     * @param renamedFields Maps source=&gt;dest renamed fields. 
Implementations should check this first, updating
+     *                      their own "source" field(s) as necessary, and if 
renaming (not copying) fields, should
+     *                      also update this map with the implementations 
"own" introduced source=&gt;dest field
+     *                      mapping
+     * @param reqFieldNames Set of explicitly requested field names; 
implementations should consult this set to
+     *                      determine whether it's appropriate to rename (vs. 
copy) a field (e.g.: <code>boolean
+     *                      copy = reqFieldNames != null &amp;&amp; 
reqFieldNames.contains(sourceField)</code>)
+     * @return A transformer to be used in processing field values in returned 
documents.
+     */
+    DocTransformer create(String field, SolrParams params, SolrQueryRequest 
req, Map<String, String> renamedFields, Set<String> reqFieldNames);
+
+    /**
+     * Returns <code>true</code> if implementations of this class may (even 
subtly) modify field values.
+     * ({@link GeoTransformerFactory} may do this, e.g.). To fail safe, the 
default implementation returns
+     * <code>true</code>. This method should be overridden to return 
<code>false</code> if the implementing
+     * class is guaranteed to not modify any values for the fields that it 
renames.
+     */
+    default boolean mayModifyValue() {
+      return true;
+    }
+  }
+
   public static final Map<String,TransformerFactory> defaultFactories = new 
HashMap<>(9, 1.0f);
   static {
     defaultFactories.put( "explain", new ExplainAugmenterFactory() );
diff --git 
a/solr/core/src/java/org/apache/solr/response/transform/WriteableGeoJSON.java 
b/solr/core/src/java/org/apache/solr/response/transform/WriteableGeoJSON.java
deleted file mode 100644
index f89f5c8..0000000
--- 
a/solr/core/src/java/org/apache/solr/response/transform/WriteableGeoJSON.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.solr.response.transform;
-
-import java.io.IOException;
-
-import org.apache.solr.common.util.JavaBinCodec;
-import org.apache.solr.common.util.TextWriter;
-import org.apache.solr.common.util.WriteableValue;
-import org.locationtech.spatial4j.io.ShapeWriter;
-import org.locationtech.spatial4j.shape.Shape;
-
-/**
- * This will let the writer add values to the response directly
- */
-public class WriteableGeoJSON extends WriteableValue {
-
-  public final Shape shape;
-  public final ShapeWriter jsonWriter;
-  
-  public WriteableGeoJSON(Shape shape, ShapeWriter jsonWriter) {
-    this.shape = shape;
-    this.jsonWriter = jsonWriter;
-  }
-
-  @Override
-  public Object resolve(Object o, JavaBinCodec codec) throws IOException {
-    codec.writeStr(jsonWriter.toString(shape));
-    return null; // this means we wrote it
-  }
-
-  @Override
-  public void write(String name, TextWriter writer) throws IOException {
-    jsonWriter.write(writer.getWriter(), shape);
-  }
-
-  @Override
-  public String toString() {
-    return jsonWriter.toString(shape);
-  }
-}
diff --git a/solr/core/src/java/org/apache/solr/search/SolrReturnFields.java 
b/solr/core/src/java/org/apache/solr/search/SolrReturnFields.java
index 6135bb2..4903e0b 100644
--- a/solr/core/src/java/org/apache/solr/search/SolrReturnFields.java
+++ b/solr/core/src/java/org/apache/solr/search/SolrReturnFields.java
@@ -16,9 +16,12 @@
  */
 package org.apache.solr.search;
 
+import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Deque;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
@@ -35,7 +38,6 @@ import org.apache.solr.common.SolrException;
 import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.params.ModifiableSolrParams;
 import org.apache.solr.common.params.SolrParams;
-import org.apache.solr.common.util.NamedList;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.transform.DocTransformer;
 import org.apache.solr.response.transform.DocTransformers;
@@ -160,6 +162,10 @@ public class SolrReturnFields extends ReturnFields {
   }
 
 
+  /**
+   * Parsing is done in two passes (see javadocs for {@link 
org.apache.solr.response.transform.TransformerFactory.FieldRenamer}
+   * for an explanation of the logic behind deferring creation of "rename 
field" transformers).
+   */
   private void parseFieldList(String[] fl, SolrQueryRequest req) {
     _wantsScore = false;
     _wantsAllFields = false;
@@ -168,32 +174,26 @@ public class SolrReturnFields extends ReturnFields {
       return;
     }
 
-    NamedList<String> rename = new NamedList<>();
+    Deque<DeferredRenameEntry> deferredRenameAugmenters = new ArrayDeque<>();
     DocTransformers augmenters = new DocTransformers();
     for (String fieldList : fl) {
-      add(fieldList,rename,augmenters,req);
+      add(fieldList,deferredRenameAugmenters,augmenters,req);
     }
-    for( int i=0; i<rename.size(); i++ ) {
-      String from = rename.getName(i);
-      String to = rename.getVal(i);
-      okFieldNames.add( to );
-      boolean copy = (reqFieldNames!=null && reqFieldNames.contains(from));
-      if(!copy) {
-        // Check that subsequent copy/rename requests have the field they need 
to copy
-        for(int j=i+1; j<rename.size(); j++) {
-          if(from.equals(rename.getName(j))) {
-            rename.setName(j, to); // copy from the current target
-            if(reqFieldNames==null) {
-              reqFieldNames = new LinkedHashSet<>();
-            }
-            reqFieldNames.add(to); // don't rename our current target
+    Map<String, String> renamedNotCopied = new HashMap<>();
+    for (DeferredRenameEntry e : deferredRenameAugmenters) {
+      DocTransformer t = e.create(renamedNotCopied, reqFieldNames);
+      augmenters.addTransformer(t);
+      if (!_wantsAllFields) {
+        final String[] extraRequestFields = t.getExtraRequestFields();
+        if (extraRequestFields != null) {
+          for (String f : extraRequestFields) {
+            fields.add(f);
           }
         }
       }
-      augmenters.addTransformer( new RenameFieldTransformer( from, to, copy ) 
);
     }
-    if (rename.size() > 0 ) {
-      renameFields = rename.asShallowMap();
+    if (!renamedNotCopied.isEmpty()) {
+      renameFields = renamedNotCopied;
     }
     if( !_wantsAllFields && !globs.isEmpty() ) {
       // TODO??? need to fill up the fields with matching field names in the 
index
@@ -236,7 +236,7 @@ public class SolrReturnFields extends ReturnFields {
     return null;
   }
 
-  private void add(String fl, NamedList<String> rename, DocTransformers 
augmenters, SolrQueryRequest req) {
+  private void add(String fl, Deque<DeferredRenameEntry> deferred, 
DocTransformers augmenters, SolrQueryRequest req) {
     if( fl == null ) {
       return;
     }
@@ -278,8 +278,9 @@ public class SolrReturnFields extends ReturnFields {
           field = sp.getId(null);
           ch = sp.ch();
           if (field != null && (Character.isWhitespace(ch) || ch == ',' || 
ch==0)) {
-            rename.add(field, key);
-            addField(field, key, augmenters, false);
+            deferred.addFirst(new DeferredRenameEntry(key, new 
ModifiableSolrParams().set(SOURCE_FIELD_ARGNAME, field), req, 
RENAME_FIELD_TRANSFORMER_FACTORY));
+            // NOTE: treat as pseudoField below because `fields` will be 
modified on deferred invocation
+            addField(field, key, augmenters, true);
             continue;
           }
           // an invalid field name... reset the position pointer to retry
@@ -326,7 +327,20 @@ public class SolrReturnFields extends ReturnFields {
           }
 
           TransformerFactory factory = req.getCore().getTransformerFactory( 
augmenterName );
-          if( factory != null ) {
+          if (factory instanceof TransformerFactory.FieldRenamer) {
+            // NOTE: `deferred` is a Deque because some TransformerFactories 
(e.g., `GeoTransformerFactory`) can
+            // subtly modify the representation of the associated value (i.e., 
it's not just a straight rename). This
+            // subverts the "update source field" phase of 
`FieldRenamer.create(...)`. We _know_ however that "simple"
+            // field renames don't do any value modification whatsoever, so 
those are added to the beginning of
+            // the `deferred` Deque so that they will be processed first, and 
all other `FieldRenamers` are
+            // added (here) to the front or back of the Deque, depending on 
the return value of `mayModifyValue()`.
+            final DeferredRenameEntry deferredEntry = new 
DeferredRenameEntry(disp, augmenterParams, req, 
(TransformerFactory.FieldRenamer) factory);
+            if (((TransformerFactory.FieldRenamer) factory).mayModifyValue()) {
+              deferred.addLast(deferredEntry);
+            } else {
+              deferred.addFirst(deferredEntry);
+            }
+          } else if (factory != null) {
             DocTransformer t = factory.create(disp, augmenterParams, req);
             if(t!=null) {
               if(!_wantsAllFields) {
@@ -415,7 +429,7 @@ public class SolrReturnFields extends ReturnFields {
             // OK, it was an oddly named field
             addField(field, key, augmenters, false);
             if( key != null ) {
-              rename.add(field, key);
+              deferred.addFirst(new DeferredRenameEntry(key, new 
ModifiableSolrParams().set(SOURCE_FIELD_ARGNAME, field), req, 
RENAME_FIELD_TRANSFORMER_FACTORY));
             }
           } else {
             throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, 
"Error parsing fieldname: " + e.getMessage(), e);
@@ -430,6 +444,43 @@ public class SolrReturnFields extends ReturnFields {
     }
   }
 
+  private static final String SOURCE_FIELD_ARGNAME = "sourceField";
+  private static final TransformerFactory.FieldRenamer 
RENAME_FIELD_TRANSFORMER_FACTORY = new TransformerFactory.FieldRenamer() {
+    @Override
+    public DocTransformer create(String to, SolrParams params, 
SolrQueryRequest req, Map<String, String> renamedFields, Set<String> 
reqFieldNames) {
+      String from = params.get(SOURCE_FIELD_ARGNAME);
+      from = renamedFields.getOrDefault(from, from);
+      final boolean copy = reqFieldNames != null && 
reqFieldNames.contains(from);
+      if (!copy) {
+        renamedFields.put(from, to);
+      }
+      return new RenameFieldTransformer(from, to, copy);
+    }
+
+    @Override
+    public boolean mayModifyValue() {
+      return false;
+    }
+  };
+
+  private static final class DeferredRenameEntry {
+    private final String field;
+    private final SolrParams params;
+    private final SolrQueryRequest req;
+    private final TransformerFactory.FieldRenamer factory;
+
+    private DeferredRenameEntry(String field, SolrParams params, 
SolrQueryRequest req, TransformerFactory.FieldRenamer factory) {
+      this.field = field;
+      this.params = params;
+      this.req = req;
+      this.factory = factory;
+    }
+
+    private DocTransformer create(Map<String, String> renamedFields, 
Set<String> reqFieldNames) {
+      return factory.create(field, params, req, renamedFields, reqFieldNames);
+    }
+  }
+
   private void addField(String field, String key, DocTransformers augmenters, 
boolean isPseudoField)
   {
     if(reqFieldNames==null) {
diff --git a/solr/core/src/test/org/apache/solr/cloud/TestRandomFlRTGCloud.java 
b/solr/core/src/test/org/apache/solr/cloud/TestRandomFlRTGCloud.java
index 34b0279..26b2fdf 100644
--- a/solr/core/src/test/org/apache/solr/cloud/TestRandomFlRTGCloud.java
+++ b/solr/core/src/test/org/apache/solr/cloud/TestRandomFlRTGCloud.java
@@ -17,6 +17,7 @@
 package org.apache.solr.cloud;
 
 import java.io.IOException;
+import java.io.StringReader;
 import java.lang.invoke.MethodHandles;
 import java.nio.file.Path;
 import java.util.ArrayList;
@@ -33,16 +34,21 @@ import java.util.TreeSet;
 
 import org.apache.commons.io.FilenameUtils;
 import org.apache.lucene.util.TestUtil;
+import org.apache.solr.client.solrj.ResponseParser;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.embedded.JettySolrRunner;
 import org.apache.solr.client.solrj.impl.CloudSolrClient;
 import org.apache.solr.client.solrj.impl.HttpSolrClient;
+import org.apache.solr.client.solrj.impl.NoOpResponseParser;
+import org.apache.solr.client.solrj.impl.XMLResponseParser;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.QueryRequest;
 import org.apache.solr.client.solrj.response.QueryResponse;
 import org.apache.solr.common.SolrDocument;
 import org.apache.solr.common.SolrDocumentList;
 import org.apache.solr.common.SolrInputDocument;
+import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.params.ModifiableSolrParams;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.util.NamedList;
@@ -52,9 +58,14 @@ import org.apache.solr.response.transform.TransformerFactory;
 import org.apache.solr.util.RandomizeSSL;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
+import org.noggit.ObjectBuilder;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
 /** @see TestCloudPseudoReturnFields */
 @RandomizeSSL(clientAuth=0.0,reason="client auth uses too much RAM")
 public class TestRandomFlRTGCloud extends SolrCloudTestCase {
@@ -91,18 +102,28 @@ public class TestRandomFlRTGCloud extends 
SolrCloudTestCase {
       new ValueAugmenterValidator(1976, "val_alias"),
       //
       new RenameFieldValueValidator("id", "my_id_alias"),
+      // NOTE: we add a SimpleFieldValueValidator below to check that we can 
enforce the presence of this field,
+      // even when it may have been "renamed" by the transformer above? (this 
and other such instances are
+      // marked with `//REQ`); also add a RenameFieldValueValidator to "fork" 
values, marked with `//FORK`.
+      new SimpleFieldValueValidator("id"), //REQ
       new SimpleFieldValueValidator("aaa_i"),
       new RenameFieldValueValidator("bbb_i", "my_int_field_alias"),
+      new RenameFieldValueValidator("bbb_i", "my_int_field_alias2"), //FORK
+      new SimpleFieldValueValidator("bbb_i"), //REQ
       new SimpleFieldValueValidator("ccc_s"),
       new RenameFieldValueValidator("ddd_s", "my_str_field_alias"),
-      //
-      // SOLR-9376: RawValueTransformerFactory doesn't work in cloud mode 
-      //
-      // new RawFieldValueValidator("json", "eee_s", "my_json_field_alias"),
-      // new RawFieldValueValidator("json", "fff_s"),
-      // new RawFieldValueValidator("xml", "ggg_s", "my_xml_field_alias"),
-      // new RawFieldValueValidator("xml", "hhh_s"),
-      //
+      new RenameFieldValueValidator("ddd_s", "my_str_field_alias2"), // FORK
+      new SimpleFieldValueValidator("ddd_s"), //REQ
+
+      new RawFieldValueValidator("json", "eee_s", "my_json_field_alias"),
+      new RenameFieldValueValidator("eee_s", "my_escaped_json_field_alias"), 
// FORK
+      new SimpleFieldValueValidator("eee_s"), //REQ
+      new RawFieldValueValidator("json", "fff_s"),
+      new RawFieldValueValidator("xml", "ggg_s", "my_xml_field_alias"),
+      new RenameFieldValueValidator("ggg_s", "my_escaped_xml_field_alias"), // 
FORK
+      new SimpleFieldValueValidator("ggg_s"), //REQ
+      new RawFieldValueValidator("xml", "hhh_s"),
+
       new NotIncludedValidator("bogus_unused_field_ss"),
       new 
NotIncludedValidator("bogus_alias","bogus_alias:other_bogus_field_i"),
       new NotIncludedValidator("bogus_raw_alias","bogus_raw_alias:[xml 
f=bogus_raw_field_ss]"),
@@ -111,6 +132,8 @@ public class TestRandomFlRTGCloud extends SolrCloudTestCase 
{
       new FunctionValidator("aaa_i", "func_aaa_alias"),
       new GeoTransformerValidator("geo_1_srpt"),
       new GeoTransformerValidator("geo_2_srpt","my_geo_alias"),
+      new RenameFieldValueValidator("geo_2_srpt", "my_geo_alias2"), // FORK
+      new SimpleFieldValueValidator("geo_2_srpt"), //REQ
       new ExplainValidator(),
       new ExplainValidator("explain_alias"),
       new SubQueryValidator(),
@@ -144,7 +167,7 @@ public class TestRandomFlRTGCloud extends SolrCloudTestCase 
{
         .withProperty("schema", "schema-pseudo-fields.xml")
         .process(CLOUD_CLIENT);
 
-    cluster.waitForActiveCollection(COLLECTION_NAME, numShards, repFactor * 
numShards); 
+    cluster.waitForActiveCollection(COLLECTION_NAME, numShards, repFactor * 
numShards);
 
     for (JettySolrRunner jetty : cluster.getJettySolrRunners()) {
       CLIENTS.add(getHttpSolrClient(jetty.getBaseUrl() + "/" + COLLECTION_NAME 
+ "/"));
@@ -187,7 +210,7 @@ public class TestRandomFlRTGCloud extends SolrCloudTestCase 
{
     // items should only be added to this list if it's known that they do not 
work with RTG
     // and a specific Jira for fixing this is listed as a comment
     final List<String> knownBugs = Arrays.asList
-      ( "xml","json", // SOLR-9376
+      (
         "child" // way to complicatd to vet with this test, see SOLR-9379 
instead
       );
 
@@ -318,10 +341,10 @@ public class TestRandomFlRTGCloud extends 
SolrCloudTestCase {
                                        //
                                        "ccc_s", 
TestUtil.randomSimpleString(random()),
                                        "ddd_s", 
TestUtil.randomSimpleString(random()),
-                                       "eee_s", 
TestUtil.randomSimpleString(random()),
-                                       "fff_s", 
TestUtil.randomSimpleString(random()),
-                                       "ggg_s", 
TestUtil.randomSimpleString(random()),
-                                       "hhh_s", 
TestUtil.randomSimpleString(random()),
+                                       "eee_s", 
makeJson(TestUtil.randomSimpleString(random())),
+                                       "fff_s", 
makeJson(TestUtil.randomSimpleString(random())),
+                                       "ggg_s", 
makeXml(TestUtil.randomSimpleString(random())),
+                                       "hhh_s", 
makeXml(TestUtil.randomSimpleString(random())),
                                        //
                                        "geo_1_srpt", 
GeoTransformerValidator.getValueForIndexing(random()),
                                        "geo_2_srpt", 
GeoTransformerValidator.getValueForIndexing(random()),
@@ -338,7 +361,60 @@ public class TestRandomFlRTGCloud extends 
SolrCloudTestCase {
     return doc;
   }
 
-  
+  private String makeJson(String s) {
+    switch (random().nextInt(3)) {
+      case 0:
+        // simple string
+        return '"' + s + '"';
+      case 1:
+        // array
+        return "[\"" + s + "\", \"" + s + "\"]";
+      case 2:
+        // map
+        return "{\"" + s + "\":\"" + s + "\"}";
+      default:
+        throw new IllegalStateException();
+    }
+  }
+
+  private String makeXml(String s) {
+    switch (random().nextInt(3)) {
+      case 0:
+        // simple string
+        return s;
+      case 1:
+        // simple element
+        return "<root>" + s + "</root>";
+      case 2:
+        // slightly more complex
+        return "<root><inner1>" + s + "</inner1><inner2>" + s + 
"</inner2></root>";
+      default:
+        throw new IllegalStateException();
+    }
+  }
+
+  private static final ResponseParser RAW_XML_RESPONSE_PARSER = new 
NoOpResponseParser();
+  private static final ResponseParser RAW_JSON_RESPONSE_PARSER = new 
NoOpResponseParser() {
+    @Override
+    public String getWriterType() {
+      return "json";
+    }
+  };
+
+  private static ResponseParser modifyParser(HttpSolrClient client, final 
String wt) {
+    final ResponseParser ret = client.getParser();
+    switch (wt) {
+      case "xml":
+        client.setParser(RAW_XML_RESPONSE_PARSER);
+        return ret;
+      case "json":
+        client.setParser(RAW_JSON_RESPONSE_PARSER);
+        return ret;
+      default:
+        return null;
+    }
+  }
+
   /**
    * Does one or more RTG request for the specified docIds with a randomized 
fl &amp; fq params, asserting
    * that the returned document (if any) makes sense given the expected 
SolrInputDocuments
@@ -397,11 +473,44 @@ public class TestRandomFlRTGCloud extends 
SolrCloudTestCase {
       assert 1 == idsToRequest.size();
       params.add("id",idsToRequest.get(0));
     }
-    
-    final QueryResponse rsp = client.query(params);
-    assertNotNull(params.toString(), rsp);
 
-    final SolrDocumentList docs = getDocsFromRTGResponse(askForList, rsp);
+    String wt = params.get(CommonParams.WT, "javabin");
+    final ResponseParser restoreResponseParser;
+    if (client instanceof HttpSolrClient) {
+      restoreResponseParser = modifyParser((HttpSolrClient) client, wt);
+    } else {
+      // unless HttpSolrClient, `wt` doesn't matter -- it'll always be binary.
+      wt = "javabin";
+      restoreResponseParser = null;
+    }
+
+    final Object rsp;
+    final SolrDocumentList docs;
+    if ("javabin".equals(wt)) {
+      // the most common case
+      final QueryResponse qRsp = client.query(params);
+      assertNotNull(params.toString(), qRsp);
+      rsp = qRsp;
+      docs = getDocsFromRTGResponse(askForList, qRsp);
+    } else {
+      final NamedList<Object> nlRsp = client.request(new QueryRequest(params));
+      assertNotNull(restoreResponseParser);
+      ((HttpSolrClient) client).setParser(restoreResponseParser);
+      assertNotNull(params.toString(), nlRsp);
+      rsp = nlRsp;
+      final String textResult = (String) nlRsp.get("response");
+      switch (wt) {
+        case "json":
+          docs = getDocsFromJsonResponse(askForList, textResult);
+          break;
+        case "xml":
+          docs = getDocsFromXmlResponse(askForList, textResult);
+          break;
+        default:
+          throw new IllegalStateException();
+      }
+    }
+
     assertNotNull(params + " => " + rsp, docs);
     
     assertEquals("num docs mismatch: " + params + " => " + docsToExpect + " vs 
" + docs,
@@ -416,7 +525,7 @@ public class TestRandomFlRTGCloud extends SolrCloudTestCase 
{
         
         Set<String> expectedFieldNames = new TreeSet<>();
         for (FlValidator v : validators) {
-          expectedFieldNames.addAll(v.assertRTGResults(validators, expected, 
actual));
+          expectedFieldNames.addAll(v.assertRTGResults(validators, expected, 
actual, wt));
         }
         // ensure only expected field names are in the actual document
         Set<String> actualFieldNames = new TreeSet<>(actual.getFieldNames());
@@ -450,8 +559,36 @@ public class TestRandomFlRTGCloud extends 
SolrCloudTestCase {
     }
     return result;
   }
-    
-  /** 
+
+  @SuppressWarnings("unchecked")
+  private static SolrDocumentList getSolrDocumentList(Map<String, Object> 
response) {
+    SolrDocumentList ret = new SolrDocumentList();
+    for (Map<String, Object> doc : (List<Map<String, Object>>) 
response.get("docs")) {
+      ret.add(new SolrDocument(doc));
+    }
+    return ret;
+  }
+
+  @SuppressWarnings("unchecked")
+  private static SolrDocumentList getDocsFromJsonResponse(final boolean 
expectList, final String rsp) throws IOException {
+    Map<String, Object> nl = (Map<String, Object>) ObjectBuilder.fromJSON(rsp);
+    if (expectList) {
+      return getSolrDocumentList((Map<String, Object>) nl.get("response"));
+    } else {
+      SolrDocumentList ret = new SolrDocumentList();
+      Map<String, Object> doc = (Map<String, Object>) nl.get("doc");
+      if (doc != null) {
+        ret.add(new SolrDocument(doc));
+      }
+      return ret;
+    }
+  }
+
+  private static SolrDocumentList getDocsFromXmlResponse(final boolean 
expectList, final String rsp) {
+    return getDocsFromRTGResponse(expectList, new QueryResponse(new 
RawCapableXMLResponseParser().processResponse(new StringReader(rsp)), null));
+  }
+
+  /**
    * returns a random SolrClient -- either a CloudSolrClient, or an 
HttpSolrClient pointed 
    * at a node in our cluster 
    */
@@ -531,11 +668,13 @@ public class TestRandomFlRTGCloud extends 
SolrCloudTestCase {
      * @param validators all validators in use for this request, including the 
current one
      * @param expected a document containing the expected fields &amp; values 
that should be in the index
      * @param actual A document that was returned by an RTG request
+     * @param wt the `wt` serialization of the response
      * @return A set of "field names" in the actual document that this 
validator expected.
      */
     public Collection<String> assertRTGResults(final Collection<FlValidator> 
validators,
                                                final SolrInputDocument 
expected,
-                                               final SolrDocument actual);
+                                               final SolrDocument actual,
+                                               final String wt);
   }
   
   /** 
@@ -554,15 +693,35 @@ public class TestRandomFlRTGCloud extends 
SolrCloudTestCase {
       this.actualFieldName = actualFieldName;
     }
     public abstract String getFlParam();
+
+    @Override
     public Collection<String> assertRTGResults(final Collection<FlValidator> 
validators,
                                                final SolrInputDocument 
expected,
-                                               final SolrDocument actual) {
+                                               final SolrDocument actual,
+                                               final String wt) {
       assertEquals(expectedFieldName + " vs " + actualFieldName,
-                   expected.getFieldValue(expectedFieldName), 
actual.getFirstValue(actualFieldName));
+              expected.getFieldValue(expectedFieldName), normalize(wt, 
actual.getFirstValue(actualFieldName)));
       return Collections.<String>singleton(actualFieldName);
     }
   }
-  
+
+  /**
+   * Json parsing results in all Long and Double number values; `expected` 
values are all (conveniently!)
+   * expressed as Integer and Float, so we do a little normalization here so 
that the values are compatible
+   */
+  private static Object normalize(String wt, Object val) {
+    if ("json".equals(wt) && val instanceof Number) {
+      if (val instanceof Long) {
+        return ((Long) val).intValue();
+      } else if (val instanceof Double) {
+        return ((Double) val).floatValue();
+      } else {
+        throw new IllegalStateException("numbers with `wt=json` only expect 
Long or Double");
+      }
+    }
+    return val;
+  }
+
   private static class SimpleFieldValueValidator extends FieldValueValidator {
     public SimpleFieldValueValidator(final String fieldName) {
       super(fieldName, fieldName);
@@ -588,15 +747,16 @@ public class TestRandomFlRTGCloud extends 
SolrCloudTestCase {
    * What we're primarily concerned with is that the transformer does it's job 
and puts the string 
    * in the response, regardless of cloud/RTG/uncommited state of the document.
    */
-  @SuppressWarnings("UnusedNestedClass") // SOLR-9376
   private static class RawFieldValueValidator extends 
RenameFieldValueValidator {
     final String type;
     final String alias;
+    final SolrParams extraParams;
     public RawFieldValueValidator(final String type, final String fieldName, 
final String alias) {
       // transformer is weird, default result key doesn't care what params are 
used...
       super(fieldName, null == alias ? "["+type+"]" : alias);
       this.type = type;
       this.alias = alias;
+      this.extraParams = new ModifiableSolrParams().set(CommonParams.WT, type);
     }
     public RawFieldValueValidator(final String type, final String fieldName) {
       this(type, fieldName, null);
@@ -604,11 +764,96 @@ public class TestRandomFlRTGCloud extends 
SolrCloudTestCase {
     public String getFlParam() {
       return (null == alias ? "" : (alias + ":")) + "[" + type + " f=" + 
expectedFieldName + "]";
     }
+    @Override
+    public Collection<String> assertRTGResults(final Collection<FlValidator> 
validators,
+                                               SolrInputDocument expected,
+                                               final SolrDocument actual,
+                                               final String wt) {
+      if ("json".equals(wt) && "json".equals(type)) {
+        Object v = actual.get(actualFieldName);
+        if (v instanceof Collection) {
+          // the json "array" type is indistinguishable from a multivalued 
field, so when `super` validates
+          // based on `actual.getFirstValue(...)`, it causes issues. Here we 
know that our raw values are only
+          // on single-valued fields, so we wrap it to work around 
`getFirstValue` in parent class.
+          // The same logic applies to `expected` (below)
+          actual.setField(actualFieldName, Collections.singleton(v));
+        }
+        try {
+          Object parsedExpected = ObjectBuilder.fromJSON((String) 
expected.getFieldValue(expectedFieldName));
+          if (parsedExpected instanceof Collection) {
+            // see note above
+            parsedExpected = Collections.singleton(parsedExpected);
+          }
+          expected = expected.deepCopy(); // need to copy before modifying 
expected!
+          expected.setField(expectedFieldName, parsedExpected);
+        } catch (IOException ex) {
+          // swallow the exception and use the un-parsed String?
+        }
+      } else if ("xml".equals(wt) && "xml".equals(type)) {
+        try {
+          Object parsedExpected = 
RawCapableXMLResponseParser.convertRawContent((String) 
expected.getFieldValue(expectedFieldName));
+          expected = expected.deepCopy(); // need to copy before modifying 
expected!
+          expected.setField(expectedFieldName, parsedExpected);
+        } catch (XMLStreamException ex) {
+          // swallow the exception and use the un-parsed String?
+        }
+      }
+      return super.assertRTGResults(validators, expected, actual, wt);
+    }
+    @Override
+    public SolrParams getExtraRequestParams() {
+      return extraParams;
+    }
     public String getDefaultTransformerFactoryName() {
       return type;
     }
   }
- 
+
+  /**
+   * Local extension of XMLResponseParser that is capable of handling "raw" 
xml field values, for the
+   * purpose of validation and consistency between expected vs. actual.
+   */
+  private static class RawCapableXMLResponseParser extends XMLResponseParser {
+
+    private static String convertRawContent(String raw) throws 
XMLStreamException {
+      return XMLResponseParser.convertRawContent(raw, (parser) -> {
+        try {
+          return consumeRawContent0(parser);
+        } catch (XMLStreamException ex) {
+          // only called in the context of this test, so the extra exception 
wrapping is totally fine
+          throw new RuntimeException(ex);
+        }
+      });
+    }
+
+    protected String consumeRawContent(XMLStreamReader parser) throws 
XMLStreamException {
+      return consumeRawContent0(parser);
+    }
+
+    private static String consumeRawContent0(XMLStreamReader parser) throws 
XMLStreamException {
+      int depth = 0;
+      StringBuilder sb = new StringBuilder();
+      for (;;) {
+        int elementType = parser.next();
+        switch (elementType) {
+          case XMLStreamConstants.START_ELEMENT:
+            depth++;
+            sb.append("START:").append(parser.getLocalName()).append(';');
+            break;
+          case XMLStreamConstants.END_ELEMENT:
+            if (--depth < 0) {
+              // exiting raw element
+              return sb.toString();
+            }
+            sb.append("END:").append(parser.getLocalName()).append(';');
+            break;
+          case XMLStreamConstants.CHARACTERS:
+            sb.append(parser.getText());
+            break;
+        }
+      }
+    }
+  }
 
   /** 
    * enforces that a valid <code>[docid]</code> is present in the response, 
possibly using a 
@@ -631,8 +876,9 @@ public class TestRandomFlRTGCloud extends SolrCloudTestCase 
{
     public String getFlParam() { return USAGE.equals(resultKey) ? resultKey : 
resultKey+":"+USAGE; }
     public Collection<String> assertRTGResults(final Collection<FlValidator> 
validators,
                                                final SolrInputDocument 
expected,
-                                               final SolrDocument actual) {
-      final Object value =  actual.getFirstValue(resultKey);
+                                               final SolrDocument actual,
+                                               final String wt) {
+      Object value =  normalize(wt, actual.getFirstValue(resultKey));
       assertNotNull(getFlParam() + " => no value in actual doc", value);
       assertTrue(USAGE + " must be an Integer: " + value, value instanceof 
Integer);
 
@@ -664,7 +910,8 @@ public class TestRandomFlRTGCloud extends SolrCloudTestCase 
{
     public String getFlParam() { return USAGE.equals(resultKey) ? resultKey : 
resultKey+":"+USAGE; }
     public Collection<String> assertRTGResults(final Collection<FlValidator> 
validators,
                                                final SolrInputDocument 
expected,
-                                               final SolrDocument actual) {
+                                               final SolrDocument actual,
+                                               final String wt) {
       final Object value =  actual.getFirstValue(resultKey);
       assertNotNull(getFlParam() + " => no value in actual doc", value);
       assertTrue(USAGE + " must be an String: " + value, value instanceof 
String);
@@ -699,8 +946,9 @@ public class TestRandomFlRTGCloud extends SolrCloudTestCase 
{
     public String getFlParam() { return fl; }
     public Collection<String> assertRTGResults(final Collection<FlValidator> 
validators,
                                                final SolrInputDocument 
expected,
-                                               final SolrDocument actual) {
-      final Object actualVal =  actual.getFirstValue(resultKey);
+                                               final SolrDocument actual,
+                                               final String wt) {
+      Object actualVal =  normalize(wt, actual.getFirstValue(resultKey));
       assertNotNull(getFlParam() + " => no value in actual doc", actualVal);
       assertEquals(getFlParam(), expectedVal, actualVal);
       return Collections.<String>singleton(resultKey);
@@ -732,11 +980,12 @@ public class TestRandomFlRTGCloud extends 
SolrCloudTestCase {
     public String getFlParam() { return fl; }
     public Collection<String> assertRTGResults(final Collection<FlValidator> 
validators,
                                                final SolrInputDocument 
expected,
-                                               final SolrDocument actual) {
+                                               final SolrDocument actual,
+                                               final String wt) {
       final Object origVal = expected.getFieldValue(fieldName);
       assertTrue("this validator only works on numeric fields: " + origVal, 
origVal instanceof Number);
       
-      assertEquals(fl, 1.3F, actual.getFirstValue(resultKey));
+      assertEquals(fl, 1.3F, normalize(wt, actual.getFirstValue(resultKey)));
       return Collections.<String>singleton(resultKey);
     }
   }
@@ -764,12 +1013,17 @@ public class TestRandomFlRTGCloud extends 
SolrCloudTestCase {
     public final static String SUBQ_KEY = "subq";
     public final static String SUBQ_FIELD = "next_2_ids_i";
     public String getFlParam() { return SUBQ_KEY+":["+NAME+"]"; }
+    @SuppressWarnings("unchecked")
     public Collection<String> assertRTGResults(final Collection<FlValidator> 
validators,
                                                final SolrInputDocument 
expected,
-                                               final SolrDocument actual) {
+                                               final SolrDocument actual,
+                                               final String wt) {
       final int compVal = assertParseInt("expected id", 
expected.getFieldValue("id"));
       
-      final Object actualVal = actual.getFieldValue(SUBQ_KEY);
+      Object actualVal = actual.getFieldValue(SUBQ_KEY);
+      if ("json".equals(wt)) {
+        actualVal = getSolrDocumentList((Map<String, Object>) actualVal);
+      }
       assertTrue("Expected a doclist: " + actualVal,
                  actualVal instanceof SolrDocumentList);
       assertTrue("should be at most 2 docs in doc list: " + actualVal,
@@ -842,11 +1096,20 @@ public class TestRandomFlRTGCloud extends 
SolrCloudTestCase {
     public String getFlParam() { return fl; }
     public Collection<String> assertRTGResults(final Collection<FlValidator> 
validators,
                                                final SolrInputDocument 
expected,
-                                               final SolrDocument actual) {
+                                               final SolrDocument actual,
+                                               final String wt) {
       final Object origVal = expected.getFieldValue(fieldName);
       assertTrue(fl + ": orig field value is not supported: " + origVal, 
VALUES.containsKey(origVal));
-      
-      assertEquals(fl, VALUES.get(origVal), actual.getFirstValue(resultKey));
+
+      Object orig = VALUES.get(origVal);
+      if ("json".equals(wt)) {
+        try {
+          orig = ObjectBuilder.fromJSON((String) orig);
+        } catch (IOException ex) {
+          // swallow exception and use raw `orig` String?
+        }
+      }
+      assertEquals(fl, orig, actual.getFirstValue(resultKey));
       return Collections.<String>singleton(resultKey);
     }
     public Set<String> getSuppressedFields() { return 
Collections.singleton(fieldName); }
@@ -881,7 +1144,8 @@ public class TestRandomFlRTGCloud extends 
SolrCloudTestCase {
                                 
     public Collection<String> assertRTGResults(final Collection<FlValidator> 
validators,
                                                final SolrInputDocument 
expected,
-                                               final SolrDocument actual) {
+                                               final SolrDocument actual,
+                                               final String wt) {
 
       final Set<String> renamed = new LinkedHashSet<>(validators.size());
       for (FlValidator v : validators) {
@@ -895,7 +1159,7 @@ public class TestRandomFlRTGCloud extends 
SolrCloudTestCase {
       for (String f : expected.getFieldNames()) {
         if ( matchesGlob(f) && (! renamed.contains(f) ) ) {
           result.add(f);
-          assertEquals(glob + " => " + f, expected.getFieldValue(f), 
actual.getFirstValue(f));
+          assertEquals(glob + " => " + f, expected.getFieldValue(f), 
normalize(wt, actual.getFirstValue(f)));
         }
       }
       return result;
@@ -919,7 +1183,8 @@ public class TestRandomFlRTGCloud extends 
SolrCloudTestCase {
     public String getFlParam() { return fl; }
     public Collection<String> assertRTGResults(final Collection<FlValidator> 
validators,
                                                final SolrInputDocument 
expected,
-                                               final SolrDocument actual) {
+                                               final SolrDocument actual,
+                                               final String wt) {
       assertEquals(fl, null, actual.getFirstValue(fieldName));
       return Collections.emptySet();
     }
diff --git a/solr/core/src/test/org/apache/solr/response/JSONWriterTest.java 
b/solr/core/src/test/org/apache/solr/response/JSONWriterTest.java
index ea0b3d3..a5a3396 100644
--- a/solr/core/src/test/org/apache/solr/response/JSONWriterTest.java
+++ b/solr/core/src/test/org/apache/solr/response/JSONWriterTest.java
@@ -203,13 +203,13 @@ public class JSONWriterTest extends SolrTestCaseJ4 {
     methodsExpectedNotOverriden.add("writeMapOpener");
     methodsExpectedNotOverriden.add("writeMapSeparator");
     methodsExpectedNotOverriden.add("writeMapCloser");
-    methodsExpectedNotOverriden.add("public default void 
org.apache.solr.common.util.JsonTextWriter.writeArray(java.lang.String,java.util.List)
 throws java.io.IOException");
+    methodsExpectedNotOverriden.add("public default void 
org.apache.solr.common.util.JsonTextWriter.writeArray(java.lang.String,java.util.List,boolean)
 throws java.io.IOException");
     methodsExpectedNotOverriden.add("writeArrayOpener");
     methodsExpectedNotOverriden.add("writeArraySeparator");
     methodsExpectedNotOverriden.add("writeArrayCloser");
     methodsExpectedNotOverriden.add("public default void 
org.apache.solr.common.util.JsonTextWriter.writeMap(org.apache.solr.common.MapWriter)
 throws java.io.IOException");
     methodsExpectedNotOverriden.add("public default void 
org.apache.solr.common.util.JsonTextWriter.writeIterator(org.apache.solr.common.IteratorWriter)
 throws java.io.IOException");
-    methodsExpectedNotOverriden.add("public default void 
org.apache.solr.common.util.JsonTextWriter.writeJsonIter(java.util.Iterator) 
throws java.io.IOException");
+    methodsExpectedNotOverriden.add("public default void 
org.apache.solr.common.util.JsonTextWriter.writeJsonIter(java.util.Iterator,boolean)
 throws java.io.IOException");
 
     final Class<?> subClass = 
JSONResponseWriter.ArrayOfNameTypeValueJSONWriter.class;
     final Class<?> superClass = subClass.getSuperclass();
diff --git 
a/solr/core/src/test/org/apache/solr/response/TestRawTransformer.java 
b/solr/core/src/test/org/apache/solr/response/TestRawTransformer.java
index ea5619f..28fa1c2 100644
--- a/solr/core/src/test/org/apache/solr/response/TestRawTransformer.java
+++ b/solr/core/src/test/org/apache/solr/response/TestRawTransformer.java
@@ -16,55 +16,201 @@
  */
 package org.apache.solr.response;
 
+import org.apache.commons.io.FileUtils;
 import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.client.solrj.embedded.JettySolrRunner;
+import org.apache.solr.client.solrj.impl.CloudSolrClient;
+import org.apache.solr.client.solrj.impl.HttpSolrClient;
+import org.apache.solr.client.solrj.impl.NoOpResponseParser;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.QueryRequest;
+import org.apache.solr.cloud.MiniSolrCloudCluster;
+import org.apache.solr.cloud.SolrCloudTestCase;
 import org.apache.solr.common.SolrInputDocument;
-import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.common.params.ModifiableSolrParams;
 import org.junit.After;
+import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
+import java.io.File;
+import java.lang.invoke.MethodHandles;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.regex.Pattern;
+
 /**
  * Tests Raw JSON output for fields when used with and without the unique key 
field.
  *
  * See SOLR-7993
  */
-public class TestRawTransformer extends SolrTestCaseJ4 {
+public class TestRawTransformer extends SolrCloudTestCase {
+
+  private static final String DEBUG_LABEL = 
MethodHandles.lookup().lookupClass().getName();
+
+  /** A basic client for operations at the cloud level, default collection 
will be set */
+  private static JettySolrRunner JSR;
+  private static HttpSolrClient CLIENT;
 
   @BeforeClass
   public static void beforeClass() throws Exception {
-    initCore("solrconfig-doctransformers.xml", "schema.xml");
+    if (random().nextBoolean()) {
+      initStandalone();
+      JSR.start();
+      CLIENT = (HttpSolrClient) JSR.newClient();
+    } else {
+      initCloud();
+      CLIENT = (HttpSolrClient) JSR.newClient();
+      JSR = null;
+    }
+    initIndex();
+  }
+
+  private static void initStandalone() throws Exception {
+    initCore("solrconfig-minimal.xml", "schema_latest.xml");
+    File homeDir = createTempDir().toFile();
+    final File collDir = new File(homeDir, "collection1");
+    final File confDir = collDir.toPath().resolve("conf").toFile();
+    confDir.mkdirs();
+    FileUtils.copyFile(new File(SolrTestCaseJ4.TEST_HOME(), "solr.xml"), new 
File(homeDir, "solr.xml"));
+    String src_dir = TEST_HOME() + "/collection1/conf";
+    FileUtils.copyFile(new File(src_dir, "schema_latest.xml"),
+            new File(confDir, "schema.xml"));
+    FileUtils.copyFile(new File(src_dir, "solrconfig-minimal.xml"),
+            new File(confDir, "solrconfig.xml"));
+    for (String file : new String[] 
{"solrconfig.snippet.randomindexconfig.xml",
+            "stopwords.txt", "synonyms.txt", "protwords.txt", "currency.xml"}) 
{
+      FileUtils.copyFile(new File(src_dir, file), new File(confDir, file));
+    }
+    Files.createFile(collDir.toPath().resolve("core.properties"));
+    Properties nodeProperties = new Properties();
+    nodeProperties.setProperty("solr.data.dir", h.getCore().getDataDir());
+    JSR = new JettySolrRunner(homeDir.getAbsolutePath(), nodeProperties, 
buildJettyConfig("/solr"));
+  }
+
+  private static void initCloud() throws Exception {
+    final String configName = DEBUG_LABEL + "_config-set";
+    final Path configDir = Paths.get(TEST_HOME(), "collection1", "conf");
+
+    final int numNodes = 3;
+    MiniSolrCloudCluster cloud = 
configureCluster(numNodes).addConfig(configName, configDir).configure();
+
+    Map<String, String> collectionProperties = new LinkedHashMap<>();
+    collectionProperties.put("config", "solrconfig-minimal.xml");
+    collectionProperties.put("schema", "schema_latest.xml");
+    CloudSolrClient cloudSolrClient = cloud.getSolrClient();
+    CollectionAdminRequest.createCollection("collection1", configName, 
numNodes, 1)
+            .setPerReplicaState(SolrCloudTestCase.USE_PER_REPLICA_STATE)
+            .setProperties(collectionProperties)
+            .process(cloudSolrClient);
+
+    JSR = cloud.getRandomJetty(random());
+  }
+
+  @AfterClass
+  private static void afterClass() throws Exception{
+    if (JSR != null) {
+      JSR.stop();
+    }
+    // NOTE: CLOUD_CLIENT should be stopped automatically in 
`SolrCloudTestCase.shutdownCluster()`
   }
 
   @After
   public void cleanup() throws Exception {
-    assertU(delQ("*:*"));
-    assertU(commit());
+    if (JSR != null) {
+      assertU(delQ("*:*"));
+      assertU(commit());
+    }
   }
 
-  @Test
-  public void testCustomTransformer() throws Exception {
+  private static final int MAX = 10;
+  private static void initIndex() throws Exception {
     // Build a simple index
-    int max = 10;
-    for (int i = 0; i < max; i++) {
+    // TODO: why are we indexing 10 docs here? Wouldn't one suffice?
+    for (int i = 0; i < MAX; i++) {
       SolrInputDocument sdoc = new SolrInputDocument();
       sdoc.addField("id", i);
+      // below are single-valued fields
       sdoc.addField("subject", 
"{poffL:[{offL:[{oGUID:\"79D5A31D-B3E4-4667-B812-09DF4336B900\",oID:\"OO73XRX\",prmryO:1,oRank:1,addTp:\"Office\",addCd:\"AA4GJ5T\",ad1:\"102
 S 3rd St Ste 100\",city:\"Carson 
City\",st:\"MI\",zip:\"48811\",lat:43.176885,lng:-84.842919,phL:[\"(989) 
584-1308\"],faxL:[\"(989) 584-6453\"]}]}]}");
-      sdoc.addField("title", "title_" + i);
-      updateJ(jsonAdd(sdoc), null);
+      sdoc.addField("author", 
"<root><child1>some</child1><child2>trivial</child2><child3>xml</child3></root>");
+      // below are multiValued fields
+      sdoc.addField("links", "{an_array:[1,2,3]}");
+      sdoc.addField("links", "{an_array:[4,5,6]}");
+      sdoc.addField("content_type", "<root>one</root>");
+      sdoc.addField("content_type", "<root>two</root>");
+      CLIENT.add("collection1", sdoc);
     }
-    assertU(commit());
-    assertQ(req("q", "*:*"), "//*[@numFound='" + max + "']");
+    CLIENT.commit("collection1");
+    assertEquals(MAX, CLIENT.query("collection1", new 
ModifiableSolrParams(Map.of("q", new 
String[]{"*:*"}))).getResults().getNumFound());
+  }
+
+  @Test
+  public void testXmlTransformer() throws Exception {
+    QueryRequest req = new QueryRequest(new ModifiableSolrParams(
+            Map.of("q", new String[]{"*:*"}, "fl", new 
String[]{"author:[xml],content_type:[xml]"}, "wt", new String[]{"xml"})
+    ));
+    req.setResponseParser(XML_NOOP_RESPONSE_PARSER);
+    String strResponse = (String) 
CLIENT.request(req,"collection1").get("response");
+    assertTrue("response does not contain raw XML encoding: " + strResponse,
+            strResponse.contains("<raw 
name=\"author\"><root><child1>some</child1><child2>trivial</child2><child3>xml</child3></root></raw>"));
+    assertTrue("response (multiValued) does not contain raw XML encoding: " + 
strResponse,
+            Pattern.compile("<arr 
name=\"content_type\">\\s*<raw><root>one</root></raw>\\s*<raw><root>two</root></raw>\\s*</arr>").matcher(strResponse).find());
 
-    SolrQueryRequest req = req("q", "*:*", "fl", "subject:[json]", "wt", 
"json");
-    String strResponse = h.query(req);
+    req = new QueryRequest(new ModifiableSolrParams(
+            Map.of("q", new String[]{"*:*"}, "fl", new 
String[]{"author,content_type"}, "wt", new String[]{"xml"})
+    ));
+    req.setResponseParser(XML_NOOP_RESPONSE_PARSER);
+    strResponse = (String) CLIENT.request(req, "collection1").get("response");
+    assertTrue("response does not contain escaped XML encoding: " + 
strResponse,
+            strResponse.contains("<str 
name=\"author\">&lt;root&gt;&lt;child1"));
+    assertTrue("response (multiValued) does not contain escaped XML encoding: 
" + strResponse,
+            Pattern.compile("<arr 
name=\"content_type\">\\s*<str>&lt;root&gt;").matcher(strResponse).find());
+
+    req = new QueryRequest(new ModifiableSolrParams(
+            Map.of("q", new String[]{"*:*"}, "fl", new 
String[]{"author:[xml],content_type:[xml]"}, "wt", new String[]{"json"})
+    ));
+    req.setResponseParser(JSON_NOOP_RESPONSE_PARSER);
+    strResponse = (String) CLIENT.request(req, "collection1").get("response");
+    assertTrue("unexpected serialization of XML field value in JSON response: 
" + strResponse,
+            strResponse.contains("\"author\":\"<root><child1>some</child1>"));
+    assertTrue("unexpected (multiValued) serialization of XML field value in 
JSON response: " + strResponse,
+            strResponse.contains("\"content_type\":[\"<root>one</root>"));
+  }
+
+  @Test
+  public void testJsonTransformer() throws Exception {
+    QueryRequest req = new QueryRequest(new ModifiableSolrParams(
+      Map.of("q", new String[]{"*:*"}, "fl", new 
String[]{"subject:[json],links:[json]"}, "wt", new String[]{"json"})
+    ));
+    req.setResponseParser(JSON_NOOP_RESPONSE_PARSER);
+    String strResponse = (String) 
CLIENT.request(req,"collection1").get("response");
     assertTrue("response does not contain right JSON encoding: " + strResponse,
-        strResponse.contains("\"subject\":[{poffL:[{offL:[{oGUID:\"7"));
+        strResponse.contains("\"subject\":{poffL:[{offL:[{oGUID:\"7"));
+    assertTrue("response (multiValued) does not contain right JSON encoding: " 
+ strResponse,
+            
Pattern.compile("\"links\":\\[\\{an_array:\\[1,2,3]},\\s*\\{an_array:\\[4,5,6]}]").matcher(strResponse).find());
 
-    req = req("q", "*:*", "fl", "id,subject", "wt", "json");
-    strResponse = h.query(req);
+    req = new QueryRequest(new ModifiableSolrParams(
+      Map.of("q", new String[]{"*:*"}, "fl", new String[]{"id", 
"subject,links"}, "wt", new String[]{"json"})
+    ));
+    req.setResponseParser(JSON_NOOP_RESPONSE_PARSER);
+    strResponse = (String) CLIENT.request(req, "collection1").get("response");
     assertTrue("response does not contain right JSON encoding: " + strResponse,
-        strResponse.contains("subject\":[\""));
+        strResponse.contains("subject\":\""));
+    assertTrue("response (multiValued) does not contain right JSON encoding: " 
+ strResponse,
+            strResponse.contains("\"links\":[\""));
   }
 
+  private static final NoOpResponseParser XML_NOOP_RESPONSE_PARSER = new 
NoOpResponseParser();
+  private static final NoOpResponseParser JSON_NOOP_RESPONSE_PARSER = new 
NoOpResponseParser() {
+    @Override
+    public String getWriterType() {
+      return "json";
+    }
+  };
+
 }
 
diff --git 
a/solr/modules/extraction/src/java/org/apache/solr/handler/extraction/XLSXResponseWriter.java
 
b/solr/modules/extraction/src/java/org/apache/solr/handler/extraction/XLSXResponseWriter.java
index b225ceb..d1e5dda 100644
--- 
a/solr/modules/extraction/src/java/org/apache/solr/handler/extraction/XLSXResponseWriter.java
+++ 
b/solr/modules/extraction/src/java/org/apache/solr/handler/extraction/XLSXResponseWriter.java
@@ -258,7 +258,7 @@ class XLSXWriter extends TabularResponseWriter {
           values = tmpList;
         }
 
-        writeArray(xlField.name, values.iterator());
+        writeArray(xlField.name, values.iterator(), false);
 
       } else {
         // normalize to first value
@@ -278,7 +278,8 @@ class XLSXWriter extends TabularResponseWriter {
   }
 
   @Override
-  public void writeArray(String name, Iterator<?> val) throws IOException {
+  public void writeArray(String name, Iterator<?> val, boolean raw) throws 
IOException {
+    assert !raw;
     StringBuffer output = new StringBuffer();
     while (val.hasNext()) {
       Object v = val.next();
diff --git a/solr/solr-ref-guide/src/major-changes-in-solr-9.adoc 
b/solr/solr-ref-guide/src/major-changes-in-solr-9.adoc
index 27eb361..6d0b109 100644
--- a/solr/solr-ref-guide/src/major-changes-in-solr-9.adoc
+++ b/solr/solr-ref-guide/src/major-changes-in-solr-9.adoc
@@ -152,6 +152,13 @@ Currently this change should only effect compatibility of 
custom code overriding
 * SOLR-14510: The `writeStartDocumentList` in `TextResponseWriter` now 
receives an extra boolean parameter representing the "exactness" of the 
`numFound` value (exact vs approximation).
 Any custom response writer extending `TextResponseWriter` will need to 
implement this abstract method now (instead previous with the same name but 
without the new boolean parameter).
 
+* SOLR-9376: The response format for field values serialized as raw XML (via 
the `[xml]` raw value DocTransformer
+and `wt=xml`) has changed. Previously, values were dropped in directly as 
top-level child elements of each `<doc>`,
+obscuring associated field names and yielding inconsistent `<doc>` structure. 
As of version 9.0, raw values are
+wrapped in a `<raw name="field_name">[...]</raw>` element at the top level of 
each `<doc>` (or within an enclosing
+`<arr name="field_name"><raw>[...]</raw></arr>` element for multi-valued 
fields). Existing clients that parse field
+values serialized in this way will need to be updated accordingly.
+
 === solr.xml maxBooleanClauses now enforced recursively
 
 Lucene 9.0 has additional safety checks over previous versions that impact how 
the `solr.xml` global 
`<<configuring-solr-xml#global-maxbooleanclauses,maxBooleanClauses>>` option is 
enforced.
diff --git 
a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/XMLResponseParser.java 
b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/XMLResponseParser.java
index 40cc649..c219e70 100644
--- 
a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/XMLResponseParser.java
+++ 
b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/XMLResponseParser.java
@@ -22,12 +22,14 @@ import javax.xml.stream.XMLStreamException;
 import javax.xml.stream.XMLStreamReader;
 import java.io.InputStream;
 import java.io.Reader;
+import java.io.StringReader;
 import java.lang.invoke.MethodHandles;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
 import java.util.Locale;
+import java.util.function.Function;
 
 import org.apache.solr.client.solrj.ResponseParser;
 import org.apache.solr.common.EmptyEntityResolver;
@@ -183,6 +185,7 @@ public class XMLResponseParser extends ResponseParser
       }
     },
 
+    RAW    (true) { @Override public Object read( String txt ) { return null; 
} },
     ARR    (false) { @Override public Object read( String txt ) { return null; 
} },
     LST    (false) { @Override public Object read( String txt ) { return null; 
} },
     RESULT (false) { @Override public Object read( String txt ) { return null; 
} },
@@ -262,6 +265,7 @@ public class XMLResponseParser extends ResponseParser
           case LONG:
           case NULL:
           case STR:
+          case RAW:
             break;
           }
           throw new XMLStreamException( "branch element not handled!", 
parser.getLocation() );
@@ -335,6 +339,7 @@ public class XMLResponseParser extends ResponseParser
           case LONG:
           case NULL:
           case STR:
+          case RAW:
             break;
           }
           throw new XMLStreamException( "branch element not handled!", 
parser.getLocation() );
@@ -454,6 +459,15 @@ public class XMLResponseParser extends ResponseParser
         } else if( type == KnownType.LST ) {
             doc.addField( name, readNamedList( parser ) );
           depth--;
+        } else if( type == KnownType.RESULT ) {
+          // e.g., from the [subquery] doc transformer
+          doc.put(name, readDocuments(parser));
+          depth--;
+        } else if( type == KnownType.RAW ) {
+          // e.g., from the raw [xml] doc transformer.
+          String raw = consumeRawContent(parser);
+          doc.addField(name, raw);
+          depth--;
         } else if( !type.isLeaf ) {
           throw new XMLStreamException( "must be value or array", 
parser.getLocation() );
         }
@@ -480,5 +494,29 @@ public class XMLResponseParser extends ResponseParser
     }
   }
 
+  /**
+   * This is a stub method for handling/validating "raw" xml field values in 
the context of tests. Before this
+   * stub method was present, "raw" content would have still thrown an 
exception, albeit a different, more inscrutable
+   * exception.
+   */
+  protected String consumeRawContent(XMLStreamReader parser) throws 
XMLStreamException {
+    throw new UnsupportedOperationException(XMLResponseParser.class + " is not 
capable of consuming field values serialized as raw XML");
+  }
+
+  /**
+   * Convenience method that converts raw String input (should be valid xml 
when wrapped in a root element) and
+   * converts it to a format compatible with how {@link XMLResponseParser} 
parses from raw xml fields.
+   * This method is intended for test validation.
+   *
+   * The main reason this method exists is to provide a consistent way of 
configuring and creating and invoking
+   * the sub-parser
+   */
+  protected static String convertRawContent(String raw, 
Function<XMLStreamReader, String> consumeRawContent) throws XMLStreamException {
+    XMLStreamReader subParser = factory.createXMLStreamReader(new 
StringReader("<raw>" + raw + "</raw>"));
+    while (subParser.next() != XMLStreamConstants.START_ELEMENT) {
+      // consume any early stuff
+    }
+    return consumeRawContent.apply(subParser);
+  }
 
 }
diff --git 
a/solr/solrj/src/java/org/apache/solr/common/util/JsonTextWriter.java 
b/solr/solrj/src/java/org/apache/solr/common/util/JsonTextWriter.java
index 42025e3..85de57b 100644
--- a/solr/solrj/src/java/org/apache/solr/common/util/JsonTextWriter.java
+++ b/solr/solrj/src/java/org/apache/solr/common/util/JsonTextWriter.java
@@ -65,6 +65,10 @@ public interface JsonTextWriter extends TextWriter {
     _writeChar(']');
   }
 
+  default void writeStrRaw(String name, String val) throws IOException {
+    _writeStr(val);
+  }
+
   default void writeStr(String name, String val, boolean needsEscaping) throws 
IOException {
     // it might be more efficient to use a stringbuilder or write substrings
     // if writing chars to the stream is slow.
@@ -185,12 +189,12 @@ public interface JsonTextWriter extends TextWriter {
     _writeChar(':');
   }
 
-  default void writeJsonIter(Iterator<?> val) throws IOException {
+  default void writeJsonIter(Iterator<?> val, boolean raw) throws IOException {
     incLevel();
     boolean first = true;
     while (val.hasNext()) {
       if (!first) indent();
-      writeVal(null, val.next());
+      writeVal(null, val.next(), raw);
       if (val.hasNext()) {
         writeArraySeparator();
       }
@@ -264,15 +268,15 @@ public interface JsonTextWriter extends TextWriter {
   }
 
 
-  default void writeArray(String name, List<?> l) throws IOException {
+  default void writeArray(String name, List<?> l, boolean raw) throws 
IOException {
     writeArrayOpener(l.size());
-    writeJsonIter(l.iterator());
+    writeJsonIter(l.iterator(), raw);
     writeArrayCloser();
   }
 
-  default void writeArray(String name, Iterator<?> val) throws IOException {
+  default void writeArray(String name, Iterator<?> val, boolean raw) throws 
IOException {
     writeArrayOpener(-1); // no trivial way to determine array size
-    writeJsonIter(val);
+    writeJsonIter(val, raw);
     writeArrayCloser();
   }
 
diff --git a/solr/solrj/src/java/org/apache/solr/common/util/TextWriter.java 
b/solr/solrj/src/java/org/apache/solr/common/util/TextWriter.java
index e02677f..a7605f1 100644
--- a/solr/solrj/src/java/org/apache/solr/common/util/TextWriter.java
+++ b/solr/solrj/src/java/org/apache/solr/common/util/TextWriter.java
@@ -43,6 +43,10 @@ import org.apache.solr.common.PushWriter;
 public interface TextWriter extends PushWriter {
 
   default void writeVal(String name, Object val) throws IOException {
+    writeVal(name, val, false);
+  }
+
+  default void writeVal(String name, Object val, boolean raw) throws 
IOException {
 
     // if there get to be enough types, perhaps hashing on the type
     // to get a handler might be faster (but types must be exact to do that...)
@@ -52,8 +56,12 @@ public interface TextWriter extends PushWriter {
     if (val == null) {
       writeNull(name);
     } else if (val instanceof CharSequence) {
-      writeStr(name, val.toString(), true);
-      // micro-optimization... using toString() avoids a cast first
+      if (raw) {
+        writeStrRaw(name, val.toString());
+      } else {
+        writeStr(name, val.toString(), true);
+        // micro-optimization... using toString() avoids a cast first
+      }
     } else if (val instanceof Number) {
       writeNumber(name, (Number) val);
     } else if (val instanceof Boolean) {
@@ -65,9 +73,14 @@ public interface TextWriter extends PushWriter {
     } else if (val instanceof NamedList) {
       writeNamedList(name, (NamedList)val);
     } else if (val instanceof Path) {
-      writeStr(name, ((Path) val).toAbsolutePath().toString(), true);
+      final String pathStr = ((Path) val).toAbsolutePath().toString();
+      if (raw) {
+        writeStrRaw(name, pathStr);
+      } else {
+        writeStr(name, pathStr, true);
+      }
     } else if (val instanceof IteratorWriter) {
-      writeIterator(name, (IteratorWriter) val);
+      writeIterator(name, (IteratorWriter) val, raw);
     } else if (val instanceof MapWriter) {
       writeMap(name, (MapWriter) val);
     } else if (val instanceof MapSerializable) {
@@ -76,29 +89,39 @@ public interface TextWriter extends PushWriter {
     } else if (val instanceof Map) {
       writeMap(name, (Map)val, false, true);
     } else if (val instanceof Iterator) { // very generic; keep towards the end
-      writeArray(name, (Iterator) val);
+      writeArray(name, (Iterator) val, raw);
     } else if (val instanceof Iterable) { // very generic; keep towards the end
-      writeArray(name,((Iterable)val).iterator());
+      writeArray(name,((Iterable)val).iterator(), raw);
     } else if (val instanceof Object[]) {
-      writeArray(name,(Object[])val);
+      writeArray(name,(Object[])val, raw);
     } else if (val instanceof byte[]) {
       byte[] arr = (byte[])val;
       writeByteArr(name, arr, 0, arr.length);
     } else if (val instanceof EnumFieldValue) {
-      writeStr(name, val.toString(), true);
-    } else if (val instanceof WriteableValue) {
-      ((WriteableValue)val).write(name, this);
+      if (raw) {
+        writeStrRaw(name, val.toString());
+      } else {
+        writeStr(name, val.toString(), true);
+      }
     } else {
       // default... for debugging only.  Would be nice to "assert false" ?
       writeStr(name, val.getClass().getName() + ':' + val.toString(), true);
     }
   }
 
+  /**
+   * Writes the specified val directly to the backing writer, without wrapping 
(e.g., in quotes) or escaping
+   * of any kind.
+   */
+  default void writeStrRaw(String name, String val) throws IOException {
+    throw new UnsupportedOperationException();
+  }
+
   void writeStr(String name, String val, boolean needsEscaping) throws 
IOException;
 
   void writeMap(String name, Map<?, ?> val, boolean excludeOuter, boolean 
isFirstVal) throws IOException;
 
-  void writeArray(String name, Iterator<?> val) throws IOException;
+  void writeArray(String name, Iterator<?> val, boolean raw) throws 
IOException;
 
   void writeNull(String name) throws IOException;
 
@@ -153,12 +176,12 @@ public interface TextWriter extends PushWriter {
     }
   }
 
-  default void writeArray(String name, Object[] val) throws IOException {
-    writeArray(name, Arrays.asList(val));
+  default void writeArray(String name, Object[] val, boolean raw) throws 
IOException {
+    writeArray(name, Arrays.asList(val), raw);
   }
 
-  default void writeArray(String name, List<?> l) throws IOException {
-    writeArray(name, l.iterator());
+  default void writeArray(String name, List<?> l, boolean raw) throws 
IOException {
+    writeArray(name, l.iterator(), raw);
   }
 
 
@@ -224,7 +247,7 @@ public interface TextWriter extends PushWriter {
     /*todo*/
   }
 
-  default void writeIterator(String name, IteratorWriter iw) throws 
IOException {
+  default void writeIterator(String name, IteratorWriter iw, boolean raw) 
throws IOException {
     writeIterator(iw);
   }
 
diff --git 
a/solr/solrj/src/java/org/apache/solr/common/util/WriteableValue.java 
b/solr/solrj/src/java/org/apache/solr/common/util/WriteableValue.java
deleted file mode 100644
index 82e7d00..0000000
--- a/solr/solrj/src/java/org/apache/solr/common/util/WriteableValue.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.solr.common.util;
-
-import java.io.IOException;
-
-import org.apache.solr.common.util.JavaBinCodec.ObjectResolver;
-
-public abstract class WriteableValue implements ObjectResolver {
-  public abstract void write(String name, TextWriter writer) throws 
IOException;
-}
\ No newline at end of file

Reply via email to