Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions changelog/unreleased/SOLR-17316-response-parsers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc

title: >
SolrJ's QueryResponse and other response objects now work when the client is configured with a
non-binary response parser (such as the JSON parser); previously their accessors could throw a
ClassCastException.
type: fixed
authors:
- name: Serhiy Bzhezytskyy
links:
- name: SOLR-17316
url: https://issues.apache.org/jira/browse/SOLR-17316
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.util.ContentStream;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.ResponseNormalizer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -229,6 +230,9 @@ protected NamedList<Object> processErrorsAndResponse(
NamedList<Object> rsp;
try {
rsp = processor.processResponse(is, encoding);
if (!processor.producesCanonicalForm()) {
rsp = ResponseNormalizer.normalize(rsp);
}
} catch (Exception e) {
throw new RemoteSolrException(urlExceptionMessage, httpStatus, e.getMessage(), e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,9 @@ protected TokenInfo buildTokenInfo(NamedList<?> tokenNL) {
String text = (String) tokenNL.get("text");
String rawText = (String) tokenNL.get("rawText");
String type = (String) tokenNL.get("type");
int start = (Integer) tokenNL.get("start");
int end = (Integer) tokenNL.get("end");
int position = (Integer) tokenNL.get("position");
int start = ((Number) tokenNL.get("start")).intValue();
int end = ((Number) tokenNL.get("end")).intValue();
int position = ((Number) tokenNL.get("position")).intValue();
Boolean match = (Boolean) tokenNL.get("match");
return new TokenInfo(
text, rawText, type, start, end, position, (match == null ? false : match));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ public void read(NamedList<Object> nl) {
} else if ("docs".equals(entry.getKey())) {
docs = ((Number) entry.getValue()).longValue();
} else if ("distinct".equals(entry.getKey())) {
distinct = (Integer) entry.getValue();
distinct = ((Number) entry.getValue()).intValue();
} else if ("cacheableFaceting".equals(entry.getKey())) {
cacheableFaceting = (Boolean) entry.getValue();
} else if ("topTerms".equals(entry.getKey())) {
Expand Down Expand Up @@ -290,7 +290,8 @@ public Long getNumDocs() {

public Integer getMaxDoc() {
if (indexInfo == null) return null;
return (Integer) indexInfo.get("maxDoc");
Object v = indexInfo.get("maxDoc");
return v == null ? null : ((Number) v).intValue();
}

public Long getDeletedDocs() {
Expand All @@ -299,7 +300,8 @@ public Long getDeletedDocs() {

public Integer getNumTerms() {
if (indexInfo == null) return null;
return (Integer) indexInfo.get("numTerms");
Object v = indexInfo.get("numTerms");
return v == null ? null : ((Number) v).intValue();
}

public Map<String, FieldTypeInfo> getFieldTypeInfo() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,11 +248,11 @@ private void extractGroupedInfo(NamedList<Object> info) {
}

if (oGroups != null) {
Integer iMatches = (Integer) oMatches;
int iMatches = ((Number) oMatches).intValue();
ArrayList<Object> groupsArr = (ArrayList<Object>) oGroups;
GroupCommand groupedCommand;
if (oNGroups != null) {
Integer iNGroups = (Integer) oNGroups;
int iNGroups = ((Number) oNGroups).intValue();
groupedCommand = new GroupCommand(fieldName, iMatches, iNGroups);
} else {
groupedCommand = new GroupCommand(fieldName, iMatches);
Expand All @@ -269,10 +269,10 @@ private void extractGroupedInfo(NamedList<Object> info) {

_groupResponse.add(groupedCommand);
} else if (queryCommand != null) {
Integer iMatches = (Integer) oMatches;
int iMatches = ((Number) oMatches).intValue();
GroupCommand groupCommand;
if (oNGroups != null) {
Integer iNGroups = (Integer) oNGroups;
int iNGroups = ((Number) oNGroups).intValue();
groupCommand = new GroupCommand(fieldName, iMatches, iNGroups);
} else {
groupCommand = new GroupCommand(fieldName, iMatches);
Expand Down Expand Up @@ -354,7 +354,9 @@ private void extractFacetInfo(NamedList<Object> info) {
List<IntervalFacet.Count> counts =
new ArrayList<IntervalFacet.Count>(intervalField.getValue().size());
for (Map.Entry<String, Object> interval : intervalField.getValue()) {
counts.add(new IntervalFacet.Count(interval.getKey(), (Integer) interval.getValue()));
counts.add(
new IntervalFacet.Count(
interval.getKey(), ((Number) interval.getValue()).intValue()));
}
_intervalFacets.add(new IntervalFacet(field, counts));
}
Expand Down Expand Up @@ -433,7 +435,7 @@ protected List<PivotField> readPivots(List<NamedList> list) {
switch (key) {
case "field" -> field = (String) val;
case "value" -> value = val;
case "count" -> count = ((Integer) val).intValue();
case "count" -> count = ((Number) val).intValue();
case "pivot" -> {
assert null != val : "Server sent back 'null' for sub pivots?";
assert val instanceof List : "Server sent non-List for sub pivots?";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,17 @@ public abstract NamedList<Object> processResponse(InputStream body, String encod
* @return the MIME types that this parser is capable of parsing. Never null.
*/
public abstract Set<String> getContentTypes();

/**
* Whether this parser already produces the canonical response shape the SolrJ response classes
* expect: a {@link NamedList} tree with {@link org.apache.solr.common.SolrDocumentList} for
* document sections. The binary and XML parsers do; a parser that yields raw {@code Map}s and
* {@code List}s (such as the JSON map parser) does not, and its output is normalized before the
* response classes read it.
*
* @return true unless the parser yields a raw, un-typed structure
*/
public boolean producesCanonicalForm() {
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ public NamedList<?> getResponseHeader() {
public int getStatus() {
NamedList<?> header = getResponseHeader();
if (header != null) {
return (Integer) header.get("status");
// ResponseParsers vary in the numeric type they produce (e.g. JSON yields Long), so widen
// via Number rather than casting to Integer. See SOLR-17316.
return ((Number) header.get("status")).intValue();
} else {
return 0;
}
Expand All @@ -101,7 +103,7 @@ public int getStatus() {
public int getQTime() {
NamedList<?> header = getResponseHeader();
if (header != null) {
return (Integer) header.get("QTime");
return ((Number) header.get("QTime")).intValue();
} else {
return 0;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,10 @@ public Suggestion(String token, NamedList<Object> suggestion) {
suggestion.forEach(
(n, val) -> {
switch (n) {
case "numFound" -> numFound = (Integer) val;
case "startOffset" -> startOffset = (Integer) val;
case "endOffset" -> endOffset = (Integer) val;
case "origFreq" -> originalFrequency = (Integer) val;
case "numFound" -> numFound = ((Number) val).intValue();
case "startOffset" -> startOffset = ((Number) val).intValue();
case "endOffset" -> endOffset = ((Number) val).intValue();
case "origFreq" -> originalFrequency = ((Number) val).intValue();
case "suggestion" -> {
List<?> list = (List<?>) val;
if (!list.isEmpty() && list.get(0) instanceof NamedList) {
Expand All @@ -157,7 +157,7 @@ public Suggestion(String token, NamedList<Object> suggestion) {
alternativeFrequencies = new ArrayList<>();
for (NamedList<?> nl : extended) {
alternatives.add((String) nl.get("word"));
alternativeFrequencies.add((Integer) nl.get("freq"));
alternativeFrequencies.add(((Number) nl.get("freq")).intValue());
}
} else {
@SuppressWarnings("unchecked")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,9 @@ public NamedList<Object> processResponse(InputStream body, String encoding) thro
public Set<String> getContentTypes() {
return CONTENT_TYPES;
}

@Override
public boolean producesCanonicalForm() {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ private static String getSchemaName(@SuppressWarnings({"rawtypes"}) Map schemaNa
}

private static Float getSchemaVersion(@SuppressWarnings({"rawtypes"}) Map schemaNamedList) {
return (Float) schemaNamedList.get("version");
Object v = schemaNamedList.get("version");
return v == null ? null : ((Number) v).floatValue();
}

private static String getSchemaUniqueKey(@SuppressWarnings({"rawtypes"}) Map schemaNamedList) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* 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.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;

/**
* Converts a parsed response into the canonical shape the SolrJ response classes expect (the shape
* the binary and XML parsers produce): nested JSON objects become {@link NamedList}s and a {@code
* {numFound, docs}} object becomes a {@link SolrDocumentList}.
*
* <p>Only unambiguous, self-describing conversions are performed. It is a no-op for values already
* in canonical form (so binary/XML responses pass through unchanged). It does not attempt to
* interpret the ambiguous flat arrays produced by {@code json.nl=flat}; a typed JSON parser should
* request {@code json.nl=map} for its own reads.
*/
public final class ResponseNormalizer {

private ResponseNormalizer() {}

/** Returns a normalized copy of the given response NamedList. */
public static NamedList<Object> normalize(NamedList<Object> response) {
if (response == null) {
return null;
}
SimpleOrderedMap<Object> out = new SimpleOrderedMap<>(response.size());
for (Map.Entry<String, Object> e : response) {
out.add(e.getKey(), normalizeValue(e.getValue()));
}
return out;
}

@SuppressWarnings("unchecked")
private static Object normalizeValue(Object val) {
if (val instanceof SolrDocumentList || val instanceof SolrDocument) {
// Already canonical (binary/XML produce these directly); leave untouched. Must precede the
// List/Map branches since SolrDocumentList is a List and SolrDocument is a Map.
return val;
} else if (val instanceof NamedList) {
// Already canonical (binary/XML), but its children may still need normalizing.
NamedList<Object> in = (NamedList<Object>) val;
SimpleOrderedMap<Object> out = new SimpleOrderedMap<>(in.size());
for (Map.Entry<String, Object> e : in) {
out.add(e.getKey(), normalizeValue(e.getValue()));
}
return out;
} else if (val instanceof Map) {
Map<String, Object> m = (Map<String, Object>) val;
if (isDocList(m)) {
return toDocList(m);
}
// SimpleOrderedMap (not plain NamedList): it is the canonical map type the binary parser
// produces, and some response extractors cast to it specifically.
SimpleOrderedMap<Object> out = new SimpleOrderedMap<>(m.size());
for (Map.Entry<String, Object> e : m.entrySet()) {
out.add(e.getKey(), normalizeValue(e.getValue()));
}
return out;
} else if (val instanceof List) {
List<Object> in = (List<Object>) val;
List<Object> out = new ArrayList<>(in.size());
for (Object item : in) {
out.add(normalizeValue(item));
}
return out;
}
return val;
}

private static boolean isDocList(Map<String, Object> m) {
return m.get("numFound") instanceof Number && m.get("docs") instanceof List;
}

@SuppressWarnings("unchecked")
private static SolrDocumentList toDocList(Map<String, Object> m) {
SolrDocumentList docs = new SolrDocumentList();
docs.setNumFound(((Number) m.get("numFound")).longValue());
if (m.get("start") instanceof Number start) {
docs.setStart(start.longValue());
}
if (m.get("maxScore") instanceof Number maxScore) {
docs.setMaxScore(maxScore.floatValue());
}
if (m.get("numFoundExact") instanceof Boolean exact) {
docs.setNumFoundExact(exact);
}
for (Object d : (List<Object>) m.get("docs")) {
docs.add(toDoc(d));
}
return docs;
}

@SuppressWarnings("unchecked")
private static SolrDocument toDoc(Object o) {
SolrDocument doc = new SolrDocument();
if (o instanceof Map) {
for (Map.Entry<String, Object> f : ((Map<String, Object>) o).entrySet()) {
// setField (not addField): addField unwraps a Collection value into a plain list, which
// would drop the type of a reconstructed SolrDocumentList held as a field value.
doc.setField(f.getKey(), normalizeValue(f.getValue()));
}
}
return doc;
}
}
Loading