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

hossman 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 90ef6ff725d SOLR-18033: Solr response writing now correctly delegates 
to FieldType implementations to determine how internal binary field values are 
represented externally
90ef6ff725d is described below

commit 90ef6ff725db6f7c530d6a8183216b07b75af38f
Author: Chris Hostetter <[email protected]>
AuthorDate: Tue Jan 13 11:10:56 2026 -0700

    SOLR-18033: Solr response writing now correctly delegates to FieldType 
implementations to determine how internal binary field values are represented 
externally
---
 ...d-fieldtype-external-representation-control.yml |   7 ++
 .../org/apache/solr/response/DocsStreamer.java     |  28 ++++-
 .../java/org/apache/solr/schema/BinaryField.java   |   5 +
 .../src/java/org/apache/solr/schema/FieldType.java |  24 ++++
 .../apache/solr/search/SolrDocumentFetcher.java    |   2 +-
 .../solr/collection1/conf/schema-binaryfield.xml   |  10 +-
 .../conf/schema-non-stored-docvalues.xml           |  15 +++
 .../org/apache/solr/schema/StrBinaryField.java     |  88 ++++++++++++++
 .../apache/solr/schema/SwapBytesBinaryField.java   |  88 ++++++++++++++
 .../org/apache/solr/schema/TestBinaryField.java    | 130 ++++++++++++--------
 .../solr/schema/TestUseDocValuesAsStored.java      | 134 ++++++++++++++++++++-
 11 files changed, 473 insertions(+), 58 deletions(-)

diff --git 
a/changelog/unreleased/SOLR-18033-binary-based-fieldtype-external-representation-control.yml
 
b/changelog/unreleased/SOLR-18033-binary-based-fieldtype-external-representation-control.yml
new file mode 100644
index 00000000000..2cfcfd34cb1
--- /dev/null
+++ 
b/changelog/unreleased/SOLR-18033-binary-based-fieldtype-external-representation-control.yml
@@ -0,0 +1,7 @@
+title: Solr response writing now correctly delegates to FieldType 
implementations to determine how internal binary field values are represented 
externally
+type: fixed
+authors:
+- name: hossman
+links:
+- name: SOLR-18033
+  url: https://issues.apache.org/jira/browse/SOLR-18033
diff --git a/solr/core/src/java/org/apache/solr/response/DocsStreamer.java 
b/solr/core/src/java/org/apache/solr/response/DocsStreamer.java
index e980e809b18..850781c079d 100644
--- a/solr/core/src/java/org/apache/solr/response/DocsStreamer.java
+++ b/solr/core/src/java/org/apache/solr/response/DocsStreamer.java
@@ -16,6 +16,8 @@
  */
 package org.apache.solr.response;
 
