Skip to content
Merged
4 changes: 2 additions & 2 deletions solr/core/src/java/org/apache/solr/core/SolrCore.java
Original file line number Diff line number Diff line change
Expand Up @@ -3033,9 +3033,9 @@ public static void postDecorateResponse(
+ "'");
}
if (echoParams == EchoParamStyle.EXPLICIT) {
responseHeader.add("params", req.getOriginalParams().toNamedList());
responseHeader.add("params", new SimpleOrderedMap<>(req.getOriginalParams()));
} else if (echoParams == EchoParamStyle.ALL) {
responseHeader.add("params", req.getParams().toNamedList());
responseHeader.add("params", new SimpleOrderedMap<>(req.getParams()));
}
}
}
Expand Down
12 changes: 6 additions & 6 deletions solr/core/src/java/org/apache/solr/core/SolrXmlConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
Expand Down Expand Up @@ -126,12 +127,11 @@ public static NodeConfig fromConfig(

// It should go inside the fillSolrSection method but
// since it is arranged as a separate section it is placed here
Map<String, String> coreAdminHandlerActions =
readNodeListAsNamedList(root.get("coreAdminHandlerActions"), "<coreAdminHandlerActions>")
.asShallowMap()
.entrySet()
.stream()
.collect(Collectors.toMap(Entry::getKey, item -> item.getValue().toString()));
Map<String, String> coreAdminHandlerActions = new LinkedHashMap<>();
for (Entry<String, Object> entry :
readNodeListAsNamedList(root.get("coreAdminHandlerActions"), "<coreAdminHandlerActions>")) {
coreAdminHandlerActions.put(entry.getKey(), entry.getValue().toString());
}

UpdateShardHandlerConfig updateConfig;
if (deprecatedUpdateConfig == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public class DumpRequestHandler extends RequestHandlerBase {
@SuppressWarnings({"unchecked"})
public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws IOException {
// Show params
rsp.add("params", req.getParams().toNamedList());
rsp.add("params", new SimpleOrderedMap<>(req.getParams()));
String[] parts = req.getParams().getParams("urlTemplateValues");
if (parts != null && parts.length > 0) {
Map<String, String> map = new LinkedHashMap<>();
Expand Down
Comment thread
dsmiley marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -455,10 +455,10 @@ protected void mergeIds(ResponseBuilder rb, ShardRequest sreq) {
populateNextCursorMarkFromMergedShards(rb);

if (thereArePartialResults) {
rb.rsp
.getResponseHeader()
.asShallowMap()
.put(SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY, Boolean.TRUE);
updateResponseHeader(
rb.rsp.getResponseHeader(),
SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY,
Boolean.TRUE);
}
if (segmentTerminatedEarly != null) {
final Object existingSegmentTerminatedEarly =
Expand All @@ -472,12 +472,10 @@ protected void mergeIds(ResponseBuilder rb, ShardRequest sreq) {
SolrQueryResponse.RESPONSE_HEADER_SEGMENT_TERMINATED_EARLY_KEY,
segmentTerminatedEarly);
} else if (!Boolean.TRUE.equals(existingSegmentTerminatedEarly) && segmentTerminatedEarly) {
rb.rsp
.getResponseHeader()
.remove(SolrQueryResponse.RESPONSE_HEADER_SEGMENT_TERMINATED_EARLY_KEY);
rb.rsp
.getResponseHeader()
.add(SolrQueryResponse.RESPONSE_HEADER_SEGMENT_TERMINATED_EARLY_KEY, true);
updateResponseHeader(
rb.rsp.getResponseHeader(),
SolrQueryResponse.RESPONSE_HEADER_SEGMENT_TERMINATED_EARLY_KEY,
true);
}
}
if (maxHitsTerminatedEarly) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1237,10 +1237,10 @@ protected void mergeIds(ResponseBuilder rb, ShardRequest sreq) {
populateNextCursorMarkFromMergedShards(rb);

if (thereArePartialResults) {
rb.rsp
.getResponseHeader()
.asShallowMap()
.put(SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY, Boolean.TRUE);
updateResponseHeader(
rb.rsp.getResponseHeader(),
SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY,
Boolean.TRUE);
}
if (segmentTerminatedEarly != null) {
final Object existingSegmentTerminatedEarly =
Expand All @@ -1255,14 +1255,10 @@ protected void mergeIds(ResponseBuilder rb, ShardRequest sreq) {
segmentTerminatedEarly);
} else if (!Boolean.TRUE.equals(existingSegmentTerminatedEarly)
&& Boolean.TRUE.equals(segmentTerminatedEarly)) {
rb.rsp
.getResponseHeader()
.remove(SolrQueryResponse.RESPONSE_HEADER_SEGMENT_TERMINATED_EARLY_KEY);
rb.rsp
.getResponseHeader()
.add(
SolrQueryResponse.RESPONSE_HEADER_SEGMENT_TERMINATED_EARLY_KEY,
segmentTerminatedEarly);
updateResponseHeader(
rb.rsp.getResponseHeader(),
SolrQueryResponse.RESPONSE_HEADER_SEGMENT_TERMINATED_EARLY_KEY,
segmentTerminatedEarly);
}
}
if (maxHitsTerminatedEarly) {
Expand All @@ -1284,6 +1280,11 @@ protected void mergeIds(ResponseBuilder rb, ShardRequest sreq) {
}
}

@SuppressWarnings("unchecked")
protected static void updateResponseHeader(NamedList<Object> header, String key, Object value) {
((SimpleOrderedMap<Object>) header).put(key, value);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you do this? I much prefer it as it was (assuming it worked)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It didn't work, that's why -- I reverted it. getResponseHeader()'s documented contract is NamedList<Object>, not SimpleOrderedMap. QueryComponentPartialResultsTest's mock stubs getResponseHeader() to return a plain NamedList via Mockito, fully within that contract, and the cast threw ClassCastException there. remove()/add() works regardless of the actual runtime type.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sigh... that really sucks because we're IMO making the code worse on the account of a test matter. I wonder how easy it might be to "just" change the return types of SolrQueryResponse in its own PR separately from this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #4809 -- tightens getResponseHeader()/addResponseHeader() to SimpleOrderedMap<Object>, which lets these call sites use put() directly. It's binary-incompatible for external callers on the old NamedList signature (verified with a NoSuchMethodError repro), flagged in the PR description since this is long-standing public API -- your call there.

@dsmiley

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you :-)

In the mean time: Couldn't we update our mocks to return a SimpleOrderedMap? Just because the "documented contract" is a NamedList, doesn't mean Solr truly needs to support a plain NamedList from these methods on SolrQueryResponse. SQR is the base implementation; subclasses add to the instances returned by the base; don't replace / override (I think).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done -- fixed MockResponseBuilder to return a real SimpleOrderedMap, restored the cast+put() (verified: QueryComponentPartialResultsTest passes without needing the return-type change from #4809 at all). That PR can stand on its own merits now rather than being a blocker here.

protected void setResultIdsAndResponseDocs(
ResponseBuilder rb,
ShardDocQueue shardDocQueue,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,7 @@ public ManagedIndexSchema adaptExistingFieldToData(
fieldProps.add("multiValued", true);
fieldProps.remove("name");
fieldProps.remove("type");
schema =
schema.replaceField(
schemaField.getName(), schemaField.getType(), fieldProps.asShallowMap());
schema = schema.replaceField(schemaField.getName(), schemaField.getType(), fieldProps);
}
// TODO: other "healing" type operations here ... but we have to be careful about overriding
// explicit user changes such as a user making a text field a string field, we wouldn't want to
Expand Down
38 changes: 37 additions & 1 deletion solr/core/src/java/org/apache/solr/jersey/SolrJacksonMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
import jakarta.ws.rs.ext.ContextResolver;
import jakarta.ws.rs.ext.Provider;
import java.io.IOException;
import java.util.Map;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.SimpleOrderedMap;

/** Customizes the ObjectMapper settings used for serialization/deserialization in Jersey */
@SuppressWarnings("rawtypes")
Expand All @@ -48,6 +50,7 @@ public static ObjectMapper getObjectMapper() {
private static ObjectMapper createObjectMapper() {
final SimpleModule customTypeModule = new SimpleModule();
customTypeModule.addSerializer(new NamedListSerializer(NamedList.class));
customTypeModule.addSerializer(new SimpleOrderedMapSerializer(SimpleOrderedMap.class));

return new ObjectMapper()
// TODO Should failOnUnknown=false be made available on a "permissive" object mapper instead
Expand All @@ -70,7 +73,40 @@ public NamedListSerializer(Class<NamedList> nlClazz) {
@Override
public void serialize(NamedList value, JsonGenerator gen, SerializerProvider provider)
throws IOException {
gen.writeObject(value.asShallowMap());
// SimpleOrderedMap goes through SimpleOrderedMapSerializer below instead. asMap(0) avoids
// recursing into this same serializer for plain NamedLists.
gen.writeObject(value.asMap(0));
}
}

/**
* Writes a {@link SimpleOrderedMap} out directly via its {@link Map} entries, without the copy
* {@link NamedListSerializer} needs to dodge infinite recursion -- {@link SimpleOrderedMap} is
* already a {@link Map}, so there is nothing to convert.
*/
public static class SimpleOrderedMapSerializer extends StdSerializer<SimpleOrderedMap> {

public SimpleOrderedMapSerializer() {
this(null);
}

public SimpleOrderedMapSerializer(Class<SimpleOrderedMap> somClazz) {
super(somClazz);
}

@Override
@SuppressWarnings("unchecked")
public void serialize(SimpleOrderedMap value, JsonGenerator gen, SerializerProvider provider)
throws IOException {
final Map<String, Object> map = (Map<String, Object>) value;
gen.writeStartObject();
for (Map.Entry<String, Object> entry : map.entrySet()) {
// defaultSerializeField() doesn't honor NON_NULL inclusion itself -- skip nulls here.
if (entry.getValue() != null) {
provider.defaultSerializeField(entry.getKey(), entry.getValue(), gen);
}
}
gen.writeEndObject();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -278,24 +278,24 @@ public Map<String, SolrPackageInstance> getPackagesDeployedAsClusterLevelPlugins
Map<String, String> packageVersions = new HashMap<>();
// map of package name to multiple values of pluginMeta(Map<String, String>)
Map<String, Set<PluginMeta>> packagePlugins = new HashMap<>();
Map<String, Object> result;
Object pluginsValue;
try {
NamedList<Object> response =
solrClient.request(
new GenericV2SolrRequest(SolrRequest.METHOD.GET, PackageUtils.CLUSTERPROPS_PATH));
Integer statusCode = (Integer) response._get(List.of("responseHeader", "status"), null);
if (statusCode == null || statusCode == ErrorCode.NOT_FOUND.code) {
// Cluster props doesn't exist, that means there are no cluster level plugins installed.
result = Map.of();
pluginsValue = null;
} else {
result = response.asShallowMap();
pluginsValue = response.get(ContainerPluginsApi.PLUGIN);
}
} catch (SolrServerException | IOException ex) {
throw new SolrException(ErrorCode.SERVER_ERROR, ex);
}
@SuppressWarnings({"unchecked"})
Map<String, Object> clusterPlugins =
(Map<String, Object>) result.getOrDefault(ContainerPluginsApi.PLUGIN, Map.of());
pluginsValue != null ? (Map<String, Object>) pluginsValue : Map.of();
for (Map.Entry<String, Object> entry : clusterPlugins.entrySet()) {
PluginMeta pluginMeta;
try {
Expand Down Expand Up @@ -421,16 +421,14 @@ private Pair<List<String>, List<String>> deployCollectionPackage(

// Get package params
try {
boolean packageParamsExist =
solrClient
.request(
new GenericV2SolrRequest(
SolrRequest.METHOD.GET,
PackageUtils.getCollectionParamsPath(collection) + "/packages")
.setRequiresCollection(
false) /* Making a collection-request, but already baked into path */)
.asShallowMap()
.containsKey("params");
NamedList<Object> collectionParams =
solrClient.request(
new GenericV2SolrRequest(
SolrRequest.METHOD.GET,
PackageUtils.getCollectionParamsPath(collection) + "/packages")
.setRequiresCollection(
false) /* Making a collection-request, but already baked into path */);
boolean packageParamsExist = collectionParams.get("params") != null;
SolrCLI.postJsonToSolr(
solrClient,
PackageUtils.getCollectionParamsPath(collection),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ public static IndexFingerprint fromObject(Object o) {
if (o instanceof Map) {
map = (Map<String, Object>) o;
} else if (o instanceof NamedList) {
map = ((NamedList<Object>) o).asShallowMap();
map = new SimpleOrderedMap<>((NamedList<Object>) o);
} else {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Unknown type " + o);
}
Expand Down
3 changes: 2 additions & 1 deletion solr/core/src/java/org/apache/solr/util/PivotListEntry.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ public <T> T extract(NamedList<T> pivotList) {
}
// otherwise...
// scan starting at the min/optional index
return pivotList.get(this.getName(), this.minIndex);
final int idx = pivotList.indexOf(this.getName(), this.minIndex);
return idx == -1 ? null : pivotList.getVal(idx);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import java.util.List;
import org.apache.solr.common.params.ShardParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.SimpleOrderedMap;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.schema.IndexSchema;
Expand Down Expand Up @@ -48,7 +48,7 @@ public static MockResponseBuilder create() {
SchemaField uniqueIdField = new SchemaField("id", new StrField());

// we need this because QueryComponent adds a property to it.
NamedList<Object> responseHeader = new NamedList<>();
SimpleOrderedMap<Object> responseHeader = new SimpleOrderedMap<>();

// the mock implementations
Mockito.when(request.getSchema()).thenReturn(indexSchema);
Expand Down

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* 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.jersey;

import static org.hamcrest.Matchers.equalTo;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.SimpleOrderedMap;
import org.junit.Test;

/** Unit tests for {@link SolrJacksonMapper}'s NamedList/SimpleOrderedMap serialization. */
public class SolrJacksonMapperTest extends SolrTestCaseJ4 {

@Test
public void testSimpleOrderedMapSerializesDirectlyWithoutRecursing() throws Exception {
final SimpleOrderedMap<Object> top = new SimpleOrderedMap<>();
top.add("status", 0);

final NamedList<Object> nestedPlainNamedList = new NamedList<>();
nestedPlainNamedList.add("nestedKey", "nestedVal");
top.add("nested_plain_namedlist", nestedPlainNamedList);

final SimpleOrderedMap<Object> nestedSimpleOrderedMap = new SimpleOrderedMap<>();
nestedSimpleOrderedMap.add("innerKey", 42);
top.add("nested_simple_ordered_map", nestedSimpleOrderedMap);

final ObjectMapper mapper = SolrJacksonMapper.getObjectMapper();
final String json = mapper.writeValueAsString(top);

assertThat(
json,
equalTo(
"{\"status\":0,"
+ "\"nested_plain_namedlist\":{\"nestedKey\":\"nestedVal\"},"
+ "\"nested_simple_ordered_map\":{\"innerKey\":42}}"));
}

@Test
public void testPlainNamedListStillSerializesViaAsMap() throws Exception {
final NamedList<Object> namedList = new NamedList<>();
namedList.add("key", "value");

final ObjectMapper mapper = SolrJacksonMapper.getObjectMapper();
final String json = mapper.writeValueAsString(namedList);

assertThat(json, equalTo("{\"key\":\"value\"}"));
}

@Test
public void testSimpleOrderedMapOmitsNullValuesLikeNamedListDoes() throws Exception {
final NamedList<Object> namedListWithNull = new NamedList<>();
namedListWithNull.add("present", "val");
namedListWithNull.add("absent", null);

final SimpleOrderedMap<Object> somWithNull = new SimpleOrderedMap<>();
somWithNull.add("present", "val");
somWithNull.add("absent", null);

final ObjectMapper mapper = SolrJacksonMapper.getObjectMapper();
assertThat(mapper.writeValueAsString(namedListWithNull), equalTo("{\"present\":\"val\"}"));
assertThat(mapper.writeValueAsString(somWithNull), equalTo("{\"present\":\"val\"}"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.IOUtils;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.SimpleOrderedMap;
import org.apache.solr.embedded.JettySolrRunner;
import org.junit.AfterClass;
import org.junit.BeforeClass;
Expand Down Expand Up @@ -499,7 +500,7 @@ private void assertFacetSKGsAreCorrect(
assertEquals(
"Unexpected keys in facet response",
expectedKeys,
actualFacetResponse.asShallowMap().keySet());
new SimpleOrderedMap<>(actualFacetResponse).keySet());
}
}

Expand Down
4 changes: 2 additions & 2 deletions solr/core/src/test/org/apache/solr/util/TestUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ public void testNamedLists() {
assertEquals("one", map.getName(0));
map.setName(0, "ONE");
assertEquals("ONE", map.getName(0));
assertEquals(Integer.valueOf(100), map.get("one", 1));
assertEquals(Integer.valueOf(100), map.getVal(map.indexOf("one", 1)));
assertEquals(4, map.indexOf(null, 1));
assertNull(map.get(null, 1));
assertNull(map.getVal(map.indexOf(null, 1)));

map = new SimpleOrderedMap<>();
map.add("one", 1);
Expand Down
Loading
Loading