diff --git a/CHANGELOG.md b/CHANGELOG.md
index aea57a6a6d2c..5cc055fa48f2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,7 +24,7 @@ This file lists Solr's raw release notes with details of every change to Solr. M
- Added new ConcurrentUpdateJdkSolrClient that works with HttpJdkSolrClient [SOLR-18065](https://issues.apache.org/jira/browse/SOLR-18065) (James Dyer)
- Support including stored fields in Export Writer output. [SOLR-18071](https://issues.apache.org/jira/browse/SOLR-18071) (Luke Kot-Zaniewski)
- Introducing support for multi valued dense vector representation in documents through nested vectors [SOLR-18074](https://issues.apache.org/jira/browse/SOLR-18074) (Alessandro Benedetti)
-- CoreAdmin API (/admin/cores?action=UPGRADECOREINDEX) to upgrade an index in-place [SOLR-18096](https://issues.apache.org/jira/browse/SOLR-18096) (Rahul Goswami)
+- CoreAdmin API (/admin/cores?action=UPGRADEINDEX) to upgrade an index in-place [SOLR-18096](https://issues.apache.org/jira/browse/SOLR-18096) (Rahul Goswami)
- CrossDC Consumer - add Prometheus metrics [SOLR-18060](https://issues.apache.org/jira/browse/SOLR-18060) (Andrzej Bialecki @ab)
- CrossDC - support arbitrary Kafka properties [SOLR-18062](https://issues.apache.org/jira/browse/SOLR-18062) (Andrzej Bialecki @ab)
diff --git a/changelog/unreleased/PR#4367-filestore-getmetadata-delete-race.yml b/changelog/unreleased/PR#4367-filestore-getmetadata-delete-race.yml
new file mode 100644
index 000000000000..96be57260aa1
--- /dev/null
+++ b/changelog/unreleased/PR#4367-filestore-getmetadata-delete-race.yml
@@ -0,0 +1,7 @@
+# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
+title: Filestore metadata API no longer returns HTTP 500 when a file is deleted concurrently with a metadata read; the response now matches the not-found case (null entry).
+type: fixed # added, changed, fixed, deprecated, removed, dependency_update, security, other
+authors:
+ - name: Eric Pugh
+links:
+ - https://github.com/apache/solr/pull/4367
diff --git a/changelog/unreleased/SOLR-17924-configset-creation-new-ui.yml b/changelog/unreleased/SOLR-17924-configset-creation-new-ui.yml
new file mode 100644
index 000000000000..b586b35ab145
--- /dev/null
+++ b/changelog/unreleased/SOLR-17924-configset-creation-new-ui.yml
@@ -0,0 +1,10 @@
+title: SOLR 17845 - Implement Configset Creation and Importing in New UI
+type: added
+authors:
+ - name: Christos Malliaridis
+ nick: malliaridis
+links:
+ - name: SOLR-17924
+ url: https://issues.apache.org/jira/browse/SOLR-17924
+ - name: PR#4166
+ url: https://github.com/apache/solr/pull/4166
diff --git a/changelog/unreleased/SOLR-18152-add-configset-download-zip-to-solrj-fix-schema-designer-bug.yml b/changelog/unreleased/SOLR-18152-add-configset-download-zip-to-solrj-fix-schema-designer-bug.yml
new file mode 100644
index 000000000000..b850dfe995a9
--- /dev/null
+++ b/changelog/unreleased/SOLR-18152-add-configset-download-zip-to-solrj-fix-schema-designer-bug.yml
@@ -0,0 +1,8 @@
+# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
+title: Schema Designer sample-doc analysis now works correctly when analyze sample documents.
+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
diff --git a/changelog/unreleased/SOLR-18198-support-missing-stats-count-in-rollup.yml b/changelog/unreleased/SOLR-18198-support-missing-stats-count-in-rollup.yml
new file mode 100644
index 000000000000..f614517e3689
--- /dev/null
+++ b/changelog/unreleased/SOLR-18198-support-missing-stats-count-in-rollup.yml
@@ -0,0 +1,8 @@
+# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
+title: "Support 'missing' stats count in rollup function for streaming expressions"
+type: added
+authors:
+ - name: khushjain
+links:
+ - name: SOLR-18198
+ url: https://issues.apache.org/jira/browse/SOLR-18198
diff --git a/changelog/unreleased/SOLR-18210-fix-GPU-vector-indexing.yml b/changelog/unreleased/SOLR-18210-fix-GPU-vector-indexing.yml
new file mode 100644
index 000000000000..54a479f18f37
--- /dev/null
+++ b/changelog/unreleased/SOLR-18210-fix-GPU-vector-indexing.yml
@@ -0,0 +1,7 @@
+title: Fix for GPU vector indexing silently falling back to using CPU instead
+type: fixed
+authors:
+ - name: Rahul Goswami
+links:
+ - name: SOLR-18210
+ url: https://issues.apache.org/jira/browse/SOLR-18210
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index e6c402e8d9b9..7018c17a31ec 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -15,6 +15,9 @@
[versions]
adobe-testing-s3mock = "3.12.0"
amazon-awssdk = "2.42.34"
+androidx-adaptive = "1.3.0-alpha07"
+androidx-lifecycle = "2.10.0"
+androidx-navigation3 = "1.1.0"
# @keep Antora version used in ref-guide
antora = "3.1.12"
# @keep Most recent commit as of 2025-08-03, this repo does not have tags
@@ -236,6 +239,12 @@ amazon-awssdk-retries-spi = { module = "software.amazon.awssdk:retries-spi", ver
amazon-awssdk-s3 = { module = "software.amazon.awssdk:s3", version.ref = "amazon-awssdk" }
amazon-awssdk-sdkcore = { module = "software.amazon.awssdk:sdk-core", version.ref = "amazon-awssdk" }
amazon-awssdk-sts = { module = "software.amazon.awssdk:sts", version.ref = "amazon-awssdk" }
+androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
+androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
+androidx-lifecycle-viewModelNav3 = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "androidx-lifecycle" }
+androidx-material3-adaptive = { module = "org.jetbrains.compose.material3.adaptive:adaptive", version.ref = "androidx-adaptive" }
+androidx-material3-adaptive-nav3 = { module = "org.jetbrains.compose.material3.adaptive:adaptive-navigation3", version.ref = "androidx-adaptive" }
+androidx-navigation3-ui = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "androidx-navigation3" }
apache-calcite-avatica-core = { module = "org.apache.calcite.avatica:avatica-core", version.ref = "apache-calcite-avatica" }
apache-calcite-core = { module = "org.apache.calcite:calcite-core", version.ref = "apache-calcite" }
apache-calcite-linq4j = { module = "org.apache.calcite:calcite-linq4j", version.ref = "apache-calcite" }
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..09c66395e068
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/SchemaDesignerApi.java
@@ -0,0 +1,200 @@
+/*
+ * 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.GENERIC_ENTITY_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 io.swagger.v3.oas.annotations.parameters.RequestBody;
+import jakarta.ws.rs.DELETE;
+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.PathParam;
+import jakarta.ws.rs.QueryParam;
+import java.io.InputStream;
+import java.util.List;
+import org.apache.solr.client.api.model.FlexibleSolrJerseyResponse;
+import org.apache.solr.client.api.model.SchemaDesignerAddRequestBody;
+import org.apache.solr.client.api.model.SchemaDesignerCollectionsResponse;
+import org.apache.solr.client.api.model.SchemaDesignerConfigsResponse;
+import org.apache.solr.client.api.model.SchemaDesignerInfoResponse;
+import org.apache.solr.client.api.model.SchemaDesignerPublishResponse;
+import org.apache.solr.client.api.model.SchemaDesignerResponse;
+import org.apache.solr.client.api.model.SchemaDesignerSchemaDiffResponse;
+import org.apache.solr.client.api.model.SchemaDesignerUpdateRequestBody;
+import org.apache.solr.client.api.model.SolrJerseyResponse;
+
+/** V2 API definitions for the Solr Schema Designer. */
+@Path("/schema-designer")
+public interface SchemaDesignerApi {
+
+ @GET
+ @Path("/{configSet}/info")
+ @Operation(
+ summary = "Get info about a configSet being designed.",
+ tags = {"schema-designer"})
+ SchemaDesignerInfoResponse getInfo(@PathParam("configSet") String configSet) throws Exception;
+
+ @POST
+ @Path("/{configSet}/prep")
+ @Operation(
+ summary = "Prepare a mutable configSet copy for schema design.",
+ tags = {"schema-designer"})
+ SchemaDesignerResponse prepNewSchema(
+ @PathParam("configSet") String configSet, @QueryParam("copyFrom") String copyFrom)
+ throws Exception;
+
+ @DELETE
+ @Path("/{configSet}")
+ @Operation(
+ summary = "Clean up temporary resources for a schema being designed.",
+ tags = {"schema-designer"})
+ SolrJerseyResponse cleanupTempSchema(@PathParam("configSet") String configSet) throws Exception;
+
+ @PUT
+ @Path("/{configSet}/file")
+ @Operation(
+ summary = "Update the contents of a file in a configSet being designed.",
+ tags = {"schema-designer"})
+ SchemaDesignerResponse updateFileContents(
+ @PathParam("configSet") String configSet,
+ @QueryParam("file") String file,
+ @RequestBody(
+ required = true,
+ extensions = {
+ @Extension(
+ properties = {
+ @ExtensionProperty(name = GENERIC_ENTITY_PROPERTY, value = "true")
+ })
+ })
+ InputStream fileContents)
+ throws Exception;
+
+ @GET
+ @Path("/{configSet}/sample")
+ @Operation(
+ summary = "Get a sample value and analysis for a field.",
+ tags = {"schema-designer"})
+ FlexibleSolrJerseyResponse getSampleValue(
+ @PathParam("configSet") String configSet,
+ @QueryParam("field") String fieldName,
+ @QueryParam("uniqueKeyField") String idField,
+ @QueryParam("docId") String docId)
+ throws Exception;
+
+ @GET
+ @Path("/{configSet}/collectionsForConfig")
+ @Operation(
+ summary = "List collections that use a given configSet.",
+ tags = {"schema-designer"})
+ SchemaDesignerCollectionsResponse listCollectionsForConfig(
+ @PathParam("configSet") String configSet) throws Exception;
+
+ @GET
+ @Path("/configs")
+ @Operation(
+ summary = "List all configSets available for schema design.",
+ tags = {"schema-designer"})
+ SchemaDesignerConfigsResponse listConfigs() throws Exception;
+
+ @POST
+ @Path("/{configSet}/add")
+ @Operation(
+ summary = "Add a new field, field type, or dynamic field to the schema being designed.",
+ tags = {"schema-designer"})
+ SchemaDesignerResponse addSchemaObject(
+ @PathParam("configSet") String configSet,
+ @QueryParam("schemaVersion") Integer schemaVersion,
+ SchemaDesignerAddRequestBody requestBody)
+ throws Exception;
+
+ @PUT
+ @Path("/{configSet}/update")
+ @Operation(
+ summary = "Update an existing field or field type in the schema being designed.",
+ tags = {"schema-designer"})
+ SchemaDesignerResponse updateSchemaObject(
+ @PathParam("configSet") String configSet,
+ @QueryParam("schemaVersion") Integer schemaVersion,
+ SchemaDesignerUpdateRequestBody requestBody)
+ throws Exception;
+
+ @PUT
+ @Path("/{configSet}/publish")
+ @Operation(
+ summary = "Publish the designed schema to a live configSet.",
+ tags = {"schema-designer"})
+ SchemaDesignerPublishResponse publish(
+ @PathParam("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;
+
+ /**
+ * Analyzes sample documents to suggest a schema.
+ *
+ *
Sample documents are read from the HTTP request body (not declared as a parameter on this
+ * interface — see {@code SchemaDesigner#loadSampleDocuments}) and dispatched to a parser based on
+ * the {@code Content-Type} header.
+ */
+ @POST
+ @Path("/{configSet}/analyze")
+ @Operation(
+ summary = "Analyze sample documents and suggest a schema.",
+ description =
+ "Sample documents are supplied in the request body. The Content-Type header selects the"
+ + " parser: application/json, text/xml or application/xml, text/csv or"
+ + " application/csv, or text/plain or application/octet-stream (treated as JSON"
+ + " lines). Capped at 5MB and 1000 documents.",
+ tags = {"schema-designer"})
+ SchemaDesignerResponse analyze(
+ @PathParam("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("/{configSet}/query")
+ @Operation(
+ summary = "Query the temporary collection used during schema design.",
+ tags = {"schema-designer"})
+ FlexibleSolrJerseyResponse query(@PathParam("configSet") String configSet) throws Exception;
+
+ @GET
+ @Path("/{configSet}/diff")
+ @Operation(
+ summary = "Get the diff between the designed schema and the published schema.",
+ tags = {"schema-designer"})
+ SchemaDesignerSchemaDiffResponse getSchemaDiff(@PathParam("configSet") String configSet)
+ throws Exception;
+}
diff --git a/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerAddRequestBody.java b/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerAddRequestBody.java
new file mode 100644
index 000000000000..821cabc4246a
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerAddRequestBody.java
@@ -0,0 +1,45 @@
+/*
+ * 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.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.Map;
+
+/**
+ * Request body for the Schema Designer add endpoint. Exactly one of the four fields should be
+ * populated; the populated field's name is the action and its value carries the schema-object
+ * attributes (e.g. for {@code addField}: {@code name}, {@code type}, {@code stored}, etc.).
+ */
+public class SchemaDesignerAddRequestBody {
+
+ @Schema(name = "addField")
+ @JsonProperty("add-field")
+ public Map addField;
+
+ @Schema(name = "addDynamicField")
+ @JsonProperty("add-dynamic-field")
+ public Map addDynamicField;
+
+ @Schema(name = "addCopyField")
+ @JsonProperty("add-copy-field")
+ public Map addCopyField;
+
+ @Schema(name = "addFieldType")
+ @JsonProperty("add-field-type")
+ public Map addFieldType;
+}
diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/overview/integration/DefaultConfigsetsOverviewComponent.kt b/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerCollectionsResponse.java
similarity index 60%
rename from solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/overview/integration/DefaultConfigsetsOverviewComponent.kt
rename to solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerCollectionsResponse.java
index 240b394815b5..2e0d31a27243 100644
--- a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/overview/integration/DefaultConfigsetsOverviewComponent.kt
+++ b/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerCollectionsResponse.java
@@ -14,16 +14,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.solr.ui.components.configsets.overview.integration
+package org.apache.solr.client.api.model;
-import com.arkivanov.mvikotlin.core.store.StoreFactory
-import io.ktor.client.HttpClient
-import org.apache.solr.ui.components.configsets.overview.ConfigsetsOverviewComponent
-import org.apache.solr.ui.utils.AppComponentContext
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
-class DefaultConfigsetsOverviewComponent(
- componentContext: AppComponentContext,
- storeFactory: StoreFactory,
- httpClient: HttpClient,
-) : ConfigsetsOverviewComponent,
- AppComponentContext by componentContext
+/** Response body for the Schema Designer list-collections-for-config endpoint. */
+public class SchemaDesignerCollectionsResponse extends SolrJerseyResponse {
+
+ @JsonProperty("collections")
+ public List collections;
+}
diff --git a/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerConfigsResponse.java b/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerConfigsResponse.java
new file mode 100644
index 000000000000..8f8025980822
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerConfigsResponse.java
@@ -0,0 +1,31 @@
+/*
+ * 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.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.Map;
+
+/** Response body for the Schema Designer list-configs endpoint. */
+public class SchemaDesignerConfigsResponse extends SolrJerseyResponse {
+
+ /**
+ * Map of configSet name to status: 0 = in-progress (temp only), 1 = disabled, 2 = enabled and
+ * published.
+ */
+ @JsonProperty("configSets")
+ public Map configSets;
+}
diff --git a/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerInfoResponse.java b/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerInfoResponse.java
new file mode 100644
index 000000000000..528f426c0abe
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerInfoResponse.java
@@ -0,0 +1,44 @@
+/*
+ * 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.model;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+
+/** Response body for the Schema Designer get-info endpoint. */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class SchemaDesignerInfoResponse extends SchemaDesignerSettingsResponse {
+
+ @JsonProperty("configSet")
+ public String configSet;
+
+ /** Whether the configSet has a published (live) version. */
+ @JsonProperty("published")
+ public Boolean published;
+
+ @JsonProperty("schemaVersion")
+ public Integer schemaVersion;
+
+ /** Collections currently using this configSet. */
+ @JsonProperty("collections")
+ public List collections;
+
+ /** Number of sample documents stored for this configSet, if available. */
+ @JsonProperty("numDocs")
+ public Integer numDocs;
+}
diff --git a/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerPublishResponse.java b/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerPublishResponse.java
new file mode 100644
index 000000000000..b6d7038ff2fa
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerPublishResponse.java
@@ -0,0 +1,45 @@
+/*
+ * 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.model;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/** Response body for the Schema Designer publish endpoint. */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class SchemaDesignerPublishResponse extends SolrJerseyResponse {
+
+ @JsonProperty("configSet")
+ public String configSet;
+
+ @JsonProperty("schemaVersion")
+ public Integer schemaVersion;
+
+ /** The new collection created during publish, if requested. */
+ @JsonProperty("newCollection")
+ public String newCollection;
+
+ /** Error message if indexing sample docs into the new collection failed. */
+ @JsonProperty("updateError")
+ public String updateError;
+
+ @JsonProperty("updateErrorCode")
+ public Integer updateErrorCode;
+
+ @JsonProperty("errorDetails")
+ public Object errorDetails;
+}
diff --git a/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerResponse.java b/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerResponse.java
new file mode 100644
index 000000000000..f7e3325d9f83
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerResponse.java
@@ -0,0 +1,150 @@
+/*
+ * 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.model;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Response body for Schema Designer endpoints that operate on a full schema: {@code prepNewSchema},
+ * {@code updateFileContents}, {@code addSchemaObject}, {@code updateSchemaObject}, and {@code
+ * analyze}.
+ *
+ * All nullable fields are omitted from JSON output when null.
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class SchemaDesignerResponse extends SchemaDesignerSettingsResponse {
+
+ // --- core schema identification ---
+
+ @JsonProperty("configSet")
+ public String configSet;
+
+ @JsonProperty("schemaVersion")
+ public Integer schemaVersion;
+
+ /** The temporary mutable collection used during design (e.g. {@code ._designer_myConfig}). */
+ @JsonProperty("tempCollection")
+ public String tempCollection;
+
+ /** Active replica core name for the temp collection, used for Luke API calls. */
+ @JsonProperty("core")
+ public String core;
+
+ @JsonProperty("uniqueKeyField")
+ public String uniqueKeyField;
+
+ /** Collections currently using the published version of this configSet. */
+ @JsonProperty("collectionsForConfig")
+ public List collectionsForConfig;
+
+ // --- schema objects ---
+
+ @JsonProperty("fields")
+ public List> fields;
+
+ @JsonProperty("dynamicFields")
+ public List> dynamicFields;
+
+ @JsonProperty("fieldTypes")
+ public List> fieldTypes;
+
+ /** ConfigSet files available in ZooKeeper (excluding managed-schema and internal files). */
+ @JsonProperty("files")
+ public List files;
+
+ /** IDs of the first 100 sample documents (present when docs were loaded/analyzed). */
+ @JsonProperty("docIds")
+ public List docIds;
+
+ /** Total number of sample documents, or -1 when no docs were passed to the endpoint. */
+ @JsonProperty("numDocs")
+ public Integer numDocs;
+
+ // --- error fields (set when sample-doc indexing fails) ---
+
+ @JsonProperty("updateError")
+ public String updateError;
+
+ @JsonProperty("updateErrorCode")
+ public Integer updateErrorCode;
+
+ @JsonProperty("errorDetails")
+ public Object errorDetails;
+
+ // --- endpoint-specific fields ---
+
+ /** Source of the sample documents (e.g. "blob", "request"); set by {@code analyze}. */
+ @JsonProperty("sampleSource")
+ public String sampleSource;
+
+ /** Analysis warning when field-type inference produced errors; set by {@code analyze}. */
+ @JsonProperty("analysisError")
+ public String analysisError;
+
+ /**
+ * The type of schema object that was updated: {@code "field"} or {@code "type"}; set by {@code
+ * updateSchemaObject}.
+ */
+ @JsonProperty("updateType")
+ public String updateType;
+
+ /**
+ * The updated field definition map; populated when {@code updateType} is {@code "field"} in
+ * {@code updateSchemaObject}, or the field name string when returned by {@code addSchemaObject}.
+ */
+ @JsonProperty("field")
+ public Object field;
+
+ /**
+ * The updated field-type definition map; populated when {@code updateType} is {@code "type"} in
+ * {@code updateSchemaObject}, or the type name string when returned by {@code addSchemaObject}.
+ */
+ @JsonProperty("type")
+ public Object type;
+
+ /** The added dynamic-field name; set by {@code addSchemaObject} when adding a dynamic field. */
+ @JsonProperty("dynamicField")
+ public Object dynamicField;
+
+ /** The added field-type name; set by {@code addSchemaObject} when adding a field type. */
+ @JsonProperty("fieldType")
+ public Object fieldType;
+
+ /**
+ * Whether the temp collection needs to be rebuilt after this update; set by {@code
+ * updateSchemaObject}.
+ */
+ @JsonProperty("rebuild")
+ public Boolean rebuild;
+
+ /**
+ * Error message when a file update (e.g. {@code solrconfig.xml}) fails validation; set by {@code
+ * updateFileContents}.
+ */
+ @JsonProperty("updateFileError")
+ public String updateFileError;
+
+ /**
+ * The raw file content returned when a file update fails validation; set by {@code
+ * updateFileContents} so the UI can display the attempted content alongside the error.
+ */
+ @JsonProperty("fileContent")
+ public String fileContent;
+}
diff --git a/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerSchemaDiffResponse.java b/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerSchemaDiffResponse.java
new file mode 100644
index 000000000000..dfe60441117b
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerSchemaDiffResponse.java
@@ -0,0 +1,34 @@
+/*
+ * 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.model;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.Map;
+
+/** Response body for the Schema Designer get-schema-diff endpoint. */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class SchemaDesignerSchemaDiffResponse extends SchemaDesignerSettingsResponse {
+
+ /** The list of field-level differences between the designed schema and the source. */
+ @JsonProperty("diff")
+ public Map diff;
+
+ /** The configSet used as the diff source (either the published configSet or copyFrom). */
+ @JsonProperty("diff-source")
+ public String diffSource;
+}
diff --git a/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerSettingsResponse.java b/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerSettingsResponse.java
new file mode 100644
index 000000000000..7f695ef48a42
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerSettingsResponse.java
@@ -0,0 +1,47 @@
+/*
+ * 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.model;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+
+/** Base response for Schema Designer endpoints that surface the designer settings. */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public abstract class SchemaDesignerSettingsResponse extends SolrJerseyResponse {
+
+ @JsonProperty("languages")
+ public List languages;
+
+ @JsonProperty("enableFieldGuessing")
+ public Boolean enableFieldGuessing;
+
+ @JsonProperty("enableDynamicFields")
+ public Boolean enableDynamicFields;
+
+ @JsonProperty("enableNestedDocs")
+ public Boolean enableNestedDocs;
+
+ @JsonProperty("disabled")
+ public Boolean disabled;
+
+ @JsonProperty("publishedVersion")
+ public Integer publishedVersion;
+
+ @JsonProperty("copyFrom")
+ public String copyFrom;
+}
diff --git a/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerUpdateRequestBody.java b/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerUpdateRequestBody.java
new file mode 100644
index 000000000000..54b9bb9e56cb
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerUpdateRequestBody.java
@@ -0,0 +1,48 @@
+/*
+ * 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.model;
+
+import com.fasterxml.jackson.annotation.JsonAnyGetter;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Request body for the Schema Designer update endpoint: a flat field or field-type definition. The
+ * {@code name} property is required; remaining schema attributes (e.g. {@code type}, {@code
+ * indexed}, {@code stored}, {@code analyzer}, {@code copyDest}) are captured via the dynamic {@code
+ * additionalProperties} map and forwarded to the Schema API.
+ */
+public class SchemaDesignerUpdateRequestBody {
+
+ @JsonProperty public String name;
+
+ // Non-final + public so the OpenAPI-generated SolrJ client can assign to it directly.
+ // Accessed via @JsonAnyGetter / @JsonAnySetter for JSON (de)serialization.
+ public Map additionalProperties = new HashMap<>();
+
+ @JsonAnyGetter
+ public Map getAdditionalProperties() {
+ return additionalProperties;
+ }
+
+ @JsonAnySetter
+ public void setAdditionalProperty(String key, Object value) {
+ additionalProperties.put(key, value);
+ }
+}
diff --git a/solr/core/src/java/org/apache/solr/core/CachingDirectoryFactory.java b/solr/core/src/java/org/apache/solr/core/CachingDirectoryFactory.java
index dcf084670f62..8b943b7dc791 100644
--- a/solr/core/src/java/org/apache/solr/core/CachingDirectoryFactory.java
+++ b/solr/core/src/java/org/apache/solr/core/CachingDirectoryFactory.java
@@ -95,14 +95,6 @@ public String toString() {
protected Set removeEntries = new HashSet<>();
- private Double maxWriteMBPerSecFlush;
-
- private Double maxWriteMBPerSecMerge;
-
- private Double maxWriteMBPerSecRead;
-
- private Double maxWriteMBPerSecDefault;
-
private boolean closed;
public interface CloseListener {
@@ -471,11 +463,6 @@ public void incRef(Directory directory) {
@Override
public void init(NamedList> args) {
- maxWriteMBPerSecFlush = (Double) args.get("maxWriteMBPerSecFlush");
- maxWriteMBPerSecMerge = (Double) args.get("maxWriteMBPerSecMerge");
- maxWriteMBPerSecRead = (Double) args.get("maxWriteMBPerSecRead");
- maxWriteMBPerSecDefault = (Double) args.get("maxWriteMBPerSecDefault");
-
// override global config
if (args.get(SolrXmlConfig.SOLR_DATA_HOME) != null) {
dataHomePath =
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 a759ce80461a..2e2cb6007f96 100644
--- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java
+++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
@@ -125,7 +125,7 @@
import org.apache.solr.handler.admin.ZookeeperRead;
import org.apache.solr.handler.admin.ZookeeperStatusHandler;
import org.apache.solr.handler.component.ShardHandlerFactory;
-import org.apache.solr.handler.designer.SchemaDesignerAPI;
+import org.apache.solr.handler.designer.SchemaDesigner;
import org.apache.solr.jersey.InjectionFactories;
import org.apache.solr.jersey.JerseyAppHandlerCache;
import org.apache.solr.logging.LogWatcher;
@@ -875,7 +875,7 @@ private void loadInternal() {
registerV2Api(clusterAPI.commands);
if (isZooKeeperAware()) {
- registerV2Api(new SchemaDesignerAPI(this));
+ registerV2Api(SchemaDesigner.class);
} // else Schema Designer not available in standalone (non-cloud) mode
/*
diff --git a/solr/core/src/java/org/apache/solr/filestore/ClusterFileStore.java b/solr/core/src/java/org/apache/solr/filestore/ClusterFileStore.java
index c405fcbcfe95..bcd4e5620534 100644
--- a/solr/core/src/java/org/apache/solr/filestore/ClusterFileStore.java
+++ b/solr/core/src/java/org/apache/solr/filestore/ClusterFileStore.java
@@ -31,6 +31,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.stream.Collectors;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.solr.api.JerseyResource;
@@ -228,13 +229,14 @@ public static FileStoreDirectoryListingResponse getMetadata(
String parentPath = path.substring(0, path.lastIndexOf('/'));
List l = fileStore.list(parentPath, s -> s.equals(fileName));
- dirListingResponse.files =
- Collections.singletonMap(path, l.isEmpty() ? null : convertToResponse(l.get(0)));
+ FileStoreEntryMetadata entry = l.isEmpty() ? null : convertToResponse(l.get(0));
+ dirListingResponse.files = Collections.singletonMap(path, entry);
break;
case DIRECTORY:
final var directoryContents =
fileStore.list(path, null).stream()
.map(details -> convertToResponse(details))
+ .filter(Objects::nonNull)
.collect(Collectors.toList());
dirListingResponse.files = Map.of(path, directoryContents);
break;
@@ -254,8 +256,18 @@ private static FileStoreEntryMetadata convertToResponse(FileStore.FileDetails de
return entryMetadata;
}
- entryMetadata.size = details.size();
- entryMetadata.timestamp = details.getTimeStamp();
+ long size = details.size();
+ if (size < 0) {
+ // File was deleted concurrently between listing and reading its attributes.
+ return null;
+ }
+ final var timestamp = details.getTimeStamp();
+ if (timestamp == null) {
+ // File was deleted concurrently between reading its size and timestamp.
+ return null;
+ }
+ entryMetadata.size = size;
+ entryMetadata.timestamp = timestamp;
if (details.getMetaData() != null) {
details.getMetaData().toMap(entryMetadata.unknownProperties());
}
diff --git a/solr/core/src/java/org/apache/solr/filestore/DistribFileStore.java b/solr/core/src/java/org/apache/solr/filestore/DistribFileStore.java
index 398075663a4c..7fffd9ebf08a 100644
--- a/solr/core/src/java/org/apache/solr/filestore/DistribFileStore.java
+++ b/solr/core/src/java/org/apache/solr/filestore/DistribFileStore.java
@@ -30,6 +30,7 @@
import java.nio.channels.SeekableByteChannel;
import java.nio.file.FileSystems;
import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
@@ -288,6 +289,9 @@ public MetaData getMetaData() {
public Date getTimeStamp() {
try {
return new Date(Files.getLastModifiedTime(realPath()).toMillis());
+ } catch (NoSuchFileException e) {
+ // File was deleted concurrently between listing and reading its attributes.
+ return null;
} catch (IOException e) {
throw new SolrException(
SERVER_ERROR, "Failed to retrieve the last modified time for: " + realPath(), e);
@@ -303,6 +307,9 @@ public boolean isDir() {
public long size() {
try {
return Files.size(realPath());
+ } catch (NoSuchFileException e) {
+ // File was deleted concurrently between listing and reading its attributes.
+ return -1;
} catch (IOException e) {
throw new SolrException(
SERVER_ERROR, "Failed to retrieve the file size for: " + realPath(), e);
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/CoreAdminOperation.java b/solr/core/src/java/org/apache/solr/handler/admin/CoreAdminOperation.java
index 3cd2bb141e73..2edc70aef30b 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/CoreAdminOperation.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/CoreAdminOperation.java
@@ -38,7 +38,7 @@
import static org.apache.solr.common.params.CoreAdminParams.CoreAdminAction.STATUS;
import static org.apache.solr.common.params.CoreAdminParams.CoreAdminAction.SWAP;
import static org.apache.solr.common.params.CoreAdminParams.CoreAdminAction.UNLOAD;
-import static org.apache.solr.common.params.CoreAdminParams.CoreAdminAction.UPGRADECOREINDEX;
+import static org.apache.solr.common.params.CoreAdminParams.CoreAdminAction.UPGRADEINDEX;
import static org.apache.solr.handler.admin.CoreAdminHandler.CallInfo;
import java.lang.invoke.MethodHandles;
@@ -258,7 +258,7 @@ public enum CoreAdminOperation implements CoreAdminOp {
V2ApiUtils.squashIntoSolrResponseWithoutHeader(it.rsp, response);
}),
- UPGRADECOREINDEX_OP(UPGRADECOREINDEX, new UpgradeCoreIndexOp());
+ UPGRADEINDEX_OP(UPGRADEINDEX, new UpgradeCoreIndexOp());
final CoreAdminParams.CoreAdminAction action;
final CoreAdminOp fun;
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/UpgradeCoreIndexOp.java b/solr/core/src/java/org/apache/solr/handler/admin/UpgradeCoreIndexOp.java
index 8fff5c93d310..350d07d99527 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/UpgradeCoreIndexOp.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/UpgradeCoreIndexOp.java
@@ -54,7 +54,7 @@ public void execute(CoreAdminHandler.CallInfo it) throws Exception {
if (it.handler.coreContainer.isZooKeeperAware()) {
throw new SolrException(
SolrException.ErrorCode.BAD_REQUEST,
- "action=UPGRADECOREINDEX is not supported in SolrCloud mode. As an alternative, in order to upgrade index, configure LatestVersionMergePolicyFactory in solrconfig.xml and reindex the data in your collection.");
+ "action=UPGRADEINDEX is not supported in SolrCloud mode. As an alternative, in order to upgrade index, configure LatestVersionMergePolicyFactory in solrconfig.xml and reindex the data in your collection.");
}
SolrParams params = it.req.getParams();
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/UpgradeCoreIndex.java b/solr/core/src/java/org/apache/solr/handler/admin/api/UpgradeCoreIndex.java
index c92d3e209471..5e80526c0e39 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/UpgradeCoreIndex.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/UpgradeCoreIndex.java
@@ -68,9 +68,9 @@
import org.slf4j.LoggerFactory;
/**
- * Implements the UPGRADECOREINDEX CoreAdmin action, which upgrades an existing core's index
- * in-place by reindexing documents from segments belonging to older Lucene versions, so that they
- * get written into latest version segments.
+ * Implements the UPGRADEINDEX CoreAdmin action, which upgrades an existing core's index in-place by
+ * reindexing documents from segments belonging to older Lucene versions, so that they get written
+ * into latest version segments.
*
* The upgrade process:
*
@@ -153,7 +153,7 @@ private UpgradeCoreIndexResponse performUpgradeImpl(
if (indexContainsChildDocs(searcherRef.get())) {
throw new SolrException(
BAD_REQUEST,
- "UPGRADECOREINDEX does not support indexes containing child/nested documents. "
+ "UPGRADEINDEX does not support indexes containing child/nested documents. "
+ " Consider reindexing your data "
+ "from the original source.");
}
diff --git a/solr/core/src/java/org/apache/solr/handler/configsets/DeleteConfigSet.java b/solr/core/src/java/org/apache/solr/handler/configsets/DeleteConfigSet.java
index 3b26c5e2fc2b..9b6337528374 100644
--- a/solr/core/src/java/org/apache/solr/handler/configsets/DeleteConfigSet.java
+++ b/solr/core/src/java/org/apache/solr/handler/configsets/DeleteConfigSet.java
@@ -51,7 +51,7 @@ public DeleteConfigSet(
@PermissionName(CONFIG_EDIT_PERM)
public SolrJerseyResponse deleteConfigSet(String configSetName) throws Exception {
final var response = instantiateJerseyResponse(SolrJerseyResponse.class);
- if (StrUtils.isNullOrEmpty(configSetName)) {
+ if (StrUtils.isNullOrEmpty(configSetName) || configSetName.isBlank()) {
throw new SolrException(
SolrException.ErrorCode.BAD_REQUEST, "No configset name provided to delete");
}
diff --git a/solr/core/src/java/org/apache/solr/handler/configsets/DownloadConfigSet.java b/solr/core/src/java/org/apache/solr/handler/configsets/DownloadConfigSet.java
index 729aaf00d914..5ea6d6be3208 100644
--- a/solr/core/src/java/org/apache/solr/handler/configsets/DownloadConfigSet.java
+++ b/solr/core/src/java/org/apache/solr/handler/configsets/DownloadConfigSet.java
@@ -63,20 +63,34 @@ public Response downloadConfigSet(String configSetName) throws Exception {
throw new SolrException(
SolrException.ErrorCode.NOT_FOUND, "ConfigSet " + configSetName + " not found!");
}
- return buildZipResponse(configSetService, configSetName);
+ return buildZipResponse(configSetService, configSetName, deriveDisplayName(configSetName));
+ }
+
+ // This is to support the schema designer's internal name and
+ // lets us not duplicate the download endpoint.
+ static String deriveDisplayName(String configSetName) {
+ if (configSetName.startsWith("._designer_")) {
+ return configSetName.substring("._designer_".length());
+ }
+ return configSetName;
}
/**
* Build a ZIP download {@link Response} for the given configset.
*
* @param configSetService the service to use for downloading the configset files
- * @param configSetName the name of the configset to download
+ * @param configSetName the name of the configset to download (internal id)
+ * @param displayName the sanitized name to use in the Content-Disposition filename
*/
- public static Response buildZipResponse(ConfigSetService configSetService, String configSetName)
+ public static Response buildZipResponse(
+ ConfigSetService configSetService, String configSetName, String displayName)
throws IOException {
final byte[] zipBytes = zipConfigSet(configSetService, configSetName);
+ final String safeName = displayName.replaceAll("[^a-zA-Z0-9_\\-.]", "_");
+ final String fileName = safeName + "_configset.zip";
return Response.ok((StreamingOutput) outputStream -> outputStream.write(zipBytes))
.type("application/zip")
+ .header("Content-Disposition", "attachment; filename=\"" + fileName + "\"")
.build();
}
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 785be168ca93..5584ec47e9d6 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);
@@ -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.isEmpty() && 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 {
@@ -203,7 +209,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;
}
@@ -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.isEmpty() && 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/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/SchemaDesigner.java
similarity index 67%
rename from solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java
rename to solr/core/src/java/org/apache/solr/handler/designer/SchemaDesigner.java
index bced3660ef74..068959645de3 100644
--- a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesigner.java
@@ -17,14 +17,12 @@
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 java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -49,7 +47,19 @@
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.SchemaDesignerAddRequestBody;
+import org.apache.solr.client.api.model.SchemaDesignerCollectionsResponse;
+import org.apache.solr.client.api.model.SchemaDesignerConfigsResponse;
+import org.apache.solr.client.api.model.SchemaDesignerInfoResponse;
+import org.apache.solr.client.api.model.SchemaDesignerPublishResponse;
+import org.apache.solr.client.api.model.SchemaDesignerResponse;
+import org.apache.solr.client.api.model.SchemaDesignerSchemaDiffResponse;
+import org.apache.solr.client.api.model.SchemaDesignerSettingsResponse;
+import org.apache.solr.client.api.model.SchemaDesignerUpdateRequestBody;
+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;
@@ -57,6 +67,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;
@@ -66,16 +77,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 +95,8 @@
import org.slf4j.LoggerFactory;
/** All V2 APIs have a prefix of /api/schema-designer/ */
-public class SchemaDesignerAPI implements SchemaDesignerConstants {
+public class SchemaDesigner extends JerseyResource
+ implements SchemaDesignerApi, SchemaDesignerConstants {
private static final Set excludeConfigSetNames = Set.of(DEFAULT_CONFIGSET_NAME);
@@ -98,21 +108,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 SchemaDesigner(CoreContainer coreContainer, SolrQueryRequest solrQueryRequest) {
this(
coreContainer,
- SchemaDesignerAPI.newSchemaSuggester(),
- SchemaDesignerAPI.newSampleDocumentsLoader());
+ SchemaDesigner.newSchemaSuggester(),
+ SchemaDesigner.newSampleDocumentsLoader(),
+ solrQueryRequest);
}
- SchemaDesignerAPI(
+ SchemaDesigner(
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,14 +161,16 @@ 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 SchemaDesignerInfoResponse getInfo(String configSet) throws Exception {
+ requireNotEmpty(CONFIG_SET_PARAM, configSet);
- Map responseMap = new HashMap<>();
- responseMap.put(CONFIG_SET_PARAM, configSet);
+ SchemaDesignerInfoResponse response =
+ instantiateJerseyResponse(SchemaDesignerInfoResponse.class);
+ response.configSet = configSet;
boolean exists = configExists(configSet);
- responseMap.put("published", exists);
+ response.published = exists;
// mutable config may not exist yet as this is just an info check to gather some basic info the
// UI needs
@@ -164,31 +181,31 @@ public void getInfo(SolrQueryRequest req, SolrQueryResponse rsp) throws IOExcept
SolrConfig srcConfig = exists ? configSetHelper.loadSolrConfig(configSet) : null;
SolrConfig solrConfig =
configExists(mutableId) ? configSetHelper.loadSolrConfig(mutableId) : srcConfig;
- addSettingsToResponse(settingsDAO.getSettings(solrConfig), responseMap);
+ addSettingsToResponse(settingsDAO.getSettings(solrConfig), response);
- responseMap.put(SCHEMA_VERSION_PARAM, configSetHelper.getCurrentSchemaVersion(mutableId));
- responseMap.put(
- "collections", exists ? configSetHelper.listCollectionsForConfig(configSet) : List.of());
+ response.schemaVersion = configSetHelper.getCurrentSchemaVersion(mutableId);
+ response.collections = exists ? configSetHelper.listCollectionsForConfig(configSet) : List.of();
// don't fail if loading sample docs fails
try {
- responseMap.put("numDocs", configSetHelper.retrieveSampleDocs(configSet).size());
+ response.numDocs = configSetHelper.retrieveSampleDocs(configSet).size();
} catch (Exception exc) {
log.warn("Failed to load sample docs from blob store for {}", configSet, exc);
}
- rsp.getValues().addAll(responseMap);
+ return response;
}
- @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 SchemaDesignerResponse 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);
+ 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);
@@ -200,36 +217,27 @@ public void prepNewSchema(SolrQueryRequest req, SolrQueryResponse rsp)
settingsDAO.persistIfChanged(mutableId, settings);
- rsp.getValues().addAll(buildResponse(configSet, schema, settings, null));
+ return buildSchemaDesignerResponse(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);
- String filePath = getConfigSetZkPath(getMutableId(configSet), file);
- byte[] data;
- try {
- data = zkStateReader().getZkClient().getData(filePath, null, null);
- } catch (KeeperException | InterruptedException e) {
- throw new IOException("Error reading file: " + filePath, SolrZkClient.checkInterrupted(e));
+ @Override
+ @PermissionName(CONFIG_EDIT_PERM)
+ public SchemaDesignerResponse updateFileContents(
+ String configSet, String file, InputStream fileContents) throws Exception {
+ requireNotEmpty(CONFIG_SET_PARAM, configSet);
+ requireNotEmpty("file", file);
+ if (fileContents == null) {
+ throw new SolrException(
+ SolrException.ErrorCode.BAD_REQUEST, "Request body with file contents is required!");
}
- String stringData =
- data != null && data.length > 0 ? new String(data, StandardCharsets.UTF_8) : "";
- rsp.getValues().addAll(Map.of(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);
String mutableId = getMutableId(configSet);
String zkPath = getConfigSetZkPath(mutableId, file);
@@ -240,7 +248,7 @@ public void updateFileContents(SolrQueryRequest req, SolrQueryResponse rsp)
}
byte[] data;
- try (InputStream in = extractSingleContentStream(req, true).getStream()) {
+ try (InputStream in = fileContents) {
data = in.readAllBytes();
}
Exception updateFileError = null;
@@ -261,11 +269,11 @@ public void updateFileContents(SolrQueryRequest req, SolrQueryResponse rsp)
// solrconfig.xml update failed, but haven't impacted the configSet yet, so just return the
// error directly
Throwable causedBy = SolrException.getRootCause(updateFileError);
- Map response = new HashMap<>();
- response.put("updateFileError", causedBy.getMessage());
- response.put(file, new String(data, StandardCharsets.UTF_8));
- rsp.getValues().addAll(response);
- return;
+ SchemaDesignerResponse errorResponse =
+ instantiateJerseyResponse(SchemaDesignerResponse.class);
+ errorResponse.updateFileError = causedBy.getMessage();
+ errorResponse.fileContent = new String(data, StandardCharsets.UTF_8);
+ return errorResponse;
}
// apply the update and reload the temp collection / re-index sample docs
@@ -295,10 +303,10 @@ public void updateFileContents(SolrQueryRequest req, SolrQueryResponse rsp)
}
}
- Map response = buildResponse(configSet, schema, null, docs);
+ SchemaDesignerResponse response = buildSchemaDesignerResponse(configSet, schema, null, docs);
if (analysisErrorHolder[0] != null) {
- response.put(ANALYSIS_ERROR, analysisErrorHolder[0]);
+ response.analysisError = analysisErrorHolder[0];
}
addErrorToResponse(
@@ -308,15 +316,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 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;
@@ -347,25 +356,31 @@ 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(Map.of("collections", configSetHelper.listCollectionsForConfig(configSet)));
+ @Override
+ @PermissionName(CONFIG_READ_PERM)
+ public SchemaDesignerCollectionsResponse listCollectionsForConfig(String configSet) {
+ requireNotEmpty(CONFIG_SET_PARAM, configSet);
+ SchemaDesignerCollectionsResponse response =
+ instantiateJerseyResponse(SchemaDesignerCollectionsResponse.class);
+ response.collections = configSetHelper.listCollectionsForConfig(configSet);
+ return response;
}
// 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(Map.of("configSets", listEnabledConfigs()));
+ @Override
+ @PermissionName(CONFIG_EDIT_PERM)
+ public SchemaDesignerConfigsResponse listConfigs() throws Exception {
+ SchemaDesignerConfigsResponse response =
+ instantiateJerseyResponse(SchemaDesignerConfigsResponse.class);
+ response.configSets = listEnabledConfigs();
+ return response;
}
protected Map listEnabledConfigs() throws IOException {
@@ -384,69 +399,73 @@ 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);
- String mutableId = getMutableId(configSet);
-
- // find the configSet to download
- SolrZkClient zkClient = zkStateReader().getZkClient();
- String configId = mutableId;
- try {
- if (!zkClient.exists(getConfigSetZkPath(mutableId, null))) {
- if (zkClient.exists(getConfigSetZkPath(configSet, null))) {
- configId = configSet;
- } else {
- throw new SolrException(
- SolrException.ErrorCode.NOT_FOUND, "ConfigSet " + configSet + " not found!");
- }
- }
- } catch (KeeperException | InterruptedException e) {
- throw new IOException("Error reading config from ZK", SolrZkClient.checkInterrupted(e));
+ @Override
+ @PermissionName(CONFIG_EDIT_PERM)
+ public SchemaDesignerResponse addSchemaObject(
+ String configSet, Integer schemaVersion, SchemaDesignerAddRequestBody requestBody)
+ throws Exception {
+ requireNotEmpty(CONFIG_SET_PARAM, configSet);
+ requireSchemaVersion(schemaVersion);
+ final String mutableId = checkMutable(configSet, schemaVersion);
+
+ String action;
+ Map attrs;
+ if (requestBody == null) {
+ action = null;
+ attrs = null;
+ } else if (requestBody.addField != null) {
+ action = "add-field";
+ attrs = requestBody.addField;
+ } else if (requestBody.addDynamicField != null) {
+ action = "add-dynamic-field";
+ attrs = requestBody.addDynamicField;
+ } else if (requestBody.addCopyField != null) {
+ action = "add-copy-field";
+ attrs = requestBody.addCopyField;
+ } else if (requestBody.addFieldType != null) {
+ action = "add-field-type";
+ attrs = requestBody.addFieldType;
+ } else {
+ action = null;
+ attrs = null;
+ }
+ if (action == null) {
+ throw new SolrException(
+ SolrException.ErrorCode.BAD_REQUEST,
+ "Request body must contain exactly one of: add-field, add-dynamic-field, add-copy-field, add-field-type");
}
- ContentStreamBase content =
- new ContentStreamBase.ByteArrayStream(
- configSetHelper.downloadAndZipConfigSet(configId),
- configSet + ".zip",
- "application/zip");
- rsp.add(RawResponseWriter.CONTENT, content);
- }
-
- @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);
-
- Map addJson = readJsonFromRequest(req);
+ Map addJson = Map.of(action, attrs);
log.info("Adding new schema object from JSON: {}", addJson);
String objectName = configSetHelper.addSchemaObject(configSet, addJson);
- String action = addJson.keySet().iterator().next();
ManagedIndexSchema schema = loadLatestSchema(mutableId);
- Map response =
- buildResponse(configSet, schema, null, configSetHelper.retrieveSampleDocs(configSet));
- response.put(action, objectName);
- rsp.getValues().addAll(response);
+ SchemaDesignerResponse response =
+ buildSchemaDesignerResponse(
+ configSet, schema, null, configSetHelper.retrieveSampleDocs(configSet));
+ setSchemaObjectField(response, action, objectName);
+ return 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 SchemaDesignerResponse updateSchemaObject(
+ String configSet, Integer schemaVersion, SchemaDesignerUpdateRequestBody requestBody)
+ 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);
- String name = (String) updateField.get("name");
- if (StrUtils.isNullOrEmpty(name)) {
+ if (requestBody == null || StrUtils.isNullOrEmpty(requestBody.name)) {
throw new SolrException(
SolrException.ErrorCode.BAD_REQUEST,
"Invalid update request! JSON payload is missing the required name property: "
- + updateField);
+ + requestBody);
}
+ String name = requestBody.name;
+ Map updateField = new HashMap<>(requestBody.getAdditionalProperties());
+ updateField.put("name", name);
log.info(
"Updating schema object: configSet={}, mutableId={}, name={}, JSON={}",
configSet,
@@ -486,29 +505,41 @@ public void updateSchemaObject(SolrQueryRequest req, SolrQueryResponse rsp)
}
}
- Map response = buildResponse(configSet, schema, settings, docs);
- response.put("updateType", updateType);
+ SchemaDesignerResponse response =
+ buildSchemaDesignerResponse(configSet, schema, settings, docs);
+ response.updateType = updateType;
if (FIELD_PARAM.equals(updateType)) {
- response.put(updateType, fieldToMap(schema.getField(name), schema));
+ response.field = fieldToMap(schema.getField(name), schema);
} else if ("type".equals(updateType)) {
- response.put(updateType, schema.getFieldTypeByName(name).getNamedPropertyValues(true));
+ response.type = schema.getFieldTypeByName(name).getNamedPropertyValues(true);
}
if (analysisErrorHolder[0] != null) {
- response.put(ANALYSIS_ERROR, analysisErrorHolder[0]);
+ response.analysisError = analysisErrorHolder[0];
}
addErrorToResponse(mutableId, solrExc, errorsDuringIndexing, response, updateError);
- response.put("rebuild", needsRebuild);
- rsp.getValues().addAll(response);
+ response.rebuild = needsRebuild;
+ return 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 SchemaDesignerPublishResponse 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
@@ -531,7 +562,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(
@@ -552,7 +582,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);
@@ -565,10 +594,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);
@@ -578,35 +605,45 @@ && 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<>();
- response.put(CONFIG_SET_PARAM, configSet);
- response.put(SCHEMA_VERSION_PARAM, configSetHelper.getCurrentSchemaVersion(configSet));
+ SchemaDesignerPublishResponse response =
+ instantiateJerseyResponse(SchemaDesignerPublishResponse.class);
+ response.configSet = configSet;
+ response.schemaVersion = configSetHelper.getCurrentSchemaVersion(configSet);
if (StrUtils.isNotNullOrEmpty(newCollection)) {
- response.put(NEW_COLLECTION_PARAM, newCollection);
+ response.newCollection = newCollection;
}
- addErrorToResponse(newCollection, null, errorsDuringIndexing, response, null);
+ addErrorToResponse(newCollection, null, errorsDuringIndexing, response);
- rsp.getValues().addAll(response);
+ return 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 SchemaDesignerResponse 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)) {
@@ -620,27 +657,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,
@@ -649,13 +684,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.getFirst()))
? List.of()
- : Arrays.asList(languages);
+ : languages;
if (!langs.equals(settings.getLanguages())) {
settings.setLanguages(langs);
langsUpdated = true;
@@ -666,7 +700,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;
@@ -697,7 +730,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);
}
@@ -712,7 +744,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);
@@ -732,20 +763,20 @@ public void analyze(SolrQueryRequest req, SolrQueryResponse rsp)
CollectionAdminRequest.reloadCollection(mutableId).process(cloudClient());
}
- Map response =
- buildResponse(configSet, loadLatestSchema(mutableId), settings, docs);
- response.put("sampleSource", sampleDocuments.getSource());
+ SchemaDesignerResponse response =
+ buildSchemaDesignerResponse(configSet, loadLatestSchema(mutableId), settings, docs);
+ response.sampleSource = sampleDocuments.getSource();
if (analysisErrorHolder[0] != null) {
- response.put(ANALYSIS_ERROR, analysisErrorHolder[0]);
+ response.analysisError = analysisErrorHolder[0];
}
addErrorToResponse(mutableId, null, errorsDuringIndexing, response, null);
- rsp.getValues().addAll(response);
+ return 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(
@@ -772,57 +803,80 @@ public void query(SolrQueryRequest req, SolrQueryResponse rsp)
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);
}
if (errorsDuringIndexing != null) {
- Map response = new HashMap<>();
- rsp.setException(
+ Map errorResponse = new HashMap<>();
+ addErrorToResponse(
+ mutableId,
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;
+ "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
- QueryResponse qr = cloudClient().query(mutableId, req.getParams());
- rsp.getValues().addAll(qr.getResponse());
+ QueryResponse qr = cloudClient().query(mutableId, solrQueryRequest.getParams());
+ 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);
}
/**
* 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 SchemaDesignerSchemaDiffResponse 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
String sourceSchema = configExists(configSet) ? configSet : settings.getCopyFrom();
- Map response = new HashMap<>();
- response.put(
- "diff", ManagedSchemaDiff.diff(loadLatestSchema(sourceSchema), settings.getSchema()));
- response.put("diff-source", sourceSchema);
+ SchemaDesignerSchemaDiffResponse response =
+ instantiateJerseyResponse(SchemaDesignerSchemaDiffResponse.class);
+ response.diff = ManagedSchemaDiff.diff(loadLatestSchema(sourceSchema), settings.getSchema());
+ response.diffSource = sourceSchema;
addSettingsToResponse(settings, response);
- rsp.getValues().addAll(response);
+ return 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
@@ -883,7 +937,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
@@ -963,8 +1017,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)
@@ -1104,7 +1158,7 @@ protected long waitToSeeSampleDocs(String collectionName, long numAdded)
return numFound;
}
- protected Map buildResponse(
+ SchemaDesignerResponse buildSchemaDesignerResponse(
String configSet,
final ManagedIndexSchema schema,
SchemaDesignerSettings settings,
@@ -1114,50 +1168,44 @@ protected Map buildResponse(
int currentVersion = configSetHelper.getCurrentSchemaVersion(mutableId);
indexedVersion.put(mutableId, currentVersion);
- // response is a map of data structures to support the schema designer
- Map response = new HashMap<>();
+ SchemaDesignerResponse response = instantiateJerseyResponse(SchemaDesignerResponse.class);
DocCollection coll = zkStateReader().getCollection(mutableId);
Collection activeSlices = coll.getActiveSlices();
if (!activeSlices.isEmpty()) {
- String coreName = activeSlices.stream().findAny().orElseThrow().getLeader().getCoreName();
- response.put("core", coreName);
+ response.core = activeSlices.stream().findAny().orElseThrow().getLeader().getCoreName();
}
- response.put(UNIQUE_KEY_FIELD_PARAM, schema.getUniqueKeyField().getName());
-
- response.put(CONFIG_SET_PARAM, configSet);
+ response.uniqueKeyField = schema.getUniqueKeyField().getName();
+ response.configSet = configSet;
// important: pass the designer the current schema zk version for MVCC
- response.put(SCHEMA_VERSION_PARAM, currentVersion);
- response.put(TEMP_COLLECTION_PARAM, mutableId);
- response.put("collectionsForConfig", configSetHelper.listCollectionsForConfig(configSet));
+ response.schemaVersion = currentVersion;
+ response.tempCollection = mutableId;
+ response.collectionsForConfig = configSetHelper.listCollectionsForConfig(configSet);
// Guess at a schema for each field found in the sample docs
// Collect all fields across all docs with mapping to values
- response.put(
- "fields",
+ response.fields =
schema.getFields().values().stream()
.map(f -> fieldToMap(f, schema))
.sorted(Comparator.comparing(map -> ((String) map.get("name"))))
- .collect(Collectors.toList()));
+ .collect(Collectors.toList());
if (settings == null) {
settings = settingsDAO.getSettings(mutableId);
}
addSettingsToResponse(settings, response);
- response.put(
- "dynamicFields",
+ response.dynamicFields =
Arrays.stream(schema.getDynamicFieldPrototypes())
.map(e -> e.getNamedPropertyValues(true))
.sorted(Comparator.comparing(map -> ((String) map.get("name"))))
- .collect(Collectors.toList()));
+ .collect(Collectors.toList());
- response.put(
- "fieldTypes",
+ response.fieldTypes =
schema.getFieldTypes().values().stream()
.map(fieldType -> fieldType.getNamedPropertyValues(true))
.sorted(Comparator.comparing(map -> ((String) map.get("name"))))
- .collect(Collectors.toList()));
+ .collect(Collectors.toList());
// files
SolrZkClient zkClient = zkStateReader().getZkClient();
@@ -1184,25 +1232,69 @@ protected Map buildResponse(
List sortedFiles = new ArrayList<>(stripPrefix);
Collections.sort(sortedFiles);
- response.put("files", sortedFiles);
+ response.files = sortedFiles;
// info about the sample docs
if (docs != null) {
final String uniqueKeyField = schema.getUniqueKeyField().getName();
- response.put(
- "docIds",
+ response.docIds =
docs.stream()
.map(d -> (String) d.getFieldValue(uniqueKeyField))
.filter(Objects::nonNull)
.limit(100)
- .collect(Collectors.toList()));
+ .collect(Collectors.toList());
}
- response.put("numDocs", docs != null ? docs.size() : -1);
+ response.numDocs = docs != null ? docs.size() : -1;
return response;
}
+ /**
+ * Sets the response's name field for the schema object that was just added, based on the action
+ * key from the Schema API request body. The four valid actions are pre-validated by {@link
+ * SchemaDesignerConfigSetHelper#addSchemaObject}, so reaching {@code default} indicates a
+ * programmer error (e.g. a new action added upstream without a corresponding case here).
+ */
+ private static void setSchemaObjectField(
+ SchemaDesignerResponse response, String action, Object value) {
+ switch (action) {
+ case "add-field" -> response.field = value;
+ case "add-dynamic-field" -> response.dynamicField = value;
+ case "add-field-type" -> response.fieldType = value;
+ case "add-copy-field" -> {
+ // Copy fields have no single "name" to surface on the response — the JS UI only checks
+ // for an error and refreshes; nothing to set.
+ }
+ default -> throw new IllegalStateException(
+ "Unhandled schema-designer action '"
+ + action
+ + "'; addSchemaObject should have rejected this upstream.");
+ }
+ }
+
+ /**
+ * Merges sample-document indexing errors into a response so the endpoint can return them to the
+ * UI instead of throwing. Sample docs are indexed into a temp collection to drive field-type
+ * inference; when that indexing fails, callers continue to build a normal response and use this
+ * method to attach the error details.
+ *
+ * Two error sources are accepted (either or both may be present):
+ *
+ *
+ * {@code solrExc} — a top-level exception (e.g. the whole indexing call failed). May be
+ * {@code null}.
+ * {@code errorsDuringIndexing} — per-document failures. Keys are the failing document's
+ * unique-id field VALUE (typed as {@code Object} because the schema's id field type is
+ * unknown — typically {@code String}, but could be e.g. {@code Long}); values are the root
+ * cause for that doc. May be {@code null} or empty.
+ *
+ *
+ * If both are absent, the response is left untouched. Otherwise, populates {@code updateError}
+ * (message), {@code updateErrorCode} (HTTP-style code; defaults to 400), and {@code errorDetails}
+ * (the per-doc map). The {@code updateError} parameter, when non-null, overrides {@code
+ * solrExc.getMessage()} as the user-facing message.
+ */
protected void addErrorToResponse(
String collection,
SolrException solrExc,
@@ -1230,6 +1322,75 @@ protected void addErrorToResponse(
}
}
+ /**
+ * Overload that writes into the typed {@link SchemaDesignerResponse}. See {@link
+ * #addErrorToResponse(String, SolrException, Map, Map, String)} for full semantics.
+ */
+ protected void addErrorToResponse(
+ String collection,
+ SolrException solrExc,
+ Map errorsDuringIndexing,
+ SchemaDesignerResponse response,
+ String updateError) {
+
+ if (solrExc == null && (errorsDuringIndexing == null || errorsDuringIndexing.isEmpty())) {
+ return; // no errors
+ }
+
+ if (updateError != null) {
+ response.updateError = updateError;
+ }
+
+ if (solrExc != null) {
+ response.updateErrorCode = solrExc.code();
+ if (response.updateError == null) {
+ response.updateError = solrExc.getMessage();
+ }
+ }
+
+ if (response.updateError == null) {
+ response.updateError = "Index sample documents into " + collection + " failed!";
+ }
+ if (response.updateErrorCode == null) {
+ response.updateErrorCode = 400;
+ }
+ if (errorsDuringIndexing != null) {
+ response.errorDetails = errorsDuringIndexing;
+ }
+ }
+
+ /**
+ * Overload that writes into the typed {@link SchemaDesignerPublishResponse}. See {@link
+ * #addErrorToResponse(String, SolrException, Map, Map, String)} for full semantics. Note this
+ * variant has no {@code updateError} override parameter — publish callers always derive the
+ * message from {@code solrExc} or the default.
+ */
+ protected void addErrorToResponse(
+ String collection,
+ SolrException solrExc,
+ Map errorsDuringIndexing,
+ SchemaDesignerPublishResponse response) {
+
+ if (solrExc == null && (errorsDuringIndexing == null || errorsDuringIndexing.isEmpty())) {
+ return; // no errors
+ }
+
+ if (solrExc != null) {
+ response.updateErrorCode = solrExc.code();
+ response.updateError = solrExc.getMessage();
+ }
+
+ if (response.updateError == null) {
+ response.updateError = "Index sample documents into " + collection + " failed!";
+ }
+ if (response.updateErrorCode == null) {
+ response.updateErrorCode = 400;
+ }
+ if (errorsDuringIndexing != null) {
+ response.errorDetails = errorsDuringIndexing;
+ }
+ }
+
protected SimpleOrderedMap fieldToMap(SchemaField f, ManagedIndexSchema schema) {
SimpleOrderedMap map = f.getNamedPropertyValues(true);
@@ -1244,8 +1405,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)) {
@@ -1258,22 +1419,18 @@ protected Map readJsonFromRequest(SolrQueryRequest req) throws I
return (Map) json;
}
- protected 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());
- response.put(ENABLE_NESTED_DOCS_PARAM, settings.nestedDocsEnabled());
- response.put(DISABLED, settings.isDisabled());
- Optional publishedVersion = settings.getPublishedVersion();
- publishedVersion.ifPresent(version -> response.put(PUBLISHED_VERSION, version));
- String copyFrom = settings.getCopyFrom();
- if (copyFrom != null) {
- response.put(COPY_FROM_PARAM, copyFrom);
- }
+ void addSettingsToResponse(
+ SchemaDesignerSettings settings, final SchemaDesignerSettingsResponse response) {
+ response.languages = settings.getLanguages();
+ response.enableFieldGuessing = settings.fieldGuessingEnabled();
+ response.enableDynamicFields = settings.dynamicFieldsEnabled();
+ response.enableNestedDocs = settings.nestedDocsEnabled();
+ response.disabled = settings.isDisabled();
+ settings.getPublishedVersion().ifPresent(v -> response.publishedVersion = v);
+ response.copyFrom = settings.getCopyFrom();
}
- 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)) {
@@ -1288,34 +1445,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 || schemaVersion < 0) {
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());
@@ -1323,6 +1473,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/java/org/apache/solr/handler/designer/SchemaDesignerConfigSetHelper.java b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerConfigSetHelper.java
index 907e0933acf4..7b5a0d41db51 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
@@ -20,24 +20,18 @@
import static org.apache.solr.common.params.CommonParams.VERSION_FIELD;
import static org.apache.solr.common.util.Utils.toJavabin;
import static org.apache.solr.handler.admin.ConfigSetsHandler.DEFAULT_CONFIGSET_NAME;
-import static org.apache.solr.handler.designer.SchemaDesignerAPI.getConfigSetZkPath;
-import static org.apache.solr.handler.designer.SchemaDesignerAPI.getMutableId;
+import static org.apache.solr.handler.designer.SchemaDesigner.getConfigSetZkPath;
+import static org.apache.solr.handler.designer.SchemaDesigner.getMutableId;
import static org.apache.solr.schema.IndexSchema.NEST_PATH_FIELD_NAME;
import static org.apache.solr.schema.IndexSchema.ROOT_FIELD_NAME;
import static org.apache.solr.schema.ManagedIndexSchemaFactory.DEFAULT_MANAGED_SCHEMA_RESOURCE_NAME;
-import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.MethodHandles;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
-import java.nio.file.FileVisitResult;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.SimpleFileVisitor;
-import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -46,16 +40,11 @@
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;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipOutputStream;
-import org.apache.commons.io.FilenameUtils;
-import org.apache.commons.io.file.PathUtils;
import org.apache.lucene.util.IOSupplier;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.SolrResponse;
@@ -73,7 +62,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;
@@ -382,7 +370,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
@@ -403,7 +391,7 @@ boolean updateField(
name,
src);
multiValued = Boolean.TRUE;
- diff.put(MULTIVALUED, multiValued);
+ diff.put(MULTIVALUED, true);
break;
}
}
@@ -414,8 +402,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(
@@ -535,29 +523,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);
}
@@ -706,7 +671,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()) {
@@ -816,8 +781,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());
@@ -958,9 +923,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);
@@ -1032,8 +997,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
@@ -1097,52 +1062,6 @@ List listConfigsInZk() throws IOException {
return cc.getConfigSetService().listConfigs();
}
- byte[] downloadAndZipConfigSet(String configId) throws IOException {
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- Path tmpDirectory =
- Files.createTempDirectory("schema-designer-" + FilenameUtils.getName(configId));
- try {
- cc.getConfigSetService().downloadConfig(configId, tmpDirectory);
- try (ZipOutputStream zipOut = new ZipOutputStream(baos)) {
- Files.walkFileTree(
- tmpDirectory,
- new SimpleFileVisitor<>() {
- @Override
- public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
- throws IOException {
- if (Files.isHidden(dir)) {
- return FileVisitResult.SKIP_SUBTREE;
- }
-
- String dirName = tmpDirectory.relativize(dir).toString();
- if (!dirName.endsWith("/")) {
- dirName += "/";
- }
- zipOut.putNextEntry(new ZipEntry(dirName));
- zipOut.closeEntry();
- return FileVisitResult.CONTINUE;
- }
-
- @Override
- public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
- throws IOException {
- if (!Files.isHidden(file)) {
- try (InputStream fis = Files.newInputStream(file)) {
- ZipEntry zipEntry = new ZipEntry(tmpDirectory.relativize(file).toString());
- zipOut.putNextEntry(zipEntry);
- fis.transferTo(zipOut);
- }
- }
- return FileVisitResult.CONTINUE;
- }
- });
- }
- } finally {
- PathUtils.deleteDirectory(tmpDirectory);
- }
- return baos.toByteArray();
- }
-
protected ZkSolrResourceLoader zkLoaderForConfigSet(final String configSet) {
SolrResourceLoader loader = cc.getResourceLoader();
return new ZkSolrResourceLoader(
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/SchemaDesignerSettingsDAO.java b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerSettingsDAO.java
index 9a09e8e5da94..939a89004978 100644
--- a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerSettingsDAO.java
+++ b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerSettingsDAO.java
@@ -17,7 +17,7 @@
package org.apache.solr.handler.designer;
-import static org.apache.solr.handler.designer.SchemaDesignerAPI.getConfigSetZkPath;
+import static org.apache.solr.handler.designer.SchemaDesigner.getConfigSetZkPath;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
diff --git a/solr/core/src/java/org/apache/solr/handler/designer/package-info.java b/solr/core/src/java/org/apache/solr/handler/designer/package-info.java
index 17e3b7af2761..c8516c1c3273 100644
--- a/solr/core/src/java/org/apache/solr/handler/designer/package-info.java
+++ b/solr/core/src/java/org/apache/solr/handler/designer/package-info.java
@@ -20,5 +20,5 @@
* limitations under the License.
*/
-/** The {@link org.apache.solr.handler.designer.SchemaDesignerAPI} and supporting classes. */
+/** The {@link org.apache.solr.handler.designer.SchemaDesigner} and supporting classes. */
package org.apache.solr.handler.designer;
diff --git a/solr/core/src/java/org/apache/solr/search/ReRankOperator.java b/solr/core/src/java/org/apache/solr/search/ReRankOperator.java
index b3b5582836cc..9072a18e25e3 100644
--- a/solr/core/src/java/org/apache/solr/search/ReRankOperator.java
+++ b/solr/core/src/java/org/apache/solr/search/ReRankOperator.java
@@ -25,6 +25,12 @@ public enum ReRankOperator implements DoubleBinaryOperator {
MULTIPLY((firstPass, secondPass) -> firstPass * secondPass),
REPLACE((firstPass, secondPass) -> secondPass);
+ /**
+ * The operators we use are immutable, even if we can't annotate them as such, and
+ * errorprone's "TIP" of using abstract methods would make this enum declaration 5 times longer --
+ * so no thanks.
+ */
+ @SuppressWarnings("ImmutableEnumChecker")
private final DoubleBinaryOperator op;
private ReRankOperator(final DoubleBinaryOperator op) {
diff --git a/solr/core/src/test-files/solr/collection1/conf/solrconfig-analytics-query.xml b/solr/core/src/test-files/solr/collection1/conf/solrconfig-analytics-query.xml
index 1012b14a0c8a..df769ec03f43 100644
--- a/solr/core/src/test-files/solr/collection1/conf/solrconfig-analytics-query.xml
+++ b/solr/core/src/test-files/solr/collection1/conf/solrconfig-analytics-query.xml
@@ -39,10 +39,6 @@
solr.StandardDirectoryFactory, the default, is filesystem based.
solr.RAMDirectoryFactory is memory based and not persistent. -->
- 1000000
- 2000000
- 3000000
- 4000000
diff --git a/solr/core/src/test-files/solr/collection1/conf/solrconfig-collapseqparser.xml b/solr/core/src/test-files/solr/collection1/conf/solrconfig-collapseqparser.xml
index b346eb08db09..de5721946bf1 100644
--- a/solr/core/src/test-files/solr/collection1/conf/solrconfig-collapseqparser.xml
+++ b/solr/core/src/test-files/solr/collection1/conf/solrconfig-collapseqparser.xml
@@ -39,10 +39,6 @@
solr.StandardDirectoryFactory, the default, is filesystem based.
solr.RAMDirectoryFactory is memory based and not persistent. -->
- 1000000
- 2000000
- 3000000
- 4000000
diff --git a/solr/core/src/test-files/solr/collection1/conf/solrconfig-minhash.xml b/solr/core/src/test-files/solr/collection1/conf/solrconfig-minhash.xml
index 8d6e8e842b84..92cc381d60cd 100644
--- a/solr/core/src/test-files/solr/collection1/conf/solrconfig-minhash.xml
+++ b/solr/core/src/test-files/solr/collection1/conf/solrconfig-minhash.xml
@@ -39,10 +39,6 @@
solr.StandardDirectoryFactory, the default, is filesystem based.
solr.RAMDirectoryFactory is memory based and not persistent. -->
- 1000000
- 2000000
- 3000000
- 4000000
diff --git a/solr/core/src/test-files/solr/collection1/conf/solrconfig-plugcollector.xml b/solr/core/src/test-files/solr/collection1/conf/solrconfig-plugcollector.xml
index f543311b8562..e3afeae98e55 100644
--- a/solr/core/src/test-files/solr/collection1/conf/solrconfig-plugcollector.xml
+++ b/solr/core/src/test-files/solr/collection1/conf/solrconfig-plugcollector.xml
@@ -39,10 +39,6 @@
solr.StandardDirectoryFactory, the default, is filesystem based.
solr.RAMDirectoryFactory is memory based and not persistent. -->
- 1000000
- 2000000
- 3000000
- 4000000
diff --git a/solr/core/src/test-files/solr/collection1/conf/solrconfig.xml b/solr/core/src/test-files/solr/collection1/conf/solrconfig.xml
index 6a960666848c..d7865f2a73aa 100644
--- a/solr/core/src/test-files/solr/collection1/conf/solrconfig.xml
+++ b/solr/core/src/test-files/solr/collection1/conf/solrconfig.xml
@@ -39,10 +39,6 @@
solr.StandardDirectoryFactory, the default, is filesystem based.
solr.RAMDirectoryFactory is memory based and not persistent. -->
- 1000000
- 2000000
- 3000000
- 4000000
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/UpgradeCoreIndexActionTest.java b/solr/core/src/test/org/apache/solr/handler/admin/UpgradeCoreIndexActionTest.java
index 536652f270d4..3a8e83171262 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/UpgradeCoreIndexActionTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/UpgradeCoreIndexActionTest.java
@@ -87,7 +87,7 @@ public void testUpgradeCoreIndexSelectiveReindexDeletesOldSegments() throws Exce
admin.handleRequestBody(
req(
CoreAdminParams.ACTION,
- CoreAdminParams.CoreAdminAction.UPGRADECOREINDEX.toString(),
+ CoreAdminParams.CoreAdminAction.UPGRADEINDEX.toString(),
CoreAdminParams.CORE,
coreName),
resp);
@@ -132,14 +132,14 @@ public void testUpgradeCoreIndexAsyncRequestStatusContainsOperationResponse() th
final Set segmentsBeforeUpgrade = listSegmentNames(core);
- final String requestId = "upgradecoreindex_async_1";
+ final String requestId = "upgradeindex_async_1";
CoreAdminHandler admin = new CoreAdminHandler(h.getCoreContainer());
try {
SolrQueryResponse submitResp = new SolrQueryResponse();
admin.handleRequestBody(
req(
CoreAdminParams.ACTION,
- CoreAdminParams.CoreAdminAction.UPGRADECOREINDEX.toString(),
+ CoreAdminParams.CoreAdminAction.UPGRADEINDEX.toString(),
CoreAdminParams.CORE,
coreName,
CommonAdminParams.ASYNC,
@@ -213,7 +213,7 @@ public void testNoUpgradeNeededWhenAllSegmentsCurrent() throws Exception {
admin.handleRequestBody(
req(
CoreAdminParams.ACTION,
- CoreAdminParams.CoreAdminAction.UPGRADECOREINDEX.toString(),
+ CoreAdminParams.CoreAdminAction.UPGRADEINDEX.toString(),
CoreAdminParams.CORE,
coreName),
resp);
@@ -360,7 +360,7 @@ public void testUpgradeCoreIndexFailsWithChildDocuments() throws Exception {
admin.handleRequestBody(
req(
CoreAdminParams.ACTION,
- CoreAdminParams.CoreAdminAction.UPGRADECOREINDEX.toString(),
+ CoreAdminParams.CoreAdminAction.UPGRADEINDEX.toString(),
CoreAdminParams.CORE,
coreName),
resp));
@@ -386,7 +386,7 @@ public void testChildDocsDetection_noChildDocs() throws Exception {
admin.handleRequestBody(
req(
CoreAdminParams.ACTION,
- CoreAdminParams.CoreAdminAction.UPGRADECOREINDEX.toString(),
+ CoreAdminParams.CoreAdminAction.UPGRADEINDEX.toString(),
CoreAdminParams.CORE,
coreName),
resp);
@@ -413,7 +413,7 @@ public void testChildDocsDetection_withChildDocs() throws Exception {
admin.handleRequestBody(
req(
CoreAdminParams.ACTION,
- CoreAdminParams.CoreAdminAction.UPGRADECOREINDEX.toString(),
+ CoreAdminParams.CoreAdminAction.UPGRADEINDEX.toString(),
CoreAdminParams.CORE,
coreName),
resp));
diff --git a/solr/core/src/test/org/apache/solr/handler/configsets/DownloadConfigSetAPITest.java b/solr/core/src/test/org/apache/solr/handler/configsets/DownloadConfigSetAPITest.java
index 10325b278ee8..7ae288e7fdaa 100644
--- a/solr/core/src/test/org/apache/solr/handler/configsets/DownloadConfigSetAPITest.java
+++ b/solr/core/src/test/org/apache/solr/handler/configsets/DownloadConfigSetAPITest.java
@@ -89,6 +89,29 @@ public void testSuccessfulDownloadReturnsZipResponse() throws Exception {
try (final Response response = api.downloadConfigSet("myconfig")) {
assertEquals(200, response.getStatus());
assertEquals("application/zip", response.getMediaType().toString());
+ assertEquals(
+ "attachment; filename=\"myconfig_configset.zip\"",
+ response.getHeaderString("Content-Disposition"));
}
}
+
+ @Test
+ public void testDesignerPrefixStrippedFromFilename() throws Exception {
+ createConfigSet("._designer_myschema", "solrconfig.xml", " ");
+
+ final var api = new DownloadConfigSet(mockCoreContainer, null, null);
+ try (final Response response = api.downloadConfigSet("._designer_myschema")) {
+ assertEquals(200, response.getStatus());
+ assertEquals(
+ "attachment; filename=\"myschema_configset.zip\"",
+ response.getHeaderString("Content-Disposition"));
+ }
+ }
+
+ @Test
+ public void testDeriveDisplayName() {
+ assertEquals("myschema", DownloadConfigSet.deriveDisplayName("._designer_myschema"));
+ assertEquals("plain", DownloadConfigSet.deriveDisplayName("plain"));
+ assertEquals("", DownloadConfigSet.deriveDisplayName("._designer_"));
+ }
}
diff --git a/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesigner.java b/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesigner.java
new file mode 100644
index 000000000000..2845fe0707fa
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesigner.java
@@ -0,0 +1,840 @@
+/*
+ * 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.handler.designer;
+
+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.SchemaDesigner.getMutableId;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+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.api.model.SchemaDesignerAddRequestBody;
+import org.apache.solr.client.api.model.SchemaDesignerCollectionsResponse;
+import org.apache.solr.client.api.model.SchemaDesignerInfoResponse;
+import org.apache.solr.client.api.model.SchemaDesignerResponse;
+import org.apache.solr.client.api.model.SchemaDesignerSchemaDiffResponse;
+import org.apache.solr.client.api.model.SchemaDesignerUpdateRequestBody;
+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;
+import org.apache.solr.common.util.ContentStream;
+import org.apache.solr.common.util.ContentStreamBase;
+import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.TestSampleDocumentsLoader;
+import org.apache.solr.jersey.SolrJacksonMapper;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.schema.ManagedIndexSchema;
+import org.apache.solr.schema.SchemaField;
+import org.apache.solr.util.ExternalPaths;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class TestSchemaDesigner extends SolrCloudTestCase implements SchemaDesignerConstants {
+
+ private CoreContainer cc;
+ private SchemaDesigner schemaDesigner;
+ private SolrQueryRequest mockReq;
+
+ @BeforeClass
+ public static void createCluster() throws Exception {
+ System.setProperty("managed.schema.mutable", "true");
+ configureCluster(1)
+ .addConfig(DEFAULT_CONFIGSET_NAME, ExternalPaths.DEFAULT_CONFIGSET)
+ .configure();
+ }
+
+ @AfterClass
+ public static void tearDownCluster() throws Exception {
+ if (cluster != null && cluster.getSolrClient() != null) {
+ cluster.deleteAllCollections();
+ cluster.deleteAllConfigSets();
+ }
+ }
+
+ @Before
+ public void setupTest() {
+ assumeWorkingMockito();
+ assertNotNull(cluster);
+ cc = cluster.getJettySolrRunner(0).getCoreContainer();
+ assertNotNull(cc);
+ mockReq = mock(SolrQueryRequest.class);
+ schemaDesigner =
+ new SchemaDesigner(
+ cc,
+ SchemaDesigner.newSchemaSuggester(),
+ SchemaDesigner.newSampleDocumentsLoader(),
+ mockReq);
+ }
+
+ public void testTSV() throws Exception {
+ String configSet = "testTSV";
+
+ ModifiableSolrParams reqParams = new ModifiableSolrParams();
+ reqParams.set(CONFIG_SET_PARAM, configSet);
+ reqParams.set(LANGUAGES_PARAM, "en");
+ reqParams.set(ENABLE_DYNAMIC_FIELDS_PARAM, false);
+ 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(mockReq.getContentStreams()).thenReturn(List.of(stream));
+
+ // POST /schema-designer/analyze
+ SchemaDesignerResponse response =
+ schemaDesigner.analyze(configSet, null, null, null, List.of("en"), false, null, null);
+ assertNotNull(response.configSet);
+ assertNotNull(response.schemaVersion);
+ assertEquals(Integer.valueOf(2), response.numDocs);
+
+ reqParams.clear();
+ reqParams.set(CONFIG_SET_PARAM, configSet);
+ when(mockReq.getContentStreams()).thenReturn(null);
+ schemaDesigner.cleanupTempSchema(configSet);
+
+ String mutableId = getMutableId(configSet);
+ assertFalse(cc.getZkController().getClusterState().hasCollection(mutableId));
+ SolrZkClient zkClient = cc.getZkController().getZkClient();
+ assertFalse(zkClient.exists("/configs/" + mutableId));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testAddTechproductsProgressively() throws Exception {
+ Path docsDir = ExternalPaths.SOURCE_HOME.resolve("example/exampledocs");
+ assertTrue(docsDir + " not found!", Files.isDirectory(docsDir));
+ List toAdd;
+ try (Stream files = Files.list(docsDir)) {
+ toAdd =
+ files
+ .filter(
+ (dir) -> {
+ String name = dir.getFileName().toString();
+ return name.endsWith(".xml")
+ || name.endsWith(".json")
+ || name.endsWith(".csv")
+ || name.endsWith(".jsonl");
+ })
+ .toList();
+ assertNotNull("No test data files found in " + docsDir, toAdd);
+ }
+
+ String configSet = "techproducts";
+
+ // GET /schema-designer/info
+ SchemaDesignerInfoResponse infoResponse = schemaDesigner.getInfo(configSet);
+ // response should just be the default values
+ Map expSettings =
+ Map.of(
+ ENABLE_DYNAMIC_FIELDS_PARAM, true,
+ ENABLE_FIELD_GUESSING_PARAM, true,
+ ENABLE_NESTED_DOCS_PARAM, false,
+ LANGUAGES_PARAM, List.of());
+ assertDesignerSettings(expSettings, infoResponse);
+ int schemaVersion = infoResponse.schemaVersion;
+ assertEquals(schemaVersion, -1); // shouldn't exist yet
+
+ // Use the prep endpoint to prepare the new schema
+ SchemaDesignerResponse response = schemaDesigner.prepNewSchema(configSet, null);
+ assertNotNull(response.configSet);
+ assertNotNull(response.schemaVersion);
+ schemaVersion = response.schemaVersion;
+
+ for (Path next : toAdd) {
+ // Analyze some sample documents to refine the schema
+ ModifiableSolrParams reqParams = new ModifiableSolrParams();
+ reqParams.set(CONFIG_SET_PARAM, configSet);
+ 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(mockReq.getContentStreams()).thenReturn(List.of(stream));
+
+ // POST /schema-designer/analyze
+ response =
+ schemaDesigner.analyze(
+ configSet, schemaVersion, null, null, List.of("en"), false, null, null);
+
+ assertNotNull(response.configSet);
+ assertNotNull(response.schemaVersion);
+ assertNotNull(response.fields);
+ assertNotNull(response.fieldTypes);
+ assertNotNull(response.docIds);
+
+ // capture the schema version for MVCC
+ schemaVersion = response.schemaVersion;
+ }
+
+ // get info (from the temp)
+ // GET /schema-designer/info
+ infoResponse = schemaDesigner.getInfo(configSet);
+ expSettings =
+ Map.of(
+ ENABLE_DYNAMIC_FIELDS_PARAM, false,
+ ENABLE_FIELD_GUESSING_PARAM, true,
+ ENABLE_NESTED_DOCS_PARAM, false,
+ LANGUAGES_PARAM, List.of("en"),
+ COPY_FROM_PARAM, "_default");
+ assertDesignerSettings(expSettings, infoResponse);
+
+ // query to see how the schema decisions impact retrieval / ranking
+ 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
+ FlexibleSolrJerseyResponse queryResp = schemaDesigner.query(configSet);
+ assertNotNull(queryResp.unknownProperties().get("responseHeader"));
+ @SuppressWarnings("unchecked")
+ Map queryResponse =
+ (Map) queryResp.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);
+ 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";
+ schemaDesigner.publish(configSet, schemaVersion, collection, true, 1, 1, true, true, true);
+ assertNotNull(cc.getZkController().zkStateReader.getCollection(collection));
+
+ // listCollectionsForConfig
+ SchemaDesignerCollectionsResponse collectionsResp =
+ schemaDesigner.listCollectionsForConfig(configSet);
+ List collections = collectionsResp.collections;
+ assertNotNull(collections);
+ assertTrue(collections.contains(collection));
+
+ // now try to create another temp, which should fail since designer is disabled for this
+ // configSet now
+ try {
+ schemaDesigner.prepNewSchema(configSet, null);
+ fail("Prep should fail for locked schema " + configSet);
+ } catch (SolrException solrExc) {
+ assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, solrExc.code());
+ }
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testSuggestFilmsXml() throws Exception {
+ String configSet = "films";
+
+ 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));
+
+ // POST some sample XML docs
+ ContentStreamBase.FileStream stream = new ContentStreamBase.FileStream(filmsXml);
+ stream.setContentType("application/xml");
+ ModifiableSolrParams reqParams = new ModifiableSolrParams();
+ reqParams.set(CONFIG_SET_PARAM, configSet);
+ when(mockReq.getParams()).thenReturn(reqParams);
+ when(mockReq.getContentStreams()).thenReturn(List.of(stream));
+
+ // POST /schema-designer/analyze
+ SchemaDesignerResponse response =
+ schemaDesigner.analyze(configSet, null, null, null, null, true, null, null);
+
+ assertNotNull(response.configSet);
+ assertNotNull(response.schemaVersion);
+ assertNotNull(response.fields);
+ assertNotNull(response.fieldTypes);
+ List docIds = response.docIds;
+ assertNotNull(docIds);
+ assertEquals(100, docIds.size()); // designer limits the doc ids to top 100
+
+ String idField = response.uniqueKeyField;
+ assertNotNull(idField);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testBasicUserWorkflow() throws Exception {
+ String configSet = "testJson";
+
+ // Use the prep endpoint to prepare the new schema
+ SchemaDesignerResponse response = schemaDesigner.prepNewSchema(configSet, null);
+ assertNotNull(response.configSet);
+ assertNotNull(response.schemaVersion);
+
+ Map expSettings =
+ Map.of(
+ ENABLE_DYNAMIC_FIELDS_PARAM, true,
+ ENABLE_FIELD_GUESSING_PARAM, true,
+ ENABLE_NESTED_DOCS_PARAM, false,
+ LANGUAGES_PARAM, List.of(),
+ COPY_FROM_PARAM, "_default");
+ assertDesignerSettings(expSettings, response);
+
+ // Analyze some sample documents to refine the schema
+ Path booksJson = ExternalPaths.SOURCE_HOME.resolve("example/exampledocs/books.json");
+ ContentStreamBase.FileStream stream = new ContentStreamBase.FileStream(booksJson);
+ stream.setContentType(JSON_MIME);
+ ModifiableSolrParams reqParams = new ModifiableSolrParams();
+ reqParams.set(CONFIG_SET_PARAM, configSet);
+ when(mockReq.getParams()).thenReturn(reqParams);
+ when(mockReq.getContentStreams()).thenReturn(List.of(stream));
+
+ // POST /schema-designer/analyze
+ response = schemaDesigner.analyze(configSet, null, null, null, null, null, null, null);
+
+ assertNotNull(response.configSet);
+ assertNotNull(response.schemaVersion);
+ assertNotNull(response.fields);
+ assertNotNull(response.fieldTypes);
+ assertNotNull(response.docIds);
+ String idField = response.uniqueKeyField;
+ assertNotNull(idField);
+ assertDesignerSettings(expSettings, response);
+
+ // capture the schema version for MVCC
+ int schemaVersion = response.schemaVersion;
+
+ // load the contents of a file
+ Collection files = response.files;
+ assertTrue(files != null && !files.isEmpty());
+
+ String file = null;
+ for (String f : files) {
+ if ("solrconfig.xml".equals(f)) {
+ file = f;
+ break;
+ }
+ }
+ assertNotNull("solrconfig.xml not found in files!", file);
+ byte[] solrconfigBytes =
+ cc.getConfigSetService().downloadFileFromConfig(getMutableId(configSet), file);
+ assertNotNull(solrconfigBytes);
+ String solrconfigXml = new String(solrconfigBytes, StandardCharsets.UTF_8);
+
+ // Update solrconfig.xml
+ response =
+ schemaDesigner.updateFileContents(
+ configSet,
+ file,
+ new ByteArrayInputStream(solrconfigXml.getBytes(StandardCharsets.UTF_8)));
+ schemaVersion = response.schemaVersion;
+
+ // this should fail b/c the updated solrconfig.xml is invalid
+ response =
+ schemaDesigner.updateFileContents(
+ configSet,
+ file,
+ new ByteArrayInputStream(" ".getBytes(StandardCharsets.UTF_8)));
+ assertNotNull(response.updateFileError);
+
+ // remove dynamic fields and change the language to "en" only
+ when(mockReq.getContentStreams()).thenReturn(null);
+ // POST /schema-designer/analyze
+ response =
+ schemaDesigner.analyze(
+ configSet, schemaVersion, null, null, List.of("en"), false, false, null);
+
+ expSettings =
+ Map.of(
+ ENABLE_DYNAMIC_FIELDS_PARAM, false,
+ ENABLE_FIELD_GUESSING_PARAM, false,
+ ENABLE_NESTED_DOCS_PARAM, false,
+ LANGUAGES_PARAM, List.of("en"),
+ COPY_FROM_PARAM, "_default");
+ assertDesignerSettings(expSettings, response);
+
+ List filesInResp = response.files;
+ assertEquals(5, filesInResp.size());
+ assertTrue(filesInResp.contains("lang/stopwords_en.txt"));
+
+ schemaVersion = response.schemaVersion;
+
+ // add the dynamic fields back and change the languages too
+ response =
+ schemaDesigner.analyze(
+ configSet, schemaVersion, null, null, Arrays.asList("en", "fr"), true, false, null);
+
+ expSettings =
+ Map.of(
+ ENABLE_DYNAMIC_FIELDS_PARAM, true,
+ ENABLE_FIELD_GUESSING_PARAM, false,
+ ENABLE_NESTED_DOCS_PARAM, false,
+ LANGUAGES_PARAM, Arrays.asList("en", "fr"),
+ COPY_FROM_PARAM, "_default");
+ assertDesignerSettings(expSettings, response);
+
+ filesInResp = response.files;
+ assertEquals(7, filesInResp.size());
+ assertTrue(filesInResp.contains("lang/stopwords_fr.txt"));
+
+ schemaVersion = response.schemaVersion;
+
+ // add back all the default languages (using "*" wildcard -> empty list)
+ response =
+ schemaDesigner.analyze(
+ configSet, schemaVersion, null, null, List.of("*"), false, null, null);
+
+ expSettings =
+ Map.of(
+ ENABLE_DYNAMIC_FIELDS_PARAM, false,
+ ENABLE_FIELD_GUESSING_PARAM, false,
+ ENABLE_NESTED_DOCS_PARAM, false,
+ LANGUAGES_PARAM, List.of(),
+ COPY_FROM_PARAM, "_default");
+ assertDesignerSettings(expSettings, response);
+
+ filesInResp = response.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"));
+
+ schemaVersion = response.schemaVersion;
+
+ // Get the value of a sample document
+ String docId = "978-0641723445";
+ String fieldName = "series_t";
+
+ // GET /schema-designer/sample
+ FlexibleSolrJerseyResponse sampleResp =
+ schemaDesigner.getSampleValue(configSet, fieldName, idField, docId);
+ assertNotNull(sampleResp.unknownProperties().get(idField));
+ assertNotNull(sampleResp.unknownProperties().get(fieldName));
+ assertNotNull(sampleResp.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
+ // POST /schema-designer/add
+ response =
+ schemaDesigner.addSchemaObject(
+ configSet, schemaVersion, loadAddBody("schema-designer/add-new-field.json"));
+ assertNotNull(response.field);
+ schemaVersion = response.schemaVersion;
+ assertNotNull(response.fields);
+
+ // update an existing field
+ // switch a single-valued field to a multivalued field, which triggers a full rebuild of the
+ // "temp" collection
+ // PUT /schema-designer/update
+ response =
+ schemaDesigner.updateSchemaObject(
+ configSet, schemaVersion, loadUpdateBody("schema-designer/update-author-field.json"));
+ assertNotNull(response.field);
+ schemaVersion = response.schemaVersion;
+
+ // add a new type
+ // POST /schema-designer/add
+ response =
+ schemaDesigner.addSchemaObject(
+ configSet, schemaVersion, loadAddBody("schema-designer/add-new-type.json"));
+ final String expectedTypeName = "test_txt";
+ assertEquals(expectedTypeName, response.fieldType);
+ schemaVersion = response.schemaVersion;
+ assertNotNull(response.fieldTypes);
+ @SuppressWarnings("unchecked")
+ List> fieldTypes = response.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());
+
+ // POST /schema-designer/update
+ response =
+ schemaDesigner.updateSchemaObject(
+ configSet, schemaVersion, loadUpdateBody("schema-designer/update-type.json"));
+ schemaVersion = response.schemaVersion;
+
+ // query to see how the schema decisions impact retrieval / ranking
+ 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
+ FlexibleSolrJerseyResponse queryResp2 = schemaDesigner.query(configSet);
+ assertNotNull(queryResp2.unknownProperties().get("responseHeader"));
+ @SuppressWarnings("unchecked")
+ Map queryResponse2 =
+ (Map) queryResp2.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());
+
+ // publish schema to a config set that can be used by real collections
+ String collection = "test123";
+ schemaDesigner.publish(configSet, schemaVersion, collection, true, 1, 1, true, true, false);
+
+ assertNotNull(cc.getZkController().zkStateReader.getCollection(collection));
+
+ // listCollectionsForConfig
+ SchemaDesignerCollectionsResponse collectionsResp2 =
+ schemaDesigner.listCollectionsForConfig(configSet);
+ List collections = collectionsResp2.collections;
+ assertNotNull(collections);
+ assertTrue(collections.contains(collection));
+
+ // verify temp designer objects were cleaned up during the publish operation ...
+ String mutableId = getMutableId(configSet);
+ assertFalse(cc.getZkController().getClusterState().hasCollection(mutableId));
+ SolrZkClient zkClient = cc.getZkController().getZkClient();
+ assertFalse(zkClient.exists("/configs/" + mutableId));
+
+ SolrQuery query = new SolrQuery("*:*");
+ query.setRows(0);
+ QueryResponse qr = cluster.getSolrClient().query(collection, query);
+ // this proves the docs were stored in the filestore too
+ assertEquals(4, qr.getResults().getNumFound());
+ }
+
+ @SuppressWarnings("unchecked")
+ public void testFieldUpdates() throws Exception {
+ String configSet = "fieldUpdates";
+
+ // Use the prep endpoint to prepare the new schema
+ SchemaDesignerResponse response = schemaDesigner.prepNewSchema(configSet, null);
+ assertNotNull(response.configSet);
+ assertNotNull(response.schemaVersion);
+ int schemaVersion = response.schemaVersion;
+
+ // add our test field that we'll test various updates to
+ // POST /schema-designer/add
+ response =
+ schemaDesigner.addSchemaObject(
+ configSet, schemaVersion, loadAddBody("schema-designer/add-new-field.json"));
+ assertNotNull(response.field);
+
+ final String fieldName = "keywords";
+
+ Optional> maybeField =
+ response.fields.stream().filter(m -> fieldName.equals(m.get("name"))).findFirst();
+ assertTrue(maybeField.isPresent());
+ Map field = maybeField.get();
+ assertEquals(Boolean.FALSE, field.get("indexed"));
+ assertEquals(Boolean.FALSE, field.get("required"));
+ assertEquals(Boolean.TRUE, field.get("stored"));
+ assertEquals(Boolean.TRUE, field.get("docValues"));
+ assertEquals(Boolean.TRUE, field.get("useDocValuesAsStored"));
+ assertEquals(Boolean.FALSE, field.get("multiValued"));
+ assertEquals("string", field.get("type"));
+
+ String mutableId = getMutableId(configSet);
+ SchemaDesignerConfigSetHelper configSetHelper =
+ new SchemaDesignerConfigSetHelper(cc, SchemaDesigner.newSchemaSuggester());
+ ManagedIndexSchema schema = schemaDesigner.loadLatestSchema(mutableId);
+
+ // make it required
+ Map updateField =
+ Map.of("name", fieldName, "type", field.get("type"), "required", true);
+ configSetHelper.updateField(configSet, updateField, schema);
+
+ schema = schemaDesigner.loadLatestSchema(mutableId);
+ SchemaField schemaField = schema.getField(fieldName);
+ assertTrue(schemaField.isRequired());
+
+ updateField =
+ Map.of("name", fieldName, "type", field.get("type"), "required", false, "stored", false);
+ configSetHelper.updateField(configSet, updateField, schema);
+ schema = schemaDesigner.loadLatestSchema(mutableId);
+ schemaField = schema.getField(fieldName);
+ assertFalse(schemaField.isRequired());
+ assertFalse(schemaField.stored());
+
+ updateField =
+ Map.of(
+ "name",
+ fieldName,
+ "type",
+ field.get("type"),
+ "required",
+ false,
+ "stored",
+ false,
+ "multiValued",
+ true);
+ configSetHelper.updateField(configSet, updateField, schema);
+ schema = schemaDesigner.loadLatestSchema(mutableId);
+ schemaField = schema.getField(fieldName);
+ assertFalse(schemaField.isRequired());
+ assertFalse(schemaField.stored());
+ assertTrue(schemaField.multiValued());
+
+ updateField = Map.of("name", fieldName, "type", "strings", "copyDest", "_text_");
+ configSetHelper.updateField(configSet, updateField, schema);
+ schema = schemaDesigner.loadLatestSchema(mutableId);
+ schemaField = schema.getField(fieldName);
+ assertTrue(schemaField.multiValued());
+ assertEquals("strings", schemaField.getType().getTypeName());
+ assertFalse(schemaField.isRequired());
+ assertTrue(schemaField.stored());
+ List srcFields = schema.getCopySources("_text_");
+ assertEquals(List.of(fieldName), srcFields);
+ }
+
+ @SuppressWarnings({"unchecked"})
+ public void testSchemaDiffEndpoint() throws Exception {
+ String configSet = "testDiff";
+
+ // Use the prep endpoint to prepare the new schema
+ SchemaDesignerResponse response = schemaDesigner.prepNewSchema(configSet, null);
+ assertNotNull(response.configSet);
+ assertNotNull(response.schemaVersion);
+ int schemaVersion = response.schemaVersion;
+
+ // publish schema to a config set that can be used by real collections
+ String collection = "diff456";
+ schemaDesigner.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
+ ModifiableSolrParams reqParams = new ModifiableSolrParams();
+ reqParams.set(CONFIG_SET_PARAM, configSet);
+ when(mockReq.getParams()).thenReturn(reqParams);
+ when(mockReq.getContentStreams()).thenReturn(null);
+ response = schemaDesigner.analyze(configSet, null, null, null, null, true, false, null);
+
+ // Update id field to not use docValues
+ @SuppressWarnings("unchecked")
+ List> fields =
+ (List>) (List>) response.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
+ SimpleOrderedMap idFieldMapUpdated = idFieldMap.clone();
+ idFieldMapUpdated.setVal(idFieldMapUpdated.indexOf("docValues", 0), Boolean.FALSE);
+ idFieldMapUpdated.setVal(idFieldMapUpdated.indexOf("useDocValuesAsStored", 0), Boolean.FALSE);
+ idFieldMapUpdated.setVal(idFieldMapUpdated.indexOf("uninvertible", 0), Boolean.TRUE);
+ idFieldMapUpdated.setVal(
+ idFieldMapUpdated.indexOf("omitTermFreqAndPositions", 0), Boolean.FALSE);
+
+ Map mapParams = idFieldMapUpdated.toSolrParams().toMap(new HashMap<>());
+ mapParams.put("termVectors", Boolean.FALSE);
+ schemaVersion = response.schemaVersion;
+
+ SchemaDesignerUpdateRequestBody idFieldUpdate =
+ SolrJacksonMapper.getObjectMapper()
+ .convertValue(mapParams, SchemaDesignerUpdateRequestBody.class);
+ response = schemaDesigner.updateSchemaObject(configSet, schemaVersion, idFieldUpdate);
+
+ // Add a new field
+ schemaVersion = response.schemaVersion;
+ // POST /schema-designer/add
+ response =
+ schemaDesigner.addSchemaObject(
+ configSet, schemaVersion, loadAddBody("schema-designer/add-new-field.json"));
+ assertNotNull(response.field);
+
+ // Add a new field type
+ schemaVersion = response.schemaVersion;
+ // POST /schema-designer/add
+ response =
+ schemaDesigner.addSchemaObject(
+ configSet, schemaVersion, loadAddBody("schema-designer/add-new-type.json"));
+ assertNotNull(response.fieldType);
+
+ // Let's do a diff now
+ SchemaDesignerSchemaDiffResponse diffResp = schemaDesigner.getSchemaDiff(configSet);
+
+ Map diff = diffResp.diff;
+
+ // field asserts
+ assertNotNull(diff.get("fields"));
+ Map fieldsDiff = (Map) diff.get("fields");
+ assertNotNull(fieldsDiff.get("updated"));
+ Map mapDiff = (Map) fieldsDiff.get("updated");
+ assertEquals(
+ Arrays.asList(
+ Map.of(
+ "omitTermFreqAndPositions",
+ true,
+ "useDocValuesAsStored",
+ true,
+ "docValues",
+ true,
+ "uninvertible",
+ false),
+ Map.of(
+ "omitTermFreqAndPositions",
+ false,
+ "useDocValuesAsStored",
+ false,
+ "docValues",
+ false,
+ "uninvertible",
+ true)),
+ mapDiff.get("id"));
+ assertNotNull(fieldsDiff.get("added"));
+ Map fieldsAdded = (Map) fieldsDiff.get("added");
+ assertNotNull(fieldsAdded.get("keywords"));
+
+ // field type asserts
+ assertNotNull(diff.get("fieldTypes"));
+ Map fieldTypesDiff = (Map) diff.get("fieldTypes");
+ assertNotNull(fieldTypesDiff.get("added"));
+ Map fieldTypesAdded = (Map) fieldTypesDiff.get("added");
+ 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
+ schemaDesigner.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(List.of(stream));
+ schemaDesigner.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"));
+ SchemaDesigner apiWithErrors =
+ new SchemaDesigner(
+ cc,
+ SchemaDesigner.newSchemaSuggester(),
+ SchemaDesigner.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"));
+ }
+
+ @Test
+ public void testRequireSchemaVersionRejectsNegativeValues() throws Exception {
+ String configSet = "schemaVersionValidation";
+ schemaDesigner.prepNewSchema(configSet, null);
+
+ // null schemaVersion must be rejected
+ SolrException nullEx =
+ expectThrows(
+ SolrException.class, () -> schemaDesigner.addSchemaObject(configSet, null, null));
+ assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, nullEx.code());
+
+ // negative schemaVersion must be rejected (was previously bypassing validation)
+ SolrException negEx =
+ expectThrows(
+ SolrException.class, () -> schemaDesigner.addSchemaObject(configSet, -1, null));
+ assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, negEx.code());
+
+ // same contract must hold for updateSchemaObject
+ SolrException updateNegEx =
+ expectThrows(
+ SolrException.class, () -> schemaDesigner.updateSchemaObject(configSet, -1, null));
+ 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);
+ assertEquals(
+ "Value for designer setting '" + expKey + "' not match expected!",
+ expValue,
+ actual.get(expKey));
+ }
+ }
+
+ protected void assertDesignerSettings(
+ Map expected, SchemaDesignerResponse response) {
+ Map actual = new HashMap<>();
+ actual.put(LANGUAGES_PARAM, response.languages);
+ actual.put(ENABLE_FIELD_GUESSING_PARAM, response.enableFieldGuessing);
+ actual.put(ENABLE_DYNAMIC_FIELDS_PARAM, response.enableDynamicFields);
+ actual.put(ENABLE_NESTED_DOCS_PARAM, response.enableNestedDocs);
+ actual.put(COPY_FROM_PARAM, response.copyFrom);
+ assertDesignerSettings(expected, actual);
+ }
+
+ protected void assertDesignerSettings(
+ Map expected, SchemaDesignerInfoResponse response) {
+ Map actual = new HashMap<>();
+ actual.put(LANGUAGES_PARAM, response.languages);
+ actual.put(ENABLE_FIELD_GUESSING_PARAM, response.enableFieldGuessing);
+ actual.put(ENABLE_DYNAMIC_FIELDS_PARAM, response.enableDynamicFields);
+ actual.put(ENABLE_NESTED_DOCS_PARAM, response.enableNestedDocs);
+ actual.put(COPY_FROM_PARAM, response.copyFrom);
+ assertDesignerSettings(expected, actual);
+ }
+
+ private SchemaDesignerAddRequestBody loadAddBody(String fixturePath) throws IOException {
+ return SolrJacksonMapper.getObjectMapper()
+ .readValue(getFile(fixturePath).toFile(), SchemaDesignerAddRequestBody.class);
+ }
+
+ private SchemaDesignerUpdateRequestBody loadUpdateBody(String fixturePath) throws IOException {
+ return SolrJacksonMapper.getObjectMapper()
+ .readValue(getFile(fixturePath).toFile(), SchemaDesignerUpdateRequestBody.class);
+ }
+}
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
deleted file mode 100644
index ed1aba6ceb58..000000000000
--- a/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerAPI.java
+++ /dev/null
@@ -1,936 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.solr.handler.designer;
-
-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;
-
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.stream.Stream;
-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;
-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;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.noggit.JSONUtil;
-
-public class TestSchemaDesignerAPI extends SolrCloudTestCase implements SchemaDesignerConstants {
-
- private CoreContainer cc;
- private SchemaDesignerAPI schemaDesignerAPI;
-
- @BeforeClass
- public static void createCluster() throws Exception {
- System.setProperty("managed.schema.mutable", "true");
- configureCluster(1)
- .addConfig(DEFAULT_CONFIGSET_NAME, ExternalPaths.DEFAULT_CONFIGSET)
- .configure();
- }
-
- @AfterClass
- public static void tearDownCluster() throws Exception {
- if (cluster != null && cluster.getSolrClient() != null) {
- cluster.deleteAllCollections();
- cluster.deleteAllConfigSets();
- }
- }
-
- @Before
- public void setupTest() {
- assumeWorkingMockito();
- assertNotNull(cluster);
- cc = cluster.getJettySolrRunner(0).getCoreContainer();
- assertNotNull(cc);
- schemaDesignerAPI = new SchemaDesignerAPI(cc);
- }
-
- 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);
-
- 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(List.of(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"));
-
- reqParams.clear();
- reqParams.set(CONFIG_SET_PARAM, configSet);
- rsp = new SolrQueryResponse();
- schemaDesignerAPI.cleanupTemp(req, rsp);
-
- String mutableId = getMutableId(configSet);
- assertFalse(cc.getZkController().getClusterState().hasCollection(mutableId));
- SolrZkClient zkClient = cc.getZkController().getZkClient();
- assertFalse(zkClient.exists("/configs/" + mutableId));
- }
-
- @Test
- @SuppressWarnings("unchecked")
- public void testAddTechproductsProgressively() throws Exception {
- Path docsDir = ExternalPaths.SOURCE_HOME.resolve("example/exampledocs");
- assertTrue(docsDir + " not found!", Files.isDirectory(docsDir));
- List toAdd;
- try (Stream files = Files.list(docsDir)) {
- toAdd =
- files
- .filter(
- (dir) -> {
- String name = dir.getFileName().toString();
- return name.endsWith(".xml")
- || name.endsWith(".json")
- || name.endsWith(".csv")
- || name.endsWith(".jsonl");
- })
- .toList();
- assertNotNull("No test data files found in " + docsDir, toAdd);
- }
-
- 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);
- // response should just be the default values
- Map expSettings =
- Map.of(
- ENABLE_DYNAMIC_FIELDS_PARAM, true,
- ENABLE_FIELD_GUESSING_PARAM, true,
- ENABLE_NESTED_DOCS_PARAM, false,
- LANGUAGES_PARAM, List.of());
- assertDesignerSettings(expSettings, rsp.getValues());
- SolrParams rspData = rsp.getValues().toSolrParams();
- int schemaVersion = rspData.getInt(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);
-
- for (Path next : toAdd) {
- // Analyze some sample documents to refine the schema
- reqParams.clear();
- 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);
-
- // POST some sample JSON docs
- ContentStreamBase.FileStream stream = new ContentStreamBase.FileStream(next);
- stream.setContentType(
- TestSampleDocumentsLoader.guessContentTypeFromFilename(next.getFileName().toString()));
- when(req.getContentStreams()).thenReturn(List.of(stream));
-
- rsp = new SolrQueryResponse();
-
- // 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"));
-
- // capture the schema version for MVCC
- rspData = rsp.getValues().toSolrParams();
- schemaVersion = rspData.getInt(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);
- expSettings =
- Map.of(
- ENABLE_DYNAMIC_FIELDS_PARAM, false,
- ENABLE_FIELD_GUESSING_PARAM, true,
- ENABLE_NESTED_DOCS_PARAM, false,
- LANGUAGES_PARAM, List.of("en"),
- COPY_FROM_PARAM, "_default");
- assertDesignerSettings(expSettings, rsp.getValues());
-
- // 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();
-
- // GET /schema-designer/query
- schemaDesignerAPI.query(req, rsp);
- assertNotNull(rsp.getResponseHeader());
- SolrDocumentList results = (SolrDocumentList) rsp.getResponse();
- 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);
- 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");
- 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);
- fail("Prep should fail for locked schema " + configSet);
- } catch (SolrException solrExc) {
- assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, solrExc.code());
- }
- }
-
- @Test
- @SuppressWarnings("unchecked")
- 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(List.of(stream));
-
- SolrQueryResponse rsp = new SolrQueryResponse();
-
- // 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");
- assertNotNull(docIds);
- assertEquals(100, docIds.size()); // designer limits the doc ids to top 100
-
- String idField = rsp.getValues()._getStr(UNIQUE_KEY_FIELD_PARAM);
- assertNotNull(idField);
- }
-
- @Test
- @SuppressWarnings("unchecked")
- 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));
-
- Map expSettings =
- Map.of(
- ENABLE_DYNAMIC_FIELDS_PARAM, true,
- ENABLE_FIELD_GUESSING_PARAM, true,
- ENABLE_NESTED_DOCS_PARAM, false,
- LANGUAGES_PARAM, List.of(),
- COPY_FROM_PARAM, "_default");
- assertDesignerSettings(expSettings, rsp.getValues());
-
- // 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(List.of(stream));
-
- rsp = new SolrQueryResponse();
-
- // 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);
- assertNotNull(idField);
- assertDesignerSettings(expSettings, rsp.getValues());
-
- // capture the schema version for MVCC
- SolrParams rspData = rsp.getValues().toSolrParams();
- reqParams.clear();
- int schemaVersion = rspData.getInt(SCHEMA_VERSION_PARAM);
-
- // load the contents of a file
- Collection files = (Collection) rsp.getValues().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)) {
- file = f;
- break;
- }
- }
- 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);
- 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())
- .thenReturn(List.of(new ContentStreamBase.StringStream(solrconfigXml, "application/xml")));
-
- schemaDesignerAPI.updateFileContents(req, rsp);
- rspData = rsp.getValues().toSolrParams();
- reqParams.clear();
- schemaVersion = rspData.getInt(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())
- .thenReturn(List.of(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"));
-
- // remove dynamic fields and change the language to "en" only
- rsp = new SolrQueryResponse();
- // 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);
-
- expSettings =
- Map.of(
- ENABLE_DYNAMIC_FIELDS_PARAM, false,
- ENABLE_FIELD_GUESSING_PARAM, false,
- ENABLE_NESTED_DOCS_PARAM, false,
- LANGUAGES_PARAM, List.of("en"),
- COPY_FROM_PARAM, "_default");
- assertDesignerSettings(expSettings, rsp.getValues());
-
- List filesInResp = (List) rsp.getValues().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();
-
- // 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);
-
- expSettings =
- Map.of(
- ENABLE_DYNAMIC_FIELDS_PARAM, true,
- ENABLE_FIELD_GUESSING_PARAM, false,
- ENABLE_NESTED_DOCS_PARAM, false,
- LANGUAGES_PARAM, Arrays.asList("en", "fr"),
- COPY_FROM_PARAM, "_default");
- assertDesignerSettings(expSettings, rsp.getValues());
-
- filesInResp = (List) rsp.getValues().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);
-
- // 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);
-
- expSettings =
- Map.of(
- ENABLE_DYNAMIC_FIELDS_PARAM, false,
- ENABLE_FIELD_GUESSING_PARAM, false,
- ENABLE_NESTED_DOCS_PARAM, false,
- LANGUAGES_PARAM, List.of(),
- COPY_FROM_PARAM, "_default");
- assertDesignerSettings(expSettings, rsp.getValues());
-
- filesInResp = (List) rsp.getValues().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);
-
- // 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();
-
- // 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(List.of(stream));
- rsp = new SolrQueryResponse();
-
- // 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"));
-
- // 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
- stream = new ContentStreamBase.FileStream(getFile("schema-designer/update-author-field.json"));
- stream.setContentType(JSON_MIME);
- when(req.getContentStreams()).thenReturn(List.of(stream));
-
- rsp = new SolrQueryResponse();
-
- // PUT /schema-designer/update
- schemaDesignerAPI.updateSchemaObject(req, rsp);
- assertNotNull(rsp.getValues().get("field"));
- rspData = rsp.getValues().toSolrParams();
- schemaVersion = rspData.getInt(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(List.of(stream));
- rsp = new SolrQueryResponse();
-
- // POST /schema-designer/add
- schemaDesignerAPI.addSchemaObject(req, rsp);
- 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"));
- List> fieldTypes =
- (List>) rsp.getValues().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(List.of(stream));
- rsp = new SolrQueryResponse();
-
- // POST /schema-designer/update
- schemaDesignerAPI.updateSchemaObject(req, rsp);
- rspData = rsp.getValues().toSolrParams();
- schemaVersion = rspData.getInt(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();
-
- // GET /schema-designer/query
- schemaDesignerAPI.query(req, rsp);
- assertNotNull(rsp.getResponseHeader());
- SolrDocumentList results = (SolrDocumentList) rsp.getResponse();
- 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));
-
- // 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);
-
- 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");
- assertNotNull(collections);
- assertTrue(collections.contains(collection));
-
- // verify temp designer objects were cleaned up during the publish operation ...
- String mutableId = getMutableId(configSet);
- assertFalse(cc.getZkController().getClusterState().hasCollection(mutableId));
- SolrZkClient zkClient = cc.getZkController().getZkClient();
- assertFalse(zkClient.exists("/configs/" + mutableId));
-
- SolrQuery query = new SolrQuery("*:*");
- query.setRows(0);
- QueryResponse qr = cluster.getSolrClient().query(collection, query);
- // this proves the docs were stored in the filestore too
- assertEquals(4, qr.getResults().getNumFound());
- }
-
- @SuppressWarnings("unchecked")
- 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);
-
- // 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(List.of(stream));
- rsp = new SolrQueryResponse();
-
- // POST /schema-designer/add
- schemaDesignerAPI.addSchemaObject(req, rsp);
- assertNotNull(rsp.getValues().get("add-field"));
-
- final String fieldName = "keywords";
-
- Optional> maybeField =
- ((List>) rsp.getValues().get("fields"))
- .stream().filter(m -> fieldName.equals(m.get("name"))).findFirst();
- assertTrue(maybeField.isPresent());
- SimpleOrderedMap field = maybeField.get();
- assertEquals(Boolean.FALSE, field.get("indexed"));
- assertEquals(Boolean.FALSE, field.get("required"));
- assertEquals(Boolean.TRUE, field.get("stored"));
- assertEquals(Boolean.TRUE, field.get("docValues"));
- assertEquals(Boolean.TRUE, field.get("useDocValuesAsStored"));
- assertEquals(Boolean.FALSE, field.get("multiValued"));
- assertEquals("string", field.get("type"));
-
- String mutableId = getMutableId(configSet);
- SchemaDesignerConfigSetHelper configSetHelper =
- new SchemaDesignerConfigSetHelper(cc, SchemaDesignerAPI.newSchemaSuggester());
- ManagedIndexSchema schema = schemaDesignerAPI.loadLatestSchema(mutableId);
-
- // make it required
- Map updateField =
- Map.of("name", fieldName, "type", field.get("type"), "required", true);
- configSetHelper.updateField(configSet, updateField, schema);
-
- schema = schemaDesignerAPI.loadLatestSchema(mutableId);
- SchemaField schemaField = schema.getField(fieldName);
- assertTrue(schemaField.isRequired());
-
- updateField =
- Map.of("name", fieldName, "type", field.get("type"), "required", false, "stored", false);
- configSetHelper.updateField(configSet, updateField, schema);
- schema = schemaDesignerAPI.loadLatestSchema(mutableId);
- schemaField = schema.getField(fieldName);
- assertFalse(schemaField.isRequired());
- assertFalse(schemaField.stored());
-
- updateField =
- Map.of(
- "name",
- fieldName,
- "type",
- field.get("type"),
- "required",
- false,
- "stored",
- false,
- "multiValued",
- true);
- configSetHelper.updateField(configSet, updateField, schema);
- schema = schemaDesignerAPI.loadLatestSchema(mutableId);
- schemaField = schema.getField(fieldName);
- assertFalse(schemaField.isRequired());
- assertFalse(schemaField.stored());
- assertTrue(schemaField.multiValued());
-
- updateField = Map.of("name", fieldName, "type", "strings", "copyDest", "_text_");
- configSetHelper.updateField(configSet, updateField, schema);
- schema = schemaDesignerAPI.loadLatestSchema(mutableId);
- schemaField = schema.getField(fieldName);
- assertTrue(schemaField.multiValued());
- assertEquals("strings", schemaField.getType().getTypeName());
- assertFalse(schemaField.isRequired());
- assertTrue(schemaField.stored());
- List srcFields = schema.getCopySources("_text_");
- assertEquals(List.of(fieldName), srcFields);
- }
-
- @SuppressWarnings({"unchecked"})
- 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));
-
- // 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);
-
- assertNotNull(cc.getZkController().zkStateReader.getCollection(collection));
-
- // Load the schema designer for the existing config set and make some changes to it
- reqParams.clear();
- 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);
-
- // Update id field to not use docValues
- List> fields =
- (List>) rsp.getValues().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
- SimpleOrderedMap idFieldMapUpdated = idFieldMap.clone();
- idFieldMapUpdated.setVal(idFieldMapUpdated.indexOf("docValues", 0), Boolean.FALSE);
- idFieldMapUpdated.setVal(idFieldMapUpdated.indexOf("useDocValuesAsStored", 0), Boolean.FALSE);
- idFieldMapUpdated.setVal(idFieldMapUpdated.indexOf("uninvertible", 0), Boolean.TRUE);
- idFieldMapUpdated.setVal(
- idFieldMapUpdated.indexOf("omitTermFreqAndPositions", 0), Boolean.FALSE);
-
- SolrParams solrParams = idFieldMapUpdated.toSolrParams();
- Map mapParams = solrParams.toMap(new HashMap<>());
- mapParams.put("termVectors", Boolean.FALSE);
- reqParams.set(
- SCHEMA_VERSION_PARAM, rsp.getValues().toSolrParams().getInt(SCHEMA_VERSION_PARAM));
-
- ContentStreamBase.StringStream stringStream =
- new ContentStreamBase.StringStream(JSONUtil.toJSON(mapParams), JSON_MIME);
- when(req.getContentStreams()).thenReturn(List.of(stringStream));
-
- rsp = new SolrQueryResponse();
- schemaDesignerAPI.updateSchemaObject(req, rsp);
-
- // Add a new field
- Integer schemaVersion = rsp.getValues().toSolrParams().getInt(SCHEMA_VERSION_PARAM);
- reqParams.set(SCHEMA_VERSION_PARAM, schemaVersion);
- ContentStreamBase.FileStream fileStream =
- new ContentStreamBase.FileStream(getFile("schema-designer/add-new-field.json"));
- fileStream.setContentType(JSON_MIME);
- when(req.getContentStreams()).thenReturn(List.of(fileStream));
- rsp = new SolrQueryResponse();
- // POST /schema-designer/add
- schemaDesignerAPI.addSchemaObject(req, rsp);
- assertNotNull(rsp.getValues().get("add-field"));
-
- // Add a new field type
- schemaVersion = rsp.getValues().toSolrParams().getInt(SCHEMA_VERSION_PARAM);
- reqParams.set(SCHEMA_VERSION_PARAM, schemaVersion);
- fileStream = new ContentStreamBase.FileStream(getFile("schema-designer/add-new-type.json"));
- fileStream.setContentType(JSON_MIME);
- when(req.getContentStreams()).thenReturn(List.of(fileStream));
- rsp = new SolrQueryResponse();
- // POST /schema-designer/add
- schemaDesignerAPI.addSchemaObject(req, rsp);
- assertNotNull(rsp.getValues().get("add-field-type"));
-
- // Let's do a diff now
- rsp = new SolrQueryResponse();
- schemaDesignerAPI.getSchemaDiff(req, rsp);
-
- Map diff = (Map) rsp.getValues().get("diff");
-
- // field asserts
- assertNotNull(diff.get("fields"));
- Map fieldsDiff = (Map) diff.get("fields");
- assertNotNull(fieldsDiff.get("updated"));
- Map mapDiff = (Map) fieldsDiff.get("updated");
- assertEquals(
- Arrays.asList(
- Map.of(
- "omitTermFreqAndPositions",
- true,
- "useDocValuesAsStored",
- true,
- "docValues",
- true,
- "uninvertible",
- false),
- Map.of(
- "omitTermFreqAndPositions",
- false,
- "useDocValuesAsStored",
- false,
- "docValues",
- false,
- "uninvertible",
- true)),
- mapDiff.get("id"));
- assertNotNull(fieldsDiff.get("added"));
- Map fieldsAdded = (Map) fieldsDiff.get("added");
- assertNotNull(fieldsAdded.get("keywords"));
-
- // field type asserts
- assertNotNull(diff.get("fieldTypes"));
- Map fieldTypesDiff = (Map) diff.get("fieldTypes");
- assertNotNull(fieldTypesDiff.get("added"));
- Map fieldTypesAdded = (Map) fieldTypesDiff.get("added");
- assertNotNull(fieldTypesAdded.get("test_txt"));
- }
-
- protected void assertDesignerSettings(Map expected, NamedList> actual) {
- for (String expKey : expected.keySet()) {
- Object expValue = expected.get(expKey);
- assertEquals(
- "Value for designer setting '" + expKey + "' not match expected!",
- expValue,
- actual.get(expKey));
- }
- }
-}
diff --git a/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerConfigSetHelper.java b/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerConfigSetHelper.java
index 82231061bdfa..300a78c35812 100644
--- a/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerConfigSetHelper.java
+++ b/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerConfigSetHelper.java
@@ -19,7 +19,7 @@
import static org.apache.solr.common.util.Utils.toJavabin;
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.handler.designer.SchemaDesigner.getMutableId;
import static org.apache.solr.schema.IndexSchema.NEST_PATH_FIELD_NAME;
import static org.apache.solr.schema.IndexSchema.ROOT_FIELD_NAME;
@@ -37,6 +37,7 @@
import org.apache.solr.core.CoreContainer;
import org.apache.solr.core.SolrConfig;
import org.apache.solr.filestore.FileStore;
+import org.apache.solr.handler.configsets.DownloadConfigSet;
import org.apache.solr.schema.FieldType;
import org.apache.solr.schema.ManagedIndexSchema;
import org.apache.solr.schema.SchemaField;
@@ -74,7 +75,7 @@ public void setupTest() {
assertNotNull(cluster);
cc = cluster.getJettySolrRunner(0).getCoreContainer();
assertNotNull(cc);
- helper = new SchemaDesignerConfigSetHelper(cc, SchemaDesignerAPI.newSchemaSuggester());
+ helper = new SchemaDesignerConfigSetHelper(cc, SchemaDesigner.newSchemaSuggester());
}
@Test
@@ -112,13 +113,14 @@ public void testSetupMutable() throws Exception {
configSet, schema, List.of(), true, DEFAULT_CONFIGSET_NAME);
assertEquals(2, schema.getSchemaZkVersion());
- byte[] zipped = helper.downloadAndZipConfigSet(mutableId);
+ byte[] zipped = DownloadConfigSet.zipConfigSet(cc.getConfigSetService(), mutableId);
assertTrue(zipped != null && zipped.length > 0);
}
@Test
public void testDownloadAndZip() throws IOException {
- byte[] zipped = helper.downloadAndZipConfigSet(DEFAULT_CONFIGSET_NAME);
+ byte[] zipped =
+ DownloadConfigSet.zipConfigSet(cc.getConfigSetService(), DEFAULT_CONFIGSET_NAME);
ZipInputStream stream = new ZipInputStream(new ByteArrayInputStream(zipped));
boolean foundSolrConfig = false;
@@ -176,7 +178,7 @@ public void testEnableDisableOptions() throws Exception {
assertTrue(
cluster
.getZkClient()
- .exists(SchemaDesignerAPI.getConfigSetZkPath(mutableId, "lang/stopwords_en.txt")));
+ .exists(SchemaDesigner.getConfigSetZkPath(mutableId, "lang/stopwords_en.txt")));
assertNotNull(schema.getFieldTypeByName("text_fr"));
assertNotNull(schema.getFieldOrNull("*_txt_fr"));
assertNull(schema.getFieldOrNull("*_txt_ga"));
@@ -198,7 +200,7 @@ public void testEnableDisableOptions() throws Exception {
assertTrue(
cluster
.getZkClient()
- .exists(SchemaDesignerAPI.getConfigSetZkPath(mutableId, "lang/stopwords_en.txt")));
+ .exists(SchemaDesigner.getConfigSetZkPath(mutableId, "lang/stopwords_en.txt")));
assertNotNull(schema.getFieldTypeByName("text_fr"));
assertNotNull(schema.getFieldOrNull("*_txt_fr"));
assertNull(schema.getFieldOrNull("*_txt_ga"));
diff --git a/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerSolrJ.java b/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerSolrJ.java
new file mode 100644
index 000000000000..781b06c9a3aa
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerSolrJ.java
@@ -0,0 +1,154 @@
+/*
+ * 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.handler.designer;
+
+import static org.apache.solr.handler.admin.ConfigSetsHandler.DEFAULT_CONFIGSET_NAME;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import org.apache.solr.client.api.model.SchemaDesignerInfoResponse;
+import org.apache.solr.client.api.model.SchemaDesignerResponse;
+import org.apache.solr.client.solrj.request.SchemaDesignerApi;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.util.ExternalPaths;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Smoke tests that exercise the Schema Designer V2 API through the generated SolrJ client. Unlike
+ * {@link TestSchemaDesigner}, which calls the handler in-process and bypasses both the JAX-RS layer
+ * and JSON (de)serialization, these tests round-trip every typed request body and response over
+ * HTTP. They guard against regressions in the {@code @JsonProperty} / {@code @Schema} /
+ * {@code @JsonAnySetter} annotations and the OpenAPI-generated SolrJ wiring. This is required as
+ * the primary interaction mechanism to Schema Designer APIs is via the schema-designer.js JSON
+ * calls, not SolrJ.
+ */
+public class TestSchemaDesignerSolrJ extends SolrCloudTestCase {
+
+ @BeforeClass
+ public static void createCluster() throws Exception {
+ System.setProperty("managed.schema.mutable", "true");
+ configureCluster(1)
+ .addConfig(DEFAULT_CONFIGSET_NAME, ExternalPaths.DEFAULT_CONFIGSET)
+ .configure();
+ }
+
+ @AfterClass
+ public static void cleanup() throws Exception {
+ if (cluster != null && cluster.getSolrClient() != null) {
+ cluster.deleteAllCollections();
+ cluster.deleteAllConfigSets();
+ }
+ }
+
+ /**
+ * Walks through prep → add (each of the four wrapper keys) → update → info, verifying both the
+ * SolrJ request wiring and the typed-response deserialization.
+ */
+ @Test
+ public void testTypedBodyRoundTrip() throws Exception {
+ final String configSet = "solrjSmoke";
+
+ // POST /schema-designer/{configSet}/prep — no body, baseline that SolrJ wiring works
+ SchemaDesignerResponse prep =
+ new SchemaDesignerApi.PrepNewSchema(configSet).process(cluster.getSolrClient());
+ assertEquals(configSet, prep.configSet);
+ int schemaVersion = prep.schemaVersion;
+
+ // POST /add — addField — exercises kebab-case @JsonProperty("add-field") / @Schema(name=…)
+ var addField = new SchemaDesignerApi.AddSchemaObject(configSet);
+ addField.setSchemaVersion(schemaVersion);
+ addField.setAddField(Map.of("name", "keywords", "type", "string", "stored", true));
+ SchemaDesignerResponse addFieldResp = addField.process(cluster.getSolrClient());
+ assertEquals("keywords", addFieldResp.field);
+ schemaVersion = addFieldResp.schemaVersion;
+
+ // POST /add — addFieldType — covers a different wrapper key
+ var addType = new SchemaDesignerApi.AddSchemaObject(configSet);
+ addType.setSchemaVersion(schemaVersion);
+ addType.setAddFieldType(
+ Map.of(
+ "name",
+ "smoke_txt",
+ "class",
+ "solr.TextField",
+ "analyzer",
+ Map.of("tokenizer", Map.of("class", "solr.StandardTokenizerFactory"))));
+ SchemaDesignerResponse addTypeResp = addType.process(cluster.getSolrClient());
+ assertEquals("smoke_txt", addTypeResp.fieldType);
+ schemaVersion = addTypeResp.schemaVersion;
+
+ // POST /add — addDynamicField
+ var addDyn = new SchemaDesignerApi.AddSchemaObject(configSet);
+ addDyn.setSchemaVersion(schemaVersion);
+ addDyn.setAddDynamicField(Map.of("name", "*_smoke", "type", "string"));
+ SchemaDesignerResponse addDynResp = addDyn.process(cluster.getSolrClient());
+ assertEquals("*_smoke", addDynResp.dynamicField);
+ schemaVersion = addDynResp.schemaVersion;
+
+ // POST /add — addCopyField — verifies the explicit no-op response branch in
+ // setSchemaObjectField (no field/type/dynamicField/fieldType is populated)
+ var addCopy = new SchemaDesignerApi.AddSchemaObject(configSet);
+ addCopy.setSchemaVersion(schemaVersion);
+ addCopy.setAddCopyField(Map.of("source", "keywords", "dest", "_text_"));
+ SchemaDesignerResponse addCopyResp = addCopy.process(cluster.getSolrClient());
+ assertNull(addCopyResp.field);
+ assertNull(addCopyResp.fieldType);
+ assertNull(addCopyResp.dynamicField);
+ schemaVersion = addCopyResp.schemaVersion;
+
+ // PUT /update — exercises the @JsonAnyGetter/@JsonAnySetter capture for arbitrary attrs
+ var update = new SchemaDesignerApi.UpdateSchemaObject(configSet);
+ update.setSchemaVersion(schemaVersion);
+ update.setName("keywords");
+ update.setAdditionalProperties(Map.of("type", "string", "stored", true, "multiValued", true));
+ SchemaDesignerResponse updateResp = update.process(cluster.getSolrClient());
+ assertNotNull(updateResp.field);
+ assertEquals("field", updateResp.updateType);
+
+ // GET /info — round-trips a typed response that extends SchemaDesignerSettingsResponse
+ SchemaDesignerInfoResponse info =
+ new SchemaDesignerApi.GetInfo(configSet).process(cluster.getSolrClient());
+ assertEquals(configSet, info.configSet);
+ }
+
+ /**
+ * Exercises the {@code InputStream} body binding on {@code updateFileContents} by sending an
+ * invalid {@code solrconfig.xml} and verifying the server returns the typed error fields rather
+ * than throwing — this also confirms the bytes actually reached the server.
+ */
+ @Test
+ public void testUpdateFileContentsBodyBinding() throws Exception {
+ final String configSet = "solrjFileSmoke";
+
+ new SchemaDesignerApi.PrepNewSchema(configSet).process(cluster.getSolrClient());
+
+ byte[] invalidXml = " ".getBytes(StandardCharsets.UTF_8);
+ var req =
+ new SchemaDesignerApi.UpdateFileContents(configSet, new ByteArrayInputStream(invalidXml));
+ req.setFile("solrconfig.xml");
+ SchemaDesignerResponse resp = req.process(cluster.getSolrClient());
+
+ assertNotNull(
+ "server should report a validation error for the invalid solrconfig.xml",
+ resp.updateFileError);
+ assertEquals(" ", resp.fileContent);
+ }
+}
diff --git a/solr/cross-dc-manager/build.gradle b/solr/cross-dc-manager/build.gradle
index 4ce538f67f7f..14ed70a5dbc3 100644
--- a/solr/cross-dc-manager/build.gradle
+++ b/solr/cross-dc-manager/build.gradle
@@ -35,6 +35,7 @@ dependencies {
implementation libs.opentelemetry.sdk.metrics
implementation libs.eclipse.jetty.server
implementation libs.eclipse.jetty.ee10.servlet
+ implementation libs.google.guava
implementation libs.jakarta.servlet.api
implementation libs.slf4j.api
runtimeOnly libs.google.protobuf.javautils
diff --git a/solr/cross-dc-manager/src/java/org/apache/solr/crossdc/manager/consumer/KafkaCrossDcConsumer.java b/solr/cross-dc-manager/src/java/org/apache/solr/crossdc/manager/consumer/KafkaCrossDcConsumer.java
index c3b5bdb3a707..d368151c6afa 100644
--- a/solr/cross-dc-manager/src/java/org/apache/solr/crossdc/manager/consumer/KafkaCrossDcConsumer.java
+++ b/solr/cross-dc-manager/src/java/org/apache/solr/crossdc/manager/consumer/KafkaCrossDcConsumer.java
@@ -75,6 +75,8 @@
public class KafkaCrossDcConsumer extends Consumer.CrossDcConsumer {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+ public static final String PROP_TOPIC_DEBUG = "solr.crossdc.consumer.topic.debug";
+
private final KafkaConsumer> kafkaConsumer;
private final AdminClient adminClient;
private final CountDownLatch startLatch;
@@ -101,6 +103,8 @@ public class KafkaCrossDcConsumer extends Consumer.CrossDcConsumer {
private volatile boolean running = false;
+ private boolean topicDebug = Boolean.parseBoolean(System.getProperty(PROP_TOPIC_DEBUG, "false"));
+
/**
* Supplier for creating and managing a working CloudSolrClient instance. This class ensures that
* the CloudSolrClient instance doesn't try to use its {@link
@@ -175,6 +179,7 @@ public KafkaCrossDcConsumer(
conf.get(CrossDcConf.COLLAPSE_UPDATES), CrossDcConf.CollapseUpdates.PARTIAL);
this.maxCollapseRecords = conf.getInt(KafkaCrossDcConf.MAX_COLLAPSE_RECORDS);
this.startLatch = startLatch;
+
final Properties kafkaConsumerProps = new Properties();
kafkaConsumerProps.put(
@@ -375,6 +380,9 @@ boolean pollAndProcessRequests() {
ConsumerRecord> lastRecord = null;
for (TopicPartition partition : records.partitions()) {
+ if (log.isTraceEnabled()) {
+ log.trace("Checking partition {}", partition.partition());
+ }
List>> partitionRecords =
records.records(partition);
@@ -396,19 +404,31 @@ boolean pollAndProcessRequests() {
metrics.incrementInputMsgCounter();
lastRecord = requestRecord;
- MirroredSolrRequest> req = requestRecord.value();
- SolrRequest> solrReq = req.getSolrRequest();
- MirroredSolrRequest.Type type = req.getType();
+ final MirroredSolrRequest> req = requestRecord.value();
+ final SolrRequest> solrReq = req.getSolrRequest();
+ final MirroredSolrRequest.Type type = req.getType();
if (type != MirroredSolrRequest.Type.UPDATE) {
String action = solrReq.getParams().get("action", "unknown");
metrics.incrementInputReqCounter(type.name(), action);
}
- ModifiableSolrParams params = new ModifiableSolrParams(solrReq.getParams());
+ final ModifiableSolrParams params = new ModifiableSolrParams(solrReq.getParams());
if (log.isTraceEnabled()) {
log.trace("-- picked type={}, params={}", req.getType(), params);
}
+ if (topicDebug) {
+ solrReq.addHeader("topic.debug", "true");
+ solrReq.addHeader("record.topic", requestRecord.topic());
+ solrReq.addHeader("record.partition", String.valueOf(requestRecord.partition()));
+ solrReq.addHeader("record.offset", String.valueOf(requestRecord.offset()));
+ solrReq.addHeader("record.timestamp", String.valueOf(requestRecord.timestamp()));
+ solrReq.addHeader("record.key", requestRecord.key());
+ solrReq.addHeader("workUnit.nextOffset", String.valueOf(workUnit.nextOffset));
+ solrReq.addHeader("workUnit.partition", String.valueOf(workUnit.partition));
+ solrReq.addHeader("workUnit.topic", workUnit.topic);
+ solrReq.addHeader("workUnit.items", String.valueOf(workUnit.workItems.size()));
+ }
// determine if it's an UPDATE with deletes, or if the existing batch has deletes
boolean hasDeletes = false;
@@ -450,6 +470,7 @@ boolean pollAndProcessRequests() {
if (updateReqBatch == null) {
// just initialize
updateReqBatch = new UpdateRequest();
+ updateReqBatch.addHeaders(solrReq.getHeaders());
} else {
if (collapseUpdates == CrossDcConf.CollapseUpdates.NONE) {
throw new RuntimeException("Can't collapse requests.");
@@ -490,6 +511,7 @@ boolean pollAndProcessRequests() {
if (updateReqBatch != null) {
sendBatch(updateReqBatch, MirroredSolrRequest.Type.UPDATE, lastRecord, workUnit);
+ updateReqBatch = null;
}
try {
partitionManager.checkForOffsetUpdates(partition);
diff --git a/solr/cross-dc-manager/src/java/org/apache/solr/crossdc/manager/consumer/PartitionManager.java b/solr/cross-dc-manager/src/java/org/apache/solr/crossdc/manager/consumer/PartitionManager.java
index c1004528ab69..c93740f25ea0 100644
--- a/solr/cross-dc-manager/src/java/org/apache/solr/crossdc/manager/consumer/PartitionManager.java
+++ b/solr/cross-dc-manager/src/java/org/apache/solr/crossdc/manager/consumer/PartitionManager.java
@@ -16,6 +16,7 @@
*/
package org.apache.solr.crossdc.manager.consumer;
+import com.google.common.annotations.VisibleForTesting;
import java.lang.invoke.MethodHandles;
import java.util.ArrayDeque;
import java.util.HashSet;
@@ -44,13 +45,16 @@ static class PartitionWork {
final Queue partitionQueue = new ArrayDeque<>();
}
- static class WorkUnit {
- final TopicPartition partition;
- Set> workItems = new HashSet<>();
+ @VisibleForTesting
+ public static class WorkUnit {
+ final int partition;
+ final String topic;
+ final Set> workItems = new HashSet<>();
long nextOffset;
- public WorkUnit(TopicPartition partition) {
- this.partition = partition;
+ WorkUnit(TopicPartition partition) {
+ this.partition = partition.partition();
+ this.topic = partition.topic();
}
}
diff --git a/solr/cross-dc-manager/src/test/org/apache/solr/crossdc/manager/SolrAndKafkaIntegrationTest.java b/solr/cross-dc-manager/src/test/org/apache/solr/crossdc/manager/SolrAndKafkaIntegrationTest.java
index e837be5fec37..73b6bd8abd5a 100644
--- a/solr/cross-dc-manager/src/test/org/apache/solr/crossdc/manager/SolrAndKafkaIntegrationTest.java
+++ b/solr/cross-dc-manager/src/test/org/apache/solr/crossdc/manager/SolrAndKafkaIntegrationTest.java
@@ -16,8 +16,12 @@
*/
package org.apache.solr.crossdc.manager;
+import static org.apache.solr.crossdc.common.CrossDcConf.COLLAPSE_UPDATES;
+import static org.apache.solr.crossdc.common.KafkaCrossDcConf.BATCH_SIZE_BYTES;
+import static org.apache.solr.crossdc.common.KafkaCrossDcConf.BOOTSTRAP_SERVERS;
import static org.apache.solr.crossdc.common.KafkaCrossDcConf.DEFAULT_MAX_REQUEST_SIZE;
import static org.apache.solr.crossdc.common.KafkaCrossDcConf.INDEX_UNMIRRORABLE_DOCS;
+import static org.apache.solr.crossdc.common.KafkaCrossDcConf.TOPIC_NAME;
import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters;
import com.carrotsearch.randomizedtesting.annotations.ThreadLeakLingering;
@@ -28,14 +32,18 @@
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.stream.IntStream;
import org.apache.commons.io.IOUtils;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerRecord;
@@ -45,6 +53,7 @@
import org.apache.solr.SolrIgnoredThreadsFilter;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.client.solrj.SolrResponse;
import org.apache.solr.client.solrj.impl.CloudSolrClient;
import org.apache.solr.client.solrj.jetty.HttpJettySolrClient;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
@@ -56,14 +65,19 @@
import org.apache.solr.cloud.MiniSolrCloudCluster;
import org.apache.solr.cloud.SolrCloudTestCase;
import org.apache.solr.common.SolrInputDocument;
+import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.util.ExecutorUtil;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.ObjectReleaseTracker;
import org.apache.solr.common.util.SolrNamedThreadFactory;
+import org.apache.solr.common.util.Utils;
import org.apache.solr.crossdc.common.KafkaCrossDcConf;
import org.apache.solr.crossdc.common.MirroredSolrRequest;
import org.apache.solr.crossdc.common.MirroredSolrRequestSerializer;
import org.apache.solr.crossdc.manager.consumer.Consumer;
+import org.apache.solr.crossdc.manager.consumer.ConsumerMetrics;
+import org.apache.solr.crossdc.manager.consumer.KafkaCrossDcConsumer;
+import org.apache.solr.crossdc.manager.consumer.PartitionManager;
import org.apache.solr.util.SolrKafkaTestsIgnoredThreadsFilter;
import org.junit.After;
import org.junit.Before;
@@ -90,11 +104,64 @@ public class SolrAndKafkaIntegrationTest extends SolrCloudTestCase {
private static final int NUM_BROKERS = 1;
public EmbeddedKafkaCluster kafkaCluster;
+ private static class ConsumerBatch {
+ final String kafkaTopic;
+ final int partitionId;
+ final MirroredSolrRequest.Type type;
+ final String collection;
+ final Map headers;
+ final Set addIds = new HashSet<>();
+ final String json;
+
+ public ConsumerBatch(final MirroredSolrRequest.Type type, final SolrRequest> solrRequest) {
+ this.kafkaTopic = solrRequest.getHeaders().get("record.topic");
+ this.partitionId = Integer.parseInt(solrRequest.getHeaders().get("record.partition"));
+ this.type = type;
+ this.collection = solrRequest.getCollection();
+ this.headers = solrRequest.getHeaders();
+ if (solrRequest instanceof UpdateRequest) {
+ UpdateRequest updateReq = (UpdateRequest) solrRequest;
+ json =
+ Utils.toJSONString(
+ Map.of("params", updateReq.getParams(), "add", updateReq.getDocuments()));
+ updateReq.getDocuments().forEach(doc -> addIds.add(doc.getFieldValue("id").toString()));
+ } else {
+ json =
+ Utils.toJSONString(
+ Map.of("params", solrRequest.getParams(), "class", solrRequest.getClass()));
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "ConsumerBatch{"
+ + "kafkaTopic='"
+ + kafkaTopic
+ + '\''
+ + ", partitionId="
+ + partitionId
+ + ", type="
+ + type
+ + ", collection='"
+ + collection
+ + '\''
+ + ", headers="
+ + headers
+ + '\''
+ + ", json='"
+ + json
+ + '\''
+ + '}';
+ }
+ }
+
protected volatile MiniSolrCloudCluster solrCluster1;
protected volatile MiniSolrCloudCluster solrCluster2;
protected volatile Consumer consumer;
+ private List consumerBatches;
+
private static final String TOPIC = "topic1";
private static final String COLLECTION = "collection1";
@@ -112,7 +179,28 @@ public void beforeSolrAndKafkaIntegrationTest() throws Exception {
Thread.setDefaultUncaughtExceptionHandler(
(t, e) -> log.error("Uncaught exception in thread {}", t, e));
System.setProperty("otel.metrics.exporter", "prometheus");
- consumer = new Consumer();
+ System.setProperty(KafkaCrossDcConsumer.PROP_TOPIC_DEBUG, "true");
+ consumerBatches = new ArrayList<>();
+ consumer =
+ new Consumer() {
+ @Override
+ protected CrossDcConsumer getCrossDcConsumer(
+ final KafkaCrossDcConf conf,
+ final ConsumerMetrics metrics,
+ final CountDownLatch startLatch) {
+ return new KafkaCrossDcConsumer(conf, metrics, startLatch) {
+ @Override
+ public void sendBatch(
+ final SolrRequest extends SolrResponse> solrReqBatch,
+ final MirroredSolrRequest.Type type,
+ final ConsumerRecord> lastRecord,
+ final PartitionManager.WorkUnit workUnit) {
+ consumerBatches.add(new ConsumerBatch(type, solrReqBatch));
+ super.sendBatch(solrReqBatch, type, lastRecord, workUnit);
+ }
+ };
+ }
+ };
Properties config = new Properties();
kafkaCluster =
@@ -124,13 +212,15 @@ public String bootstrapServers() {
};
kafkaCluster.start();
- kafkaCluster.createTopic(TOPIC, 10, 1);
+ // create many partitions to test for re-ordered reads
+ kafkaCluster.createTopic(TOPIC, 3, 1);
// ensure small batches to test multi-partition ordering
- System.setProperty("batchSizeBytes", "128");
- System.setProperty("solr.crossdc.topicName", TOPIC);
- System.setProperty("solr.crossdc.bootstrapServers", kafkaCluster.bootstrapServers());
+ System.setProperty(BATCH_SIZE_BYTES, "100");
+ System.setProperty(TOPIC_NAME, TOPIC);
+ System.setProperty(BOOTSTRAP_SERVERS, kafkaCluster.bootstrapServers());
System.setProperty(INDEX_UNMIRRORABLE_DOCS, "false");
+ System.setProperty(COLLAPSE_UPDATES, "none");
solrCluster1 =
configureCluster(1).addConfig("conf", getFile("configs/cloud-minimal/conf")).configure();
@@ -238,10 +328,62 @@ public void testProducerToCloud() throws Exception {
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
@Test
- @Ignore("SOLR-18077")
+ public void testPartitioning() throws Exception {
+ CollectionAdminRequest.Create create =
+ CollectionAdminRequest.createCollection(ALT_COLLECTION, "conf", 1, 1);
+ create.process(solrCluster1.getSolrClient());
+ create.process(solrCluster2.getSolrClient());
+ solrCluster1.waitForActiveCollection(ALT_COLLECTION, 1, 1);
+ solrCluster2.waitForActiveCollection(ALT_COLLECTION, 1, 1);
+
+ CloudSolrClient client = solrCluster1.getSolrClient();
+ int NUM_DOCS = 200;
+ for (int i = 0; i < NUM_DOCS; i++) {
+ SolrInputDocument doc = new SolrInputDocument();
+ doc.addField("id", "id-" + i);
+ doc.addField("id_i", i);
+ doc.addField("text", "some test with a relatively long field. " + LOREM_IPSUM);
+ doc.addField("collection_t", COLLECTION);
+
+ client.add(COLLECTION, doc);
+
+ doc = new SolrInputDocument();
+ doc.addField("id", "id-" + i);
+ doc.addField("id_i", i);
+ doc.addField("text", "some test with a relatively long field. " + LOREM_IPSUM);
+ doc.addField("collection_t", ALT_COLLECTION);
+
+ client.add(ALT_COLLECTION, doc);
+ }
+ client.commit(COLLECTION);
+ client.commit(ALT_COLLECTION);
+ // check that updates to different collections were always sent to the same partition
+ Map partitionsPerCol = new HashMap<>();
+ Map> docsPerCol = new HashMap<>();
+ for (ConsumerBatch batch : consumerBatches) {
+ String collection =
+ partitionsPerCol.computeIfAbsent(batch.partitionId, k -> batch.collection);
+ docsPerCol.computeIfAbsent(collection, col -> new HashSet<>()).addAll(batch.addIds);
+ assertEquals(
+ "request in partition "
+ + batch.partitionId
+ + " has wrong collection "
+ + batch.collection
+ + ": "
+ + batch
+ + "\npartitions: "
+ + partitionsPerCol,
+ collection,
+ batch.collection);
+ }
+ docsPerCol.forEach(
+ (col, ids) -> assertEquals("incorrect count in collection " + col, NUM_DOCS, ids.size()));
+ }
+
+ @Test
public void testStrictOrdering() throws Exception {
CloudSolrClient client = solrCluster1.getSolrClient();
- int NUM_DOCS = 5000;
+ int NUM_DOCS = 1000;
// delay deletes by this many docs
int DELTA = 100;
for (int i = 0; i < NUM_DOCS; i++) {
@@ -454,11 +596,12 @@ private void assertClusterEventuallyHasDocs(
boolean foundUpdates = false;
for (int i = 0; i < 100; i++) {
client.commit(collection);
- results = client.query(collection, new SolrQuery(query));
+ results =
+ client.query(collection, new SolrQuery(CommonParams.Q, query, CommonParams.FL, "*"));
if (results.getResults().getNumFound() == expectedNumDocs) {
foundUpdates = true;
} else {
- Thread.sleep(200);
+ Thread.sleep(300);
}
}
diff --git a/solr/modules/cuvs/src/java/org/apache/solr/cuvs/GpuMetricsService.java b/solr/modules/cuvs/src/java/org/apache/solr/cuvs/GpuMetricsService.java
index 9156a55841c4..02268a373688 100644
--- a/solr/modules/cuvs/src/java/org/apache/solr/cuvs/GpuMetricsService.java
+++ b/solr/modules/cuvs/src/java/org/apache/solr/cuvs/GpuMetricsService.java
@@ -77,6 +77,17 @@ public static synchronized GpuMetricsService getInstance() {
public void initialize(CoreContainer coreContainer) {
if (initialized.compareAndSet(false, true)) {
+ // Ensure CUDA runtime is loaded before any cuVS provider access
+ try {
+ System.loadLibrary("cudart");
+ } catch (UnsatisfiedLinkError e) {
+ String msg = e.getMessage();
+ if (msg != null && msg.contains("already loaded in another classloader")) {
+ // cudart is available, just loaded elsewhere - safe to continue
+ } else {
+ log.warn("Could not load CUDA runtime library (libcudart not available) ", e);
+ }
+ }
this.metricManager = coreContainer.getMetricManager();
startBackgroundService();
log.info("GPU metrics service initialized");
diff --git a/solr/solr-ref-guide/modules/configuration-guide/pages/coreadmin-api.adoc b/solr/solr-ref-guide/modules/configuration-guide/pages/coreadmin-api.adoc
index 6cd1d566ac28..349ff1e47c41 100644
--- a/solr/solr-ref-guide/modules/configuration-guide/pages/coreadmin-api.adoc
+++ b/solr/solr-ref-guide/modules/configuration-guide/pages/coreadmin-api.adoc
@@ -780,10 +780,10 @@ This command is used as part of SolrCloud's xref:deployment-guide:shard-manageme
When used against a core in a user-managed cluster without `split.key` parameter, this action will split the source index and distribute its documents alternately so that each split piece contains an equal number of documents.
If the `split.key` parameter is specified then only documents having the same route key will be split from the source index.
-[[coreadmin-upgradecoreindex]]
-== UPGRADECOREINDEX
+[[coreadmin-upgradeindex]]
+== UPGRADEINDEX
-The `UPGRADECOREINDEX` action upgrades an existing core's index in-place after a Solr major-version upgrade by reindexing documents from older-format segments.
+The `UPGRADEINDEX` action upgrades an existing core's index in-place after a Solr major-version upgrade by reindexing documents from older-format segments.
If a core is upgraded by this action, it ensures index compatibility with the next Solr major version (upon a future Solr upgrade) without having to re-create the index from source.
This action is expensive and can take a while to complete on large indexes. Consider running with `async` option in such cases.
@@ -797,7 +797,7 @@ Fields that are neither stored nor docValues-backed will lose their data, unless
It is recommended to test on a copy and have a backup before running on production data.
-=== UPGRADECOREINDEX Parameters
+=== UPGRADEINDEX Parameters
`core`::
+
@@ -828,7 +828,7 @@ Use <> with the provided `requestid` to p
The update processor chain to use for reindexing.
If omitted, Solr uses the chain configured for the `/update` handler, or the default update chain for the core (in that order).
-=== UPGRADECOREINDEX Response
+=== UPGRADEINDEX Response
On success, the response includes:
@@ -845,20 +845,20 @@ The number of segments successfully processed.
One of `UPGRADE_SUCCESSFUL` or `NO_UPGRADE_NEEDED`.
On failure, an exception is thrown with error details.
-=== UPGRADECOREINDEX Examples
+=== UPGRADEINDEX Examples
*Synchronous:*
[source,bash]
----
-http://localhost:8983/solr/admin/cores?action=UPGRADECOREINDEX&core=techproducts
+http://localhost:8983/solr/admin/cores?action=UPGRADEINDEX&core=techproducts
----
*Asynchronous (recommended for large cores):*
[source,bash]
----
-http://localhost:8983/solr/admin/cores?action=UPGRADECOREINDEX&core=techproducts&async=upgrade_1
+http://localhost:8983/solr/admin/cores?action=UPGRADEINDEX&core=techproducts&async=upgrade_1
----
Then poll status:
diff --git a/solr/solr-ref-guide/modules/query-guide/pages/stream-decorator-reference.adoc b/solr/solr-ref-guide/modules/query-guide/pages/stream-decorator-reference.adoc
index 03740f8f1d9f..7f14117ef1dd 100644
--- a/solr/solr-ref-guide/modules/query-guide/pages/stream-decorator-reference.adoc
+++ b/solr/solr-ref-guide/modules/query-guide/pages/stream-decorator-reference.adoc
@@ -1448,7 +1448,7 @@ For faster aggregation over low to moderate cardinality fields, the `facet` func
* `StreamExpression` (Mandatory)
* `over`: (Mandatory) A list of fields to group by.
* `metrics`: (Mandatory) The list of metrics to compute.
-Currently supported metrics are `sum(col)`, `avg(col)`, `min(col)`, `max(col)`, `count(*)`.
+Currently supported metrics are `sum(col)`, `avg(col)`, `min(col)`, `max(col)`, `count(*)`, `missing(col)`.
=== rollup Syntax
@@ -1465,7 +1465,8 @@ rollup(
max(a_f),
avg(a_i),
avg(a_f),
- count(*)
+ count(*),
+ missing(a_i)
)
----
diff --git a/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/Lang.java b/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/Lang.java
index 927fb1eef5d8..2c0e3a4de7a9 100644
--- a/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/Lang.java
+++ b/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/Lang.java
@@ -325,6 +325,7 @@
import org.apache.solr.client.solrj.io.stream.metrics.MaxMetric;
import org.apache.solr.client.solrj.io.stream.metrics.MeanMetric;
import org.apache.solr.client.solrj.io.stream.metrics.MinMetric;
+import org.apache.solr.client.solrj.io.stream.metrics.MissingMetric;
import org.apache.solr.client.solrj.io.stream.metrics.PercentileMetric;
import org.apache.solr.client.solrj.io.stream.metrics.StdMetric;
import org.apache.solr.client.solrj.io.stream.metrics.SumMetric;
@@ -406,6 +407,7 @@ public static void register(StreamFactory streamFactory) {
.withFunctionName("std", StdMetric.class)
.withFunctionName("count", CountMetric.class)
.withFunctionName("countDist", CountDistinctMetric.class)
+ .withFunctionName("missing", MissingMetric.class)
// tuple manipulation operations
.withFunctionName("replace", ReplaceOperation.class)
diff --git a/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/stream/ParallelMetricsRollup.java b/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/stream/ParallelMetricsRollup.java
index 752581b2f8fc..584932c1e016 100644
--- a/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/stream/ParallelMetricsRollup.java
+++ b/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/stream/ParallelMetricsRollup.java
@@ -26,6 +26,7 @@
import org.apache.solr.client.solrj.io.stream.metrics.MeanMetric;
import org.apache.solr.client.solrj.io.stream.metrics.Metric;
import org.apache.solr.client.solrj.io.stream.metrics.MinMetric;
+import org.apache.solr.client.solrj.io.stream.metrics.MissingMetric;
import org.apache.solr.client.solrj.io.stream.metrics.SumMetric;
import org.apache.solr.client.solrj.io.stream.metrics.WeightedSumMetric;
@@ -133,6 +134,10 @@ default Optional getRollupMetrics(Metric[] metrics) {
// can't properly rollup mean metrics w/o a count (reqd by WeightedSumMetric)
return Optional.empty();
}
+ } else if (next instanceof MissingMetric) {
+ // sum of missing counts
+ nextRollup = new SumMetric(next.getIdentifier());
+ nextRollup.outputLong = next.outputLong;
} else if (next instanceof CountDistinctMetric) {
// rollup of count distinct is the max across the tiers
nextRollup = new MaxMetric(next.getIdentifier());
diff --git a/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/stream/metrics/MissingMetric.java b/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/stream/metrics/MissingMetric.java
new file mode 100644
index 000000000000..8f60dfc5f3b6
--- /dev/null
+++ b/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/stream/metrics/MissingMetric.java
@@ -0,0 +1,87 @@
+/*
+ * 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.solrj.io.stream.metrics;
+
+import java.io.IOException;
+import java.util.Locale;
+import org.apache.solr.client.solrj.io.Tuple;
+import org.apache.solr.client.solrj.io.stream.expr.StreamExpression;
+import org.apache.solr.client.solrj.io.stream.expr.StreamExpressionParameter;
+import org.apache.solr.client.solrj.io.stream.expr.StreamFactory;
+
+public class MissingMetric extends Metric {
+ private String columnName;
+ private long count;
+
+ public MissingMetric(String columnName) {
+ init("missing", columnName);
+ }
+
+ public MissingMetric(StreamExpression expression, StreamFactory factory) throws IOException {
+ String functionName = expression.getFunctionName();
+ String columnName = factory.getValueOperand(expression, 0);
+
+ if (null == columnName) {
+ throw new IOException(
+ String.format(
+ Locale.ROOT,
+ "Invalid expression %s - expected %s(columnName)",
+ expression,
+ functionName));
+ }
+ if (1 != expression.getParameters().size()) {
+ throw new IOException(
+ String.format(Locale.ROOT, "Invalid expression %s - unknown operands found", expression));
+ }
+
+ init(functionName, columnName);
+ }
+
+ private void init(String functionName, String columnName) {
+ this.columnName = columnName;
+ this.outputLong = true;
+ setFunctionName(functionName);
+ setIdentifier(functionName, "(", columnName, ")");
+ }
+
+ @Override
+ public String[] getColumns() {
+ return new String[] {columnName};
+ }
+
+ @Override
+ public void update(Tuple tuple) {
+ if (tuple.get(columnName) == null) {
+ ++count;
+ }
+ }
+
+ @Override
+ public Long getValue() {
+ return count;
+ }
+
+ @Override
+ public Metric newInstance() {
+ return new MissingMetric(columnName);
+ }
+
+ @Override
+ public StreamExpressionParameter toExpression(StreamFactory factory) throws IOException {
+ return new StreamExpression(getFunctionName()).withParameter(columnName);
+ }
+}
diff --git a/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/TestLang.java b/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/TestLang.java
index 3a6f58580efa..9afb1fabc3fd 100644
--- a/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/TestLang.java
+++ b/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/TestLang.java
@@ -351,7 +351,8 @@ public class TestLang extends SolrTestCase {
"std",
"drill",
"input",
- "countDist"
+ "countDist",
+ "missing"
};
@Test
diff --git a/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/StreamingTest.java b/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/StreamingTest.java
index a0b72d8d2a10..601a4fe6f676 100644
--- a/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/StreamingTest.java
+++ b/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/StreamingTest.java
@@ -46,6 +46,7 @@
import org.apache.solr.client.solrj.io.stream.metrics.MeanMetric;
import org.apache.solr.client.solrj.io.stream.metrics.Metric;
import org.apache.solr.client.solrj.io.stream.metrics.MinMetric;
+import org.apache.solr.client.solrj.io.stream.metrics.MissingMetric;
import org.apache.solr.client.solrj.io.stream.metrics.SumMetric;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.client.solrj.request.UpdateRequest;
@@ -145,16 +146,16 @@ public static void configureCluster() throws Exception {
// Update request shared by many of the tests
private final UpdateRequest helloDocsUpdateRequest =
new UpdateRequest()
- .add(id, "0", "a_s", "hello0", "a_i", "0", "a_f", "1")
+ .add(id, "0", "a_s", "hello0", "a_i", "0", "a_f", "1", "b_f", "1.5")
.add(id, "2", "a_s", "hello0", "a_i", "2", "a_f", "2")
.add(id, "3", "a_s", "hello3", "a_i", "3", "a_f", "3")
- .add(id, "4", "a_s", "hello4", "a_i", "4", "a_f", "4")
+ .add(id, "4", "a_s", "hello4", "a_i", "4", "a_f", "4", "b_f", "4.5")
.add(id, "1", "a_s", "hello0", "a_i", "1", "a_f", "5")
- .add(id, "5", "a_s", "hello3", "a_i", "10", "a_f", "6")
- .add(id, "6", "a_s", "hello4", "a_i", "11", "a_f", "7")
+ .add(id, "5", "a_s", "hello3", "a_i", "10", "a_f", "6", "b_f", "6.5")
+ .add(id, "6", "a_s", "hello4", "a_i", "11", "a_f", "7", "b_f", "7.5")
.add(id, "7", "a_s", "hello3", "a_i", "12", "a_f", "8")
.add(id, "8", "a_s", "hello3", "a_i", "13", "a_f", "9")
- .add(id, "9", "a_s", "hello0", "a_i", "14", "a_f", "10");
+ .add(id, "9", "a_s", "hello0", "a_i", "14", "a_f", "10", "b_f", "10.5");
@Before
public void clearCollection() throws Exception {
@@ -1646,7 +1647,7 @@ public void testRollupStream() throws Exception {
streamContext.setSolrClientCache(solrClientCache);
try {
- SolrParams sParamsA = params("q", "*:*", "fl", "a_s,a_i,a_f", "sort", "a_s asc");
+ SolrParams sParamsA = params("q", "*:*", "fl", "a_s,a_i,a_f,b_f", "sort", "a_s asc");
CloudSolrStream stream = new CloudSolrStream(zkHost, COLLECTIONORALIAS, sParamsA);
Bucket[] buckets = {new Bucket("a_s")};
@@ -1660,7 +1661,8 @@ public void testRollupStream() throws Exception {
new MaxMetric("a_f"),
new MeanMetric("a_i"),
new MeanMetric("a_f"),
- new CountMetric()
+ new CountMetric(),
+ new MissingMetric("b_f")
};
RollupStream rollupStream = new RollupStream(stream, buckets, metrics);
@@ -1682,6 +1684,7 @@ public void testRollupStream() throws Exception {
Double avgi = tuple.getDouble("avg(a_i)");
Double avgf = tuple.getDouble("avg(a_f)");
Double count = tuple.getDouble("count(*)");
+ Double missingBf = tuple.getDouble("missing(b_f)");
assertEquals("hello0", bucket);
assertEquals(17, sumi, 0.001);
@@ -1693,6 +1696,7 @@ public void testRollupStream() throws Exception {
assertEquals(4.25, avgi, 0.001);
assertEquals(4.5, avgf, 0.001);
assertEquals(4, count, 0.001);
+ assertEquals(2, missingBf, 0.001);
tuple = tuples.get(1);
bucket = tuple.getString("a_s");
@@ -1705,6 +1709,7 @@ public void testRollupStream() throws Exception {
avgi = tuple.getDouble("avg(a_i)");
avgf = tuple.getDouble("avg(a_f)");
count = tuple.getDouble("count(*)");
+ missingBf = tuple.getDouble("missing(b_f)");
assertEquals("hello3", bucket);
assertEquals(38, sumi, 0.001);
@@ -1716,6 +1721,7 @@ public void testRollupStream() throws Exception {
assertEquals(9.5, avgi, 0.001);
assertEquals(6.5, avgf, 0.001);
assertEquals(4, count, 0.001);
+ assertEquals(3, missingBf, 0.001);
tuple = tuples.get(2);
bucket = tuple.getString("a_s");
@@ -1728,6 +1734,7 @@ public void testRollupStream() throws Exception {
avgi = tuple.getDouble("avg(a_i)");
avgf = tuple.getDouble("avg(a_f)");
count = tuple.getDouble("count(*)");
+ missingBf = tuple.getDouble("missing(b_f)");
assertEquals("hello4", bucket);
assertEquals(15, sumi.longValue());
@@ -1739,6 +1746,7 @@ public void testRollupStream() throws Exception {
assertEquals(7.5, avgi, 0.01);
assertEquals(5.5, avgf, 0.01);
assertEquals(2, count, 0.01);
+ assertEquals(0, missingBf, 0.01);
// Test will null metrics
rollupStream = new RollupStream(stream, buckets, metrics);
@@ -1763,7 +1771,7 @@ public void testRollupStream() throws Exception {
.add(id, "12", "a_s", null, "a_i", "14", "a_f", "10")
.commit(cluster.getSolrClient(), COLLECTIONORALIAS);
- sParamsA = params("q", "*:*", "fl", "a_s,a_i,a_f", "sort", "a_s asc", "qt", "/export");
+ sParamsA = params("q", "*:*", "fl", "a_s,a_i,a_f,b_f", "sort", "a_s asc", "qt", "/export");
stream = new CloudSolrStream(zkHost, COLLECTIONORALIAS, sParamsA);
Bucket[] buckets1 = {new Bucket("a_s")};
@@ -1776,7 +1784,8 @@ public void testRollupStream() throws Exception {
new MaxMetric("a_f"),
new MeanMetric("a_i"),
new MeanMetric("a_f"),
- new CountMetric()
+ new CountMetric(),
+ new MissingMetric("b_f")
};
rollupStream = new RollupStream(stream, buckets1, metrics1);
@@ -1796,6 +1805,7 @@ public void testRollupStream() throws Exception {
avgi = tuple.getDouble("avg(a_i)");
avgf = tuple.getDouble("avg(a_f)");
count = tuple.getDouble("count(*)");
+ missingBf = tuple.getDouble("missing(b_f)");
assertEquals(14, sumi, 0.01);
assertEquals(10, sumf, 0.01);
@@ -1806,6 +1816,7 @@ public void testRollupStream() throws Exception {
assertEquals(14, avgi, 0.01);
assertEquals(10, avgf, 0.01);
assertEquals(1, count, 0.01);
+ assertEquals(1, missingBf, 0.01);
} finally {
solrClientCache.close();
}
@@ -1956,12 +1967,51 @@ public void testRollupWithNoParallel() throws Exception {
"expr",
"rollup(search("
+ COLLECTIONORALIAS
- + ",q=\"*:*\",fl=\"a_s,a_i,a_f\",sort=\"a_s desc\",partitionKeys=\"a_s\", qt=\"/export\"),over=\"a_s\")\n");
+ + ",q=\"*:*\",fl=\"a_s,a_i,a_f,b_f\",sort=\"a_s asc\",partitionKeys=\"a_s\", qt=\"/export\"),over=\"a_s\",sum(a_i),sum(a_f),min(a_i),min(a_f),max(a_i),max(a_f),avg(a_i),avg(a_f),count(*),missing(b_f))\n");
SolrStream solrStream = new SolrStream(shardUrls.get(0), solrParams);
streamContext = new StreamContext();
solrStream.setStreamContext(streamContext);
tuples = getTuples(solrStream);
assertEquals(3, tuples.size());
+
+ Tuple exprTuple = tuples.get(0);
+ assertEquals("hello0", exprTuple.getString("a_s"));
+ assertEquals(17, exprTuple.getDouble("sum(a_i)"), 0.001);
+ assertEquals(18, exprTuple.getDouble("sum(a_f)"), 0.001);
+ assertEquals(0, exprTuple.getDouble("min(a_i)"), 0.001);
+ assertEquals(1, exprTuple.getDouble("min(a_f)"), 0.001);
+ assertEquals(14, exprTuple.getDouble("max(a_i)"), 0.001);
+ assertEquals(10, exprTuple.getDouble("max(a_f)"), 0.001);
+ assertEquals(4.25, exprTuple.getDouble("avg(a_i)"), 0.001);
+ assertEquals(4.5, exprTuple.getDouble("avg(a_f)"), 0.001);
+ assertEquals(4, exprTuple.getDouble("count(*)"), 0.001);
+ assertEquals(2, exprTuple.getDouble("missing(b_f)"), 0.001);
+
+ exprTuple = tuples.get(1);
+ assertEquals("hello3", exprTuple.getString("a_s"));
+ assertEquals(38, exprTuple.getDouble("sum(a_i)"), 0.001);
+ assertEquals(26, exprTuple.getDouble("sum(a_f)"), 0.001);
+ assertEquals(3, exprTuple.getDouble("min(a_i)"), 0.001);
+ assertEquals(3, exprTuple.getDouble("min(a_f)"), 0.001);
+ assertEquals(13, exprTuple.getDouble("max(a_i)"), 0.001);
+ assertEquals(9, exprTuple.getDouble("max(a_f)"), 0.001);
+ assertEquals(9.5, exprTuple.getDouble("avg(a_i)"), 0.001);
+ assertEquals(6.5, exprTuple.getDouble("avg(a_f)"), 0.001);
+ assertEquals(4, exprTuple.getDouble("count(*)"), 0.001);
+ assertEquals(3, exprTuple.getDouble("missing(b_f)"), 0.001);
+
+ exprTuple = tuples.get(2);
+ assertEquals("hello4", exprTuple.getString("a_s"));
+ assertEquals(15, exprTuple.getDouble("sum(a_i)"), 0.001);
+ assertEquals(11, exprTuple.getDouble("sum(a_f)"), 0.001);
+ assertEquals(4, exprTuple.getDouble("min(a_i)"), 0.001);
+ assertEquals(4, exprTuple.getDouble("min(a_f)"), 0.001);
+ assertEquals(11, exprTuple.getDouble("max(a_i)"), 0.001);
+ assertEquals(7, exprTuple.getDouble("max(a_f)"), 0.001);
+ assertEquals(7.5, exprTuple.getDouble("avg(a_i)"), 0.001);
+ assertEquals(5.5, exprTuple.getDouble("avg(a_f)"), 0.001);
+ assertEquals(2, exprTuple.getDouble("count(*)"), 0.001);
+ assertEquals(0, exprTuple.getDouble("missing(b_f)"), 0.001);
} finally {
solrClientCache.close();
}
@@ -1982,7 +2032,7 @@ public void testParallelRollupStream() throws Exception {
"q",
"*:*",
"fl",
- "a_s,a_i,a_f",
+ "a_s,a_i,a_f,b_f",
"sort",
"a_s asc",
"partitionKeys",
@@ -2002,7 +2052,8 @@ public void testParallelRollupStream() throws Exception {
new MaxMetric("a_f"),
new MeanMetric("a_i"),
new MeanMetric("a_f"),
- new CountMetric()
+ new CountMetric(),
+ new MissingMetric("b_f")
};
RollupStream rollupStream = new RollupStream(stream, buckets, metrics);
@@ -2027,6 +2078,7 @@ public void testParallelRollupStream() throws Exception {
Double avgi = tuple.getDouble("avg(a_i)");
Double avgf = tuple.getDouble("avg(a_f)");
Double count = tuple.getDouble("count(*)");
+ Double missingBf = tuple.getDouble("missing(b_f)");
assertEquals("hello0", bucket);
assertEquals(17, sumi, 0.001);
@@ -2038,6 +2090,7 @@ public void testParallelRollupStream() throws Exception {
assertEquals(4.25, avgi, 0.001);
assertEquals(4.5, avgf, 0.001);
assertEquals(4, count, 0.001);
+ assertEquals(2, missingBf, 0.001);
tuple = tuples.get(1);
bucket = tuple.getString("a_s");
@@ -2050,6 +2103,7 @@ public void testParallelRollupStream() throws Exception {
avgi = tuple.getDouble("avg(a_i)");
avgf = tuple.getDouble("avg(a_f)");
count = tuple.getDouble("count(*)");
+ missingBf = tuple.getDouble("missing(b_f)");
assertEquals("hello3", bucket);
assertEquals(38, sumi, 0.001);
@@ -2061,6 +2115,7 @@ public void testParallelRollupStream() throws Exception {
assertEquals(9.5, avgi, 0.001);
assertEquals(6.5, avgf, 0.001);
assertEquals(4, count, 0.001);
+ assertEquals(3, missingBf, 0.001);
tuple = tuples.get(2);
bucket = tuple.getString("a_s");
@@ -2073,6 +2128,7 @@ public void testParallelRollupStream() throws Exception {
avgi = tuple.getDouble("avg(a_i)");
avgf = tuple.getDouble("avg(a_f)");
count = tuple.getDouble("count(*)");
+ missingBf = tuple.getDouble("missing(b_f)");
assertEquals("hello4", bucket);
assertEquals(15, sumi.longValue());
@@ -2084,6 +2140,7 @@ public void testParallelRollupStream() throws Exception {
assertEquals(7.5, avgi, 0.001);
assertEquals(5.5, avgf, 0.001);
assertEquals(2, count, 0.001);
+ assertEquals(0, missingBf, 0.001);
} finally {
solrClientCache.close();
}
diff --git a/solr/solrj/src/java/org/apache/solr/common/params/CoreAdminParams.java b/solr/solrj/src/java/org/apache/solr/common/params/CoreAdminParams.java
index ad41867cc31b..0401de380f1b 100644
--- a/solr/solrj/src/java/org/apache/solr/common/params/CoreAdminParams.java
+++ b/solr/solrj/src/java/org/apache/solr/common/params/CoreAdminParams.java
@@ -179,7 +179,7 @@ public enum CoreAdminAction {
CREATESNAPSHOT,
DELETESNAPSHOT,
LISTSNAPSHOTS,
- UPGRADECOREINDEX;
+ UPGRADEINDEX;
public final boolean isRead;
diff --git a/solr/test-framework/src/test-files/solr/collection1/conf/solrconfig.xml b/solr/test-framework/src/test-files/solr/collection1/conf/solrconfig.xml
index a91253e1b0b9..4884c5905e54 100644
--- a/solr/test-framework/src/test-files/solr/collection1/conf/solrconfig.xml
+++ b/solr/test-framework/src/test-files/solr/collection1/conf/solrconfig.xml
@@ -39,10 +39,6 @@
solr.StandardDirectoryFactory, the default, is filesystem based.
solr.RAMDirectoryFactory is memory based and not persistent. -->
- 1000000
- 2000000
- 3000000
- 4000000
diff --git a/solr/ui/build.gradle.kts b/solr/ui/build.gradle.kts
index 080a7643a0f8..ca6a48898660 100644
--- a/solr/ui/build.gradle.kts
+++ b/solr/ui/build.gradle.kts
@@ -67,49 +67,51 @@ kotlin {
sourceSets {
// Shared multiplatform dependencies
- val commonMain by getting {
- dependencies {
- implementation(project.dependencies.platform(project(":platform")))
- implementation(libs.compose.runtime)
- implementation(libs.compose.foundation)
- implementation(libs.compose.material3)
- implementation(libs.compose.ui)
- implementation(libs.compose.components.resources)
- implementation(libs.compose.uiToolingPreview)
-
- implementation(libs.kotlinx.serialization.core)
- implementation(libs.kotlinx.serialization.json)
- implementation(libs.kotlinx.coroutines.core)
- implementation(libs.kotlinx.datetime)
-
- implementation(libs.decompose.decompose)
- implementation(libs.essenty.lifecycle)
- implementation(libs.decompose.extensions.compose)
- implementation(libs.mvikotlin.extensions.coroutines)
- implementation(libs.mvikotlin.mvikotlin)
- implementation(libs.mvikotlin.main)
- implementation(libs.mvikotlin.logging)
-
- implementation(project.dependencies.platform(libs.ktor.bom))
- implementation(libs.ktor.client.auth)
- implementation(libs.ktor.client.core)
- implementation(libs.ktor.client.cio)
- implementation(libs.ktor.client.contentNegotiation)
- implementation(libs.ktor.client.serialization.json)
- implementation(libs.squareup.okio)
-
- implementation(libs.oshai.logging)
- implementation(libs.slf4j.api)
- }
+ commonMain.dependencies {
+ implementation(project.dependencies.platform(project(":platform")))
+ implementation(libs.compose.runtime)
+ implementation(libs.compose.foundation)
+ implementation(libs.compose.material3)
+ implementation(libs.compose.ui)
+ implementation(libs.compose.components.resources)
+ implementation(libs.compose.uiToolingPreview)
+ implementation(libs.androidx.lifecycle.viewmodelCompose)
+ implementation(libs.androidx.lifecycle.runtimeCompose)
+ implementation(libs.androidx.lifecycle.viewModelNav3)
+ implementation(libs.androidx.navigation3.ui)
+ implementation(libs.androidx.material3.adaptive.asProvider())
+ implementation(libs.androidx.material3.adaptive.nav3)
+
+ implementation(libs.kotlinx.serialization.core)
+ implementation(libs.kotlinx.serialization.json)
+ implementation(libs.kotlinx.coroutines.core)
+ implementation(libs.kotlinx.datetime)
+
+ implementation(libs.decompose.decompose)
+ implementation(libs.essenty.lifecycle)
+ implementation(libs.decompose.extensions.compose)
+ implementation(libs.mvikotlin.extensions.coroutines)
+ implementation(libs.mvikotlin.mvikotlin)
+ implementation(libs.mvikotlin.main)
+ implementation(libs.mvikotlin.logging)
+
+ implementation(project.dependencies.platform(libs.ktor.bom))
+ implementation(libs.ktor.client.auth)
+ implementation(libs.ktor.client.core)
+ implementation(libs.ktor.client.cio)
+ implementation(libs.ktor.client.contentNegotiation)
+ implementation(libs.ktor.client.serialization.json)
+ implementation(libs.squareup.okio)
+
+ implementation(libs.oshai.logging)
+ implementation(libs.slf4j.api)
}
- val commonTest by getting {
- dependencies {
- implementation(kotlin("test"))
- implementation(libs.kotlinx.coroutines.test)
- implementation(libs.compose.uiTest)
- implementation(libs.ktor.client.mock)
- }
+ commonTest.dependencies {
+ implementation(kotlin("test"))
+ implementation(libs.kotlinx.coroutines.test)
+ implementation(libs.compose.uiTest)
+ implementation(libs.ktor.client.mock)
}
val desktopMain by getting {
@@ -122,6 +124,10 @@ kotlin {
}
}
}
+
+ compilerOptions {
+ freeCompilerArgs.add("-Xexplicit-backing-fields")
+ }
}
configurations {
diff --git a/solr/ui/gradle.lockfile b/solr/ui/gradle.lockfile
index f9381b46e275..521d5efe58d0 100644
--- a/solr/ui/gradle.lockfile
+++ b/solr/ui/gradle.lockfile
@@ -4,10 +4,10 @@
androidx.annotation:annotation-jvm:1.9.1=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,composeHotReloadDevTools,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
androidx.annotation:annotation-wasm-js:1.9.1=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
androidx.annotation:annotation:1.9.1=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,composeHotReloadDevTools,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
-androidx.arch.core:core-common:2.2.0=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,composeHotReloadDevTools,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsTestResolvableDependenciesMetadata,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
-androidx.collection:collection-jvm:1.5.0=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,composeHotReloadDevTools,desktopDevRuntimeClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestRuntimeClasspath
-androidx.collection:collection-wasm-js:1.5.0=wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
-androidx.collection:collection:1.5.0=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,composeHotReloadDevTools,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
+androidx.arch.core:core-common:2.2.0=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,composeHotReloadDevTools,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
+androidx.collection:collection-jvm:1.5.0=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,composeHotReloadDevTools,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
+androidx.collection:collection-wasm-js:1.5.0=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+androidx.collection:collection:1.5.0=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,composeHotReloadDevTools,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
androidx.compose.runtime:runtime-annotation-jvm:1.10.5=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
androidx.compose.runtime:runtime-annotation-jvm:1.9.0=composeHotReloadDevTools
androidx.compose.runtime:runtime-annotation-wasm-js:1.10.5=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
@@ -27,40 +27,46 @@ androidx.compose.runtime:runtime:1.9.0=composeHotReloadDevTools
androidx.graphics:graphics-shapes-desktop:1.1.0=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
androidx.graphics:graphics-shapes-wasm-js:1.1.0=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
androidx.graphics:graphics-shapes:1.1.0=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
+androidx.lifecycle:lifecycle-common-jvm:2.10.0=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
androidx.lifecycle:lifecycle-common-jvm:2.9.2=composeHotReloadDevTools
-androidx.lifecycle:lifecycle-common-jvm:2.9.4=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
-androidx.lifecycle:lifecycle-common-wasm-js:2.9.4=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+androidx.lifecycle:lifecycle-common-wasm-js:2.10.0=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+androidx.lifecycle:lifecycle-common:2.10.0=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
androidx.lifecycle:lifecycle-common:2.9.2=composeHotReloadDevTools
-androidx.lifecycle:lifecycle-common:2.9.4=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
-androidx.lifecycle:lifecycle-runtime-compose-desktop:2.9.4=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopDevRuntimeClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestRuntimeClasspath
-androidx.lifecycle:lifecycle-runtime-compose-wasm-js:2.9.4=wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
-androidx.lifecycle:lifecycle-runtime-compose:2.9.4=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
+androidx.lifecycle:lifecycle-runtime-compose-desktop:2.10.0=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
+androidx.lifecycle:lifecycle-runtime-compose-wasm-js:2.10.0=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+androidx.lifecycle:lifecycle-runtime-compose:2.10.0=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
+androidx.lifecycle:lifecycle-runtime-desktop:2.10.0=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
androidx.lifecycle:lifecycle-runtime-desktop:2.9.2=composeHotReloadDevTools
-androidx.lifecycle:lifecycle-runtime-desktop:2.9.4=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopDevRuntimeClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestRuntimeClasspath
-androidx.lifecycle:lifecycle-runtime-wasm-js:2.9.4=wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+androidx.lifecycle:lifecycle-runtime-wasm-js:2.10.0=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+androidx.lifecycle:lifecycle-runtime:2.10.0=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
androidx.lifecycle:lifecycle-runtime:2.9.2=composeHotReloadDevTools
-androidx.lifecycle:lifecycle-runtime:2.9.4=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
+androidx.lifecycle:lifecycle-viewmodel-desktop:2.10.0=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
androidx.lifecycle:lifecycle-viewmodel-desktop:2.9.2=composeHotReloadDevTools
-androidx.lifecycle:lifecycle-viewmodel-desktop:2.9.4=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopDevRuntimeClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestRuntimeClasspath
+androidx.lifecycle:lifecycle-viewmodel-savedstate-desktop:2.10.0=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
androidx.lifecycle:lifecycle-viewmodel-savedstate-desktop:2.9.2=composeHotReloadDevTools
-androidx.lifecycle:lifecycle-viewmodel-savedstate-desktop:2.9.4=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopDevRuntimeClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestRuntimeClasspath
-androidx.lifecycle:lifecycle-viewmodel-savedstate-wasm-js:2.9.4=wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+androidx.lifecycle:lifecycle-viewmodel-savedstate-wasm-js:2.10.0=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+androidx.lifecycle:lifecycle-viewmodel-savedstate:2.10.0=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.2=composeHotReloadDevTools
-androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.4=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
-androidx.lifecycle:lifecycle-viewmodel-wasm-js:2.9.4=wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+androidx.lifecycle:lifecycle-viewmodel-wasm-js:2.10.0=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+androidx.lifecycle:lifecycle-viewmodel:2.10.0=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
androidx.lifecycle:lifecycle-viewmodel:2.9.2=composeHotReloadDevTools
-androidx.lifecycle:lifecycle-viewmodel:2.9.4=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
-androidx.navigationevent:navigationevent-desktop:1.0.2=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopDevRuntimeClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestRuntimeClasspath
-androidx.navigationevent:navigationevent-wasm-js:1.0.2=wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
-androidx.navigationevent:navigationevent:1.0.2=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
-androidx.savedstate:savedstate-compose-desktop:1.3.3=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
-androidx.savedstate:savedstate-compose-wasm-js:1.3.3=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
-androidx.savedstate:savedstate-compose:1.3.3=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
+androidx.navigation3:navigation3-runtime-desktop:1.1.0=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
+androidx.navigation3:navigation3-runtime-wasm-js:1.1.0=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+androidx.navigation3:navigation3-runtime:1.1.0=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
+androidx.navigationevent:navigationevent-desktop:1.0.2=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
+androidx.navigationevent:navigationevent-wasm-js:1.0.2=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+androidx.navigationevent:navigationevent:1.0.2=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
+androidx.savedstate:savedstate-compose-desktop:1.4.0=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
+androidx.savedstate:savedstate-compose-wasm-js:1.4.0=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+androidx.savedstate:savedstate-compose:1.4.0=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
androidx.savedstate:savedstate-desktop:1.3.1=composeHotReloadDevTools
-androidx.savedstate:savedstate-desktop:1.3.3=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
-androidx.savedstate:savedstate-wasm-js:1.3.3=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+androidx.savedstate:savedstate-desktop:1.4.0=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
+androidx.savedstate:savedstate-wasm-js:1.4.0=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
androidx.savedstate:savedstate:1.3.1=composeHotReloadDevTools
-androidx.savedstate:savedstate:1.3.3=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
+androidx.savedstate:savedstate:1.4.0=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
+androidx.window:window-core-jvm:1.5.0=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
+androidx.window:window-core-wasm-js:1.5.0=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+androidx.window:window-core:1.5.0=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
com.arkivanov.decompose:decompose-jvm:3.4.0=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
com.arkivanov.decompose:decompose-wasm-js:3.4.0=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
com.arkivanov.decompose:decompose:3.4.0=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
@@ -180,23 +186,35 @@ org.checkerframework:checker-compat-qual:2.5.5=composeHotReloadDevDesktopTestRun
org.fusesource.jansi:jansi:2.4.2=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopDevRuntimeClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestRuntimeClasspath
org.hamcrest:hamcrest-core:1.3=composeHotReloadDevDesktopTestRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
org.javassist:javassist:3.30.2-GA=composeHotReloadAgent
-org.jetbrains.androidx.lifecycle:lifecycle-common-wasm-js:2.9.6=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+org.jetbrains.androidx.lifecycle:lifecycle-common-wasm-js:2.10.0=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+org.jetbrains.androidx.lifecycle:lifecycle-common:2.10.0=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
org.jetbrains.androidx.lifecycle:lifecycle-common:2.9.4=composeHotReloadDevTools
-org.jetbrains.androidx.lifecycle:lifecycle-common:2.9.6=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
+org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose-desktop:2.10.0=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose-desktop:2.9.4=composeHotReloadDevTools
-org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose-desktop:2.9.6=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopDevRuntimeClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestRuntimeClasspath
-org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose-wasm-js:2.9.6=wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose-wasm-js:2.10.0=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.10.0=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.9.4=composeHotReloadDevTools
-org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.9.6=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
-org.jetbrains.androidx.lifecycle:lifecycle-runtime-wasm-js:2.9.6=wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+org.jetbrains.androidx.lifecycle:lifecycle-runtime-wasm-js:2.10.0=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.10.0=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.9.4=composeHotReloadDevTools
-org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.9.6=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
-org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate-wasm-js:2.9.6=wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose-desktop:2.10.0=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
+org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose-wasm-js:2.10.0=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
+org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-navigation3-desktop:2.10.0=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
+org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-navigation3-wasm-js:2.10.0=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-navigation3:2.10.0=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
+org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate-wasm-js:2.10.0=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.10.0=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.4=composeHotReloadDevTools
-org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.6=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
-org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-wasm-js:2.9.6=wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-wasm-js:2.10.0=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.10.0=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.9.4=composeHotReloadDevTools
-org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.9.6=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
+org.jetbrains.androidx.navigation3:navigation3-ui-desktop:1.1.0=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
+org.jetbrains.androidx.navigation3:navigation3-ui-wasm-js:1.1.0=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+org.jetbrains.androidx.navigation3:navigation3-ui:1.1.0=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
+org.jetbrains.androidx.navigationevent:navigationevent-compose-desktop:1.0.1=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
+org.jetbrains.androidx.navigationevent:navigationevent-compose-wasm-js:1.0.1=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+org.jetbrains.androidx.navigationevent:navigationevent-compose:1.0.1=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
org.jetbrains.androidx.savedstate:savedstate-compose-desktop:1.3.4=composeHotReloadDevTools
org.jetbrains.androidx.savedstate:savedstate-compose-desktop:1.3.6=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
org.jetbrains.androidx.savedstate:savedstate-compose-wasm-js:1.3.6=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
@@ -205,6 +223,9 @@ org.jetbrains.androidx.savedstate:savedstate-compose:1.3.6=allDevSourceSetsCompi
org.jetbrains.androidx.savedstate:savedstate-wasm-js:1.3.6=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
org.jetbrains.androidx.savedstate:savedstate:1.3.4=composeHotReloadDevTools
org.jetbrains.androidx.savedstate:savedstate:1.3.6=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
+org.jetbrains.androidx.window:window-core-desktop:1.5.0=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
+org.jetbrains.androidx.window:window-core-wasm-js:1.5.0=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+org.jetbrains.androidx.window:window-core:1.5.0=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
org.jetbrains.compose.animation:animation-core-desktop:1.10.3=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
org.jetbrains.compose.animation:animation-core-desktop:1.9.0=composeHotReloadDevTools
org.jetbrains.compose.animation:animation-core-wasm-js:1.10.3=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
@@ -251,6 +272,18 @@ org.jetbrains.compose.hot-reload:hot-reload-orchestration:1.0.0=composeHotReload
org.jetbrains.compose.hot-reload:hot-reload-runtime-api-jvm:1.0.0=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevTools,desktopDevCompileClasspath,desktopDevRuntimeClasspath
org.jetbrains.compose.hot-reload:hot-reload-runtime-api:1.0.0=allDevSourceSetsCompileDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevTools,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath
org.jetbrains.compose.hot-reload:hot-reload-runtime-jvm:1.0.0=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,composeHotReloadRuntime
+org.jetbrains.compose.material3.adaptive:adaptive-desktop:1.3.0-alpha07=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
+org.jetbrains.compose.material3.adaptive:adaptive-layout-desktop:1.3.0-alpha07=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
+org.jetbrains.compose.material3.adaptive:adaptive-layout-wasm-js:1.3.0-alpha07=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+org.jetbrains.compose.material3.adaptive:adaptive-layout:1.3.0-alpha07=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
+org.jetbrains.compose.material3.adaptive:adaptive-navigation-desktop:1.3.0-alpha07=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
+org.jetbrains.compose.material3.adaptive:adaptive-navigation-wasm-js:1.3.0-alpha07=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+org.jetbrains.compose.material3.adaptive:adaptive-navigation3-desktop:1.3.0-alpha07=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
+org.jetbrains.compose.material3.adaptive:adaptive-navigation3-wasm-js:1.3.0-alpha07=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+org.jetbrains.compose.material3.adaptive:adaptive-navigation3:1.3.0-alpha07=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
+org.jetbrains.compose.material3.adaptive:adaptive-navigation:1.3.0-alpha07=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
+org.jetbrains.compose.material3.adaptive:adaptive-wasm-js:1.3.0-alpha07=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
+org.jetbrains.compose.material3.adaptive:adaptive:1.3.0-alpha07=allDevSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevResolvableDependenciesMetadata,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainResolvableDependenciesMetadata,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestResolvableDependenciesMetadata,desktopTestRuntimeClasspath,metadataCommonMainCompileClasspath,metadataCompileClasspath,wasmJsCompileClasspath,wasmJsMainResolvableDependenciesMetadata,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestResolvableDependenciesMetadata,wasmJsTestRuntimeClasspath,webMainResolvableDependenciesMetadata,webTestResolvableDependenciesMetadata
org.jetbrains.compose.material3:material3-desktop:1.10.0-alpha05=composeHotReloadDevDesktopDevRuntimeClasspath,composeHotReloadDevDesktopRuntimeClasspath,composeHotReloadDevDesktopTestRuntimeClasspath,desktopCompileClasspath,desktopDevCompileClasspath,desktopDevRuntimeClasspath,desktopMainCompileClasspath,desktopMainRuntimeClasspath,desktopRuntimeClasspath,desktopTestCompileClasspath,desktopTestRuntimeClasspath
org.jetbrains.compose.material3:material3-desktop:1.8.2=composeHotReloadDevTools
org.jetbrains.compose.material3:material3-wasm-js:1.10.0-alpha05=wasmJsCompileClasspath,wasmJsNpmAggregated,wasmJsRuntimeClasspath,wasmJsTestCompileClasspath,wasmJsTestNpmAggregated,wasmJsTestRuntimeClasspath
diff --git a/solr/ui/src/commonMain/composeResources/values/strings.xml b/solr/ui/src/commonMain/composeResources/values/strings.xml
index b3efd3119bb0..57c9927b34ea 100644
--- a/solr/ui/src/commonMain/composeResources/values/strings.xml
+++ b/solr/ui/src/commonMain/composeResources/values/strings.xml
@@ -18,8 +18,12 @@
+ Cancel
Connect
+ Create Configset
+ Edit solrconfig.xml
Go Back
+ Import Configset
Logout
Sign In with Credentials
Sign In with %1$s
@@ -67,8 +71,10 @@
http://127.0.0.1:8983/
- Welcome to Solr Admin UI
+ Create Configset
+ Import Configset
Sign In to Solr
+ Welcome to Solr Admin UI
Authenticating...
@@ -89,8 +95,10 @@
Zookeeper
- Username
+ Configset Name
Password
+ Select Configset File
+ Username
diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/cluster/integration/DefaultClusterComponent.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/cluster/integration/DefaultClusterComponent.kt
index 4058cafb5608..02f230f59010 100644
--- a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/cluster/integration/DefaultClusterComponent.kt
+++ b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/cluster/integration/DefaultClusterComponent.kt
@@ -17,35 +17,41 @@
package org.apache.solr.ui.components.cluster.integration
+import com.arkivanov.decompose.router.slot.ChildSlot
+import com.arkivanov.decompose.router.slot.SlotNavigation
+import com.arkivanov.decompose.router.slot.activate
+import com.arkivanov.decompose.router.slot.childSlot
+import com.arkivanov.decompose.value.Value
import org.apache.solr.ui.components.cluster.ClusterComponent
import org.apache.solr.ui.components.cluster.ClusterComponent.Child
import org.apache.solr.ui.components.cluster.ClusterComponent.ClusterTab
import org.apache.solr.ui.components.navigation.TabNavigationComponent
-import org.apache.solr.ui.components.navigation.integration.DefaultTabNavigationComponent
import org.apache.solr.ui.utils.AppComponentContext
class DefaultClusterComponent(
componentContext: AppComponentContext,
- tabNavigation: TabNavigationComponent,
) : ClusterComponent,
AppComponentContext by componentContext,
- TabNavigationComponent by tabNavigation {
+ TabNavigationComponent {
- constructor(
- componentContext: AppComponentContext,
- ) : this(
- componentContext = componentContext,
- tabNavigation = DefaultTabNavigationComponent(
- componentContext = componentContext,
- initialTab = ClusterTab.Zookeeper,
- tabSerializer = ClusterTab.serializer(),
- childFactory = { configuration, childContext ->
- when (configuration.tab) {
- ClusterTab.Zookeeper -> Child.Zookeeper
- ClusterTab.Nodes -> Child.Nodes
- ClusterTab.Cores -> Child.Cores
- }
- },
- ),
+ private val navigation = SlotNavigation()
+
+ override val tabSlot: Value> = childSlot(
+ source = navigation,
+ serializer = ClusterTab.serializer(),
+ handleBackButton = true,
+ childFactory = { configuration, childContext ->
+ when (configuration) {
+ ClusterTab.Zookeeper -> Child.Zookeeper
+ ClusterTab.Nodes -> Child.Nodes
+ ClusterTab.Cores -> Child.Cores
+ }
+ },
)
+
+ init {
+ navigation.activate(configuration = ClusterTab.Zookeeper)
+ }
+
+ override fun onNavigate(tab: ClusterTab) = navigation.activate(configuration = tab)
}
diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/ConfigsetsComponent.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/ConfigsetsComponent.kt
deleted file mode 100644
index 19580da55675..000000000000
--- a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/ConfigsetsComponent.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.solr.ui.components.configsets
-
-import kotlinx.coroutines.flow.StateFlow
-import org.apache.solr.ui.components.configsets.ConfigsetsComponent.Child
-import org.apache.solr.ui.components.configsets.overview.ConfigsetsOverviewComponent
-import org.apache.solr.ui.components.navigation.TabNavigationComponent
-import org.apache.solr.ui.domain.Configset
-import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
-
-/**
- * The configsets component provides the main entry point for managing Solr's configets.
- */
-interface ConfigsetsComponent : TabNavigationComponent {
-
- /**
- * All possible navigation targets (children) within the Configsets section.
- */
- sealed interface Child {
- data class Overview(val component: ConfigsetsOverviewComponent) : Child
-
- /**
- * TODO Remove once other sections are added
- */
- data class Placeholder(val tabName: String) : Child
- }
-
- /**
- * Model that holds the data of the [ConfigsetsComponent].
- *
- * @property configsets The configsets names available.
- * @property selectedConfigset The current configset name to display. Leave empty if no
- * selection is made.
- */
- data class Model(
- val configsets: List = emptyList(),
- val selectedConfigset: String = "",
- )
-
- /** Hot, observable stream of [Model] for Compose/UI. */
- val model: StateFlow
-
- /** Select the active configset by name. */
- fun onSelectConfigset(name: String)
-}
diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/views/navigation/configsets/ConfigsetsTab.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/data/CreateConfigset.kt
similarity index 81%
rename from solr/ui/src/commonMain/kotlin/org/apache/solr/ui/views/navigation/configsets/ConfigsetsTab.kt
rename to solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/data/CreateConfigset.kt
index 3a68a7cd8dfb..a7b06bf5a3ee 100644
--- a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/views/navigation/configsets/ConfigsetsTab.kt
+++ b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/data/CreateConfigset.kt
@@ -14,17 +14,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.solr.ui.views.navigation.configsets
+
+package org.apache.solr.ui.components.configsets.data
import kotlinx.serialization.Serializable
@Serializable
-enum class ConfigsetsTab {
- Overview,
- Files,
- Schema,
- UpdateConfig,
- IndexQuery,
- Handlers,
- SearchComponents,
-}
+data class CreateConfigset(
+ val name: String,
+ val baseConfigSet: String? = null,
+)
diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/data/HttpConfigsetsRepository.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/data/HttpConfigsetsRepository.kt
new file mode 100644
index 000000000000..24f7933aef86
--- /dev/null
+++ b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/data/HttpConfigsetsRepository.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.ui.components.configsets.data
+
+import io.ktor.client.HttpClient
+import io.ktor.client.call.body
+import io.ktor.client.request.get
+import io.ktor.client.request.post
+import io.ktor.client.request.put
+import io.ktor.client.request.setBody
+import io.ktor.http.ContentType
+import io.ktor.http.contentType
+import io.ktor.http.isSuccess
+import org.apache.solr.ui.components.configsets.repository.ConfigsetsRepository
+import org.apache.solr.ui.domain.Configset
+import org.apache.solr.ui.domain.PickedFile
+
+class HttpConfigsetsRepository(private val httpClient: HttpClient) : ConfigsetsRepository {
+ override suspend fun createConfigset(name: String, baseConfigset: String?): Result {
+ val response = httpClient.post("api/configsets") {
+ setBody(
+ CreateConfigset(
+ name = name,
+ baseConfigSet = baseConfigset,
+ ),
+ )
+ }
+ return when {
+ response.status.isSuccess() ->
+ // Success result will not contain any information,
+ // therefore, create the configset from input
+ Result.success(Configset(name))
+
+ else -> Result.failure(Exception("Unknown Error"))
+ }
+ }
+
+ override suspend fun importConfigset(name: String, file: PickedFile): Result {
+ val response = httpClient.put("api/configsets/$name") {
+ contentType(ContentType.parse("application/zip"))
+ setBody(file.bytes)
+ }
+ return when {
+ response.status.isSuccess() -> Result.success(Configset(name = name))
+ else -> Result.failure(Exception("Unknown Error"))
+ }
+ }
+
+ override suspend fun loadConfigsets(): Result> {
+ val response = httpClient.get("api/configsets")
+ return when {
+ response.status.isSuccess() -> {
+ val data = response.body()
+ Result.success(data.configSets.map { Configset(name = it) })
+ }
+
+ else -> Result.failure(Exception("Unknown Error"))
+ // TODO Add proper error handling
+ }
+ }
+}
diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/di/ConfigsetsComponent.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/di/ConfigsetsComponent.kt
new file mode 100644
index 000000000000..a7d2c7bec227
--- /dev/null
+++ b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/di/ConfigsetsComponent.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.ui.components.configsets.di
+
+import org.apache.solr.ui.components.configsets.domain.CreateConfigsetUseCase
+import org.apache.solr.ui.components.configsets.domain.ImportConfigsetUseCase
+import org.apache.solr.ui.components.configsets.domain.LoadConfigsetsUseCase
+import org.apache.solr.ui.components.configsets.repository.ConfigsetsRepository
+import org.apache.solr.ui.components.configsets.viewmodel.ConfigsetsOverviewViewModel
+import org.apache.solr.ui.components.configsets.viewmodel.ConfigsetsRouteViewModel
+import org.apache.solr.ui.components.configsets.viewmodel.ConfigsetsViewModel
+import org.apache.solr.ui.components.configsets.viewmodel.CreateConfigsetViewModel
+import org.apache.solr.ui.components.configsets.viewmodel.ImportConfigsetViewModel
+import org.apache.solr.ui.components.files.domain.SelectFileUseCase
+
+/**
+ * The configsets component keeps record of the currently available configsets, and a selected
+ * configset that may be used for additional operations.
+ */
+interface ConfigsetsComponent {
+
+ /**
+ * Dependencies provided by the application.
+ */
+ val configsetsRepository: ConfigsetsRepository
+
+ /**
+ * Use case responsible for loading the available configsets.
+ */
+ val loadConfigsetsUseCase: LoadConfigsetsUseCase
+
+ /**
+ * Factory method to create a [ConfigsetsRouteViewModel] instance.
+ */
+ fun createConfigsetsRouteViewModel(): ConfigsetsRouteViewModel
+
+ /**
+ * Factory method to create a [ConfigsetsViewModel] instance.
+ */
+ fun createConfigsetsViewModel(): ConfigsetsViewModel
+
+ fun createConfigsetsOverviewComponent(): ConfigsetsOverviewComponent
+}
diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/di/ConfigsetsOverviewComponent.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/di/ConfigsetsOverviewComponent.kt
new file mode 100644
index 000000000000..876045310bae
--- /dev/null
+++ b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/di/ConfigsetsOverviewComponent.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.ui.components.configsets.di
+
+import org.apache.solr.ui.components.configsets.domain.CreateConfigsetUseCase
+import org.apache.solr.ui.components.configsets.domain.ImportConfigsetUseCase
+import org.apache.solr.ui.components.configsets.domain.LoadConfigsetsUseCase
+import org.apache.solr.ui.components.configsets.repository.ConfigsetsRepository
+import org.apache.solr.ui.components.configsets.viewmodel.ConfigsetsOverviewViewModel
+import org.apache.solr.ui.components.configsets.viewmodel.ConfigsetsRouteViewModel
+import org.apache.solr.ui.components.configsets.viewmodel.ConfigsetsViewModel
+import org.apache.solr.ui.components.configsets.viewmodel.CreateConfigsetViewModel
+import org.apache.solr.ui.components.configsets.viewmodel.ImportConfigsetViewModel
+import org.apache.solr.ui.components.files.domain.SelectFileUseCase
+
+/**
+ * The configsets component keeps record of the currently available configsets, and a selected
+ * configset that may be used for additional operations.
+ */
+interface ConfigsetsOverviewComponent {
+
+ /**
+ * Dependencies provided by the application.
+ */
+ val configsetsRepository: ConfigsetsRepository
+
+ /**
+ * Use case responsible for creating a new configset.
+ */
+ val createConfigsetUseCase: CreateConfigsetUseCase
+
+ /**
+ * Use case responsible for importing a configset from a file.
+ */
+ val importConfigsetUseCase: ImportConfigsetUseCase
+
+ /**
+ * Use case responsible for loading the available configsets.
+ */
+ val loadConfigsetsUseCase: LoadConfigsetsUseCase
+
+ /**
+ * Use case responsible for selecting a configset file.
+ */
+ val selectFileUseCase: SelectFileUseCase
+
+ /**
+ * Factory method to create a [ConfigsetsViewModel] instance.
+ */
+ fun createConfigsetsViewModel(): ConfigsetsViewModel
+
+ /**
+ * Factory method to create a [CreateConfigsetViewModel] instance.
+ */
+ fun createCreateConfigsetViewModel(): CreateConfigsetViewModel
+
+ /**
+ * Factory method to create a [ImportConfigsetViewModel] instance.
+ */
+ fun createImportConfigsetViewModel(): ImportConfigsetViewModel
+
+ /**
+ * Factory method to create a [ConfigsetsOverviewViewModel] instance.
+ */
+ fun createConfigsetsOverviewViewModel(): ConfigsetsOverviewViewModel
+}
diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/di/DefaultConfigsetsComponent.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/di/DefaultConfigsetsComponent.kt
new file mode 100644
index 000000000000..f7382ec4e781
--- /dev/null
+++ b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/di/DefaultConfigsetsComponent.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.ui.components.configsets.di
+
+import io.ktor.client.HttpClient
+import org.apache.solr.ui.components.configsets.data.HttpConfigsetsRepository
+import org.apache.solr.ui.components.configsets.domain.DefaultLoadConfigsetsUseCase
+import org.apache.solr.ui.components.configsets.domain.LoadConfigsetsUseCase
+import org.apache.solr.ui.components.configsets.repository.ConfigsetsRepository
+import org.apache.solr.ui.components.configsets.viewmodel.ConfigsetsRouteViewModel
+import org.apache.solr.ui.components.configsets.viewmodel.ConfigsetsViewModel
+import org.apache.solr.ui.utils.AppDispatchers
+import org.apache.solr.ui.utils.platformDispatchers
+
+/**
+ * Default implementation of [ConfigsetsComponent].
+ *
+ * This implementation is using HTTP for configsets operations.
+ *
+ * @param httpClient The pre-configured HTTP client to use for user registration operations.
+ */
+class DefaultConfigsetsComponent(
+ httpClient: HttpClient,
+ private val dispatchers: AppDispatchers = platformDispatchers(),
+) : ConfigsetsComponent {
+
+ override val configsetsRepository: ConfigsetsRepository by lazy {
+ HttpConfigsetsRepository(httpClient)
+ }
+
+ override val loadConfigsetsUseCase: LoadConfigsetsUseCase by lazy {
+ DefaultLoadConfigsetsUseCase(configsetsRepository)
+ }
+
+ override fun createConfigsetsRouteViewModel(): ConfigsetsRouteViewModel = ConfigsetsRouteViewModel()
+
+ override fun createConfigsetsViewModel(): ConfigsetsViewModel = ConfigsetsViewModel(loadConfigsetsUseCase, dispatchers)
+
+ override fun createConfigsetsOverviewComponent(): ConfigsetsOverviewComponent = DefaultConfigsetsOverviewComponent(configsetsRepository)
+}
diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/di/DefaultConfigsetsOverviewComponent.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/di/DefaultConfigsetsOverviewComponent.kt
new file mode 100644
index 000000000000..dd269723325c
--- /dev/null
+++ b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/di/DefaultConfigsetsOverviewComponent.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.ui.components.configsets.di
+
+import org.apache.solr.ui.components.configsets.domain.CreateConfigsetUseCase
+import org.apache.solr.ui.components.configsets.domain.DefaultCreateConfigsetUseCase
+import org.apache.solr.ui.components.configsets.domain.DefaultImportConfigsetUseCase
+import org.apache.solr.ui.components.configsets.domain.DefaultLoadConfigsetsUseCase
+import org.apache.solr.ui.components.configsets.domain.ImportConfigsetUseCase
+import org.apache.solr.ui.components.configsets.domain.LoadConfigsetsUseCase
+import org.apache.solr.ui.components.configsets.repository.ConfigsetsRepository
+import org.apache.solr.ui.components.configsets.viewmodel.ConfigsetsOverviewViewModel
+import org.apache.solr.ui.components.configsets.viewmodel.ConfigsetsViewModel
+import org.apache.solr.ui.components.configsets.viewmodel.CreateConfigsetViewModel
+import org.apache.solr.ui.components.configsets.viewmodel.ImportConfigsetViewModel
+import org.apache.solr.ui.components.files.domain.DefaultSelectFileUseCase
+import org.apache.solr.ui.components.files.domain.SelectFileUseCase
+import org.apache.solr.ui.utils.AppDispatchers
+import org.apache.solr.ui.utils.platformDispatchers
+
+/**
+ * Default implementation of [ConfigsetsOverviewComponent].
+ */
+class DefaultConfigsetsOverviewComponent(
+ override val configsetsRepository: ConfigsetsRepository,
+ private val dispatchers: AppDispatchers = platformDispatchers(),
+) : ConfigsetsOverviewComponent {
+
+ override val createConfigsetUseCase: CreateConfigsetUseCase by lazy {
+ DefaultCreateConfigsetUseCase(configsetsRepository)
+ }
+
+ override val importConfigsetUseCase: ImportConfigsetUseCase by lazy {
+ DefaultImportConfigsetUseCase(configsetsRepository)
+ }
+
+ // TODO Consider implementing special SelectFileUseCase for configsets import cases
+ override val selectFileUseCase: SelectFileUseCase by lazy { DefaultSelectFileUseCase() }
+
+ override val loadConfigsetsUseCase: LoadConfigsetsUseCase by lazy {
+ DefaultLoadConfigsetsUseCase(configsetsRepository)
+ }
+
+ override fun createConfigsetsViewModel(): ConfigsetsViewModel = ConfigsetsViewModel(loadConfigsetsUseCase, dispatchers)
+
+ override fun createCreateConfigsetViewModel(): CreateConfigsetViewModel = CreateConfigsetViewModel(createConfigsetUseCase, loadConfigsetsUseCase, dispatchers)
+
+ override fun createImportConfigsetViewModel(): ImportConfigsetViewModel = ImportConfigsetViewModel(importConfigsetUseCase, selectFileUseCase, dispatchers)
+
+ override fun createConfigsetsOverviewViewModel(): ConfigsetsOverviewViewModel = ConfigsetsOverviewViewModel()
+}
diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/domain/CreateConfigsetEvent.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/domain/CreateConfigsetEvent.kt
new file mode 100644
index 000000000000..02b56f69b8c5
--- /dev/null
+++ b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/domain/CreateConfigsetEvent.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.ui.components.configsets.domain
+
+import org.apache.solr.ui.domain.Configset
+
+sealed interface CreateConfigsetEvent {
+
+ /**
+ * Event that is omitted when a configset was successfully created.
+ *
+ * @property configset The configset that has been created.
+ */
+ data class ConfigsetCreated(val configset: Configset) : CreateConfigsetEvent
+
+ /**
+ * Event that is omitted when the creation process failed with an error.
+ *
+ * @property error The error that was thrown during the creation process.
+ */
+ data class ConfigsetCreationFailed(val error: Exception) : CreateConfigsetEvent
+
+ /**
+ * Event that is omitted when the creation process is aborted.
+ */
+ data object ConfigsetCreationAborted : CreateConfigsetEvent
+
+ /**
+ * Event that is omitted when the creation process input form is toggled. It can be either
+ * text input or file input.
+ *
+ * @property useFileInput Whether the file input form is used (true) or the text field form
+ * instead (false).
+ */
+ data class ConfigsetCreateToggleInputForm(val useFileInput: Boolean) : CreateConfigsetEvent
+}
diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/domain/CreateConfigsetUseCase.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/domain/CreateConfigsetUseCase.kt
new file mode 100644
index 000000000000..2c04c6161fe8
--- /dev/null
+++ b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/domain/CreateConfigsetUseCase.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.ui.components.configsets.domain
+
+import org.apache.solr.ui.domain.Configset
+
+/**
+ * Use case for creating a new configset.
+ */
+interface CreateConfigsetUseCase {
+ /**
+ * Default invocation for creating a new configset with user inputs.
+ *
+ * @param configsetName The configset name to use.
+ * @param baseConfigset The configset to use as base for the new configset.
+ *
+ * @return A Result containing the Configset of the newly created configset or an error if
+ * creation failed.
+ */
+ suspend operator fun invoke(
+ configsetName: String,
+ baseConfigset: String? = null,
+ ): CreateConfigsetResult
+}
+
+sealed interface CreateConfigsetResult {
+ data class Success(val configset: Configset) : CreateConfigsetResult
+
+ data class ValidationFailure(val error: Error) : CreateConfigsetResult
+
+ data class UnexpectedFailure(val cause: Throwable) : CreateConfigsetResult
+
+ enum class Error {
+ /**
+ * Error for indicating that the configset name contains invalid characters.
+ */
+ ConfigsetNameContainsInvalidCharacters,
+
+ /**
+ * Error for indicating that the configset name is too long.
+ */
+ ConfigsetNameTooLong,
+
+ /**
+ * Error for indicating that the configset with the given name already exists.
+ */
+ DuplicateConfigset,
+ }
+}
diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/domain/DefaultCreateConfigsetUseCase.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/domain/DefaultCreateConfigsetUseCase.kt
new file mode 100644
index 000000000000..472996f2fbf1
--- /dev/null
+++ b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/domain/DefaultCreateConfigsetUseCase.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.ui.components.configsets.domain
+
+import org.apache.solr.ui.components.configsets.repository.ConfigsetsRepository
+import org.apache.solr.ui.utils.MAX_CONFIGSET_NAME_LENGTH
+import org.apache.solr.ui.utils.configsetNameRegex
+
+internal class DefaultCreateConfigsetUseCase(
+ private val repository: ConfigsetsRepository,
+) : CreateConfigsetUseCase {
+ override suspend fun invoke(
+ configsetName: String,
+ baseConfigset: String?,
+ ): CreateConfigsetResult {
+ if (!configsetName.matches(configsetNameRegex)) {
+ return CreateConfigsetResult.ValidationFailure(
+ error = CreateConfigsetResult.Error.ConfigsetNameContainsInvalidCharacters,
+ )
+ }
+
+ if (configsetName.length > MAX_CONFIGSET_NAME_LENGTH) {
+ return CreateConfigsetResult.ValidationFailure(
+ error = CreateConfigsetResult.Error.ConfigsetNameTooLong,
+ )
+ }
+
+ // TODO v2 API requires baseConfigSet to be set,
+ // remove fallback to _default once it is truly optional
+ repository.createConfigset(configsetName, baseConfigset ?: "_default")
+ .onSuccess {
+ return CreateConfigsetResult.Success(configset = it)
+ }
+ .onFailure { return CreateConfigsetResult.UnexpectedFailure(cause = it) }
+ return CreateConfigsetResult.UnexpectedFailure(cause = Error("Unknown error"))
+ }
+}
diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/domain/DefaultImportConfigsetUseCase.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/domain/DefaultImportConfigsetUseCase.kt
new file mode 100644
index 000000000000..82745c4d0980
--- /dev/null
+++ b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/domain/DefaultImportConfigsetUseCase.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.ui.components.configsets.domain
+
+import org.apache.solr.ui.components.configsets.repository.ConfigsetsRepository
+import org.apache.solr.ui.domain.Configset
+import org.apache.solr.ui.domain.PickedFile
+
+internal class DefaultImportConfigsetUseCase(
+ private val repository: ConfigsetsRepository,
+) : ImportConfigsetUseCase {
+ override suspend fun invoke(configsetName: String, file: PickedFile): ImportConfigsetResult {
+ // TODO Validate input data
+ repository.importConfigset(configsetName, file).onSuccess {
+ return ImportConfigsetResult.Success(configset = it)
+ }.onFailure {
+ // TODO Handle failure correctly
+ return ImportConfigsetResult.UnexpectedFailure(cause = it)
+ }
+ return ImportConfigsetResult.UnexpectedFailure(Error("Unknown error"))
+ }
+}
diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/integration/Mappers.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/domain/DefaultLoadConfigsetsUseCase.kt
similarity index 63%
rename from solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/integration/Mappers.kt
rename to solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/domain/DefaultLoadConfigsetsUseCase.kt
index 00a428faa326..cabac8319a5b 100644
--- a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/integration/Mappers.kt
+++ b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/domain/DefaultLoadConfigsetsUseCase.kt
@@ -15,16 +15,13 @@
* limitations under the License.
*/
-package org.apache.solr.ui.components.configsets.integration
+package org.apache.solr.ui.components.configsets.domain
-import org.apache.solr.ui.components.configsets.ConfigsetsComponent
-import org.apache.solr.ui.components.configsets.store.ConfigsetsStore
+import org.apache.solr.ui.components.configsets.repository.ConfigsetsRepository
import org.apache.solr.ui.domain.Configset
-internal val configsetsStateToModel: (ConfigsetsStore.State) -> ConfigsetsComponent.Model = {
- ConfigsetsComponent.Model(
- configsets = it.configSets.configSets.sorted()
- .map { s -> Configset(s) },
- selectedConfigset = it.selectedConfigset ?: "",
- )
+internal class DefaultLoadConfigsetsUseCase(
+ private val repository: ConfigsetsRepository,
+) : LoadConfigsetsUseCase {
+ override suspend fun invoke(): Result> = repository.loadConfigsets()
}
diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/domain/ImportConfigsetUseCase.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/domain/ImportConfigsetUseCase.kt
new file mode 100644
index 000000000000..1e10e675ca47
--- /dev/null
+++ b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/domain/ImportConfigsetUseCase.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.ui.components.configsets.domain
+
+import org.apache.solr.ui.domain.Configset
+import org.apache.solr.ui.domain.PickedFile
+
+/**
+ * Use case for importing a configset from a file.
+ */
+interface ImportConfigsetUseCase {
+ /**
+ * Default invocation for importing a configset from a simple zip file.
+ *
+ * @param configsetName The name of the configset to use.
+ * @param file The configset file to import.
+ *
+ * @return A Result containing the Configset of the created configset or an error if the import
+ * failed.
+ */
+ suspend operator fun invoke(configsetName: String, file: PickedFile): ImportConfigsetResult
+}
+
+sealed interface ImportConfigsetResult {
+ data class Success(val configset: Configset) : ImportConfigsetResult
+
+ data class ValidationFailure(val error: Error) : ImportConfigsetResult
+
+ data class UnexpectedFailure(val cause: Throwable) : ImportConfigsetResult
+
+ enum class Error {
+ /**
+ * Error for indicating an invalid configset name.
+ */
+ InvalidConfigsetName,
+
+ /**
+ * Error for indicating that the configset with the given name already exists.
+ */
+ DuplicateConfigset,
+ }
+}
diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/domain/LoadConfigsetsUseCase.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/domain/LoadConfigsetsUseCase.kt
new file mode 100644
index 000000000000..a45efa476f5a
--- /dev/null
+++ b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/domain/LoadConfigsetsUseCase.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.ui.components.configsets.domain
+
+import org.apache.solr.ui.domain.Configset
+import org.apache.solr.ui.domain.PickedFile
+
+/**
+ * Use case for loading the available configsets.
+ */
+interface LoadConfigsetsUseCase {
+ /**
+ * Default invocation for loading the available configsets.
+ *
+ * @return A Result containing the list of available Configsets.
+ */
+ suspend operator fun invoke(): Result>
+}
diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/integration/DefaultConfigsetsComponent.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/integration/DefaultConfigsetsComponent.kt
deleted file mode 100644
index b3ff89b435fa..000000000000
--- a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/integration/DefaultConfigsetsComponent.kt
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.solr.ui.components.configsets.integration
-
-import com.arkivanov.decompose.childContext
-import com.arkivanov.mvikotlin.core.instancekeeper.getStore
-import com.arkivanov.mvikotlin.core.store.StoreFactory
-import com.arkivanov.mvikotlin.extensions.coroutines.stateFlow
-import io.ktor.client.HttpClient
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.SupervisorJob
-import org.apache.solr.ui.components.configsets.ConfigsetsComponent
-import org.apache.solr.ui.components.configsets.ConfigsetsComponent.Child
-import org.apache.solr.ui.components.configsets.ConfigsetsComponent.Child.Overview
-import org.apache.solr.ui.components.configsets.ConfigsetsComponent.Child.Placeholder
-import org.apache.solr.ui.components.configsets.overview.integration.DefaultConfigsetsOverviewComponent
-import org.apache.solr.ui.components.configsets.store.ConfigsetsStore.Intent
-import org.apache.solr.ui.components.configsets.store.ConfigsetsStoreProvider
-import org.apache.solr.ui.components.navigation.TabNavigationComponent
-import org.apache.solr.ui.components.navigation.TabNavigationComponent.Configuration
-import org.apache.solr.ui.components.navigation.integration.DefaultTabNavigationComponent
-import org.apache.solr.ui.utils.AppComponentContext
-import org.apache.solr.ui.utils.coroutineScope
-import org.apache.solr.ui.utils.map
-import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
-
-/**
- * Default implementation of the [ConfigsetsComponent].
- */
-class DefaultConfigsetsComponent internal constructor(
- componentContext: AppComponentContext,
- tabNavigation: TabNavigationComponent,
- storeFactory: StoreFactory,
- httpClient: HttpClient,
-) : ConfigsetsComponent,
- AppComponentContext by componentContext,
- TabNavigationComponent by tabNavigation {
-
- constructor(
- componentContext: AppComponentContext,
- storeFactory: StoreFactory,
- httpClient: HttpClient,
- ) : this (
- componentContext = componentContext,
- storeFactory = storeFactory,
- httpClient = httpClient,
- tabNavigation = DefaultTabNavigationComponent(
- componentContext = componentContext.childContext("ConfigsetsTabs"),
- initialTab = ConfigsetsTab.Overview,
- tabSerializer = ConfigsetsTab.serializer(),
- childFactory = { configuration, childContext ->
- configsetsChildFactory(storeFactory, httpClient, configuration, childContext)
- },
- ),
- )
-
- private val mainScope = coroutineScope(SupervisorJob() + mainContext)
- private val ioScope = coroutineScope(SupervisorJob() + ioContext)
-
- private val store = instanceKeeper.getStore {
- ConfigsetsStoreProvider(
- storeFactory = storeFactory,
- client = HttpConfigsetsStoreClient(httpClient),
- mainContext = mainScope.coroutineContext,
- ioContext = ioScope.coroutineContext,
- ).provide()
- }
-
- @OptIn(ExperimentalCoroutinesApi::class)
- override val model = store.stateFlow.map(mainScope, configsetsStateToModel)
-
- override fun onSelectConfigset(name: String) {
- store.accept(Intent.SelectConfigSet(configSetName = name))
- }
-}
-
-fun configsetsChildFactory(
- storeFactory: StoreFactory,
- httpClient: HttpClient,
- configuration: Configuration,
- childContext: AppComponentContext,
-): Child = when (configuration.tab) {
- ConfigsetsTab.Overview -> Overview(
- DefaultConfigsetsOverviewComponent(
- componentContext = childContext,
- storeFactory = storeFactory,
- httpClient = httpClient,
- ),
- )
-
- else -> Placeholder(tabName = configuration.tab.name)
-}
diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/integration/HttpConfigsetsStoreClient.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/integration/HttpConfigsetsStoreClient.kt
deleted file mode 100644
index cd8a7eaa726d..000000000000
--- a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/integration/HttpConfigsetsStoreClient.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.solr.ui.components.configsets.integration
-
-import io.ktor.client.HttpClient
-import io.ktor.client.call.body
-import io.ktor.client.request.get
-import io.ktor.http.isSuccess
-import org.apache.solr.ui.components.configsets.data.ListConfigsets
-import org.apache.solr.ui.components.configsets.store.ConfigsetsStoreProvider
-
-/**
- * Client implementation of the [ConfigsetsStoreProvider.Client] that makes use
- * of a preconfigured HTTP client for accessing the Solr API.
- *
- * @property httpClient HTTP client to use for accessing the API. The client has to be
- * configured with a default request that includes the host, port and schema. The client
- * should also include the necessary authentication data if authentication / authorization
- * is enabled.
- */
-class HttpConfigsetsStoreClient(
- private val httpClient: HttpClient,
-) : ConfigsetsStoreProvider.Client {
- override suspend fun fetchConfigSets(): Result {
- val response = httpClient.get("api/configsets")
- return when {
- response.status.isSuccess() -> Result.success(response.body())
- else -> Result.failure(Exception("Unknown Error"))
- // TODO Add proper error handling
- }
- }
-}
diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/repository/ConfigsetsRepository.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/repository/ConfigsetsRepository.kt
new file mode 100644
index 000000000000..f1b89627dd20
--- /dev/null
+++ b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/repository/ConfigsetsRepository.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.ui.components.configsets.repository
+
+import org.apache.solr.ui.components.configsets.data.CreateConfigset
+import org.apache.solr.ui.domain.Configset
+import org.apache.solr.ui.domain.PickedFile
+
+interface ConfigsetsRepository {
+ /**
+ * Create a new configset.
+ *
+ * @param name The name to use for the new configset.
+ * @param baseConfigset The configset to use as base.
+ * @result Result with the created configset.
+ */
+ suspend fun createConfigset(name: String, baseConfigset: String? = null): Result
+
+ /**
+ * Import a configset from a zip file.
+ *
+ * @param name The name to use for the configset that is being imported.
+ * @param file The file to upload for the import.
+ * @result Result with the created configset.
+ */
+ suspend fun importConfigset(name: String, file: PickedFile): Result
+
+ /**
+ * Load the available configsets.
+ */
+ suspend fun loadConfigsets(): Result>
+}
diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/store/ConfigsetsStore.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/store/ConfigsetsStore.kt
deleted file mode 100644
index 7dbbb9f69dda..000000000000
--- a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/store/ConfigsetsStore.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.solr.ui.components.configsets.store
-
-import com.arkivanov.mvikotlin.core.store.Store
-import org.apache.solr.ui.components.configsets.data.ListConfigsets
-import org.apache.solr.ui.components.configsets.store.ConfigsetsStore.Intent
-import org.apache.solr.ui.components.configsets.store.ConfigsetsStore.State
-import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
-
-internal interface ConfigsetsStore : Store {
-
- sealed interface Intent {
- /**
- * Intent for selecting configset.
- */
- data class SelectConfigSet(val configSetName: String) : Intent
- }
-
- data class State(
- val selectedConfigset: String? = null,
- val configSets: ListConfigsets = ListConfigsets(),
- )
-}
diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/store/ConfigsetsStoreProvider.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/store/ConfigsetsStoreProvider.kt
deleted file mode 100644
index e3c73e4bcfd3..000000000000
--- a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/store/ConfigsetsStoreProvider.kt
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.solr.ui.components.configsets.store
-
-import com.arkivanov.mvikotlin.core.store.Reducer
-import com.arkivanov.mvikotlin.core.store.SimpleBootstrapper
-import com.arkivanov.mvikotlin.core.store.Store
-import com.arkivanov.mvikotlin.core.store.StoreFactory
-import com.arkivanov.mvikotlin.extensions.coroutines.CoroutineExecutor
-import kotlin.coroutines.CoroutineContext
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import org.apache.solr.ui.components.configsets.data.ListConfigsets
-import org.apache.solr.ui.components.configsets.store.ConfigsetsStore.Intent
-import org.apache.solr.ui.components.configsets.store.ConfigsetsStore.State
-import org.apache.solr.ui.views.navigation.configsets.ConfigsetsTab
-
-/**
- * Store provider that [provide]s instances of [ConfigsetsStore].
- *
- * @property storeFactory Store factory to use for creating the store.
- * @property client Client implementation to use for resolving [Intent]s and [Action]s.
- * @property ioContext Coroutine context used for IO activity.
- */
-internal class ConfigsetsStoreProvider(
- private val storeFactory: StoreFactory,
- private val client: Client,
- private val mainContext: CoroutineContext,
- private val ioContext: CoroutineContext,
-) {
-
- fun provide(): ConfigsetsStore = object :
- ConfigsetsStore,
- Store by storeFactory.create(
- name = "ConfigsetsStore",
- initialState = State(),
- bootstrapper = SimpleBootstrapper(Action.FetchInitialConfigsets),
- executorFactory = ::ExecutorImpl,
- reducer = ReducerImpl,
- ) {}
-
- private sealed interface Action {
- /**
- * Action used for initiating the initial fetch of configsets data.
- */
- data object FetchInitialConfigsets : Action
- }
-
- private sealed interface Message {
- data class ConfigSetsUpdated(val configsets: ListConfigsets) : Message
- data class SelectedConfigSetChanged(val configsetName: String) : Message
- }
-
- private inner class ExecutorImpl : CoroutineExecutor(mainContext) {
- override fun executeAction(action: Action) = when (action) {
- Action.FetchInitialConfigsets -> {
- fetchConfigSets()
- }
- }
-
- override fun executeIntent(intent: Intent) = when (intent) {
- is Intent.SelectConfigSet -> dispatch(Message.SelectedConfigSetChanged(intent.configSetName))
- }
-
- private fun fetchConfigSets() {
- scope.launch {
- withContext(ioContext) {
- client.fetchConfigSets()
- }.onSuccess { sets ->
- dispatch(Message.ConfigSetsUpdated(sets))
- }
- }
- }
- }
-
- /**
- * Reducer implementation that consumes [Message]s and updates the store's [State].
- */
- private object ReducerImpl : Reducer {
- override fun State.reduce(msg: Message): State = when (msg) {
- is Message.ConfigSetsUpdated -> copy(configSets = msg.configsets)
- is Message.SelectedConfigSetChanged -> copy(selectedConfigset = msg.configsetName)
- }
- }
-
- /**
- * Client interface for fetching configsets information.
- */
- interface Client {
- /** To fetch a list of configsets. */
- suspend fun fetchConfigSets(): Result
- }
-}
diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/viewmodel/ConfigsetsOverviewViewModel.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/viewmodel/ConfigsetsOverviewViewModel.kt
new file mode 100644
index 000000000000..e0bf9a8884df
--- /dev/null
+++ b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/viewmodel/ConfigsetsOverviewViewModel.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.ui.components.configsets.viewmodel
+
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.snapshots.SnapshotStateList
+import androidx.lifecycle.ViewModel
+import androidx.navigation3.runtime.NavKey
+import kotlinx.serialization.Serializable
+import org.apache.solr.ui.components.configsets.viewmodel.ConfigsetsOverviewEntry.ConfigsetsOverviewDialog.CreateConfigsetDialog
+import org.apache.solr.ui.components.configsets.viewmodel.ConfigsetsOverviewEntry.ConfigsetsOverviewDialog.ImportConfigsetDialog
+
+class ConfigsetsOverviewViewModel : ViewModel() {
+
+ val backStack: SnapshotStateList =
+ mutableStateListOf(ConfigsetsOverviewEntry.Main)
+
+ /**
+ * Initiates the creation of a new configset.
+ */
+ fun openCreateConfigsetDialog() = backStack.apply {
+ clearDialogs()
+ add(CreateConfigsetDialog)
+ }
+
+ /**
+ * Initiates the import of a configset.
+ */
+ fun openImportConfigsetDialog() = backStack.apply {
+ clearDialogs()
+ add(ImportConfigsetDialog)
+ }
+
+ /**
+ * Closes any opened dialog.
+ */
+ fun closeDialog() {
+ backStack.clearDialogs()
+ }
+
+ /**
+ * Edit solrconfig.xml for the configset with the given [name].
+ *
+ * @param name the name of the configset to edit.
+ */
+ fun editSolrConfig(name: String) {
+ TODO()
+ }
+
+ private fun SnapshotStateList.clearDialogs() = removeAll { entry -> entry is ConfigsetsOverviewEntry.ConfigsetsOverviewDialog }
+}
+
+@Serializable
+sealed interface ConfigsetsOverviewEntry : NavKey {
+
+ @Serializable
+ data object Main : ConfigsetsOverviewEntry
+
+ @Serializable
+ sealed interface ConfigsetsOverviewDialog : ConfigsetsOverviewEntry {
+
+ @Serializable
+ data object CreateConfigsetDialog : ConfigsetsOverviewDialog
+
+ @Serializable
+ data object ImportConfigsetDialog : ConfigsetsOverviewDialog
+ }
+}
diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/viewmodel/ConfigsetsRouteViewModel.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/viewmodel/ConfigsetsRouteViewModel.kt
new file mode 100644
index 000000000000..2d0cf9ea1775
--- /dev/null
+++ b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/viewmodel/ConfigsetsRouteViewModel.kt
@@ -0,0 +1,104 @@
+/*
+ * 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.ui.components.configsets.viewmodel
+
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.snapshots.SnapshotStateList
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.navigation3.runtime.NavKey
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.serialization.Serializable
+
+// TODO Make savedStateHandle mandatory
+class ConfigsetsRouteViewModel(
+ savedStateHandle: SavedStateHandle? = null,
+) : ViewModel() {
+
+ private val initialTab = savedStateHandle
+ ?.get("tab")
+ ?.let { ConfigsetsTab.valueOf(it) }
+ ?: ConfigsetsTab.Overview
+
+ val backStack: SnapshotStateList = mutableStateListOf(
+ when (initialTab) {
+ ConfigsetsTab.Overview -> ConfigsetsScene.Overview
+ ConfigsetsTab.Files -> ConfigsetsScene.Files
+ ConfigsetsTab.Schema -> ConfigsetsScene.Schema
+ ConfigsetsTab.UpdateConfig -> ConfigsetsScene.UpdateConfig
+ ConfigsetsTab.IndexQuery -> ConfigsetsScene.IndexQuery
+ ConfigsetsTab.Handlers -> ConfigsetsScene.Handlers
+ ConfigsetsTab.SearchComponents -> ConfigsetsScene.SearchComponents
+ },
+ )
+
+ /**
+ * The dialog that is currently open, if any.
+ */
+ val uiState: StateFlow
+ field = MutableStateFlow(ConfigsetsSceneUiState(selectedTab = initialTab))
+
+ /**
+ * Switches to the configset [tab] that was provided.
+ *
+ * @param tab The tab to select.
+ */
+ fun selectTab(tab: ConfigsetsTab) = uiState.update { it.copy(selectedTab = tab) }
+}
+
+data class ConfigsetsSceneUiState(
+ val selectedTab: ConfigsetsTab = ConfigsetsTab.Overview,
+)
+
+@Serializable
+sealed interface ConfigsetsScene : NavKey {
+
+ @Serializable
+ data object Overview : ConfigsetsScene
+
+ @Serializable
+ data object Files : ConfigsetsScene
+
+ @Serializable
+ data object Schema : ConfigsetsScene
+
+ @Serializable
+ data object UpdateConfig : ConfigsetsScene
+
+ @Serializable
+ data object IndexQuery : ConfigsetsScene
+
+ @Serializable
+ data object Handlers : ConfigsetsScene
+
+ @Serializable
+ data object SearchComponents : ConfigsetsScene
+}
+
+@Serializable
+enum class ConfigsetsTab {
+ Overview,
+ Files,
+ Schema,
+ UpdateConfig,
+ IndexQuery,
+ Handlers,
+ SearchComponents,
+}
diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/viewmodel/ConfigsetsStateHolder.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/viewmodel/ConfigsetsStateHolder.kt
new file mode 100644
index 000000000000..0f17f5b26d42
--- /dev/null
+++ b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/viewmodel/ConfigsetsStateHolder.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.ui.components.configsets.viewmodel
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.apache.solr.ui.components.configsets.domain.LoadConfigsetsUseCase
+import org.apache.solr.ui.domain.Configset
+import org.apache.solr.ui.utils.AppDispatchers
+
+class ConfigsetsStateHolder(
+ private val scope: CoroutineScope,
+ private val loadConfigsetsUseCase: LoadConfigsetsUseCase,
+ private val dispatchers: AppDispatchers,
+) {
+
+ /**
+ * State of the configset create form.
+ */
+ val uiState: StateFlow
+ field = MutableStateFlow(ConfigsetsUiState())
+
+ init {
+ loadConfigsets()
+ }
+
+ /**
+ * Selects the given [configset].
+ *
+ * @param configset The configset to select.
+ */
+ fun selectConfigset(configset: String) = uiState.update {
+ it.copy(selectedConfigset = configset)
+ }
+
+ /**
+ * Clears the currently selected configset, if any.
+ */
+ fun clearSelectedConfigset() = uiState.update { it.copy(selectedConfigset = null) }
+
+ /**
+ * Reloads the configsets.
+ */
+ fun reloadConfigsets() {
+ loadConfigsets(clearOnFailure = true)
+ }
+
+ private fun loadConfigsets(clearOnFailure: Boolean = false) = scope.launch {
+ withContext(dispatchers.io) {
+ loadConfigsetsUseCase()
+ }.onSuccess { configsets ->
+ uiState.update { it.copy(configsets = configsets.sortedBy(Configset::name)) }
+ if (configsets.none { it.name == uiState.value.selectedConfigset }) {
+ // Unselect current configset
+ clearSelectedConfigset()
+ }
+ }.onFailure {
+ // TODO Notify user about loading issue
+ if (clearOnFailure) {
+ uiState.update {
+ it.copy(
+ configsets = emptyList(),
+ selectedConfigset = null,
+ )
+ }
+ }
+ }
+ }
+}
+
+data class ConfigsetsUiState(
+ val configsets: List = emptyList(),
+ val selectedConfigset: String? = null,
+)
diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/viewmodel/ConfigsetsViewModel.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/viewmodel/ConfigsetsViewModel.kt
new file mode 100644
index 000000000000..0bb1b9cbd8c4
--- /dev/null
+++ b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/viewmodel/ConfigsetsViewModel.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.ui.components.configsets.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import org.apache.solr.ui.components.configsets.domain.LoadConfigsetsUseCase
+import org.apache.solr.ui.utils.AppDispatchers
+
+class ConfigsetsViewModel(
+ loadConfigsetsUseCase: LoadConfigsetsUseCase,
+ dispatchers: AppDispatchers,
+) : ViewModel() {
+
+ private val configsetsState = ConfigsetsStateHolder(
+ scope = viewModelScope,
+ loadConfigsetsUseCase = loadConfigsetsUseCase,
+ dispatchers = dispatchers,
+ )
+
+ /**
+ * UI State of the configsets.
+ */
+ val uiState = configsetsState.uiState
+
+ /**
+ * Selects the given [configset].
+ *
+ * @param configset The configset to select.
+ */
+ fun selectConfigset(configset: String) = configsetsState.selectConfigset(configset)
+
+ /**
+ * Clears the currently selected configset, if any.
+ */
+ fun clearSelectedConfigset() = configsetsState.clearSelectedConfigset()
+
+ /**
+ * Reloads the configsets.
+ */
+ fun reloadConfigsets() = configsetsState.reloadConfigsets()
+}
diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/viewmodel/CreateConfigsetViewModel.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/viewmodel/CreateConfigsetViewModel.kt
new file mode 100644
index 000000000000..e2a6fcdff696
--- /dev/null
+++ b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/configsets/viewmodel/CreateConfigsetViewModel.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.ui.components.configsets.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.apache.solr.ui.components.configsets.domain.CreateConfigsetEvent
+import org.apache.solr.ui.components.configsets.domain.CreateConfigsetResult
+import org.apache.solr.ui.components.configsets.domain.CreateConfigsetUseCase
+import org.apache.solr.ui.components.configsets.domain.LoadConfigsetsUseCase
+import org.apache.solr.ui.utils.AppDispatchers
+
+class CreateConfigsetViewModel(
+ private val createConfigsetUseCase: CreateConfigsetUseCase,
+ loadConfigsetsUseCase: LoadConfigsetsUseCase,
+ private val dispatchers: AppDispatchers,
+) : ViewModel() {
+
+ private val configsetsState = ConfigsetsStateHolder(
+ scope = viewModelScope,
+ loadConfigsetsUseCase = loadConfigsetsUseCase,
+ dispatchers = dispatchers,
+ )
+
+ /**
+ * State of the configset create form.
+ */
+ val uiState: StateFlow