From 88121297871b000f5daad9d4415260484863c490 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 14:50:00 +0000 Subject: [PATCH 01/13] Initial plan From 42395bc1ba7e5f5683503cf792f98a645cc36f82 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 15:26:45 +0000 Subject: [PATCH 02/13] Migrate SchemaDesignerAPI to JAX-RS V2 annotations Co-authored-by: epugh <22395+epugh@users.noreply.github.com> --- .../api/endpoint/SchemaDesignerApi.java | 182 ++++++ .../org/apache/solr/core/CoreContainer.java | 2 +- .../handler/designer/SchemaDesignerAPI.java | 347 ++++++----- .../designer/TestSchemaDesignerAPI.java | 574 ++++++------------ 4 files changed, 551 insertions(+), 554 deletions(-) create mode 100644 solr/api/src/java/org/apache/solr/client/api/endpoint/SchemaDesignerApi.java diff --git a/solr/api/src/java/org/apache/solr/client/api/endpoint/SchemaDesignerApi.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/SchemaDesignerApi.java new file mode 100644 index 000000000000..295a77f2d70a --- /dev/null +++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/SchemaDesignerApi.java @@ -0,0 +1,182 @@ +/* + * 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.client.api.endpoint; + +import static org.apache.solr.client.api.util.Constants.RAW_OUTPUT_PROPERTY; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.extensions.Extension; +import io.swagger.v3.oas.annotations.extensions.ExtensionProperty; +import jakarta.ws.rs.DefaultValue; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.StreamingOutput; +import java.util.List; +import org.apache.solr.client.api.model.FlexibleSolrJerseyResponse; +import org.apache.solr.client.api.model.SolrJerseyResponse; + +/** V2 API definitions for the Solr Schema Designer. */ +@Path("/schema-designer") +public interface SchemaDesignerApi { + + @GET + @Path("/info") + @Operation( + summary = "Get info about a configSet being designed.", + tags = {"schema-designer"}) + FlexibleSolrJerseyResponse getInfo(@QueryParam("configSet") String configSet) throws Exception; + + @POST + @Path("/prep") + @Operation( + summary = "Prepare a mutable configSet copy for schema design.", + tags = {"schema-designer"}) + FlexibleSolrJerseyResponse prepNewSchema( + @QueryParam("configSet") String configSet, @QueryParam("copyFrom") String copyFrom) + throws Exception; + + @PUT + @Path("/cleanup") + @Operation( + summary = "Clean up temporary resources for a schema being designed.", + tags = {"schema-designer"}) + SolrJerseyResponse cleanupTempSchema(@QueryParam("configSet") String configSet) throws Exception; + + @GET + @Path("/file") + @Operation( + summary = "Get the contents of a file in a configSet being designed.", + tags = {"schema-designer"}) + FlexibleSolrJerseyResponse getFileContents( + @QueryParam("configSet") String configSet, @QueryParam("file") String file) throws Exception; + + @POST + @Path("/file") + @Operation( + summary = "Update the contents of a file in a configSet being designed.", + tags = {"schema-designer"}) + FlexibleSolrJerseyResponse updateFileContents( + @QueryParam("configSet") String configSet, @QueryParam("file") String file) throws Exception; + + @GET + @Path("/sample") + @Operation( + summary = "Get a sample value and analysis for a field.", + tags = {"schema-designer"}) + FlexibleSolrJerseyResponse getSampleValue( + @QueryParam("configSet") String configSet, + @QueryParam("field") String fieldName, + @QueryParam("uniqueKeyField") String idField, + @QueryParam("docId") String docId) + throws Exception; + + @GET + @Path("/collectionsForConfig") + @Operation( + summary = "List collections that use a given configSet.", + tags = {"schema-designer"}) + FlexibleSolrJerseyResponse listCollectionsForConfig(@QueryParam("configSet") String configSet) + throws Exception; + + @GET + @Path("/configs") + @Operation( + summary = "List all configSets available for schema design.", + tags = {"schema-designer"}) + FlexibleSolrJerseyResponse listConfigs() throws Exception; + + @GET + @Path("/download") + @Operation( + summary = "Download a configSet as a ZIP archive.", + tags = {"schema-designer"}, + extensions = { + @Extension(properties = {@ExtensionProperty(name = RAW_OUTPUT_PROPERTY, value = "true")}) + }) + @Produces("application/zip") + StreamingOutput downloadConfig(@QueryParam("configSet") String configSet) throws Exception; + + @POST + @Path("/add") + @Operation( + summary = "Add a new field, field type, or dynamic field to the schema being designed.", + tags = {"schema-designer"}) + FlexibleSolrJerseyResponse addSchemaObject( + @QueryParam("configSet") String configSet, @QueryParam("schemaVersion") Integer schemaVersion) + throws Exception; + + @PUT + @Path("/update") + @Operation( + summary = "Update an existing field or field type in the schema being designed.", + tags = {"schema-designer"}) + FlexibleSolrJerseyResponse updateSchemaObject( + @QueryParam("configSet") String configSet, @QueryParam("schemaVersion") Integer schemaVersion) + throws Exception; + + @PUT + @Path("/publish") + @Operation( + summary = "Publish the designed schema to a live configSet.", + tags = {"schema-designer"}) + FlexibleSolrJerseyResponse publish( + @QueryParam("configSet") String configSet, + @QueryParam("schemaVersion") Integer schemaVersion, + @QueryParam("newCollection") String newCollection, + @QueryParam("reloadCollections") @DefaultValue("false") Boolean reloadCollections, + @QueryParam("numShards") @DefaultValue("1") Integer numShards, + @QueryParam("replicationFactor") @DefaultValue("1") Integer replicationFactor, + @QueryParam("indexToCollection") @DefaultValue("false") Boolean indexToCollection, + @QueryParam("cleanupTemp") @DefaultValue("true") Boolean cleanupTempParam, + @QueryParam("disableDesigner") @DefaultValue("false") Boolean disableDesigner) + throws Exception; + + @POST + @Path("/analyze") + @Operation( + summary = "Analyze sample documents and suggest a schema.", + tags = {"schema-designer"}) + FlexibleSolrJerseyResponse analyze( + @QueryParam("configSet") String configSet, + @QueryParam("schemaVersion") Integer schemaVersion, + @QueryParam("copyFrom") String copyFrom, + @QueryParam("uniqueKeyField") String uniqueKeyField, + @QueryParam("languages") List languages, + @QueryParam("enableDynamicFields") Boolean enableDynamicFields, + @QueryParam("enableFieldGuessing") Boolean enableFieldGuessing, + @QueryParam("enableNestedDocs") Boolean enableNestedDocs) + throws Exception; + + @GET + @Path("/query") + @Operation( + summary = "Query the temporary collection used during schema design.", + tags = {"schema-designer"}) + FlexibleSolrJerseyResponse query(@QueryParam("configSet") String configSet) throws Exception; + + @GET + @Path("/diff") + @Operation( + summary = "Get the diff between the designed schema and the published schema.", + tags = {"schema-designer"}) + FlexibleSolrJerseyResponse getSchemaDiff(@QueryParam("configSet") String configSet) + throws Exception; +} diff --git a/solr/core/src/java/org/apache/solr/core/CoreContainer.java b/solr/core/src/java/org/apache/solr/core/CoreContainer.java index f6bf1dfb36ab..f57e5029d200 100644 --- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java +++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java @@ -869,7 +869,7 @@ private void loadInternal() { registerV2ApiIfEnabled(clusterAPI.commands); if (isZooKeeperAware()) { - registerV2ApiIfEnabled(new SchemaDesignerAPI(this)); + registerV2ApiIfEnabled(SchemaDesignerAPI.class); } // else Schema Designer not available in standalone (non-cloud) mode /* diff --git a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java index 426ec449cb45..7a12f4d1f9ad 100644 --- a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java +++ b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java @@ -17,14 +17,13 @@ package org.apache.solr.handler.designer; -import static org.apache.solr.client.solrj.SolrRequest.METHOD.GET; -import static org.apache.solr.client.solrj.SolrRequest.METHOD.POST; -import static org.apache.solr.client.solrj.SolrRequest.METHOD.PUT; import static org.apache.solr.common.params.CommonParams.JSON_MIME; import static org.apache.solr.handler.admin.ConfigSetsHandler.DEFAULT_CONFIGSET_NAME; import static org.apache.solr.security.PermissionNameProvider.Name.CONFIG_EDIT_PERM; import static org.apache.solr.security.PermissionNameProvider.Name.CONFIG_READ_PERM; +import jakarta.inject.Inject; +import jakarta.ws.rs.core.StreamingOutput; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -49,7 +48,10 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import org.apache.solr.api.EndPoint; +import org.apache.solr.api.JerseyResource; +import org.apache.solr.client.api.endpoint.SchemaDesignerApi; +import org.apache.solr.client.api.model.FlexibleSolrJerseyResponse; +import org.apache.solr.client.api.model.SolrJerseyResponse; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.impl.CloudSolrClient; import org.apache.solr.client.solrj.request.CollectionAdminRequest; @@ -66,16 +68,14 @@ import org.apache.solr.common.cloud.ZkMaintenanceUtils; import org.apache.solr.common.cloud.ZkStateReader; import org.apache.solr.common.util.ContentStream; -import org.apache.solr.common.util.ContentStreamBase; import org.apache.solr.common.util.NamedList; import org.apache.solr.common.util.SimpleOrderedMap; import org.apache.solr.common.util.StrUtils; import org.apache.solr.core.CoreContainer; import org.apache.solr.core.SolrConfig; import org.apache.solr.core.SolrResourceLoader; +import org.apache.solr.jersey.PermissionName; import org.apache.solr.request.SolrQueryRequest; -import org.apache.solr.response.RawResponseWriter; -import org.apache.solr.response.SolrQueryResponse; import org.apache.solr.schema.ManagedIndexSchema; import org.apache.solr.schema.SchemaField; import org.apache.solr.util.RTimer; @@ -86,7 +86,8 @@ import org.slf4j.LoggerFactory; /** All V2 APIs have a prefix of /api/schema-designer/ */ -public class SchemaDesignerAPI implements SchemaDesignerConstants { +public class SchemaDesignerAPI extends JerseyResource + implements SchemaDesignerApi, SchemaDesignerConstants { private static final Set excludeConfigSetNames = Set.of(DEFAULT_CONFIGSET_NAME); @@ -98,21 +99,26 @@ public class SchemaDesignerAPI implements SchemaDesignerConstants { private final SchemaDesignerSettingsDAO settingsDAO; private final SchemaDesignerConfigSetHelper configSetHelper; private final Map indexedVersion = new ConcurrentHashMap<>(); + private final SolrQueryRequest solrQueryRequest; - public SchemaDesignerAPI(CoreContainer coreContainer) { + @Inject + public SchemaDesignerAPI(CoreContainer coreContainer, SolrQueryRequest solrQueryRequest) { this( coreContainer, SchemaDesignerAPI.newSchemaSuggester(), - SchemaDesignerAPI.newSampleDocumentsLoader()); + SchemaDesignerAPI.newSampleDocumentsLoader(), + solrQueryRequest); } SchemaDesignerAPI( CoreContainer coreContainer, SchemaSuggester schemaSuggester, - SampleDocumentsLoader sampleDocLoader) { + SampleDocumentsLoader sampleDocLoader, + SolrQueryRequest solrQueryRequest) { this.coreContainer = coreContainer; this.schemaSuggester = schemaSuggester; this.sampleDocLoader = sampleDocLoader; + this.solrQueryRequest = solrQueryRequest; this.configSetHelper = new SchemaDesignerConfigSetHelper(this.coreContainer, this.schemaSuggester); this.settingsDAO = new SchemaDesignerSettingsDAO(coreContainer, configSetHelper); @@ -146,9 +152,10 @@ static String getMutableId(final String configSet) { return DESIGNER_PREFIX + configSet; } - @EndPoint(method = GET, path = "/schema-designer/info", permission = CONFIG_READ_PERM) - public void getInfo(SolrQueryRequest req, SolrQueryResponse rsp) throws IOException { - final String configSet = getRequiredParam(CONFIG_SET_PARAM, req); + @Override + @PermissionName(CONFIG_READ_PERM) + public FlexibleSolrJerseyResponse getInfo(String configSet) throws Exception { + requireNotEmpty(CONFIG_SET_PARAM, configSet); Map responseMap = new HashMap<>(); responseMap.put(CONFIG_SET_PARAM, configSet); @@ -178,16 +185,19 @@ public void getInfo(SolrQueryRequest req, SolrQueryResponse rsp) throws IOExcept log.warn("Failed to load sample docs from blob store for {}", configSet, exc); } - rsp.getValues().addAll(responseMap); + return buildFlexibleResponse(responseMap); } - @EndPoint(method = POST, path = "/schema-designer/prep", permission = CONFIG_EDIT_PERM) - public void prepNewSchema(SolrQueryRequest req, SolrQueryResponse rsp) - throws IOException, SolrServerException { - final String configSet = getRequiredParam(CONFIG_SET_PARAM, req); + @Override + @PermissionName(CONFIG_EDIT_PERM) + public FlexibleSolrJerseyResponse prepNewSchema(String configSet, String copyFrom) + throws Exception { + requireNotEmpty(CONFIG_SET_PARAM, configSet); validateNewConfigSetName(configSet); - final String copyFrom = req.getParams().get(COPY_FROM_PARAM, DEFAULT_CONFIGSET_NAME); + if (copyFrom == null) { + copyFrom = DEFAULT_CONFIGSET_NAME; + } SchemaDesignerSettings settings = getMutableSchemaForConfigSet(configSet, -1, copyFrom); ManagedIndexSchema schema = settings.getSchema(); @@ -201,19 +211,23 @@ public void prepNewSchema(SolrQueryRequest req, SolrQueryResponse rsp) settingsDAO.persistIfChanged(mutableId, settings); - rsp.getValues().addAll(buildResponse(configSet, schema, settings, null)); + return buildFlexibleResponse(buildResponse(configSet, schema, settings, null)); } - @EndPoint(method = PUT, path = "/schema-designer/cleanup", permission = CONFIG_EDIT_PERM) - public void cleanupTemp(SolrQueryRequest req, SolrQueryResponse rsp) - throws IOException, SolrServerException { - cleanupTemp(getRequiredParam(CONFIG_SET_PARAM, req)); + @Override + @PermissionName(CONFIG_EDIT_PERM) + public SolrJerseyResponse cleanupTempSchema(String configSet) throws Exception { + requireNotEmpty(CONFIG_SET_PARAM, configSet); + doCleanupTemp(configSet); + return instantiateJerseyResponse(SolrJerseyResponse.class); } - @EndPoint(method = GET, path = "/schema-designer/file", permission = CONFIG_READ_PERM) - public void getFileContents(SolrQueryRequest req, SolrQueryResponse rsp) throws IOException { - final String configSet = getRequiredParam(CONFIG_SET_PARAM, req); - final String file = getRequiredParam("file", req); + @Override + @PermissionName(CONFIG_READ_PERM) + public FlexibleSolrJerseyResponse getFileContents(String configSet, String file) + throws Exception { + requireNotEmpty(CONFIG_SET_PARAM, configSet); + requireNotEmpty("file", file); String filePath = getConfigSetZkPath(getMutableId(configSet), file); byte[] data; try { @@ -223,14 +237,15 @@ public void getFileContents(SolrQueryRequest req, SolrQueryResponse rsp) throws } String stringData = data != null && data.length > 0 ? new String(data, StandardCharsets.UTF_8) : ""; - rsp.getValues().addAll(Collections.singletonMap(file, stringData)); + return buildFlexibleResponse(Collections.singletonMap(file, stringData)); } - @EndPoint(method = POST, path = "/schema-designer/file", permission = CONFIG_EDIT_PERM) - public void updateFileContents(SolrQueryRequest req, SolrQueryResponse rsp) - throws IOException, SolrServerException { - final String configSet = getRequiredParam(CONFIG_SET_PARAM, req); - final String file = getRequiredParam("file", req); + @Override + @PermissionName(CONFIG_EDIT_PERM) + public FlexibleSolrJerseyResponse updateFileContents(String configSet, String file) + throws Exception { + requireNotEmpty(CONFIG_SET_PARAM, configSet); + requireNotEmpty("file", file); String mutableId = getMutableId(configSet); String zkPath = getConfigSetZkPath(mutableId, file); @@ -241,7 +256,7 @@ public void updateFileContents(SolrQueryRequest req, SolrQueryResponse rsp) } byte[] data; - try (InputStream in = extractSingleContentStream(req, true).getStream()) { + try (InputStream in = extractSingleContentStream(true).getStream()) { data = in.readAllBytes(); } Exception updateFileError = null; @@ -265,8 +280,7 @@ public void updateFileContents(SolrQueryRequest req, SolrQueryResponse rsp) Map response = new HashMap<>(); response.put("updateFileError", causedBy.getMessage()); response.put(file, new String(data, StandardCharsets.UTF_8)); - rsp.getValues().addAll(response); - return; + return buildFlexibleResponse(response); } // apply the update and reload the temp collection / re-index sample docs @@ -309,15 +323,16 @@ public void updateFileContents(SolrQueryRequest req, SolrQueryResponse rsp) response, "Failed to re-index sample documents after update to the " + file + " file"); - rsp.getValues().addAll(response); + return buildFlexibleResponse(response); } - @EndPoint(method = GET, path = "/schema-designer/sample", permission = CONFIG_READ_PERM) - public void getSampleValue(SolrQueryRequest req, SolrQueryResponse rsp) throws IOException { - final String configSet = getRequiredParam(CONFIG_SET_PARAM, req); - final String fieldName = getRequiredParam(FIELD_PARAM, req); - final String idField = getRequiredParam(UNIQUE_KEY_FIELD_PARAM, req); - String docId = req.getParams().get(DOC_ID_PARAM); + @Override + @PermissionName(CONFIG_READ_PERM) + public FlexibleSolrJerseyResponse getSampleValue( + String configSet, String fieldName, String idField, String docId) throws Exception { + requireNotEmpty(CONFIG_SET_PARAM, configSet); + requireNotEmpty(FIELD_PARAM, fieldName); + requireNotEmpty(UNIQUE_KEY_FIELD_PARAM, idField); final List docs = configSetHelper.retrieveSampleDocs(configSet); String textValue = null; @@ -348,27 +363,27 @@ public void getSampleValue(SolrQueryRequest req, SolrQueryResponse rsp) throws I if (textValue != null) { var analysis = configSetHelper.analyzeField(configSet, fieldName, textValue); - rsp.getValues().addAll(Map.of(idField, docId, fieldName, textValue, "analysis", analysis)); + return buildFlexibleResponse( + Map.of(idField, docId, fieldName, textValue, "analysis", analysis)); } + return instantiateJerseyResponse(FlexibleSolrJerseyResponse.class); } - @EndPoint( - method = GET, - path = "/schema-designer/collectionsForConfig", - permission = CONFIG_READ_PERM) - public void listCollectionsForConfig(SolrQueryRequest req, SolrQueryResponse rsp) { - final String configSet = getRequiredParam(CONFIG_SET_PARAM, req); - rsp.getValues() - .addAll( - Collections.singletonMap( - "collections", configSetHelper.listCollectionsForConfig(configSet))); + @Override + @PermissionName(CONFIG_READ_PERM) + public FlexibleSolrJerseyResponse listCollectionsForConfig(String configSet) throws Exception { + requireNotEmpty(CONFIG_SET_PARAM, configSet); + return buildFlexibleResponse( + Collections.singletonMap( + "collections", configSetHelper.listCollectionsForConfig(configSet))); } // CONFIG_EDIT_PERM is required here since this endpoint is used by the UI to determine if the // user has access to the Schema Designer UI - @EndPoint(method = GET, path = "/schema-designer/configs", permission = CONFIG_EDIT_PERM) - public void listConfigs(SolrQueryRequest req, SolrQueryResponse rsp) throws IOException { - rsp.getValues().addAll(Collections.singletonMap("configSets", listEnabledConfigs())); + @Override + @PermissionName(CONFIG_EDIT_PERM) + public FlexibleSolrJerseyResponse listConfigs() throws Exception { + return buildFlexibleResponse(Collections.singletonMap("configSets", listEnabledConfigs())); } protected Map listEnabledConfigs() throws IOException { @@ -387,9 +402,10 @@ protected Map listEnabledConfigs() throws IOException { return configs; } - @EndPoint(method = GET, path = "/schema-designer/download/*", permission = CONFIG_READ_PERM) - public void downloadConfig(SolrQueryRequest req, SolrQueryResponse rsp) throws IOException { - final String configSet = getRequiredParam(CONFIG_SET_PARAM, req); + @Override + @PermissionName(CONFIG_READ_PERM) + public StreamingOutput downloadConfig(String configSet) throws Exception { + requireNotEmpty(CONFIG_SET_PARAM, configSet); String mutableId = getMutableId(configSet); // find the configSet to download @@ -408,21 +424,19 @@ public void downloadConfig(SolrQueryRequest req, SolrQueryResponse rsp) throws I throw new IOException("Error reading config from ZK", SolrZkClient.checkInterrupted(e)); } - ContentStreamBase content = - new ContentStreamBase.ByteArrayStream( - configSetHelper.downloadAndZipConfigSet(configId), - configSet + ".zip", - "application/zip"); - rsp.add(RawResponseWriter.CONTENT, content); + final byte[] zipBytes = configSetHelper.downloadAndZipConfigSet(configId); + return outputStream -> outputStream.write(zipBytes); } - @EndPoint(method = POST, path = "/schema-designer/add", permission = CONFIG_EDIT_PERM) - public void addSchemaObject(SolrQueryRequest req, SolrQueryResponse rsp) - throws IOException, SolrServerException { - final String configSet = getRequiredParam(CONFIG_SET_PARAM, req); - final String mutableId = checkMutable(configSet, req); + @Override + @PermissionName(CONFIG_EDIT_PERM) + public FlexibleSolrJerseyResponse addSchemaObject(String configSet, Integer schemaVersion) + throws Exception { + requireNotEmpty(CONFIG_SET_PARAM, configSet); + requireSchemaVersion(schemaVersion); + final String mutableId = checkMutable(configSet, schemaVersion); - Map addJson = readJsonFromRequest(req); + Map addJson = readJsonFromRequest(); log.info("Adding new schema object from JSON: {}", addJson); String objectName = configSetHelper.addSchemaObject(configSet, addJson); @@ -432,17 +446,19 @@ public void addSchemaObject(SolrQueryRequest req, SolrQueryResponse rsp) Map response = buildResponse(configSet, schema, null, configSetHelper.retrieveSampleDocs(configSet)); response.put(action, objectName); - rsp.getValues().addAll(response); + return buildFlexibleResponse(response); } - @EndPoint(method = PUT, path = "/schema-designer/update", permission = CONFIG_EDIT_PERM) - public void updateSchemaObject(SolrQueryRequest req, SolrQueryResponse rsp) - throws IOException, SolrServerException { - final String configSet = getRequiredParam(CONFIG_SET_PARAM, req); - final String mutableId = checkMutable(configSet, req); + @Override + @PermissionName(CONFIG_EDIT_PERM) + public FlexibleSolrJerseyResponse updateSchemaObject(String configSet, Integer schemaVersion) + throws Exception { + requireNotEmpty(CONFIG_SET_PARAM, configSet); + requireSchemaVersion(schemaVersion); + final String mutableId = checkMutable(configSet, schemaVersion); // Updated field definition is in the request body as JSON - Map updateField = readJsonFromRequest(req); + Map updateField = readJsonFromRequest(); String name = (String) updateField.get("name"); if (StrUtils.isNullOrEmpty(name)) { throw new SolrException( @@ -504,14 +520,25 @@ public void updateSchemaObject(SolrQueryRequest req, SolrQueryResponse rsp) addErrorToResponse(mutableId, solrExc, errorsDuringIndexing, response, updateError); response.put("rebuild", needsRebuild); - rsp.getValues().addAll(response); + return buildFlexibleResponse(response); } - @EndPoint(method = PUT, path = "/schema-designer/publish", permission = CONFIG_EDIT_PERM) - public void publish(SolrQueryRequest req, SolrQueryResponse rsp) - throws IOException, SolrServerException { - final String configSet = getRequiredParam(CONFIG_SET_PARAM, req); - final String mutableId = checkMutable(configSet, req); + @Override + @PermissionName(CONFIG_EDIT_PERM) + public FlexibleSolrJerseyResponse publish( + String configSet, + Integer schemaVersion, + String newCollection, + Boolean reloadCollections, + Integer numShards, + Integer replicationFactor, + Boolean indexToCollection, + Boolean cleanupTempParam, + Boolean disableDesigner) + throws Exception { + requireNotEmpty(CONFIG_SET_PARAM, configSet); + requireSchemaVersion(schemaVersion); + final String mutableId = checkMutable(configSet, schemaVersion); // verify the configSet we're going to apply changes to hasn't been changed since being loaded // for @@ -534,7 +561,6 @@ public void publish(SolrQueryRequest req, SolrQueryResponse rsp) } } - String newCollection = req.getParams().get(NEW_COLLECTION_PARAM); if (StrUtils.isNotNullOrEmpty(newCollection) && zkStateReader().getClusterState().hasCollection(newCollection)) { throw new SolrException( @@ -555,7 +581,6 @@ && zkStateReader().getClusterState().hasCollection(newCollection)) { copyConfig(mutableId, configSet); } - boolean reloadCollections = req.getParams().getBool(RELOAD_COLLECTIONS_PARAM, false); if (reloadCollections) { log.debug("Reloading collections after update to configSet: {}", configSet); List collectionsForConfig = configSetHelper.listCollectionsForConfig(configSet); @@ -568,10 +593,8 @@ && zkStateReader().getClusterState().hasCollection(newCollection)) { // create new collection Map errorsDuringIndexing = null; if (StrUtils.isNotNullOrEmpty(newCollection)) { - int numShards = req.getParams().getInt("numShards", 1); - int rf = req.getParams().getInt("replicationFactor", 1); - configSetHelper.createCollection(newCollection, configSet, numShards, rf); - if (req.getParams().getBool(INDEX_TO_COLLECTION_PARAM, false)) { + configSetHelper.createCollection(newCollection, configSet, numShards, replicationFactor); + if (indexToCollection) { List docs = configSetHelper.retrieveSampleDocs(configSet); if (!docs.isEmpty()) { ManagedIndexSchema schema = loadLatestSchema(mutableId); @@ -581,16 +604,16 @@ && zkStateReader().getClusterState().hasCollection(newCollection)) { } } - if (req.getParams().getBool(CLEANUP_TEMP_PARAM, true)) { + if (cleanupTempParam) { try { - cleanupTemp(configSet); + doCleanupTemp(configSet); } catch (IOException | SolrServerException | SolrException exc) { final String excStr = exc.toString(); log.warn("Failed to clean-up temp collection {} due to: {}", mutableId, excStr); } } - settings.setDisabled(req.getParams().getBool(DISABLE_DESIGNER_PARAM, false)); + settings.setDisabled(disableDesigner); settingsDAO.persistIfChanged(configSet, settings); Map response = new HashMap<>(); @@ -602,14 +625,23 @@ && zkStateReader().getClusterState().hasCollection(newCollection)) { addErrorToResponse(newCollection, null, errorsDuringIndexing, response, null); - rsp.getValues().addAll(response); + return buildFlexibleResponse(response); } - @EndPoint(method = POST, path = "/schema-designer/analyze", permission = CONFIG_EDIT_PERM) - public void analyze(SolrQueryRequest req, SolrQueryResponse rsp) - throws IOException, SolrServerException { - final int schemaVersion = req.getParams().getInt(SCHEMA_VERSION_PARAM, -1); - final String configSet = getRequiredParam(CONFIG_SET_PARAM, req); + @Override + @PermissionName(CONFIG_EDIT_PERM) + public FlexibleSolrJerseyResponse analyze( + String configSet, + Integer schemaVersion, + String copyFrom, + String uniqueKeyField, + List languages, + Boolean enableDynamicFields, + Boolean enableFieldGuessing, + Boolean enableNestedDocs) + throws Exception { + final int schemaVersionInt = schemaVersion != null ? schemaVersion : -1; + requireNotEmpty(CONFIG_SET_PARAM, configSet); // don't let the user edit the _default configSet with the designer (for now) if (DEFAULT_CONFIGSET_NAME.equals(configSet)) { @@ -623,27 +655,25 @@ public void analyze(SolrQueryRequest req, SolrQueryResponse rsp) // Get the sample documents to analyze, preferring those in the request but falling back to // previously stored - SampleDocuments sampleDocuments = loadSampleDocuments(req, configSet); + SampleDocuments sampleDocuments = loadSampleDocuments(configSet); // Get a mutable "temp" schema either from the specified copy source or configSet if it already // exists. - String copyFrom = - configExists(configSet) - ? configSet - : req.getParams().get(COPY_FROM_PARAM, DEFAULT_CONFIGSET_NAME); + if (copyFrom == null) { + copyFrom = configExists(configSet) ? configSet : DEFAULT_CONFIGSET_NAME; + } String mutableId = getMutableId(configSet); // holds additional settings needed by the designer to maintain state SchemaDesignerSettings settings = - getMutableSchemaForConfigSet(configSet, schemaVersion, copyFrom); + getMutableSchemaForConfigSet(configSet, schemaVersionInt, copyFrom); ManagedIndexSchema schema = settings.getSchema(); - String uniqueKeyFieldParam = req.getParams().get(UNIQUE_KEY_FIELD_PARAM); - if (StrUtils.isNotNullOrEmpty(uniqueKeyFieldParam)) { - String uniqueKeyField = + if (StrUtils.isNotNullOrEmpty(uniqueKeyField)) { + String existingKeyField = schema.getUniqueKeyField() != null ? schema.getUniqueKeyField().getName() : null; - if (!uniqueKeyFieldParam.equals(uniqueKeyField)) { + if (!uniqueKeyField.equals(existingKeyField)) { // The Schema API doesn't support changing the ID field so would have to use XML directly throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, @@ -652,13 +682,12 @@ public void analyze(SolrQueryRequest req, SolrQueryResponse rsp) } boolean langsUpdated = false; - String[] languages = req.getParams().getParams(LANGUAGES_PARAM); List langs; if (languages != null) { langs = - languages.length == 0 || (languages.length == 1 && "*".equals(languages[0])) + languages.isEmpty() || (languages.size() == 1 && "*".equals(languages.get(0))) ? Collections.emptyList() - : Arrays.asList(languages); + : languages; if (!langs.equals(settings.getLanguages())) { settings.setLanguages(langs); langsUpdated = true; @@ -669,7 +698,6 @@ public void analyze(SolrQueryRequest req, SolrQueryResponse rsp) } boolean dynamicUpdated = false; - Boolean enableDynamicFields = req.getParams().getBool(ENABLE_DYNAMIC_FIELDS_PARAM); if (enableDynamicFields != null && enableDynamicFields != settings.dynamicFieldsEnabled()) { settings.setDynamicFieldsEnabled(enableDynamicFields); dynamicUpdated = true; @@ -700,7 +728,6 @@ public void analyze(SolrQueryRequest req, SolrQueryResponse rsp) // persist the updated schema schema.persistManagedSchema(false); - Boolean enableFieldGuessing = req.getParams().getBool(ENABLE_FIELD_GUESSING_PARAM); if (enableFieldGuessing != null && enableFieldGuessing != settings.fieldGuessingEnabled()) { settings.setFieldGuessingEnabled(enableFieldGuessing); } @@ -715,7 +742,6 @@ public void analyze(SolrQueryRequest req, SolrQueryResponse rsp) } // nested docs - Boolean enableNestedDocs = req.getParams().getBool(ENABLE_NESTED_DOCS_PARAM); if (enableNestedDocs != null && enableNestedDocs != settings.nestedDocsEnabled()) { settings.setNestedDocsEnabled(enableNestedDocs); configSetHelper.toggleNestedDocsFields(schema, enableNestedDocs); @@ -742,13 +768,13 @@ public void analyze(SolrQueryRequest req, SolrQueryResponse rsp) response.put(ANALYSIS_ERROR, analysisErrorHolder[0]); } addErrorToResponse(mutableId, null, errorsDuringIndexing, response, null); - rsp.getValues().addAll(response); + return buildFlexibleResponse(response); } - @EndPoint(method = GET, path = "/schema-designer/query", permission = CONFIG_READ_PERM) - public void query(SolrQueryRequest req, SolrQueryResponse rsp) - throws IOException, SolrServerException { - final String configSet = getRequiredParam(CONFIG_SET_PARAM, req); + @Override + @PermissionName(CONFIG_READ_PERM) + public FlexibleSolrJerseyResponse query(String configSet) throws Exception { + requireNotEmpty(CONFIG_SET_PARAM, configSet); String mutableId = getMutableId(configSet); if (!configExists(mutableId)) { throw new SolrException( @@ -785,28 +811,26 @@ public void query(SolrQueryRequest req, SolrQueryResponse rsp) } if (errorsDuringIndexing != null) { - Map response = new HashMap<>(); - rsp.setException( - new SolrException( - SolrException.ErrorCode.BAD_REQUEST, - "Failed to re-index sample documents after schema updated.")); - response.put(ERROR_DETAILS, errorsDuringIndexing); - rsp.getValues().addAll(response); - return; + throw new SolrException( + SolrException.ErrorCode.BAD_REQUEST, + "Failed to re-index sample documents after schema updated."); } // execute the user's query against the temp collection - QueryResponse qr = cloudClient().query(mutableId, req.getParams()); - rsp.getValues().addAll(qr.getResponse()); + QueryResponse qr = cloudClient().query(mutableId, solrQueryRequest.getParams()); + Map response = new HashMap<>(); + qr.getResponse().forEach((name, val) -> response.put(name, val)); + return buildFlexibleResponse(response); } /** * Return the diff of designer schema with the source schema (either previously published or the * copyFrom). */ - @EndPoint(method = GET, path = "/schema-designer/diff", permission = CONFIG_READ_PERM) - public void getSchemaDiff(SolrQueryRequest req, SolrQueryResponse rsp) throws IOException { - final String configSet = getRequiredParam(CONFIG_SET_PARAM, req); + @Override + @PermissionName(CONFIG_READ_PERM) + public FlexibleSolrJerseyResponse getSchemaDiff(String configSet) throws Exception { + requireNotEmpty(CONFIG_SET_PARAM, configSet); SchemaDesignerSettings settings = getMutableSchemaForConfigSet(configSet, -1, null); // diff the published if found, else use the original source schema @@ -816,16 +840,17 @@ public void getSchemaDiff(SolrQueryRequest req, SolrQueryResponse rsp) throws IO "diff", ManagedSchemaDiff.diff(loadLatestSchema(sourceSchema), settings.getSchema())); response.put("diff-source", sourceSchema); addSettingsToResponse(settings, response); - rsp.getValues().addAll(response); + return buildFlexibleResponse(response); } - protected SampleDocuments loadSampleDocuments(SolrQueryRequest req, String configSet) - throws IOException { + protected SampleDocuments loadSampleDocuments(String configSet) throws IOException { List docs = null; - ContentStream stream = extractSingleContentStream(req, false); + ContentStream stream = extractSingleContentStream(false); SampleDocuments sampleDocs = null; if (stream != null && stream.getContentType() != null) { - sampleDocs = sampleDocLoader.parseDocsFromStream(req.getParams(), stream, MAX_SAMPLE_DOCS); + sampleDocs = + sampleDocLoader.parseDocsFromStream( + solrQueryRequest.getParams(), stream, MAX_SAMPLE_DOCS); docs = sampleDocs.parsed; if (!docs.isEmpty()) { // user posted in some docs, if there are already docs stored in the blob store, then add @@ -966,8 +991,8 @@ ManagedIndexSchema loadLatestSchema(String configSet) { return configSetHelper.loadLatestSchema(configSet); } - protected ContentStream extractSingleContentStream(final SolrQueryRequest req, boolean required) { - Iterable streams = req.getContentStreams(); + protected ContentStream extractSingleContentStream(boolean required) { + Iterable streams = solrQueryRequest.getContentStreams(); Iterator iter = streams != null ? streams.iterator() : null; ContentStream stream = iter != null && iter.hasNext() ? iter.next() : null; if (required && stream == null) @@ -1247,8 +1272,8 @@ protected SimpleOrderedMap fieldToMap(SchemaField f, ManagedIndexSchema } @SuppressWarnings("unchecked") - protected Map readJsonFromRequest(SolrQueryRequest req) throws IOException { - ContentStream stream = extractSingleContentStream(req, true); + protected Map readJsonFromRequest() throws IOException { + ContentStream stream = extractSingleContentStream(true); String contentType = stream.getContentType(); if (StrUtils.isNullOrEmpty(contentType) || !contentType.toLowerCase(Locale.ROOT).contains(JSON_MIME)) { @@ -1276,7 +1301,7 @@ protected void addSettingsToResponse( } } - protected String checkMutable(String configSet, SolrQueryRequest req) throws IOException { + protected String checkMutable(String configSet, int clientSchemaVersion) throws IOException { // an apply just copies over the temp config to the "live" location String mutableId = getMutableId(configSet); if (!configExists(mutableId)) { @@ -1291,34 +1316,27 @@ protected String checkMutable(String configSet, SolrQueryRequest req) throws IOE final int schemaVersionInZk = configSetHelper.getCurrentSchemaVersion(mutableId); if (schemaVersionInZk != -1) { // check the versions agree - configSetHelper.checkSchemaVersion( - mutableId, requireSchemaVersionFromClient(req), schemaVersionInZk); + configSetHelper.checkSchemaVersion(mutableId, clientSchemaVersion, schemaVersionInZk); } // else the stored is -1, can't really enforce here return mutableId; } - protected int requireSchemaVersionFromClient(SolrQueryRequest req) { - final int schemaVersion = req.getParams().getInt(SCHEMA_VERSION_PARAM, -1); - if (schemaVersion == -1) { + protected void requireSchemaVersion(Integer schemaVersion) { + if (schemaVersion == null) { throw new SolrException( - SolrException.ErrorCode.BAD_REQUEST, - SCHEMA_VERSION_PARAM + " is a required parameter for the " + req.getPath() + " endpoint"); + SolrException.ErrorCode.BAD_REQUEST, SCHEMA_VERSION_PARAM + " is a required parameter!"); } - return schemaVersion; } - protected String getRequiredParam(final String param, final SolrQueryRequest req) { - final String paramValue = req.getParams().get(param); - if (StrUtils.isNullOrEmpty(paramValue)) { + protected void requireNotEmpty(final String param, final String value) { + if (StrUtils.isNullOrEmpty(value)) { throw new SolrException( - SolrException.ErrorCode.BAD_REQUEST, - param + " is a required parameter for the " + req.getPath() + " endpoint!"); + SolrException.ErrorCode.BAD_REQUEST, param + " is a required parameter!"); } - return paramValue; } - protected void cleanupTemp(String configSet) throws IOException, SolrServerException { + protected void doCleanupTemp(String configSet) throws IOException, SolrServerException { String mutableId = getMutableId(configSet); indexedVersion.remove(mutableId); CollectionAdminRequest.deleteCollection(mutableId).process(cloudClient()); @@ -1326,6 +1344,13 @@ protected void cleanupTemp(String configSet) throws IOException, SolrServerExcep deleteConfig(mutableId); } + protected FlexibleSolrJerseyResponse buildFlexibleResponse(Map responseMap) { + FlexibleSolrJerseyResponse response = + instantiateJerseyResponse(FlexibleSolrJerseyResponse.class); + responseMap.forEach(response::setUnknownProperty); + return response; + } + private boolean configExists(String configSet) throws IOException { return coreContainer.getConfigSetService().checkConfigExists(configSet); } diff --git a/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java b/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java index 0822693db8d7..50ebe9b9abc2 100644 --- a/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java +++ b/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java @@ -20,7 +20,6 @@ import static org.apache.solr.common.params.CommonParams.JSON_MIME; import static org.apache.solr.handler.admin.ConfigSetsHandler.DEFAULT_CONFIGSET_NAME; import static org.apache.solr.handler.designer.SchemaDesignerAPI.getMutableId; -import static org.apache.solr.response.RawResponseWriter.CONTENT; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -34,6 +33,7 @@ import java.util.Map; import java.util.Optional; import java.util.stream.Stream; +import org.apache.solr.client.api.model.FlexibleSolrJerseyResponse; import org.apache.solr.client.solrj.request.SolrQuery; import org.apache.solr.client.solrj.response.QueryResponse; import org.apache.solr.cloud.SolrCloudTestCase; @@ -42,15 +42,12 @@ import org.apache.solr.common.cloud.SolrZkClient; 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.ContentStream; import org.apache.solr.common.util.ContentStreamBase; -import org.apache.solr.common.util.NamedList; import org.apache.solr.common.util.SimpleOrderedMap; import org.apache.solr.core.CoreContainer; import org.apache.solr.handler.TestSampleDocumentsLoader; import org.apache.solr.request.SolrQueryRequest; -import org.apache.solr.response.SolrQueryResponse; import org.apache.solr.schema.ManagedIndexSchema; import org.apache.solr.schema.SchemaField; import org.apache.solr.util.ExternalPaths; @@ -64,6 +61,7 @@ public class TestSchemaDesignerAPI extends SolrCloudTestCase implements SchemaDe private CoreContainer cc; private SchemaDesignerAPI schemaDesignerAPI; + private SolrQueryRequest mockReq; @BeforeClass public static void createCluster() throws Exception { @@ -87,39 +85,41 @@ public void setupTest() { assertNotNull(cluster); cc = cluster.getJettySolrRunner(0).getCoreContainer(); assertNotNull(cc); - schemaDesignerAPI = new SchemaDesignerAPI(cc); + mockReq = mock(SolrQueryRequest.class); + schemaDesignerAPI = + new SchemaDesignerAPI( + cc, + SchemaDesignerAPI.newSchemaSuggester(), + SchemaDesignerAPI.newSampleDocumentsLoader(), + mockReq); } public void testTSV() throws Exception { String configSet = "testTSV"; ModifiableSolrParams reqParams = new ModifiableSolrParams(); - - // GET /schema-designer/info - SolrQueryResponse rsp = new SolrQueryResponse(); - SolrQueryRequest req = mock(SolrQueryRequest.class); - reqParams.set(CONFIG_SET_PARAM, configSet); reqParams.set(LANGUAGES_PARAM, "en"); reqParams.set(ENABLE_DYNAMIC_FIELDS_PARAM, false); - when(req.getParams()).thenReturn(reqParams); + when(mockReq.getParams()).thenReturn(reqParams); String tsv = "id\tcol1\tcol2\n1\tfoo\tbar\n2\tbaz\tbah\n"; // POST some sample TSV docs ContentStream stream = new ContentStreamBase.StringStream(tsv, "text/csv"); - when(req.getContentStreams()).thenReturn(Collections.singletonList(stream)); + when(mockReq.getContentStreams()).thenReturn(Collections.singletonList(stream)); // POST /schema-designer/analyze - schemaDesignerAPI.analyze(req, rsp); - assertNotNull(rsp.getValues().get(CONFIG_SET_PARAM)); - assertNotNull(rsp.getValues().get(SCHEMA_VERSION_PARAM)); - assertEquals(2, rsp.getValues().get("numDocs")); + FlexibleSolrJerseyResponse response = + schemaDesignerAPI.analyze(configSet, null, null, null, List.of("en"), false, null, null); + assertNotNull(response.unknownProperties().get(CONFIG_SET_PARAM)); + assertNotNull(response.unknownProperties().get(SCHEMA_VERSION_PARAM)); + assertEquals(2, response.unknownProperties().get("numDocs")); reqParams.clear(); reqParams.set(CONFIG_SET_PARAM, configSet); - rsp = new SolrQueryResponse(); - schemaDesignerAPI.cleanupTemp(req, rsp); + when(mockReq.getContentStreams()).thenReturn(null); + schemaDesignerAPI.cleanupTempSchema(configSet); String mutableId = getMutableId(configSet); assertFalse(cc.getZkController().getClusterState().hasCollection(mutableId)); @@ -150,14 +150,8 @@ public void testAddTechproductsProgressively() throws Exception { String configSet = "techproducts"; - ModifiableSolrParams reqParams = new ModifiableSolrParams(); - // GET /schema-designer/info - SolrQueryResponse rsp = new SolrQueryResponse(); - SolrQueryRequest req = mock(SolrQueryRequest.class); - reqParams.set(CONFIG_SET_PARAM, configSet); - when(req.getParams()).thenReturn(reqParams); - schemaDesignerAPI.getInfo(req, rsp); + FlexibleSolrJerseyResponse response = schemaDesignerAPI.getInfo(configSet); // response should just be the default values Map expSettings = Map.of( @@ -165,64 +159,46 @@ public void testAddTechproductsProgressively() throws Exception { ENABLE_FIELD_GUESSING_PARAM, true, ENABLE_NESTED_DOCS_PARAM, false, LANGUAGES_PARAM, Collections.emptyList()); - assertDesignerSettings(expSettings, rsp.getValues()); - SolrParams rspData = rsp.getValues().toSolrParams(); - int schemaVersion = rspData.getInt(SCHEMA_VERSION_PARAM); + assertDesignerSettings(expSettings, response.unknownProperties()); + int schemaVersion = (Integer) response.unknownProperties().get(SCHEMA_VERSION_PARAM); assertEquals(schemaVersion, -1); // shouldn't exist yet // Use the prep endpoint to prepare the new schema - reqParams.clear(); - reqParams.set(CONFIG_SET_PARAM, configSet); - rsp = new SolrQueryResponse(); - req = mock(SolrQueryRequest.class); - when(req.getParams()).thenReturn(reqParams); - schemaDesignerAPI.prepNewSchema(req, rsp); - assertNotNull(rsp.getValues().get(CONFIG_SET_PARAM)); - assertNotNull(rsp.getValues().get(SCHEMA_VERSION_PARAM)); - rspData = rsp.getValues().toSolrParams(); - schemaVersion = rspData.getInt(SCHEMA_VERSION_PARAM); + response = schemaDesignerAPI.prepNewSchema(configSet, null); + assertNotNull(response.unknownProperties().get(CONFIG_SET_PARAM)); + assertNotNull(response.unknownProperties().get(SCHEMA_VERSION_PARAM)); + schemaVersion = (Integer) response.unknownProperties().get(SCHEMA_VERSION_PARAM); for (Path next : toAdd) { // Analyze some sample documents to refine the schema - reqParams.clear(); + ModifiableSolrParams reqParams = new ModifiableSolrParams(); reqParams.set(CONFIG_SET_PARAM, configSet); - reqParams.set(LANGUAGES_PARAM, "en"); - reqParams.set(ENABLE_DYNAMIC_FIELDS_PARAM, false); - reqParams.set(SCHEMA_VERSION_PARAM, String.valueOf(schemaVersion)); - req = mock(SolrQueryRequest.class); - when(req.getParams()).thenReturn(reqParams); + when(mockReq.getParams()).thenReturn(reqParams); // POST some sample JSON docs ContentStreamBase.FileStream stream = new ContentStreamBase.FileStream(next); stream.setContentType( TestSampleDocumentsLoader.guessContentTypeFromFilename(next.getFileName().toString())); - when(req.getContentStreams()).thenReturn(Collections.singletonList(stream)); - - rsp = new SolrQueryResponse(); + when(mockReq.getContentStreams()).thenReturn(Collections.singletonList(stream)); // POST /schema-designer/analyze - schemaDesignerAPI.analyze(req, rsp); + response = + schemaDesignerAPI.analyze( + configSet, schemaVersion, null, null, List.of("en"), false, null, null); - assertNotNull(rsp.getValues().get(CONFIG_SET_PARAM)); - assertNotNull(rsp.getValues().get(SCHEMA_VERSION_PARAM)); - assertNotNull(rsp.getValues().get("fields")); - assertNotNull(rsp.getValues().get("fieldTypes")); - assertNotNull(rsp.getValues().get("docIds")); + assertNotNull(response.unknownProperties().get(CONFIG_SET_PARAM)); + assertNotNull(response.unknownProperties().get(SCHEMA_VERSION_PARAM)); + assertNotNull(response.unknownProperties().get("fields")); + assertNotNull(response.unknownProperties().get("fieldTypes")); + assertNotNull(response.unknownProperties().get("docIds")); // capture the schema version for MVCC - rspData = rsp.getValues().toSolrParams(); - schemaVersion = rspData.getInt(SCHEMA_VERSION_PARAM); + schemaVersion = (Integer) response.unknownProperties().get(SCHEMA_VERSION_PARAM); } // get info (from the temp) - reqParams.clear(); - reqParams.set(CONFIG_SET_PARAM, configSet); - req = mock(SolrQueryRequest.class); - when(req.getParams()).thenReturn(reqParams); - rsp = new SolrQueryResponse(); - // GET /schema-designer/info - schemaDesignerAPI.getInfo(req, rsp); + response = schemaDesignerAPI.getInfo(configSet); expSettings = Map.of( ENABLE_DYNAMIC_FIELDS_PARAM, false, @@ -230,57 +206,37 @@ public void testAddTechproductsProgressively() throws Exception { ENABLE_NESTED_DOCS_PARAM, false, LANGUAGES_PARAM, Collections.singletonList("en"), COPY_FROM_PARAM, "_default"); - assertDesignerSettings(expSettings, rsp.getValues()); + assertDesignerSettings(expSettings, response.unknownProperties()); // query to see how the schema decisions impact retrieval / ranking - reqParams.clear(); - reqParams.set(SCHEMA_VERSION_PARAM, String.valueOf(schemaVersion)); - reqParams.set(CONFIG_SET_PARAM, configSet); - reqParams.set(CommonParams.Q, "*:*"); - req = mock(SolrQueryRequest.class); - when(req.getParams()).thenReturn(reqParams); - rsp = new SolrQueryResponse(); + ModifiableSolrParams queryParams = new ModifiableSolrParams(); + queryParams.set(SCHEMA_VERSION_PARAM, String.valueOf(schemaVersion)); + queryParams.set(CONFIG_SET_PARAM, configSet); + queryParams.set(CommonParams.Q, "*:*"); + when(mockReq.getParams()).thenReturn(queryParams); + when(mockReq.getContentStreams()).thenReturn(null); // GET /schema-designer/query - schemaDesignerAPI.query(req, rsp); - assertNotNull(rsp.getResponseHeader()); - SolrDocumentList results = (SolrDocumentList) rsp.getResponse(); + response = schemaDesignerAPI.query(configSet); + assertNotNull(response.unknownProperties().get("responseHeader")); + SolrDocumentList results = (SolrDocumentList) response.unknownProperties().get("response"); assertEquals(47, results.getNumFound()); // publish schema to a config set that can be used by real collections - reqParams.clear(); - reqParams.set(SCHEMA_VERSION_PARAM, String.valueOf(schemaVersion)); - reqParams.set(CONFIG_SET_PARAM, configSet); - String collection = "techproducts"; - reqParams.set(NEW_COLLECTION_PARAM, collection); - reqParams.set(INDEX_TO_COLLECTION_PARAM, true); - reqParams.set(RELOAD_COLLECTIONS_PARAM, true); - reqParams.set(CLEANUP_TEMP_PARAM, true); - reqParams.set(DISABLE_DESIGNER_PARAM, true); - - rsp = new SolrQueryResponse(); - schemaDesignerAPI.publish(req, rsp); + schemaDesignerAPI.publish(configSet, schemaVersion, collection, true, 1, 1, true, true, true); assertNotNull(cc.getZkController().zkStateReader.getCollection(collection)); // listCollectionsForConfig - reqParams.clear(); - reqParams.set(CONFIG_SET_PARAM, configSet); - rsp = new SolrQueryResponse(); - schemaDesignerAPI.listCollectionsForConfig(req, rsp); - List collections = (List) rsp.getValues().get("collections"); + response = schemaDesignerAPI.listCollectionsForConfig(configSet); + List collections = (List) response.unknownProperties().get("collections"); assertNotNull(collections); assertTrue(collections.contains(collection)); // now try to create another temp, which should fail since designer is disabled for this // configSet now - reqParams.clear(); - reqParams.set(CONFIG_SET_PARAM, configSet); - rsp = new SolrQueryResponse(); - req = mock(SolrQueryRequest.class); - when(req.getParams()).thenReturn(reqParams); try { - schemaDesignerAPI.prepNewSchema(req, rsp); + schemaDesignerAPI.prepNewSchema(configSet, null); fail("Prep should fail for locked schema " + configSet); } catch (SolrException solrExc) { assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, solrExc.code()); @@ -292,38 +248,32 @@ public void testAddTechproductsProgressively() throws Exception { public void testSuggestFilmsXml() throws Exception { String configSet = "films"; - ModifiableSolrParams reqParams = new ModifiableSolrParams(); - Path filmsDir = ExternalPaths.SOURCE_HOME.resolve("example/films"); assertTrue(filmsDir + " not found!", Files.isDirectory(filmsDir)); Path filmsXml = filmsDir.resolve("films.xml"); assertTrue("example/films/films.xml not found", Files.isRegularFile(filmsXml)); - reqParams.set(CONFIG_SET_PARAM, configSet); - reqParams.set(ENABLE_DYNAMIC_FIELDS_PARAM, "true"); - - SolrQueryRequest req = mock(SolrQueryRequest.class); - when(req.getParams()).thenReturn(reqParams); - // POST some sample XML docs ContentStreamBase.FileStream stream = new ContentStreamBase.FileStream(filmsXml); stream.setContentType("application/xml"); - when(req.getContentStreams()).thenReturn(Collections.singletonList(stream)); - - SolrQueryResponse rsp = new SolrQueryResponse(); + ModifiableSolrParams reqParams = new ModifiableSolrParams(); + reqParams.set(CONFIG_SET_PARAM, configSet); + when(mockReq.getParams()).thenReturn(reqParams); + when(mockReq.getContentStreams()).thenReturn(Collections.singletonList(stream)); // POST /schema-designer/analyze - schemaDesignerAPI.analyze(req, rsp); - - assertNotNull(rsp.getValues().get(CONFIG_SET_PARAM)); - assertNotNull(rsp.getValues().get(SCHEMA_VERSION_PARAM)); - assertNotNull(rsp.getValues().get("fields")); - assertNotNull(rsp.getValues().get("fieldTypes")); - List docIds = (List) rsp.getValues().get("docIds"); + FlexibleSolrJerseyResponse response = + schemaDesignerAPI.analyze(configSet, null, null, null, null, true, null, null); + + assertNotNull(response.unknownProperties().get(CONFIG_SET_PARAM)); + assertNotNull(response.unknownProperties().get(SCHEMA_VERSION_PARAM)); + assertNotNull(response.unknownProperties().get("fields")); + assertNotNull(response.unknownProperties().get("fieldTypes")); + List docIds = (List) response.unknownProperties().get("docIds"); assertNotNull(docIds); assertEquals(100, docIds.size()); // designer limits the doc ids to top 100 - String idField = rsp.getValues()._getStr(UNIQUE_KEY_FIELD_PARAM); + String idField = (String) response.unknownProperties().get(UNIQUE_KEY_FIELD_PARAM); assertNotNull(idField); } @@ -332,16 +282,10 @@ public void testSuggestFilmsXml() throws Exception { public void testBasicUserWorkflow() throws Exception { String configSet = "testJson"; - ModifiableSolrParams reqParams = new ModifiableSolrParams(); - // Use the prep endpoint to prepare the new schema - reqParams.set(CONFIG_SET_PARAM, configSet); - SolrQueryResponse rsp = new SolrQueryResponse(); - SolrQueryRequest req = mock(SolrQueryRequest.class); - when(req.getParams()).thenReturn(reqParams); - schemaDesignerAPI.prepNewSchema(req, rsp); - assertNotNull(rsp.getValues().get(CONFIG_SET_PARAM)); - assertNotNull(rsp.getValues().get(SCHEMA_VERSION_PARAM)); + FlexibleSolrJerseyResponse response = schemaDesignerAPI.prepNewSchema(configSet, null); + assertNotNull(response.unknownProperties().get(CONFIG_SET_PARAM)); + assertNotNull(response.unknownProperties().get(SCHEMA_VERSION_PARAM)); Map expSettings = Map.of( @@ -350,44 +294,36 @@ public void testBasicUserWorkflow() throws Exception { ENABLE_NESTED_DOCS_PARAM, false, LANGUAGES_PARAM, Collections.emptyList(), COPY_FROM_PARAM, "_default"); - assertDesignerSettings(expSettings, rsp.getValues()); + assertDesignerSettings(expSettings, response.unknownProperties()); // Analyze some sample documents to refine the schema - reqParams.clear(); - reqParams.set(CONFIG_SET_PARAM, configSet); - req = mock(SolrQueryRequest.class); - when(req.getParams()).thenReturn(reqParams); - - // POST some sample JSON docs Path booksJson = ExternalPaths.SOURCE_HOME.resolve("example/exampledocs/books.json"); ContentStreamBase.FileStream stream = new ContentStreamBase.FileStream(booksJson); stream.setContentType(JSON_MIME); - when(req.getContentStreams()).thenReturn(Collections.singletonList(stream)); - - rsp = new SolrQueryResponse(); + ModifiableSolrParams reqParams = new ModifiableSolrParams(); + reqParams.set(CONFIG_SET_PARAM, configSet); + when(mockReq.getParams()).thenReturn(reqParams); + when(mockReq.getContentStreams()).thenReturn(Collections.singletonList(stream)); // POST /schema-designer/analyze - schemaDesignerAPI.analyze(req, rsp); - - assertNotNull(rsp.getValues().get(CONFIG_SET_PARAM)); - assertNotNull(rsp.getValues().get(SCHEMA_VERSION_PARAM)); - assertNotNull(rsp.getValues().get("fields")); - assertNotNull(rsp.getValues().get("fieldTypes")); - assertNotNull(rsp.getValues().get("docIds")); - String idField = rsp.getValues()._getStr(UNIQUE_KEY_FIELD_PARAM); + response = schemaDesignerAPI.analyze(configSet, null, null, null, null, null, null, null); + + assertNotNull(response.unknownProperties().get(CONFIG_SET_PARAM)); + assertNotNull(response.unknownProperties().get(SCHEMA_VERSION_PARAM)); + assertNotNull(response.unknownProperties().get("fields")); + assertNotNull(response.unknownProperties().get("fieldTypes")); + assertNotNull(response.unknownProperties().get("docIds")); + String idField = (String) response.unknownProperties().get(UNIQUE_KEY_FIELD_PARAM); assertNotNull(idField); - assertDesignerSettings(expSettings, rsp.getValues()); + assertDesignerSettings(expSettings, response.unknownProperties()); // capture the schema version for MVCC - SolrParams rspData = rsp.getValues().toSolrParams(); - reqParams.clear(); - int schemaVersion = rspData.getInt(SCHEMA_VERSION_PARAM); + int schemaVersion = (Integer) response.unknownProperties().get(SCHEMA_VERSION_PARAM); // load the contents of a file - Collection files = (Collection) rsp.getValues().get("files"); + Collection files = (Collection) response.unknownProperties().get("files"); assertTrue(files != null && !files.isEmpty()); - reqParams.set(CONFIG_SET_PARAM, configSet); String file = null; for (String f : files) { if ("solrconfig.xml".equals(f)) { @@ -396,63 +332,34 @@ public void testBasicUserWorkflow() throws Exception { } } assertNotNull("solrconfig.xml not found in files!", file); - reqParams.set("file", file); - req = mock(SolrQueryRequest.class); - when(req.getParams()).thenReturn(reqParams); - rsp = new SolrQueryResponse(); - schemaDesignerAPI.getFileContents(req, rsp); - String solrconfigXml = (String) rsp.getValues().get(file); + response = schemaDesignerAPI.getFileContents(configSet, file); + String solrconfigXml = (String) response.unknownProperties().get(file); assertNotNull(solrconfigXml); - reqParams.clear(); // Update solrconfig.xml - rsp = new SolrQueryResponse(); - reqParams.set(SCHEMA_VERSION_PARAM, String.valueOf(schemaVersion)); - reqParams.set(CONFIG_SET_PARAM, configSet); - reqParams.set("file", file); - - req = mock(SolrQueryRequest.class); - when(req.getParams()).thenReturn(reqParams); - when(req.getContentStreams()) + when(mockReq.getContentStreams()) .thenReturn( Collections.singletonList( new ContentStreamBase.StringStream(solrconfigXml, "application/xml"))); - - schemaDesignerAPI.updateFileContents(req, rsp); - rspData = rsp.getValues().toSolrParams(); - reqParams.clear(); - schemaVersion = rspData.getInt(SCHEMA_VERSION_PARAM); + response = schemaDesignerAPI.updateFileContents(configSet, file); + schemaVersion = (Integer) response.unknownProperties().get(SCHEMA_VERSION_PARAM); // update solrconfig.xml with some invalid XML mess - rsp = new SolrQueryResponse(); - reqParams.set(SCHEMA_VERSION_PARAM, String.valueOf(schemaVersion)); - reqParams.set(CONFIG_SET_PARAM, configSet); - reqParams.set("file", file); - - req = mock(SolrQueryRequest.class); - when(req.getParams()).thenReturn(reqParams); - when(req.getContentStreams()) + when(mockReq.getContentStreams()) .thenReturn( Collections.singletonList( new ContentStreamBase.StringStream("", "application/xml"))); // this should fail b/c the updated solrconfig.xml is invalid - schemaDesignerAPI.updateFileContents(req, rsp); - rspData = rsp.getValues().toSolrParams(); - reqParams.clear(); - assertNotNull(rspData.get("updateFileError")); + response = schemaDesignerAPI.updateFileContents(configSet, file); + assertNotNull(response.unknownProperties().get("updateFileError")); // remove dynamic fields and change the language to "en" only - rsp = new SolrQueryResponse(); + when(mockReq.getContentStreams()).thenReturn(null); // POST /schema-designer/analyze - reqParams.set(SCHEMA_VERSION_PARAM, String.valueOf(schemaVersion)); - reqParams.set(CONFIG_SET_PARAM, configSet); - reqParams.set(LANGUAGES_PARAM, "en"); - reqParams.set(ENABLE_DYNAMIC_FIELDS_PARAM, false); - reqParams.set(ENABLE_FIELD_GUESSING_PARAM, false); - req = mock(SolrQueryRequest.class); - when(req.getParams()).thenReturn(reqParams); - schemaDesignerAPI.analyze(req, rsp); + response = + schemaDesignerAPI.analyze( + configSet, schemaVersion, null, null, List.of("en"), false, false, null); expSettings = Map.of( @@ -461,28 +368,18 @@ public void testBasicUserWorkflow() throws Exception { ENABLE_NESTED_DOCS_PARAM, false, LANGUAGES_PARAM, Collections.singletonList("en"), COPY_FROM_PARAM, "_default"); - assertDesignerSettings(expSettings, rsp.getValues()); + assertDesignerSettings(expSettings, response.unknownProperties()); - List filesInResp = (List) rsp.getValues().get("files"); + List filesInResp = (List) response.unknownProperties().get("files"); assertEquals(5, filesInResp.size()); assertTrue(filesInResp.contains("lang/stopwords_en.txt")); - rspData = rsp.getValues().toSolrParams(); - schemaVersion = rspData.getInt(SCHEMA_VERSION_PARAM); - - reqParams.clear(); + schemaVersion = (Integer) response.unknownProperties().get(SCHEMA_VERSION_PARAM); // add the dynamic fields back and change the languages too - rsp = new SolrQueryResponse(); - reqParams.set(SCHEMA_VERSION_PARAM, String.valueOf(schemaVersion)); - reqParams.set(CONFIG_SET_PARAM, configSet); - reqParams.add(LANGUAGES_PARAM, "en"); - reqParams.add(LANGUAGES_PARAM, "fr"); - reqParams.set(ENABLE_DYNAMIC_FIELDS_PARAM, true); - reqParams.set(ENABLE_FIELD_GUESSING_PARAM, false); - req = mock(SolrQueryRequest.class); - when(req.getParams()).thenReturn(reqParams); - schemaDesignerAPI.analyze(req, rsp); + response = + schemaDesignerAPI.analyze( + configSet, schemaVersion, null, null, Arrays.asList("en", "fr"), true, false, null); expSettings = Map.of( @@ -491,25 +388,18 @@ public void testBasicUserWorkflow() throws Exception { ENABLE_NESTED_DOCS_PARAM, false, LANGUAGES_PARAM, Arrays.asList("en", "fr"), COPY_FROM_PARAM, "_default"); - assertDesignerSettings(expSettings, rsp.getValues()); + assertDesignerSettings(expSettings, response.unknownProperties()); - filesInResp = (List) rsp.getValues().get("files"); + filesInResp = (List) response.unknownProperties().get("files"); assertEquals(7, filesInResp.size()); assertTrue(filesInResp.contains("lang/stopwords_fr.txt")); - rspData = rsp.getValues().toSolrParams(); - reqParams.clear(); - schemaVersion = rspData.getInt(SCHEMA_VERSION_PARAM); + schemaVersion = (Integer) response.unknownProperties().get(SCHEMA_VERSION_PARAM); - // add back all the default languages - rsp = new SolrQueryResponse(); - reqParams.set(SCHEMA_VERSION_PARAM, String.valueOf(schemaVersion)); - reqParams.set(CONFIG_SET_PARAM, configSet); - reqParams.add(LANGUAGES_PARAM, "*"); - reqParams.set(ENABLE_DYNAMIC_FIELDS_PARAM, false); - req = mock(SolrQueryRequest.class); - when(req.getParams()).thenReturn(reqParams); - schemaDesignerAPI.analyze(req, rsp); + // add back all the default languages (using "*" wildcard → empty list) + response = + schemaDesignerAPI.analyze( + configSet, schemaVersion, null, null, List.of("*"), false, null, null); expSettings = Map.of( @@ -518,168 +408,105 @@ public void testBasicUserWorkflow() throws Exception { ENABLE_NESTED_DOCS_PARAM, false, LANGUAGES_PARAM, Collections.emptyList(), COPY_FROM_PARAM, "_default"); - assertDesignerSettings(expSettings, rsp.getValues()); + assertDesignerSettings(expSettings, response.unknownProperties()); - filesInResp = (List) rsp.getValues().get("files"); + filesInResp = (List) response.unknownProperties().get("files"); assertEquals(43, filesInResp.size()); assertTrue(filesInResp.contains("lang/stopwords_fr.txt")); assertTrue(filesInResp.contains("lang/stopwords_en.txt")); assertTrue(filesInResp.contains("lang/stopwords_it.txt")); - rspData = rsp.getValues().toSolrParams(); - reqParams.clear(); - schemaVersion = rspData.getInt(SCHEMA_VERSION_PARAM); + schemaVersion = (Integer) response.unknownProperties().get(SCHEMA_VERSION_PARAM); // Get the value of a sample document String docId = "978-0641723445"; String fieldName = "series_t"; - reqParams.set(CONFIG_SET_PARAM, configSet); - reqParams.set(DOC_ID_PARAM, docId); - reqParams.set(FIELD_PARAM, fieldName); - reqParams.set(UNIQUE_KEY_FIELD_PARAM, idField); - req = mock(SolrQueryRequest.class); - when(req.getParams()).thenReturn(reqParams); - rsp = new SolrQueryResponse(); // GET /schema-designer/sample - schemaDesignerAPI.getSampleValue(req, rsp); - rspData = rsp.getValues().toSolrParams(); - assertNotNull(rspData.get(idField)); - assertNotNull(rspData.get(fieldName)); - assertNotNull(rspData.get("analysis")); - - reqParams.clear(); + response = schemaDesignerAPI.getSampleValue(configSet, fieldName, idField, docId); + assertNotNull(response.unknownProperties().get(idField)); + assertNotNull(response.unknownProperties().get(fieldName)); + assertNotNull(response.unknownProperties().get("analysis")); // at this point the user would refine the schema by // editing suggestions for fields and adding/removing fields / field types as needed // add a new field - reqParams.set(SCHEMA_VERSION_PARAM, String.valueOf(schemaVersion)); - reqParams.set(CONFIG_SET_PARAM, configSet); - - req = mock(SolrQueryRequest.class); - when(req.getParams()).thenReturn(reqParams); stream = new ContentStreamBase.FileStream(getFile("schema-designer/add-new-field.json")); stream.setContentType(JSON_MIME); - when(req.getContentStreams()).thenReturn(Collections.singletonList(stream)); - rsp = new SolrQueryResponse(); + when(mockReq.getContentStreams()).thenReturn(Collections.singletonList(stream)); // POST /schema-designer/add - schemaDesignerAPI.addSchemaObject(req, rsp); - assertNotNull(rsp.getValues().get("add-field")); - rspData = rsp.getValues().toSolrParams(); - schemaVersion = rspData.getInt(SCHEMA_VERSION_PARAM); - assertNotNull(rsp.getValues().get("fields")); + response = schemaDesignerAPI.addSchemaObject(configSet, schemaVersion); + assertNotNull(response.unknownProperties().get("add-field")); + schemaVersion = (Integer) response.unknownProperties().get(SCHEMA_VERSION_PARAM); + assertNotNull(response.unknownProperties().get("fields")); // update an existing field - reqParams.clear(); - reqParams.set(SCHEMA_VERSION_PARAM, String.valueOf(schemaVersion)); - reqParams.set(CONFIG_SET_PARAM, configSet); - - req = mock(SolrQueryRequest.class); - when(req.getParams()).thenReturn(reqParams); - // switch a single-valued field to a multi-valued field, which triggers a full rebuild of the - // "temp" collection + // switch a single-valued field to a multi-valued field, which triggers a full rebuild stream = new ContentStreamBase.FileStream(getFile("schema-designer/update-author-field.json")); stream.setContentType(JSON_MIME); - when(req.getContentStreams()).thenReturn(Collections.singletonList(stream)); - - rsp = new SolrQueryResponse(); + when(mockReq.getContentStreams()).thenReturn(Collections.singletonList(stream)); // PUT /schema-designer/update - schemaDesignerAPI.updateSchemaObject(req, rsp); - assertNotNull(rsp.getValues().get("field")); - rspData = rsp.getValues().toSolrParams(); - schemaVersion = rspData.getInt(SCHEMA_VERSION_PARAM); + response = schemaDesignerAPI.updateSchemaObject(configSet, schemaVersion); + assertNotNull(response.unknownProperties().get("field")); + schemaVersion = (Integer) response.unknownProperties().get(SCHEMA_VERSION_PARAM); // add a new type - reqParams.set(SCHEMA_VERSION_PARAM, String.valueOf(schemaVersion)); - reqParams.set(CONFIG_SET_PARAM, configSet); - - req = mock(SolrQueryRequest.class); - when(req.getParams()).thenReturn(reqParams); stream = new ContentStreamBase.FileStream(getFile("schema-designer/add-new-type.json")); stream.setContentType(JSON_MIME); - when(req.getContentStreams()).thenReturn(Collections.singletonList(stream)); - rsp = new SolrQueryResponse(); + when(mockReq.getContentStreams()).thenReturn(Collections.singletonList(stream)); // POST /schema-designer/add - schemaDesignerAPI.addSchemaObject(req, rsp); + response = schemaDesignerAPI.addSchemaObject(configSet, schemaVersion); final String expectedTypeName = "test_txt"; - assertEquals(expectedTypeName, rsp.getValues().get("add-field-type")); - rspData = rsp.getValues().toSolrParams(); - schemaVersion = rspData.getInt(SCHEMA_VERSION_PARAM); - assertNotNull(rsp.getValues().get("fieldTypes")); + assertEquals(expectedTypeName, response.unknownProperties().get("add-field-type")); + schemaVersion = (Integer) response.unknownProperties().get(SCHEMA_VERSION_PARAM); + assertNotNull(response.unknownProperties().get("fieldTypes")); List> fieldTypes = - (List>) rsp.getValues().get("fieldTypes"); + (List>) response.unknownProperties().get("fieldTypes"); Optional> expected = fieldTypes.stream().filter(m -> expectedTypeName.equals(m.get("name"))).findFirst(); assertTrue( "New field type '" + expectedTypeName + "' not found in add type response!", expected.isPresent()); - reqParams.clear(); - reqParams.set(SCHEMA_VERSION_PARAM, String.valueOf(schemaVersion)); - reqParams.set(CONFIG_SET_PARAM, configSet); - - req = mock(SolrQueryRequest.class); - when(req.getParams()).thenReturn(reqParams); stream = new ContentStreamBase.FileStream(getFile("schema-designer/update-type.json")); stream.setContentType(JSON_MIME); - when(req.getContentStreams()).thenReturn(Collections.singletonList(stream)); - rsp = new SolrQueryResponse(); + when(mockReq.getContentStreams()).thenReturn(Collections.singletonList(stream)); // POST /schema-designer/update - schemaDesignerAPI.updateSchemaObject(req, rsp); - rspData = rsp.getValues().toSolrParams(); - schemaVersion = rspData.getInt(SCHEMA_VERSION_PARAM); + response = schemaDesignerAPI.updateSchemaObject(configSet, schemaVersion); + schemaVersion = (Integer) response.unknownProperties().get(SCHEMA_VERSION_PARAM); // query to see how the schema decisions impact retrieval / ranking - reqParams.clear(); - reqParams.set(SCHEMA_VERSION_PARAM, String.valueOf(schemaVersion)); - reqParams.set(CONFIG_SET_PARAM, configSet); - reqParams.set(CommonParams.Q, "*:*"); - req = mock(SolrQueryRequest.class); - when(req.getParams()).thenReturn(reqParams); - rsp = new SolrQueryResponse(); + ModifiableSolrParams queryParams = new ModifiableSolrParams(); + queryParams.set(SCHEMA_VERSION_PARAM, String.valueOf(schemaVersion)); + queryParams.set(CONFIG_SET_PARAM, configSet); + queryParams.set(CommonParams.Q, "*:*"); + when(mockReq.getParams()).thenReturn(queryParams); + when(mockReq.getContentStreams()).thenReturn(null); // GET /schema-designer/query - schemaDesignerAPI.query(req, rsp); - assertNotNull(rsp.getResponseHeader()); - SolrDocumentList results = (SolrDocumentList) rsp.getResponse(); + response = schemaDesignerAPI.query(configSet); + assertNotNull(response.unknownProperties().get("responseHeader")); + SolrDocumentList results = (SolrDocumentList) response.unknownProperties().get("response"); assertEquals(4, results.size()); // Download ZIP - reqParams.clear(); - reqParams.set(CONFIG_SET_PARAM, configSet); - req = mock(SolrQueryRequest.class); - when(req.getParams()).thenReturn(reqParams); - rsp = new SolrQueryResponse(); - schemaDesignerAPI.downloadConfig(req, rsp); - assertNotNull(rsp.getValues().get(CONTENT)); + when(mockReq.getContentStreams()).thenReturn(null); + assertNotNull(schemaDesignerAPI.downloadConfig(configSet)); // publish schema to a config set that can be used by real collections - reqParams.clear(); - reqParams.set(SCHEMA_VERSION_PARAM, String.valueOf(schemaVersion)); - reqParams.set(CONFIG_SET_PARAM, configSet); - String collection = "test123"; - reqParams.set(NEW_COLLECTION_PARAM, collection); - reqParams.set(INDEX_TO_COLLECTION_PARAM, true); - reqParams.set(RELOAD_COLLECTIONS_PARAM, true); - reqParams.set(CLEANUP_TEMP_PARAM, true); - - rsp = new SolrQueryResponse(); - schemaDesignerAPI.publish(req, rsp); + schemaDesignerAPI.publish(configSet, schemaVersion, collection, true, 1, 1, true, true, false); assertNotNull(cc.getZkController().zkStateReader.getCollection(collection)); // listCollectionsForConfig - reqParams.clear(); - reqParams.set(CONFIG_SET_PARAM, configSet); - rsp = new SolrQueryResponse(); - schemaDesignerAPI.listCollectionsForConfig(req, rsp); - List collections = (List) rsp.getValues().get("collections"); + response = schemaDesignerAPI.listCollectionsForConfig(configSet); + List collections = (List) response.unknownProperties().get("collections"); assertNotNull(collections); assertTrue(collections.contains(collection)); @@ -700,39 +527,26 @@ public void testBasicUserWorkflow() throws Exception { public void testFieldUpdates() throws Exception { String configSet = "fieldUpdates"; - ModifiableSolrParams reqParams = new ModifiableSolrParams(); - // Use the prep endpoint to prepare the new schema - reqParams.set(CONFIG_SET_PARAM, configSet); - SolrQueryResponse rsp = new SolrQueryResponse(); - SolrQueryRequest req = mock(SolrQueryRequest.class); - when(req.getParams()).thenReturn(reqParams); - schemaDesignerAPI.prepNewSchema(req, rsp); - assertNotNull(rsp.getValues().get(CONFIG_SET_PARAM)); - assertNotNull(rsp.getValues().get(SCHEMA_VERSION_PARAM)); - SolrParams rspData = rsp.getValues().toSolrParams(); - int schemaVersion = rspData.getInt(SCHEMA_VERSION_PARAM); + FlexibleSolrJerseyResponse response = schemaDesignerAPI.prepNewSchema(configSet, null); + assertNotNull(response.unknownProperties().get(CONFIG_SET_PARAM)); + assertNotNull(response.unknownProperties().get(SCHEMA_VERSION_PARAM)); + int schemaVersion = (Integer) response.unknownProperties().get(SCHEMA_VERSION_PARAM); // add our test field that we'll test various updates to - reqParams.clear(); - reqParams.set(SCHEMA_VERSION_PARAM, String.valueOf(schemaVersion)); - reqParams.set(CONFIG_SET_PARAM, configSet); - req = mock(SolrQueryRequest.class); - when(req.getParams()).thenReturn(reqParams); ContentStreamBase.FileStream stream = new ContentStreamBase.FileStream(getFile("schema-designer/add-new-field.json")); stream.setContentType(JSON_MIME); - when(req.getContentStreams()).thenReturn(Collections.singletonList(stream)); - rsp = new SolrQueryResponse(); + when(mockReq.getContentStreams()).thenReturn(Collections.singletonList(stream)); // POST /schema-designer/add - schemaDesignerAPI.addSchemaObject(req, rsp); - assertNotNull(rsp.getValues().get("add-field")); + response = schemaDesignerAPI.addSchemaObject(configSet, schemaVersion); + assertNotNull(response.unknownProperties().get("add-field")); final String fieldName = "keywords"; Optional> maybeField = - ((List>) rsp.getValues().get("fields")) + ((List>) response.unknownProperties().get("fields")) .stream().filter(m -> fieldName.equals(m.get("name"))).findFirst(); assertTrue(maybeField.isPresent()); SimpleOrderedMap field = maybeField.get(); @@ -801,44 +615,28 @@ public void testFieldUpdates() throws Exception { public void testSchemaDiffEndpoint() throws Exception { String configSet = "testDiff"; - ModifiableSolrParams reqParams = new ModifiableSolrParams(); - // Use the prep endpoint to prepare the new schema - reqParams.set(CONFIG_SET_PARAM, configSet); - SolrQueryResponse rsp = new SolrQueryResponse(); - SolrQueryRequest req = mock(SolrQueryRequest.class); - when(req.getParams()).thenReturn(reqParams); - schemaDesignerAPI.prepNewSchema(req, rsp); - assertNotNull(rsp.getValues().get(CONFIG_SET_PARAM)); - assertNotNull(rsp.getValues().get(SCHEMA_VERSION_PARAM)); + FlexibleSolrJerseyResponse response = schemaDesignerAPI.prepNewSchema(configSet, null); + assertNotNull(response.unknownProperties().get(CONFIG_SET_PARAM)); + assertNotNull(response.unknownProperties().get(SCHEMA_VERSION_PARAM)); + int schemaVersion = (Integer) response.unknownProperties().get(SCHEMA_VERSION_PARAM); // publish schema to a config set that can be used by real collections - reqParams.clear(); - reqParams.set(SCHEMA_VERSION_PARAM, String.valueOf(rsp.getValues().get(SCHEMA_VERSION_PARAM))); - reqParams.set(CONFIG_SET_PARAM, configSet); - String collection = "diff456"; - reqParams.set(NEW_COLLECTION_PARAM, collection); - reqParams.set(INDEX_TO_COLLECTION_PARAM, true); - reqParams.set(RELOAD_COLLECTIONS_PARAM, true); - reqParams.set(CLEANUP_TEMP_PARAM, true); - - rsp = new SolrQueryResponse(); - schemaDesignerAPI.publish(req, rsp); + schemaDesignerAPI.publish(configSet, schemaVersion, collection, true, 1, 1, true, true, false); assertNotNull(cc.getZkController().zkStateReader.getCollection(collection)); // Load the schema designer for the existing config set and make some changes to it - reqParams.clear(); + ModifiableSolrParams reqParams = new ModifiableSolrParams(); reqParams.set(CONFIG_SET_PARAM, configSet); - reqParams.set(ENABLE_DYNAMIC_FIELDS_PARAM, "true"); - reqParams.set(ENABLE_FIELD_GUESSING_PARAM, "false"); - rsp = new SolrQueryResponse(); - schemaDesignerAPI.analyze(req, rsp); + when(mockReq.getParams()).thenReturn(reqParams); + when(mockReq.getContentStreams()).thenReturn(null); + response = schemaDesignerAPI.analyze(configSet, null, null, null, null, true, false, null); // Update id field to not use docValues List> fields = - (List>) rsp.getValues().get("fields"); + (List>) response.unknownProperties().get("fields"); SimpleOrderedMap idFieldMap = fields.stream().filter(field -> field.get("name").equals("id")).findFirst().get(); idFieldMap.remove("copyDest"); // Don't include copyDest as it is not a property of SchemaField @@ -849,47 +647,39 @@ public void testSchemaDiffEndpoint() throws Exception { idFieldMapUpdated.setVal( idFieldMapUpdated.indexOf("omitTermFreqAndPositions", 0), Boolean.FALSE); - SolrParams solrParams = idFieldMapUpdated.toSolrParams(); - Map mapParams = solrParams.toMap(new HashMap<>()); + Map mapParams = idFieldMapUpdated.toSolrParams().toMap(new HashMap<>()); mapParams.put("termVectors", Boolean.FALSE); - reqParams.set( - SCHEMA_VERSION_PARAM, rsp.getValues().toSolrParams().getInt(SCHEMA_VERSION_PARAM)); + schemaVersion = (Integer) response.unknownProperties().get(SCHEMA_VERSION_PARAM); ContentStreamBase.StringStream stringStream = new ContentStreamBase.StringStream(JSONUtil.toJSON(mapParams), JSON_MIME); - when(req.getContentStreams()).thenReturn(Collections.singletonList(stringStream)); + when(mockReq.getContentStreams()).thenReturn(Collections.singletonList(stringStream)); - rsp = new SolrQueryResponse(); - schemaDesignerAPI.updateSchemaObject(req, rsp); + response = schemaDesignerAPI.updateSchemaObject(configSet, schemaVersion); // Add a new field - Integer schemaVersion = rsp.getValues().toSolrParams().getInt(SCHEMA_VERSION_PARAM); - reqParams.set(SCHEMA_VERSION_PARAM, schemaVersion); + schemaVersion = (Integer) response.unknownProperties().get(SCHEMA_VERSION_PARAM); ContentStreamBase.FileStream fileStream = new ContentStreamBase.FileStream(getFile("schema-designer/add-new-field.json")); fileStream.setContentType(JSON_MIME); - when(req.getContentStreams()).thenReturn(Collections.singletonList(fileStream)); - rsp = new SolrQueryResponse(); + when(mockReq.getContentStreams()).thenReturn(Collections.singletonList(fileStream)); // POST /schema-designer/add - schemaDesignerAPI.addSchemaObject(req, rsp); - assertNotNull(rsp.getValues().get("add-field")); + response = schemaDesignerAPI.addSchemaObject(configSet, schemaVersion); + assertNotNull(response.unknownProperties().get("add-field")); // Add a new field type - schemaVersion = rsp.getValues().toSolrParams().getInt(SCHEMA_VERSION_PARAM); - reqParams.set(SCHEMA_VERSION_PARAM, schemaVersion); + schemaVersion = (Integer) response.unknownProperties().get(SCHEMA_VERSION_PARAM); fileStream = new ContentStreamBase.FileStream(getFile("schema-designer/add-new-type.json")); fileStream.setContentType(JSON_MIME); - when(req.getContentStreams()).thenReturn(Collections.singletonList(fileStream)); - rsp = new SolrQueryResponse(); + when(mockReq.getContentStreams()).thenReturn(Collections.singletonList(fileStream)); // POST /schema-designer/add - schemaDesignerAPI.addSchemaObject(req, rsp); - assertNotNull(rsp.getValues().get("add-field-type")); + response = schemaDesignerAPI.addSchemaObject(configSet, schemaVersion); + assertNotNull(response.unknownProperties().get("add-field-type")); // Let's do a diff now - rsp = new SolrQueryResponse(); - schemaDesignerAPI.getSchemaDiff(req, rsp); + response = schemaDesignerAPI.getSchemaDiff(configSet); - Map diff = (Map) rsp.getValues().get("diff"); + Map diff = (Map) response.unknownProperties().get("diff"); // field asserts assertNotNull(diff.get("fields")); @@ -929,7 +719,7 @@ public void testSchemaDiffEndpoint() throws Exception { assertNotNull(fieldTypesAdded.get("test_txt")); } - protected void assertDesignerSettings(Map expected, NamedList actual) { + protected void assertDesignerSettings(Map expected, Map actual) { for (String expKey : expected.keySet()) { Object expValue = expected.get(expKey); assertEquals( From 20f4431b95c42e26920646deaeb378e34fcfb5fd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 15:31:31 +0000 Subject: [PATCH 03/13] Address code review feedback: fix copyFrom parameter shadowing and comments Co-authored-by: epugh <22395+epugh@users.noreply.github.com> --- .../apache/solr/handler/designer/SchemaDesignerAPI.java | 7 +++---- .../solr/handler/designer/TestSchemaDesignerAPI.java | 5 +++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java index 7a12f4d1f9ad..7ae28e0fb79d 100644 --- a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java +++ b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java @@ -195,11 +195,10 @@ public FlexibleSolrJerseyResponse prepNewSchema(String configSet, String copyFro requireNotEmpty(CONFIG_SET_PARAM, configSet); validateNewConfigSetName(configSet); - if (copyFrom == null) { - copyFrom = DEFAULT_CONFIGSET_NAME; - } + final String effectiveCopyFrom = copyFrom != null ? copyFrom : DEFAULT_CONFIGSET_NAME; - SchemaDesignerSettings settings = getMutableSchemaForConfigSet(configSet, -1, copyFrom); + SchemaDesignerSettings settings = + getMutableSchemaForConfigSet(configSet, -1, effectiveCopyFrom); ManagedIndexSchema schema = settings.getSchema(); String mutableId = getMutableId(configSet); diff --git a/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java b/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java index 50ebe9b9abc2..8fff2fec0652 100644 --- a/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java +++ b/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java @@ -396,7 +396,7 @@ public void testBasicUserWorkflow() throws Exception { schemaVersion = (Integer) response.unknownProperties().get(SCHEMA_VERSION_PARAM); - // add back all the default languages (using "*" wildcard → empty list) + // add back all the default languages (using "*" wildcard -> empty list) response = schemaDesignerAPI.analyze( configSet, schemaVersion, null, null, List.of("*"), false, null, null); @@ -443,7 +443,8 @@ public void testBasicUserWorkflow() throws Exception { assertNotNull(response.unknownProperties().get("fields")); // update an existing field - // switch a single-valued field to a multi-valued field, which triggers a full rebuild + // switch a single-valued field to a multi-valued field, which triggers a full rebuild of the + // "temp" collection stream = new ContentStreamBase.FileStream(getFile("schema-designer/update-author-field.json")); stream.setContentType(JSON_MIME); when(mockReq.getContentStreams()).thenReturn(Collections.singletonList(stream)); From de905296eb14e89a21cd696ab37b89a51b726e1a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 21:23:45 +0000 Subject: [PATCH 04/13] Fix query() returning no results when sample docs not in memory (empty docs guard) Co-authored-by: epugh <22395+epugh@users.noreply.github.com> --- .../solr/handler/designer/SchemaDesignerAPI.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java index 7ae28e0fb79d..ffcdea1c6459 100644 --- a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java +++ b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java @@ -800,12 +800,14 @@ public FlexibleSolrJerseyResponse query(String configSet) throws Exception { version, currentVersion); List docs = configSetHelper.retrieveSampleDocs(configSet); - ManagedIndexSchema schema = loadLatestSchema(mutableId); - errorsDuringIndexing = - indexSampleDocsWithRebuildOnAnalysisError( - schema.getUniqueKeyField().getName(), docs, mutableId, true, null); - // the version changes when you index (due to field guessing URP) - currentVersion = configSetHelper.getCurrentSchemaVersion(mutableId); + if (!docs.isEmpty()) { + ManagedIndexSchema schema = loadLatestSchema(mutableId); + errorsDuringIndexing = + indexSampleDocsWithRebuildOnAnalysisError( + schema.getUniqueKeyField().getName(), docs, mutableId, true, null); + // the version changes when you index (due to field guessing URP) + currentVersion = configSetHelper.getCurrentSchemaVersion(mutableId); + } indexedVersion.put(mutableId, currentVersion); } From 912c76260ed2806d7b2ffdcab2f7ce8754734348 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 12:15:10 +0000 Subject: [PATCH 05/13] Fix schema designer download: correct URL, add Content-Disposition header, sanitize filename Co-authored-by: epugh <22395+epugh@users.noreply.github.com> --- .../solr/client/api/endpoint/SchemaDesignerApi.java | 4 ++-- .../solr/handler/designer/SchemaDesignerAPI.java | 11 +++++++++-- .../solr/handler/designer/TestSchemaDesignerAPI.java | 7 ++++++- .../web/js/angular/controllers/schema-designer.js | 4 ++-- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/solr/api/src/java/org/apache/solr/client/api/endpoint/SchemaDesignerApi.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/SchemaDesignerApi.java index 295a77f2d70a..d2705876e420 100644 --- a/solr/api/src/java/org/apache/solr/client/api/endpoint/SchemaDesignerApi.java +++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/SchemaDesignerApi.java @@ -28,7 +28,7 @@ import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.StreamingOutput; +import jakarta.ws.rs.core.Response; import java.util.List; import org.apache.solr.client.api.model.FlexibleSolrJerseyResponse; import org.apache.solr.client.api.model.SolrJerseyResponse; @@ -112,7 +112,7 @@ FlexibleSolrJerseyResponse listCollectionsForConfig(@QueryParam("configSet") Str @Extension(properties = {@ExtensionProperty(name = RAW_OUTPUT_PROPERTY, value = "true")}) }) @Produces("application/zip") - StreamingOutput downloadConfig(@QueryParam("configSet") String configSet) throws Exception; + Response downloadConfig(@QueryParam("configSet") String configSet) throws Exception; @POST @Path("/add") diff --git a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java index ffcdea1c6459..c8b1917b99b9 100644 --- a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java +++ b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java @@ -23,6 +23,7 @@ import static org.apache.solr.security.PermissionNameProvider.Name.CONFIG_READ_PERM; import jakarta.inject.Inject; +import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.StreamingOutput; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -403,7 +404,7 @@ protected Map listEnabledConfigs() throws IOException { @Override @PermissionName(CONFIG_READ_PERM) - public StreamingOutput downloadConfig(String configSet) throws Exception { + public Response downloadConfig(String configSet) throws Exception { requireNotEmpty(CONFIG_SET_PARAM, configSet); String mutableId = getMutableId(configSet); @@ -424,7 +425,13 @@ public StreamingOutput downloadConfig(String configSet) throws Exception { } final byte[] zipBytes = configSetHelper.downloadAndZipConfigSet(configId); - return outputStream -> outputStream.write(zipBytes); + // Sanitize configSet to safe filename characters to prevent header injection + final String safeConfigSet = configSet.replaceAll("[^a-zA-Z0-9_\\-.]", "_"); + final String fileName = safeConfigSet + "_configset.zip"; + return Response.ok((StreamingOutput) outputStream -> outputStream.write(zipBytes)) + .type("application/zip") + .header("Content-Disposition", "attachment; filename=\"" + fileName + "\"") + .build(); } @Override diff --git a/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java b/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java index 8fff2fec0652..21bd2eef32a8 100644 --- a/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java +++ b/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java @@ -497,7 +497,12 @@ public void testBasicUserWorkflow() throws Exception { // Download ZIP when(mockReq.getContentStreams()).thenReturn(null); - assertNotNull(schemaDesignerAPI.downloadConfig(configSet)); + jakarta.ws.rs.core.Response downloadResponse = schemaDesignerAPI.downloadConfig(configSet); + assertNotNull(downloadResponse); + assertEquals(200, downloadResponse.getStatus()); + assertTrue( + String.valueOf(downloadResponse.getHeaderString("Content-Disposition")) + .contains("_configset.zip")); // publish schema to a config set that can be used by real collections String collection = "test123"; diff --git a/solr/webapp/web/js/angular/controllers/schema-designer.js b/solr/webapp/web/js/angular/controllers/schema-designer.js index 07013446a279..f4572ec6d629 100644 --- a/solr/webapp/web/js/angular/controllers/schema-designer.js +++ b/solr/webapp/web/js/angular/controllers/schema-designer.js @@ -1526,7 +1526,7 @@ solrAdminApp.controller('SchemaDesignerController', function ($scope, $timeout, if (sessionStorage.getItem("auth.header")) { var fileName = $scope.currentSchema+"_configset.zip"; var xhr = new XMLHttpRequest(); - xhr.open("GET", "/api/schema-designer/download/"+fileName+"?wt=raw&configSet="+$scope.currentSchema, true); + xhr.open("GET", "/api/schema-designer/download?configSet="+$scope.currentSchema, true); xhr.setRequestHeader('Authorization', sessionStorage.getItem("auth.header")); xhr.responseType = 'blob'; xhr.addEventListener('load',function() { @@ -1543,7 +1543,7 @@ solrAdminApp.controller('SchemaDesignerController', function ($scope, $timeout, }) xhr.send(); } else { - location.href = "/api/schema-designer/download/"+$scope.currentSchema+"_configset.zip?wt=raw&configSet=" + $scope.currentSchema; + location.href = "/api/schema-designer/download?configSet=" + $scope.currentSchema; } }; From 7c16593a979bd43b6de5d1f03dd414d73ecdf2c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:49:54 +0000 Subject: [PATCH 06/13] Fix query response structure: wrap SolrDocumentList in numFound/start/docs map for JS rendering Co-authored-by: epugh <22395+epugh@users.noreply.github.com> --- .../handler/designer/SchemaDesignerAPI.java | 22 ++++++++++++++++--- .../designer/TestSchemaDesignerAPI.java | 21 +++++++++++++----- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java index c8b1917b99b9..daa78c208a93 100644 --- a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java +++ b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java @@ -60,6 +60,7 @@ import org.apache.solr.client.solrj.response.QueryResponse; import org.apache.solr.cloud.ZkConfigSetService; import org.apache.solr.cloud.ZkSolrResourceLoader; +import org.apache.solr.common.SolrDocumentList; import org.apache.solr.common.SolrException; import org.apache.solr.common.SolrInputDocument; import org.apache.solr.common.SolrInputField; @@ -826,9 +827,24 @@ public FlexibleSolrJerseyResponse query(String configSet) throws Exception { // execute the user's query against the temp collection QueryResponse qr = cloudClient().query(mutableId, solrQueryRequest.getParams()); - Map response = new HashMap<>(); - qr.getResponse().forEach((name, val) -> response.put(name, val)); - return buildFlexibleResponse(response); + Map responseMap = new HashMap<>(); + qr.getResponse() + .forEach( + (name, val) -> { + if ("response".equals(name) && val instanceof SolrDocumentList) { + // SolrDocumentList extends ArrayList, so Jackson would serialize it as a plain + // array, losing numFound/start metadata that the UI expects at data.response.docs + SolrDocumentList docList = (SolrDocumentList) val; + Map responseObj = new HashMap<>(); + responseObj.put("numFound", docList.getNumFound()); + responseObj.put("start", docList.getStart()); + responseObj.put("docs", new ArrayList<>(docList)); + responseMap.put(name, responseObj); + } else { + responseMap.put(name, val); + } + }); + return buildFlexibleResponse(responseMap); } /** diff --git a/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java b/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java index 21bd2eef32a8..58bc0e34b06c 100644 --- a/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java +++ b/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java @@ -37,7 +37,6 @@ import org.apache.solr.client.solrj.request.SolrQuery; import org.apache.solr.client.solrj.response.QueryResponse; import org.apache.solr.cloud.SolrCloudTestCase; -import org.apache.solr.common.SolrDocumentList; import org.apache.solr.common.SolrException; import org.apache.solr.common.cloud.SolrZkClient; import org.apache.solr.common.params.CommonParams; @@ -219,8 +218,15 @@ public void testAddTechproductsProgressively() throws Exception { // GET /schema-designer/query response = schemaDesignerAPI.query(configSet); assertNotNull(response.unknownProperties().get("responseHeader")); - SolrDocumentList results = (SolrDocumentList) response.unknownProperties().get("response"); - assertEquals(47, results.getNumFound()); + @SuppressWarnings("unchecked") + Map queryResponse = + (Map) response.unknownProperties().get("response"); + assertNotNull("response object must be a map with numFound/docs", queryResponse); + assertEquals(47L, queryResponse.get("numFound")); + @SuppressWarnings("unchecked") + List queryDocs = (List) queryResponse.get("docs"); + assertNotNull("response.docs must be a list", queryDocs); + assertTrue("response.docs must be non-empty", queryDocs.size() > 0); // publish schema to a config set that can be used by real collections String collection = "techproducts"; @@ -492,8 +498,13 @@ public void testBasicUserWorkflow() throws Exception { // GET /schema-designer/query response = schemaDesignerAPI.query(configSet); assertNotNull(response.unknownProperties().get("responseHeader")); - SolrDocumentList results = (SolrDocumentList) response.unknownProperties().get("response"); - assertEquals(4, results.size()); + @SuppressWarnings("unchecked") + Map queryResponse2 = + (Map) response.unknownProperties().get("response"); + assertNotNull("response object must be a map with numFound/docs", queryResponse2); + @SuppressWarnings("unchecked") + List queryDocs2 = (List) queryResponse2.get("docs"); + assertEquals(4, queryDocs2.size()); // Download ZIP when(mockReq.getContentStreams()).thenReturn(null); From b382a4cea484f364b21e30076b17430be7a234f4 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Fri, 6 Mar 2026 12:30:50 -0500 Subject: [PATCH 07/13] Lint clean ups. "multivalued" is how we spell it ;-) Not "multi-valued" ;-) --- .../designer/DefaultSampleDocumentsLoader.java | 8 ++++---- .../handler/designer/DefaultSchemaSuggester.java | 15 +++++---------- .../solr/handler/designer/SampleDocuments.java | 2 +- .../solr/handler/designer/SchemaDesignerAPI.java | 2 +- .../designer/SchemaDesignerConfigSetHelper.java | 10 +++++----- .../handler/designer/SchemaDesignerConstants.java | 5 ----- .../handler/designer/SchemaDesignerSettings.java | 2 +- .../handler/designer/TestSchemaDesignerAPI.java | 4 ++-- 8 files changed, 19 insertions(+), 29 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/handler/designer/DefaultSampleDocumentsLoader.java b/solr/core/src/java/org/apache/solr/handler/designer/DefaultSampleDocumentsLoader.java index 921c72bfcdc8..dc60df984b1d 100644 --- a/solr/core/src/java/org/apache/solr/handler/designer/DefaultSampleDocumentsLoader.java +++ b/solr/core/src/java/org/apache/solr/handler/designer/DefaultSampleDocumentsLoader.java @@ -101,7 +101,7 @@ public SampleDocuments parseDocsFromStream( + MAX_STREAM_SIZE + " bytes is the max upload size for sample documents."); } - // use a byte stream for the parsers in case they need to re-parse using a different strategy + // use a byte stream for the parsers in case they need to reparse using a different strategy // e.g. JSON vs. JSON lines or different CSV strategies ... ContentStreamBase.ByteArrayStream byteStream = new ContentStreamBase.ByteArrayStream(uploadedBytes, fileSource, contentType); @@ -161,7 +161,7 @@ protected List loadJsonLines( String line; while ((line = br.readLine()) != null) { line = line.trim(); - if (!line.isEmpty() && line.startsWith("{") && line.endsWith("}")) { + if (line.startsWith("{") && line.endsWith("}")) { Object jsonLine = ObjectBuilder.getVal(new JSONParser(line)); if (jsonLine instanceof Map) { docs.add((Map) jsonLine); @@ -203,7 +203,7 @@ protected List loadJsonDocs( if (lines.length > 1) { for (String line : lines) { line = line.trim(); - if (!line.isEmpty() && line.startsWith("{") && line.endsWith("}")) { + if (line.startsWith("{") && line.endsWith("}")) { isJsonLines = true; break; } @@ -298,7 +298,7 @@ protected List> loadJsonLines(String[] lines) throws IOExcep List> docs = new ArrayList<>(lines.length); for (String line : lines) { line = line.trim(); - if (!line.isEmpty() && line.startsWith("{") && line.endsWith("}")) { + if (line.startsWith("{") && line.endsWith("}")) { Object jsonLine = ObjectBuilder.getVal(new JSONParser(line)); if (jsonLine instanceof Map) { docs.add((Map) jsonLine); diff --git a/solr/core/src/java/org/apache/solr/handler/designer/DefaultSchemaSuggester.java b/solr/core/src/java/org/apache/solr/handler/designer/DefaultSchemaSuggester.java index 543b7a77af16..963cae662830 100644 --- a/solr/core/src/java/org/apache/solr/handler/designer/DefaultSchemaSuggester.java +++ b/solr/core/src/java/org/apache/solr/handler/designer/DefaultSchemaSuggester.java @@ -170,8 +170,7 @@ public Optional suggestField( throw new IllegalStateException("FieldType '" + fieldTypeName + "' not found in the schema!"); } - Map fieldProps = - guessFieldProps(fieldName, fieldType, sampleValues, isMV, schema); + Map fieldProps = guessFieldProps(fieldName, fieldType, isMV, schema); SchemaField schemaField = schema.newField(fieldName, fieldTypeName, fieldProps); return Optional.of(schemaField); } @@ -179,9 +178,9 @@ public Optional suggestField( @Override public ManagedIndexSchema adaptExistingFieldToData( SchemaField schemaField, List sampleValues, ManagedIndexSchema schema) { - // Promote a single-valued to multi-valued if needed + // Promote a single-valued to multivalued if needed if (!schemaField.multiValued() && isMultiValued(sampleValues)) { - // this existing field needs to be promoted to multi-valued + // this existing field needs to be promoted to multivalued SimpleOrderedMap fieldProps = schemaField.getNamedPropertyValues(false); fieldProps.add("multiValued", true); fieldProps.remove("name"); @@ -210,7 +209,7 @@ public Map> transposeDocs(List docs) { Collection fieldValues = doc.getFieldValues(f); if (fieldValues != null && !fieldValues.isEmpty()) { if (fieldValues.size() == 1) { - // flatten so every field doesn't end up multi-valued + // flatten so every field doesn't end up multivalued values.add(fieldValues.iterator().next()); } else { // truly multi-valued @@ -395,11 +394,7 @@ protected boolean isMultiValued(final List sampleValues) { } protected Map guessFieldProps( - String fieldName, - FieldType fieldType, - List sampleValues, - boolean isMV, - IndexSchema schema) { + String fieldName, FieldType fieldType, boolean isMV, IndexSchema schema) { Map props = new HashMap<>(); props.put("indexed", "true"); diff --git a/solr/core/src/java/org/apache/solr/handler/designer/SampleDocuments.java b/solr/core/src/java/org/apache/solr/handler/designer/SampleDocuments.java index b98c5995db28..6037db515241 100644 --- a/solr/core/src/java/org/apache/solr/handler/designer/SampleDocuments.java +++ b/solr/core/src/java/org/apache/solr/handler/designer/SampleDocuments.java @@ -56,7 +56,7 @@ public List appendDocs( return id != null && !ids.contains(id); // doc has ID, and it's not already in the set }) - .collect(Collectors.toList()); + .toList(); parsed.addAll(toAdd); if (maxDocsToLoad > 0 && parsed.size() > maxDocsToLoad) { parsed = parsed.subList(0, maxDocsToLoad); diff --git a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java index daa78c208a93..8be794493af6 100644 --- a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java +++ b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java @@ -372,7 +372,7 @@ public FlexibleSolrJerseyResponse getSampleValue( @Override @PermissionName(CONFIG_READ_PERM) - public FlexibleSolrJerseyResponse listCollectionsForConfig(String configSet) throws Exception { + public FlexibleSolrJerseyResponse listCollectionsForConfig(String configSet) { requireNotEmpty(CONFIG_SET_PARAM, configSet); return buildFlexibleResponse( Collections.singletonMap( diff --git a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerConfigSetHelper.java b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerConfigSetHelper.java index 90a7763a16e7..b90a11ab8026 100644 --- a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerConfigSetHelper.java +++ b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerConfigSetHelper.java @@ -383,7 +383,7 @@ boolean updateField( } } - // detect if they're trying to copy multi-valued fields into a single-valued field + // detect if they're trying to copy multivalued fields into a single-valued field Object multiValued = diff.get(MULTIVALUED); if (multiValued == null) { // mv not overridden explicitly, but we need the actual value, which will come from the new @@ -404,7 +404,7 @@ boolean updateField( name, src); multiValued = Boolean.TRUE; - diff.put(MULTIVALUED, multiValued); + diff.put(MULTIVALUED, true); break; } } @@ -415,8 +415,8 @@ boolean updateField( validateMultiValuedChange(configSet, schemaField, Boolean.FALSE); } - // switch from single-valued to multi-valued requires a full rebuild - // See SOLR-12185 ... if we're switching from single to multi-valued, then it's a big operation + // switch from single-valued to multivalued requires a full rebuild + // See SOLR-12185 ... if we're switching from single to multivalued, then it's a big operation if (fieldHasMultiValuedChange(multiValued, schemaField)) { needsRebuild = true; log.warn( @@ -709,7 +709,7 @@ boolean applyCopyFieldUpdates( continue; // cannot copy to self } - // make sure the field exists and is multi-valued if this field is + // make sure the field exists and is multivalued if this field is SchemaField toAddField = schema.getFieldOrNull(toAdd); if (toAddField != null) { if (!field.multiValued() || toAddField.multiValued()) { diff --git a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerConstants.java b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerConstants.java index 0ad93d90d27c..5cb31ec954a6 100644 --- a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerConstants.java +++ b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerConstants.java @@ -21,18 +21,13 @@ public interface SchemaDesignerConstants { String CONFIG_SET_PARAM = "configSet"; String COPY_FROM_PARAM = "copyFrom"; String SCHEMA_VERSION_PARAM = "schemaVersion"; - String RELOAD_COLLECTIONS_PARAM = "reloadCollections"; - String INDEX_TO_COLLECTION_PARAM = "indexToCollection"; String NEW_COLLECTION_PARAM = "newCollection"; - String CLEANUP_TEMP_PARAM = "cleanupTemp"; String ENABLE_DYNAMIC_FIELDS_PARAM = "enableDynamicFields"; String ENABLE_FIELD_GUESSING_PARAM = "enableFieldGuessing"; String ENABLE_NESTED_DOCS_PARAM = "enableNestedDocs"; String TEMP_COLLECTION_PARAM = "tempCollection"; String PUBLISHED_VERSION = "publishedVersion"; - String DISABLE_DESIGNER_PARAM = "disableDesigner"; String DISABLED = "disabled"; - String DOC_ID_PARAM = "docId"; String FIELD_PARAM = "field"; String UNIQUE_KEY_FIELD_PARAM = "uniqueKeyField"; String AUTO_CREATE_FIELDS = "update.autoCreateFields"; diff --git a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerSettings.java b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerSettings.java index 0216d433ee56..53955e9a0395 100644 --- a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerSettings.java +++ b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerSettings.java @@ -25,7 +25,7 @@ import java.util.Optional; import org.apache.solr.schema.ManagedIndexSchema; -class SchemaDesignerSettings implements SchemaDesignerConstants { +public class SchemaDesignerSettings implements SchemaDesignerConstants { private String copyFrom; private boolean isDisabled; diff --git a/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java b/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java index 58bc0e34b06c..a64f8cfbce35 100644 --- a/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java +++ b/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java @@ -226,7 +226,7 @@ public void testAddTechproductsProgressively() throws Exception { @SuppressWarnings("unchecked") List queryDocs = (List) queryResponse.get("docs"); assertNotNull("response.docs must be a list", queryDocs); - assertTrue("response.docs must be non-empty", queryDocs.size() > 0); + assertFalse("response.docs must be non-empty", queryDocs.isEmpty()); // publish schema to a config set that can be used by real collections String collection = "techproducts"; @@ -449,7 +449,7 @@ public void testBasicUserWorkflow() throws Exception { assertNotNull(response.unknownProperties().get("fields")); // update an existing field - // switch a single-valued field to a multi-valued field, which triggers a full rebuild of the + // switch a single-valued field to a multivalued field, which triggers a full rebuild of the // "temp" collection stream = new ContentStreamBase.FileStream(getFile("schema-designer/update-author-field.json")); stream.setContentType(JSON_MIME); From 233162e28d4dae97fdc2ddb343ca272a29e1dc11 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Tue, 10 Mar 2026 05:48:41 -0400 Subject: [PATCH 08/13] code review and manual testing --- .../DefaultSampleDocumentsLoader.java | 31 +++++++++-------- .../SchemaDesignerConfigSetHelper.java | 33 +++---------------- 2 files changed, 19 insertions(+), 45 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/handler/designer/DefaultSampleDocumentsLoader.java b/solr/core/src/java/org/apache/solr/handler/designer/DefaultSampleDocumentsLoader.java index dc60df984b1d..4b5e3d61d3ac 100644 --- a/solr/core/src/java/org/apache/solr/handler/designer/DefaultSampleDocumentsLoader.java +++ b/solr/core/src/java/org/apache/solr/handler/designer/DefaultSampleDocumentsLoader.java @@ -152,7 +152,6 @@ protected List loadCsvDocs( .loadDocs(stream); } - @SuppressWarnings("unchecked") protected List loadJsonLines( ContentStreamBase.ByteArrayStream stream, final int maxDocsToLoad) throws IOException { List> docs = new ArrayList<>(); @@ -160,13 +159,7 @@ protected List loadJsonLines( BufferedReader br = new BufferedReader(r); String line; while ((line = br.readLine()) != null) { - line = line.trim(); - if (line.startsWith("{") && line.endsWith("}")) { - Object jsonLine = ObjectBuilder.getVal(new JSONParser(line)); - if (jsonLine instanceof Map) { - docs.add((Map) jsonLine); - } - } + parseStringToJson(docs, line); if (maxDocsToLoad > 0 && docs.size() == maxDocsToLoad) { break; } @@ -176,6 +169,19 @@ protected List loadJsonLines( return docs.stream().map(JsonLoader::buildDoc).collect(Collectors.toList()); } + private void parseStringToJson(List> docs, String line) throws IOException { + line = line.trim(); + if (line.startsWith("{") && line.endsWith("}")) { + Object jsonLine = ObjectBuilder.getVal(new JSONParser(line)); + if (jsonLine instanceof Map rawMap) { + // JSON object keys are always Strings; the cast is safe + @SuppressWarnings("unchecked") + Map typedMap = (Map) rawMap; + docs.add(typedMap); + } + } + } + @SuppressWarnings("unchecked") protected List loadJsonDocs( ContentStreamBase.ByteArrayStream stream, final int maxDocsToLoad) throws IOException { @@ -293,17 +299,10 @@ protected List parseXmlDocs(XMLStreamReader parser, final int } } - @SuppressWarnings("unchecked") protected List> loadJsonLines(String[] lines) throws IOException { List> docs = new ArrayList<>(lines.length); for (String line : lines) { - line = line.trim(); - if (line.startsWith("{") && line.endsWith("}")) { - Object jsonLine = ObjectBuilder.getVal(new JSONParser(line)); - if (jsonLine instanceof Map) { - docs.add((Map) jsonLine); - } - } + parseStringToJson(docs, line); } return docs; } diff --git a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerConfigSetHelper.java b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerConfigSetHelper.java index b90a11ab8026..40708f73e995 100644 --- a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerConfigSetHelper.java +++ b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerConfigSetHelper.java @@ -47,7 +47,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -74,7 +73,6 @@ import org.apache.solr.common.SolrException.ErrorCode; import org.apache.solr.common.SolrInputDocument; import org.apache.solr.common.cloud.DocCollection; -import org.apache.solr.common.cloud.Replica; import org.apache.solr.common.cloud.SolrZkClient; import org.apache.solr.common.cloud.ZkMaintenanceUtils; import org.apache.solr.common.cloud.ZkStateReader; @@ -536,29 +534,6 @@ static byte[] readAllBytes(IOSupplier hasStream) throws IOException } } - private String getBaseUrl(final String collection) { - String baseUrl = null; - try { - Set liveNodes = zkStateReader().getClusterState().getLiveNodes(); - DocCollection docColl = zkStateReader().getCollection(collection); - if (docColl != null && !liveNodes.isEmpty()) { - Optional maybeActive = - docColl.getReplicas().stream().filter(r -> r.isActive(liveNodes)).findAny(); - if (maybeActive.isPresent()) { - baseUrl = maybeActive.get().getBaseUrl(); - } - } - } catch (Exception exc) { - log.warn("Failed to lookup base URL for collection {}", collection, exc); - } - - if (baseUrl == null) { - baseUrl = zkStateReader().getBaseUrlForNodeName(cc.getZkController().getNodeName()); - } - - return baseUrl; - } - protected String getManagedSchemaZkPath(final String configSet) { return getConfigSetZkPath(configSet, DEFAULT_MANAGED_SCHEMA_RESOURCE_NAME); } @@ -819,8 +794,8 @@ protected ManagedIndexSchema removeLanguageSpecificObjectsAndFiles( final Set toRemove = types.values().stream() .filter(this::isTextType) - .filter(t -> !languages.contains(t.getTypeName().substring(TEXT_PREFIX_LEN))) .map(FieldType::getTypeName) + .filter(typeName -> !languages.contains(typeName.substring(TEXT_PREFIX_LEN))) .filter(t -> !usedTypes.contains(t)) // not explicitly used by a field .collect(Collectors.toSet()); @@ -961,9 +936,9 @@ protected ManagedIndexSchema restoreLanguageSpecificObjectsAndFiles( List addDynFields = Arrays.stream(copyFromSchema.getDynamicFields()) - .filter(df -> langFieldTypeNames.contains(df.getPrototype().getType().getTypeName())) - .filter(df -> !existingDynFields.contains(df.getPrototype().getName())) .map(IndexSchema.DynamicField::getPrototype) + .filter(prototype -> langFieldTypeNames.contains(prototype.getType().getTypeName())) + .filter(prototype -> !existingDynFields.contains(prototype.getName())) .collect(Collectors.toList()); if (!addDynFields.isEmpty()) { schema = schema.addDynamicFields(addDynFields, null, false); @@ -1035,8 +1010,8 @@ protected ManagedIndexSchema restoreDynamicFields( .collect(Collectors.toSet()); List toAdd = Arrays.stream(dynamicFields) - .filter(df -> !existingDFNames.contains(df.getPrototype().getName())) .map(IndexSchema.DynamicField::getPrototype) + .filter(prototype -> !existingDFNames.contains(prototype.getName())) .collect(Collectors.toList()); // only restore language specific dynamic fields that match our langSet From 11f5209ddcb4a780dc56942f6ac904400d17a674 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Tue, 10 Mar 2026 05:59:20 -0400 Subject: [PATCH 09/13] track change --- ...-18152-migrate-schemadesignerapi-to-v2-annotations.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 changelog/unreleased/SOLR-18152-migrate-schemadesignerapi-to-v2-annotations.yml diff --git a/changelog/unreleased/SOLR-18152-migrate-schemadesignerapi-to-v2-annotations.yml b/changelog/unreleased/SOLR-18152-migrate-schemadesignerapi-to-v2-annotations.yml new file mode 100644 index 000000000000..f91837f7d7ce --- /dev/null +++ b/changelog/unreleased/SOLR-18152-migrate-schemadesignerapi-to-v2-annotations.yml @@ -0,0 +1,8 @@ +# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc +title: Migrate Schema Designer API to JAX-RS. Fix bug preventing analysis of sample documents from running. +type: fixed # added, changed, fixed, deprecated, removed, dependency_update, security, other +authors: + - name: Eric Pugh +links: + - name: SOLR-18152 + url: https://issues.apache.org/jira/browse/SOLR-18152 From 6a46d4097f50c4aad2ec3663d4163853daf4e7f1 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Tue, 10 Mar 2026 06:06:10 -0400 Subject: [PATCH 10/13] Finally fix the visibility warning! --- .../apache/solr/handler/designer/SchemaDesignerAPI.java | 7 +++---- .../solr/handler/designer/SchemaDesignerSettings.java | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java index 8be794493af6..336b683701d2 100644 --- a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java +++ b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java @@ -935,7 +935,7 @@ protected ManagedIndexSchema analyzeInputDocs( return schema; } - protected SchemaDesignerSettings getMutableSchemaForConfigSet( + SchemaDesignerSettings getMutableSchemaForConfigSet( final String configSet, final int schemaVersion, String copyFrom) throws IOException { // The designer works with mutable config sets stored in a "temp" znode in ZK instead of the // "live" configSet @@ -1156,7 +1156,7 @@ protected long waitToSeeSampleDocs(String collectionName, long numAdded) return numFound; } - protected Map buildResponse( + Map buildResponse( String configSet, final ManagedIndexSchema schema, SchemaDesignerSettings settings, @@ -1310,8 +1310,7 @@ protected Map readJsonFromRequest() throws IOException { return (Map) json; } - protected void addSettingsToResponse( - SchemaDesignerSettings settings, final Map response) { + void addSettingsToResponse(SchemaDesignerSettings settings, final Map response) { response.put(LANGUAGES_PARAM, settings.getLanguages()); response.put(ENABLE_FIELD_GUESSING_PARAM, settings.fieldGuessingEnabled()); response.put(ENABLE_DYNAMIC_FIELDS_PARAM, settings.dynamicFieldsEnabled()); diff --git a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerSettings.java b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerSettings.java index 53955e9a0395..0216d433ee56 100644 --- a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerSettings.java +++ b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerSettings.java @@ -25,7 +25,7 @@ import java.util.Optional; import org.apache.solr.schema.ManagedIndexSchema; -public class SchemaDesignerSettings implements SchemaDesignerConstants { +class SchemaDesignerSettings implements SchemaDesignerConstants { private String copyFrom; private boolean isDisabled; From bcb6a86b750a85354a8e416c47e3f09264fc1003 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Tue, 10 Mar 2026 06:36:35 -0400 Subject: [PATCH 11/13] Restore surfacing indexing errors. --- .../handler/designer/SchemaDesignerAPI.java | 11 +++- .../designer/TestSchemaDesignerAPI.java | 52 +++++++++++++++++++ .../js/angular/controllers/schema-designer.js | 5 ++ 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java index 336b683701d2..587109ffe8f3 100644 --- a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java +++ b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java @@ -820,9 +820,16 @@ public FlexibleSolrJerseyResponse query(String configSet) throws Exception { } if (errorsDuringIndexing != null) { - throw new SolrException( - SolrException.ErrorCode.BAD_REQUEST, + Map errorResponse = new HashMap<>(); + addErrorToResponse( + mutableId, + new SolrException( + SolrException.ErrorCode.BAD_REQUEST, + "Failed to re-index sample documents after schema updated."), + errorsDuringIndexing, + errorResponse, "Failed to re-index sample documents after schema updated."); + return buildFlexibleResponse(errorResponse); } // execute the user's query against the temp collection diff --git a/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java b/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java index a64f8cfbce35..5c40e71dc372 100644 --- a/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java +++ b/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; @@ -34,10 +35,12 @@ import java.util.Optional; import java.util.stream.Stream; import org.apache.solr.client.api.model.FlexibleSolrJerseyResponse; +import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.request.SolrQuery; import org.apache.solr.client.solrj.response.QueryResponse; import org.apache.solr.cloud.SolrCloudTestCase; import org.apache.solr.common.SolrException; +import org.apache.solr.common.SolrInputDocument; import org.apache.solr.common.cloud.SolrZkClient; import org.apache.solr.common.params.CommonParams; import org.apache.solr.common.params.ModifiableSolrParams; @@ -736,6 +739,55 @@ public void testSchemaDiffEndpoint() throws Exception { assertNotNull(fieldTypesAdded.get("test_txt")); } + @Test + @SuppressWarnings("unchecked") + public void testQueryReturnsErrorDetailsOnIndexingFailure() throws Exception { + String configSet = "queryIndexErrTest"; + + // Prep the schema and analyze sample docs so the temp collection and stored docs exist + schemaDesignerAPI.prepNewSchema(configSet, null); + ContentStreamBase.StringStream stream = + new ContentStreamBase.StringStream( + "[{\"id\":\"doc1\",\"title\":\"test doc\"}]", JSON_MIME); + ModifiableSolrParams reqParams = new ModifiableSolrParams(); + reqParams.set(CONFIG_SET_PARAM, configSet); + when(mockReq.getParams()).thenReturn(reqParams); + when(mockReq.getContentStreams()).thenReturn(Collections.singletonList(stream)); + schemaDesignerAPI.analyze(configSet, null, null, null, null, null, null, null); + + // Build a fresh API instance whose indexedVersion cache is empty (so it always + // attempts to re-index before running the query), and which simulates indexing errors. + Map fakeErrors = new HashMap<>(); + fakeErrors.put("doc1", new RuntimeException("simulated indexing failure")); + SchemaDesignerAPI apiWithErrors = + new SchemaDesignerAPI( + cc, + SchemaDesignerAPI.newSchemaSuggester(), + SchemaDesignerAPI.newSampleDocumentsLoader(), + mockReq) { + @Override + protected Map indexSampleDocsWithRebuildOnAnalysisError( + String idField, + List docs, + String collectionName, + boolean asBatch, + String[] analysisErrorHolder) + throws IOException, SolrServerException { + return fakeErrors; + } + }; + + when(mockReq.getContentStreams()).thenReturn(null); + FlexibleSolrJerseyResponse response = apiWithErrors.query(configSet); + + Map props = response.unknownProperties(); + assertNotNull("updateError must be present in error response", props.get(UPDATE_ERROR)); + assertEquals(400, props.get("updateErrorCode")); + Map details = (Map) props.get(ERROR_DETAILS); + assertNotNull("errorDetails must be present in error response", details); + assertTrue("errorDetails must contain the failing doc id", details.containsKey("doc1")); + } + protected void assertDesignerSettings(Map expected, Map actual) { for (String expKey : expected.keySet()) { Object expValue = expected.get(expKey); diff --git a/solr/webapp/web/js/angular/controllers/schema-designer.js b/solr/webapp/web/js/angular/controllers/schema-designer.js index f4572ec6d629..6b7176fb8c4d 100644 --- a/solr/webapp/web/js/angular/controllers/schema-designer.js +++ b/solr/webapp/web/js/angular/controllers/schema-designer.js @@ -1834,6 +1834,11 @@ solrAdminApp.controller('SchemaDesignerController', function ($scope, $timeout, } SchemaDesigner.get(params, function (data) { + if (data.updateError != null) { + $scope.onError(data.updateError, data.updateErrorCode, data.errorDetails); + return; + } + $("#sort").trigger("chosen:updated"); $("#ff").trigger("chosen:updated"); $("#hl").trigger("chosen:updated"); From 115fccbd64c8b9ad66d93c2a1550692bf6aabf6f Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Tue, 10 Mar 2026 06:42:23 -0400 Subject: [PATCH 12/13] Fix error prone. --- .../apache/solr/handler/designer/TestSchemaDesignerAPI.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java b/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java index 5c40e71dc372..dbc922470407 100644 --- a/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java +++ b/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import jakarta.ws.rs.core.Response; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -511,7 +512,7 @@ public void testBasicUserWorkflow() throws Exception { // Download ZIP when(mockReq.getContentStreams()).thenReturn(null); - jakarta.ws.rs.core.Response downloadResponse = schemaDesignerAPI.downloadConfig(configSet); + Response downloadResponse = schemaDesignerAPI.downloadConfig(configSet); assertNotNull(downloadResponse); assertEquals(200, downloadResponse.getStatus()); assertTrue( @@ -747,8 +748,7 @@ public void testQueryReturnsErrorDetailsOnIndexingFailure() throws Exception { // Prep the schema and analyze sample docs so the temp collection and stored docs exist schemaDesignerAPI.prepNewSchema(configSet, null); ContentStreamBase.StringStream stream = - new ContentStreamBase.StringStream( - "[{\"id\":\"doc1\",\"title\":\"test doc\"}]", JSON_MIME); + new ContentStreamBase.StringStream("[{\"id\":\"doc1\",\"title\":\"test doc\"}]", JSON_MIME); ModifiableSolrParams reqParams = new ModifiableSolrParams(); reqParams.set(CONFIG_SET_PARAM, configSet); when(mockReq.getParams()).thenReturn(reqParams); From 6756865160885ed1b1b0026282cfb03f3dc361cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:57:17 +0000 Subject: [PATCH 13/13] Fix requireSchemaVersion to also reject negative values (restores -1 sentinel contract) Co-authored-by: epugh <22395+epugh@users.noreply.github.com> --- .../handler/designer/SchemaDesignerAPI.java | 2 +- .../designer/TestSchemaDesignerAPI.java | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java index 587109ffe8f3..a94027fdcc60 100644 --- a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java +++ b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java @@ -1353,7 +1353,7 @@ protected String checkMutable(String configSet, int clientSchemaVersion) throws } protected void requireSchemaVersion(Integer schemaVersion) { - if (schemaVersion == null) { + if (schemaVersion == null || schemaVersion < 0) { throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, SCHEMA_VERSION_PARAM + " is a required parameter!"); } diff --git a/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java b/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java index dbc922470407..9659373be09f 100644 --- a/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java +++ b/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java @@ -788,6 +788,28 @@ protected Map indexSampleDocsWithRebuildOnAnalysisError( assertTrue("errorDetails must contain the failing doc id", details.containsKey("doc1")); } + @Test + public void testRequireSchemaVersionRejectsNegativeValues() throws Exception { + String configSet = "schemaVersionValidation"; + schemaDesignerAPI.prepNewSchema(configSet, null); + + // null schemaVersion must be rejected + SolrException nullEx = + expectThrows(SolrException.class, () -> schemaDesignerAPI.addSchemaObject(configSet, null)); + assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, nullEx.code()); + + // negative schemaVersion must be rejected (was previously bypassing validation) + SolrException negEx = + expectThrows(SolrException.class, () -> schemaDesignerAPI.addSchemaObject(configSet, -1)); + assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, negEx.code()); + + // same contract must hold for updateSchemaObject + SolrException updateNegEx = + expectThrows( + SolrException.class, () -> schemaDesignerAPI.updateSchemaObject(configSet, -1)); + assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, updateNegEx.code()); + } + protected void assertDesignerSettings(Map expected, Map actual) { for (String expKey : expected.keySet()) { Object expValue = expected.get(expKey);