+import static 
org.apache.solr.schema.FieldType.ExternalizeStoredValuesAsObjects;
+
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -55,7 +57,22 @@ import org.apache.solr.search.SolrReturnFields;
 
 /** This streams SolrDocuments from a DocList and applies transformer */
 public class DocsStreamer implements Iterator<SolrDocument> {
-  public static final Set<Class<? extends FieldType>> KNOWN_TYPES = new 
HashSet<>();
+  /**
+   * A hardcoded list of known Solr field types that will be trusted to 
control their own conversion
+   * of stored field values into external Objects (via {@link 
FieldType#toObject}) when returning
+   * {@link SolrDocument} instances to clients.
+   *
+   * <p>For historic reasons, this Set is consulted using an <em>equality</em> 
basis, so subclasses
+   * of these "known" types are not given the same level of trust.
+   *
+   * <p>Any field type not found in this list will have stored values 
externalized as
+   * <em>Strings</em> using {@link FieldType#toExternal} unless they implement 
{@link
+   * ExternalizeStoredValuesAsObjects}
+   *
+   * @deprecated new field types should not be added to this list, instead use 
{@link
+   *     ExternalizeStoredValuesAsObjects}
+   */
+  @Deprecated public static final Set<Class<? extends FieldType>> KNOWN_TYPES 
= new HashSet<>();
 
   private final org.apache.solr.response.ResultContext rctx;
   private final SolrDocumentFetcher docFetcher; // a collaborator of 
SolrIndexSearcher
@@ -200,7 +217,8 @@ public class DocsStreamer implements Iterator<SolrDocument> 
{
         return f.stringValue();
       }
     } else {
-      if (KNOWN_TYPES.contains(ft.getClass())) {
+      if (KNOWN_TYPES.contains(ft.getClass())
+          || ft instanceof FieldType.ExternalizeStoredValuesAsObjects) {
         return ft.toObject(f);
       } else {
         return ft.toExternal(f);
@@ -209,6 +227,9 @@ public class DocsStreamer implements Iterator<SolrDocument> 
{
   }
 
   static {
+    // DO NOT ADD TO THIS SET ! ! ! !
+    // SEE JAVADOCS FOR KNOWN_TYPES !
+
     KNOWN_TYPES.add(BoolField.class);
     KNOWN_TYPES.add(StrField.class);
     KNOWN_TYPES.add(TextField.class);
@@ -229,5 +250,8 @@ public class DocsStreamer implements Iterator<SolrDocument> 
{
     KNOWN_TYPES.add(DatePointField.class);
     // We do not add UUIDField because UUID object is not a supported type in 
JavaBinCodec
     // and if we write UUIDField.toObject, we wouldn't know how to handle it 
in the client side
+
+    // DO NOT ADD TO THIS SET ! ! ! !
+    // SEE JAVADOCS FOR KNOWN_TYPES !
   }
 }
diff --git a/solr/core/src/java/org/apache/solr/schema/BinaryField.java 
b/solr/core/src/java/org/apache/solr/schema/BinaryField.java
index 1659e51f870..db05597d75f 100644
--- a/solr/core/src/java/org/apache/solr/schema/BinaryField.java
+++ b/solr/core/src/java/org/apache/solr/schema/BinaryField.java
@@ -96,6 +96,11 @@ public class BinaryField extends FieldType {
     return ByteBuffer.wrap(bytes.bytes, bytes.offset, bytes.length);
   }
 
+  @Override
+  public Object toObject(SchemaField sf, BytesRef term) {
+    return BytesRef.deepCopyOf(term).bytes;
+  }
+
   @Override
   public IndexableField createField(SchemaField field, Object val) {
     if (val == null) return null;
diff --git a/solr/core/src/java/org/apache/solr/schema/FieldType.java 
b/solr/core/src/java/org/apache/solr/schema/FieldType.java
index 2f922473e50..15387038bba 100644
--- a/solr/core/src/java/org/apache/solr/schema/FieldType.java
+++ b/solr/core/src/java/org/apache/solr/schema/FieldType.java
@@ -366,6 +366,10 @@ public abstract class FieldType extends FieldProperties {
   /**
    * Convert the stored-field format to an external (string, human readable) 
value
    *
+   * <p>This is the default method used for converting a stored field value 
into an external value
+   * to be returned to clients. See {@link ExternalizeStoredValuesAsObjects} 
for more details
+   *
+   * @see #toObject(IndexableField)
    * @see #toInternal
    */
   public String toExternal(IndexableField f) {
@@ -383,6 +387,10 @@ public abstract class FieldType extends FieldProperties {
   /**
    * Convert the stored-field format to an external object.
    *
+   * <p>This method is not typically used for custom FieldTypes, see {@link
+   * ExternalizeStoredValuesAsObjects} for more details
+   *
+   * @see #toExternal
    * @see #toInternal
    * @since solr 1.3
    */
@@ -1460,6 +1468,22 @@ public abstract class FieldType extends FieldProperties {
     return new BytesRef(bytes);
   }
 
+  /**
+   * A marker interface that can be implemented by any FieldType to indicate 
that Solr should trust
+   * &amp; delegate to this field type's implementation of {@link
+   * FieldType#toObject(IndexableField)} when converted internal stored fields 
to an external
+   * representation that will be returned to clients.
+   *
+   * <p>The default behavior if this interface is not implemented, is to 
delegate to {@link
+   * FieldType#toExternal(IndexableField)}, unless the field type is (exactly 
equal to) one of a
+   * specific list of {@link org.apache.solr.response.DocsStreamer#KNOWN_TYPES}
+   *
+   * @see #toExternal
+   * @see #toObject(IndexableField)
+   * @see org.apache.solr.response.DocsStreamer#KNOWN_TYPES
+   */
+  public static interface ExternalizeStoredValuesAsObjects {}
+
   /**
    * An enumeration representing various options that may exist for selecting 
a single value from a
    * multivalued field. This class is designed to be an abstract 
representation, agnostic of some of
diff --git a/solr/core/src/java/org/apache/solr/search/SolrDocumentFetcher.java 
b/solr/core/src/java/org/apache/solr/search/SolrDocumentFetcher.java
index d1a3f3ddef9..6c31193014b 100644
--- a/solr/core/src/java/org/apache/solr/search/SolrDocumentFetcher.java
+++ b/solr/core/src/java/org/apache/solr/search/SolrDocumentFetcher.java
@@ -634,7 +634,7 @@ public class SolrDocumentFetcher {
       case BINARY:
         BinaryDocValues bdv = e.getBinaryDocValues(localId, leafReader, 
readerOrd);
         if (bdv != null) {
-          return BytesRef.deepCopyOf(bdv.binaryValue()).bytes;
+          return e.schemaField.getType().toObject(e.schemaField, 
bdv.binaryValue());
         }
         return null;
       case SORTED:
diff --git 
a/solr/core/src/test-files/solr/collection1/conf/schema-binaryfield.xml 
b/solr/core/src/test-files/solr/collection1/conf/schema-binaryfield.xml
index 51c2b26a832..0046ecd8fd1 100644
--- a/solr/core/src/test-files/solr/collection1/conf/schema-binaryfield.xml
+++ b/solr/core/src/test-files/solr/collection1/conf/schema-binaryfield.xml
@@ -28,15 +28,23 @@
 <schema name="test" version="1.7">
 
   <fieldType name="binary" class="solr.BinaryField"/>
+  <fieldType name="binary_rev" class="solr.SwapBytesBinaryField" />
+  <fieldType name="binary_str" class="solr.StrBinaryField" />
+  
   <fieldType name="boolean" class="solr.BoolField" sortMissingLast="true"/>
   <fieldType name="string" class="solr.StrField" sortMissingLast="true"/>
 
   <field name="id" type="string" indexed="true" stored="true" 
multiValued="false" required="true"/>
+  
   <field name="data" type="binary" stored="true"/>
   <field name="data_dv" type="binary" stored="false" docValues="true" />
 
+  <field name="rev_data" type="binary_rev" stored="true"/>
+  <field name="rev_data_dv" type="binary_rev" stored="false" docValues="true" 
/>
+  
+  <field name="str_data" type="binary_str" stored="true"/>
+  <field name="str_data_dv" type="binary_str" stored="false" docValues="true" 
/>
 
   <uniqueKey>id</uniqueKey>
 
-
 </schema>
diff --git 
a/solr/core/src/test-files/solr/collection1/conf/schema-non-stored-docvalues.xml
 
b/solr/core/src/test-files/solr/collection1/conf/schema-non-stored-docvalues.xml
index aa9a3a9ea51..6a7987fa38d 100644
--- 
a/solr/core/src/test-files/solr/collection1/conf/schema-non-stored-docvalues.xml
+++ 
b/solr/core/src/test-files/solr/collection1/conf/schema-non-stored-docvalues.xml
@@ -28,6 +28,9 @@
   <fieldType name="date" class="${solr.tests.DateFieldType}" 
precisionStep="0"/>
   <fieldType name="enumField" class="solr.EnumFieldType" docValues="true" 
enumsConfig="enumsConfig.xml" enumName="severity"/>
 
+  <fieldType name="binary" class="solr.BinaryField" />
+  <fieldType name="binary_rev" class="solr.SwapBytesBinaryField" />
+  <fieldType name="binary_str" class="solr.StrBinaryField" />
 
   <field name="id" type="string" indexed="true" stored="true" 
multiValued="false" required="false"/>
 
@@ -65,6 +68,18 @@
   <dynamicField name="*_ls_dvo" multiValued="true" type="long" indexed="true" 
stored="false"/>
   <dynamicField name="*_dts_dvo" multiValued="true" type="date" indexed="true" 
stored="false"/>
 
+  <!-- binary fields (exclusively single valued) -->
+  <dynamicField name="*_bin" multiValued="false" type="binary" indexed="false" 
stored="true" docValues="false" />
+  <dynamicField name="*_bin_dv" multiValued="false" type="binary" 
indexed="false" stored="true" docValues="true" />
+  <dynamicField name="*_bin_dvo" multiValued="false" type="binary" 
indexed="false" stored="false" docValues="true" />
+  
+  <dynamicField name="*_rev_bin" multiValued="false" type="binary_rev" 
indexed="false" stored="true" docValues="false" />
+  <dynamicField name="*_rev_bin_dv" multiValued="false" type="binary_rev" 
indexed="false" stored="true" docValues="true" />
+  <dynamicField name="*_rev_bin_dvo" multiValued="false" type="binary_rev" 
indexed="false" stored="false" docValues="true" />
+  
+  <dynamicField name="*_str_bin" multiValued="false" type="binary_str" 
indexed="false" stored="true" docValues="false" />
+  <dynamicField name="*_str_bin_dv" multiValued="false" type="binary_str" 
indexed="false" stored="true" docValues="true" />
+  <dynamicField name="*_str_bin_dvo" multiValued="false" type="binary_str" 
indexed="false" stored="false" docValues="true" />
 
   <uniqueKey>id</uniqueKey>
 
diff --git a/solr/core/src/test/org/apache/solr/schema/StrBinaryField.java 
b/solr/core/src/test/org/apache/solr/schema/StrBinaryField.java
new file mode 100644
index 00000000000..adc2b8cee20
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/schema/StrBinaryField.java
@@ -0,0 +1,88 @@
+/*
+ * 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.schema;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.List;
+import org.apache.lucene.index.IndexableField;
+import org.apache.lucene.util.BytesRef;
+import org.apache.solr.response.TextResponseWriter;
+
+/**
+ * Custom binary field that is always Base64 stringified with external 
clients, using a special
+ * prefix
+ */
+public final class StrBinaryField extends BinaryField {
+  public static final String PREFIX = "CUSTOMPRE_";
+
+  @Override
+  public void write(TextResponseWriter writer, String name, IndexableField f) 
throws IOException {
+    writer.writeStr(name, toExternal(f), false);
+  }
+
+  @Override
+  public String toExternal(IndexableField f) {
+    return PREFIX + super.toExternal(f);
+  }
+
+  @Override
+  public Object toObject(SchemaField sf, BytesRef term) {
+    return PREFIX
+        + Base64.getEncoder()
+            .encodeToString(Arrays.copyOfRange(term.bytes, term.offset, 
term.offset + term.length));
+  }
+
+  @Override
+  public List<IndexableField> createFields(SchemaField field, Object val) {
+    if (val instanceof String valStr) {
+      if (valStr.startsWith(PREFIX)) {
+        return super.createFields(field, valStr.substring(PREFIX.length()));
+      }
+      throw new RuntimeException(
+          field.getName() + " values must be strings PREFIXED with " + PREFIX 
+ "; got: " + valStr);
+    }
+    throw new RuntimeException(
+        field.getName()
+            + " values must be STRINGS starting with "
+            + PREFIX
+            + "; got: "
+            + val.getClass());
+  }
+
+  @Override
+  public Object toNativeType(Object val) {
+    Object result = super.toNativeType(val);
+    if (result instanceof ByteBuffer buf) {
+      // Kludge because super doesn't give us access to it's method...
+      result =
+          new String(
+              Base64.getEncoder()
+                  .encode(
+                      ByteBuffer.wrap(
+                              buf.array(),
+                              buf.arrayOffset() + buf.position(),
+                              buf.limit() - buf.position())
+                          .array()),
+              StandardCharsets.ISO_8859_1);
+    }
+    return PREFIX + result.toString();
+  }
+}
diff --git 
a/solr/core/src/test/org/apache/solr/schema/SwapBytesBinaryField.java 
b/solr/core/src/test/org/apache/solr/schema/SwapBytesBinaryField.java
new file mode 100644
index 00000000000..55997dcfc21
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/schema/SwapBytesBinaryField.java
@@ -0,0 +1,88 @@
+/*
+ * 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.schema;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.index.IndexableField;
+import org.apache.lucene.util.BytesRef;
+
+/**
+ * Custom binary field that internaly reverses the btes, but if all the I/O 
layers of Solr work
+ * correctly, clients should never know
+ */
+public final class SwapBytesBinaryField extends BinaryField
+    implements FieldType.ExternalizeStoredValuesAsObjects {
+
+  public static byte[] copyAndReverse(final byte[] array, final int offset, 
final int length) {
+    final byte[] result = new byte[length];
+    for (int i = 0; i < length; i++) {
+      result[i] = array[offset + length - 1 - i];
+    }
+    return result;
+  }
+
+  public static ByteBuffer copyAndReverse(final ByteBuffer in) {
+    return ByteBuffer.wrap(
+        copyAndReverse(in.array(), in.arrayOffset() + in.position(), 
in.remaining()));
+  }
+
+  public static BytesRef copyAndReverse(final BytesRef in) {
+    return new BytesRef(copyAndReverse(in.bytes, in.offset, in.length));
+  }
+
+  @Override
+  public ByteBuffer toObject(IndexableField f) {
+    return copyAndReverse(super.toObject(f));
+  }
+
+  @Override
+  public Object toObject(SchemaField sf, BytesRef term) {
+    return copyAndReverse(term).bytes;
+  }
+
+  /**
+   * This is kludgy, but since BinaryField doesn't let us override 
"getBytesRef(Object)", this is
+   * the simplest place for us to reverse things after super has generated 
fields with binary values
+   * for us.
+   */
+  @Override
+  public List<IndexableField> createFields(SchemaField field, Object val) {
+    final List<IndexableField> results = super.createFields(field, val);
+    for (IndexableField indexable : results) {
+      if (indexable instanceof Field f) {
+        if (null != f.binaryValue()) {
+          f.setBytesValue(copyAndReverse(f.binaryValue()));
+        }
+      } else {
+        throw new RuntimeException(
+            "WTF: test is broken by unexpected type of IndexableField from 
super");
+      }
+    }
+    return results;
+  }
+
+  @Override
+  public Object toNativeType(Object val) {
+    Object result = super.toNativeType(val);
+    if (result instanceof ByteBuffer originalBuf) {
+      result = copyAndReverse(originalBuf);
+    }
+    return result;
+  }
+}
diff --git a/solr/core/src/test/org/apache/solr/schema/TestBinaryField.java 
b/solr/core/src/test/org/apache/solr/schema/TestBinaryField.java
index 0c830249ae0..759856dd8ae 100644
--- a/solr/core/src/test/org/apache/solr/schema/TestBinaryField.java
+++ b/solr/core/src/test/org/apache/solr/schema/TestBinaryField.java
@@ -20,7 +20,11 @@ import java.nio.ByteBuffer;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.SolrTestCaseJ4.SuppressSSL;
 import org.apache.solr.client.solrj.SolrClient;
@@ -34,6 +38,7 @@ import org.apache.solr.util.SolrJettyTestRule;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
 
+/** Test "binary" based fields using SolrJ and javabn codec (with and w/o bean 
mapping) */
 @SuppressSSL(bugUrl = "https://issues.apache.org/jira/browse/SOLR-5776";)
 public class TestBinaryField extends SolrTestCaseJ4 {
 
@@ -56,97 +61,116 @@ public class TestBinaryField extends SolrTestCaseJ4 {
     solrTestRule.startSolr(homeDir);
   }
 
+  /**
+   * @see TestUseDocValuesAsStored#testBinary
+   */
   public void testSimple() throws Exception {
     try (SolrClient client = solrTestRule.getSolrClient()) {
       byte[] buf = new byte[10];
       for (int i = 0; i < 10; i++) {
         buf[i] = (byte) i;
       }
+
+      final Map<String, byte[]> expected = new HashMap<>();
+
       SolrInputDocument doc = null;
       doc = new SolrInputDocument();
-      doc.addField("id", 1);
+      doc.addField("id", "1");
+      expected.put("1", Arrays.copyOfRange(buf, 2, (2 + 5)));
       doc.addField("data", ByteBuffer.wrap(buf, 2, 5));
       doc.addField("data_dv", ByteBuffer.wrap(buf, 2, 5));
+      doc.addField("rev_data", ByteBuffer.wrap(buf, 2, 5));
+      doc.addField("rev_data_dv", ByteBuffer.wrap(buf, 2, 5));
+      doc.addField("str_data", prefix_base64(Arrays.copyOfRange(buf, 2, (2 + 
5))));
+      doc.addField("str_data_dv", prefix_base64(Arrays.copyOfRange(buf, 2, (2 
+ 5))));
       client.add(doc);
 
       doc = new SolrInputDocument();
-      doc.addField("id", 2);
+      doc.addField("id", "2");
+      expected.put("2", Arrays.copyOfRange(buf, 4, (4 + 3)));
       doc.addField("data", ByteBuffer.wrap(buf, 4, 3));
       doc.addField("data_dv", ByteBuffer.wrap(buf, 4, 3));
+      doc.addField("rev_data", ByteBuffer.wrap(buf, 4, 3));
+      doc.addField("rev_data_dv", ByteBuffer.wrap(buf, 4, 3));
+      doc.addField("str_data", prefix_base64(Arrays.copyOfRange(buf, 4, (4 + 
3))));
+      doc.addField("str_data_dv", prefix_base64(Arrays.copyOfRange(buf, 4, (4 
+ 3))));
       client.add(doc);
 
       doc = new SolrInputDocument();
-      doc.addField("id", 3);
+      doc.addField("id", "3");
+      expected.put("3", Arrays.copyOf(buf, buf.length));
       doc.addField("data", buf);
       doc.addField("data_dv", buf);
+      doc.addField("rev_data", buf);
+      doc.addField("rev_data_dv", buf);
+      doc.addField("str_data", prefix_base64(buf));
+      doc.addField("str_data_dv", prefix_base64(buf));
       client.add(doc);
 
       client.commit();
 
-      QueryResponse resp = client.query(new SolrQuery("*:*").setFields("id", 
"data", "data_dv"));
+      QueryResponse resp =
+          client.query(
+              new SolrQuery("*:*")
+                  .setFields(
+                      "id",
+                      "data",
+                      "data_dv",
+                      "rev_data",
+                      "rev_data_dv",
+                      "str_data",
+                      "str_data_dv"));
       SolrDocumentList res = resp.getResults();
       List<Bean> beans = resp.getBeans(Bean.class);
       assertEquals(3, res.size());
       assertEquals(3, beans.size());
+
       for (SolrDocument d : res) {
-        int id = Integer.parseInt(d.getFieldValue("id").toString());
-        for (String field : new String[] {"data", "data_dv"}) {
-          byte[] data = (byte[]) d.getFieldValue(field);
-          if (id == 1) {
-            assertEquals(5, data.length);
-            for (int i = 0; i < data.length; i++) {
-              byte b = data[i];
-              assertEquals((byte) (i + 2), b);
-            }
-
-          } else if (id == 2) {
-            assertEquals(3, data.length);
-            for (int i = 0; i < data.length; i++) {
-              byte b = data[i];
-              assertEquals((byte) (i + 4), b);
-            }
-
-          } else if (id == 3) {
-            assertEquals(10, data.length);
-            for (int i = 0; i < data.length; i++) {
-              byte b = data[i];
-              assertEquals((byte) i, b);
-            }
-          }
-        }
+        final String id = d.getFieldValue("id").toString();
+        assertTrue("Unexpected id: " + id, expected.containsKey(id));
+        final byte[] expected_bytes = expected.get(id);
+        final String expected_string = prefix_base64(expected_bytes);
+
+        assertArrayEquals(expected_bytes, (byte[]) d.getFieldValue("data"));
+        assertArrayEquals(expected_bytes, (byte[]) d.getFieldValue("data_dv"));
+
+        assertArrayEquals(expected_bytes, (byte[]) 
d.getFieldValue("rev_data"));
+        assertArrayEquals(expected_bytes, (byte[]) 
d.getFieldValue("rev_data_dv"));
+
+        assertEquals(expected_string, d.getFieldValue("str_data"));
+        assertEquals(expected_string, d.getFieldValue("str_data_dv"));
       }
       for (Bean d : beans) {
-        int id = Integer.parseInt(d.id);
-        for (byte[] data : new byte[][] {d.data, d.data_dv}) {
-          if (id == 1) {
-            assertEquals(5, data.length);
-            for (int i = 0; i < data.length; i++) {
-              byte b = data[i];
-              assertEquals((byte) (i + 2), b);
-            }
-
-          } else if (id == 2) {
-            assertEquals(3, data.length);
-            for (int i = 0; i < data.length; i++) {
-              byte b = data[i];
-              assertEquals((byte) (i + 4), b);
-            }
-
-          } else if (id == 3) {
-            assertEquals(10, data.length);
-            for (int i = 0; i < data.length; i++) {
-              byte b = data[i];
-              assertEquals((byte) i, b);
-            }
-          }
-        }
+        assertTrue("Unexpected id: " + d.id, expected.containsKey(d.id));
+        final byte[] expected_bytes = expected.get(d.id);
+        final String expected_string = prefix_base64(expected_bytes);
+
+        assertArrayEquals(expected_bytes, d.data);
+        assertArrayEquals(expected_bytes, d.data_dv);
+
+        assertArrayEquals(expected_bytes, d.rev_data);
+        assertArrayEquals(expected_bytes, d.rev_data_dv);
+
+        assertEquals(expected_string, d.str_data);
+        assertEquals(expected_string, d.str_data_dv);
       }
     }
   }
 
+  /**
+   * @see StrBinaryField
+   */
+  public static String prefix_base64(final byte[] val) {
+    return StrBinaryField.PREFIX + Base64.getEncoder().encodeToString(val);
+  }
+
   public static class Bean {
     @Field String id;
     @Field byte[] data;
     @Field byte[] data_dv;
+    @Field byte[] rev_data;
+    @Field byte[] rev_data_dv;
+    @Field String str_data;
+    @Field String str_data_dv;
   }
 }
diff --git 
a/solr/core/src/test/org/apache/solr/schema/TestUseDocValuesAsStored.java 
b/solr/core/src/test/org/apache/solr/schema/TestUseDocValuesAsStored.java
index 59c40f16edc..296684404de 100644
--- a/solr/core/src/test/org/apache/solr/schema/TestUseDocValuesAsStored.java
+++ b/solr/core/src/test/org/apache/solr/schema/TestUseDocValuesAsStored.java
@@ -16,7 +16,9 @@
  */
 package org.apache.solr.schema;
 
+import java.io.IOException;
 import java.io.InputStream;
+import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -25,6 +27,7 @@ import java.time.LocalDateTime;
 import java.time.Month;
 import java.time.ZoneOffset;
 import java.util.Arrays;
+import java.util.Base64;
 import java.util.HashSet;
 import java.util.Set;
 import java.util.regex.Pattern;
@@ -34,11 +37,19 @@ import javax.xml.xpath.XPath;
 import javax.xml.xpath.XPathConstants;
 import javax.xml.xpath.XPathFactory;
 import org.apache.commons.io.file.PathUtils;
+import org.apache.lucene.index.BinaryDocValues;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.IndexableField;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.index.StoredFields;
 import org.apache.lucene.tests.mockfile.FilterPath;
 import org.apache.lucene.tests.util.TestUtil;
+import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.IOUtils;
 import org.apache.solr.common.util.DOMUtil;
 import org.apache.solr.core.AbstractBadConfigTestBase;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.search.SolrIndexSearcher;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -46,7 +57,11 @@ import org.w3c.dom.Document;
 import org.w3c.dom.NodeList;
 import org.xml.sax.InputSource;
 
-/** Tests the useDocValuesAsStored functionality. */
+/**
+ * Tests the useDocValuesAsStored functionality.
+ *
+ * <p>This test uses XML/json (text writer) based responses.
+ */
 public class TestUseDocValuesAsStored extends AbstractBadConfigTestBase {
 
   private int id = 1;
@@ -241,6 +256,96 @@ public class TestUseDocValuesAsStored extends 
AbstractBadConfigTestBase {
     }
   }
 
+  /**
+   * @see TestBinaryField
+   * @see #testInternallyReversedBytesBinary
+   * @see #testExternallyPrefixedStringifiedBinary
+   */
+  public void testBinary() throws Exception {
+
+    // When indexing: BinaryField can parse base64 encoded string values
+    // In XML/json output: response values will also be base64 encoded
+    final String data = Base64.getEncoder().encodeToString(new byte[] {0, 0, 
1, 2, 3});
+
+    doTest("binary stored only", "foo_bin", "str", data);
+    doTest("binary stored+DV", "foo_bin_dv", "str", data);
+    doTest("binary DV only", "foo_bin_dvo", "str", data);
+
+    // sanity check our low level internal values
+    final BytesRef expectedInternalValue = new BytesRef(new byte[] {0, 0, 1, 
2, 3});
+    assertAllInternalBinaryValuesMatch("foo_bin", expectedInternalValue);
+    assertAllInternalBinaryValuesMatch("foo_bin_dv", expectedInternalValue);
+    assertAllInternalBinaryValuesMatch("foo_bin_dvo", expectedInternalValue);
+  }
+
+  /**
+   * @see TestBinaryField
+   * @see #testBinary
+   * @see #testInternallyReversedBytesBinary
+   */
+  public void testExternallyPrefixedStringifiedBinary() throws Exception {
+
+    // This field type *enforces* base64 encoded binary data, with a custom 
prefix
+    final String data =
+        StrBinaryField.PREFIX + Base64.getEncoder().encodeToString(new byte[] 
{0, 0, 1, 2, 3});
+
+    doTest("(str) binary stored only", "foo_str_bin", "str", data);
+    doTest("(str) binary stored+DV", "foo_str_bin_dv", "str", data);
+    doTest("(str) binary DV only", "foo_str_bin_dvo", "str", data);
+
+    // sanity check our low level internal values
+    final BytesRef expectedInternalValue = new BytesRef(new byte[] {0, 0, 1, 
2, 3});
+    assertAllInternalBinaryValuesMatch("foo_str_bin", expectedInternalValue);
+    assertAllInternalBinaryValuesMatch("foo_str_bin_dv", 
expectedInternalValue);
+    assertAllInternalBinaryValuesMatch("foo_str_bin_dvo", 
expectedInternalValue);
+  }
+
+  /**
+   * @see TestBinaryField
+   * @see #testBinary
+   * @see #testExternallyPrefixedStringifiedBinary
+   */
+  public void testInternallyReversedBytesBinary() throws Exception {
+
+    // When indexing: BinaryField can parse base64 encoded string values
+    // In XML/json output: response values will also be base64 encoded
+    final String data = Base64.getEncoder().encodeToString(new byte[] {0, 0, 
1, 2, 3});
+
+    doTest("(rev) binary stored only", "foo_rev_bin", "str", data);
+    doTest("(rev) binary stored+DV", "foo_rev_bin_dv", "str", data);
+    doTest("(rev) binary DV only", "foo_rev_bin_dvo", "str", data);
+
+    // sanity check our low level internal values
+    final BytesRef expectedInternalValue = new BytesRef(new byte[] {3, 2, 1, 
0, 0});
+    assertAllInternalBinaryValuesMatch("foo_rev_bin", expectedInternalValue);
+    assertAllInternalBinaryValuesMatch("foo_rev_bin_dv", 
expectedInternalValue);
+    assertAllInternalBinaryValuesMatch("foo_rev_bin_dvo", 
expectedInternalValue);
+  }
+
+  public void testSanityCheckOfSwapBytesBinaryField() throws Exception {
+    final byte[] expected = new byte[] {1, 2, 3, 4, 5};
+
+    assertArrayEquals(
+        expected, SwapBytesBinaryField.copyAndReverse(new byte[] {5, 4, 3, 2, 
1}, 0, 5));
+    assertArrayEquals(
+        expected, SwapBytesBinaryField.copyAndReverse(new byte[] {9, 5, 4, 3, 
2, 1, 0}, 1, 5));
+
+    assertEquals(
+        ByteBuffer.wrap(expected),
+        SwapBytesBinaryField.copyAndReverse(ByteBuffer.wrap(new byte[] {5, 4, 
3, 2, 1})));
+    assertEquals(
+        ByteBuffer.wrap(expected),
+        SwapBytesBinaryField.copyAndReverse(
+            ByteBuffer.wrap(new byte[] {9, 5, 4, 3, 2, 1, 0}, 1, 5)));
+
+    assertEquals(
+        new BytesRef(expected),
+        SwapBytesBinaryField.copyAndReverse(new BytesRef(new byte[] {5, 4, 3, 
2, 1})));
+    assertEquals(
+        new BytesRef(expected),
+        SwapBytesBinaryField.copyAndReverse(new BytesRef(new byte[] {9, 5, 4, 
3, 2, 1, 0}, 1, 5)));
+  }
+
   private String plural(int arity) {
     return arity > 1 ? "s" : "";
   }
@@ -540,4 +645,31 @@ public class TestUseDocValuesAsStored extends 
AbstractBadConfigTestBase {
         "/response/docs/[0]/test_mvt_dvu_st_str/[1]==aaaa",
         "/response/docs/[0]/test_mvt_dvu_st_str/[2]==bbbb");
   }
+
+  /**
+   * A fairly niche helper method that asserts all binary doc values and/or 
stored values in the
+   * specified field (regardless of doc id) all match an explicitly expected 
value.
+   */
+  private final void assertAllInternalBinaryValuesMatch(
+      final String fieldName, final BytesRef expected) throws IOException {
+    try (SolrCore core = h.getCoreInc()) {
+      final IndexReader top = 
core.withSearcher(SolrIndexSearcher::getIndexReader);
+      final StoredFields stored = top.storedFields();
+      for (int id = 0; id < top.maxDoc(); id++) {
+        final IndexableField storedValue = 
stored.document(id).getField(fieldName);
+        // we might be called on a non-stored field, but if it is stored it 
better be binary
+        if (null != storedValue) {
+          assertEquals(expected, storedValue.binaryValue());
+        }
+      }
+      for (LeafReaderContext context : top.leaves()) {
+        // Note: explicitly avoid DocValues.getBinary(...) to bypass type 
check.
+        // For our purposes, we're fine with NONE if the field type isn't 
using docValues at all
+        final BinaryDocValues dv = 
context.reader().getBinaryDocValues(fieldName);
+        while (null != dv && dv.nextDoc() < BinaryDocValues.NO_MORE_DOCS) {
+          assertEquals(expected, dv.binaryValue());
+        }
+      }
+    }
+  }
 }

Reply via email to