blob: 2dc9942e3470d8cc08e0bc37aed9a82586d62424 [file] [log] [blame]
/*
* 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 java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.lang.invoke.MethodHandles;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
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.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.CloudSolrClient;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.cloud.ZkSolrResourceLoader;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.SolrInputField;
import org.apache.solr.common.cloud.DocCollection;
import org.apache.solr.common.cloud.SolrZkClient;
import org.apache.solr.common.cloud.ZkConfigManager;
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.core.CoreContainer;
import org.apache.solr.core.SolrConfig;
import org.apache.solr.core.SolrResourceLoader;
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;
import org.apache.zookeeper.KeeperException;
import org.noggit.JSONParser;
import org.noggit.ObjectBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.StringUtils.isEmpty;
import static org.apache.solr.common.params.CommonParams.JSON_MIME;
import static org.apache.solr.common.util.Utils.makeMap;
import static org.apache.solr.handler.admin.ConfigSetsHandler.DEFAULT_CONFIGSET_NAME;
import static org.apache.solr.schema.ManagedIndexSchemaFactory.DEFAULT_MANAGED_SCHEMA_RESOURCE_NAME;
import static org.apache.solr.security.PermissionNameProvider.Name.CONFIG_EDIT_PERM;
import static org.apache.solr.security.PermissionNameProvider.Name.CONFIG_READ_PERM;
/**
* All V2 APIs that have a prefix of /api/schema-designer/
*/
public class SchemaDesignerAPI implements SchemaDesignerConstants {
private static final Set<String> excludeConfigSetNames = new HashSet<>(Arrays.asList(DEFAULT_CONFIGSET_NAME, BLOB_STORE_ID));
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private final CoreContainer coreContainer;
private final SchemaSuggester schemaSuggester;
private final SampleDocumentsLoader sampleDocLoader;
private final SchemaDesignerSettingsDAO settingsDAO;
private final SchemaDesignerConfigSetHelper configSetHelper;
private final Map<String, Integer> indexedVersion = new ConcurrentHashMap<>();
public SchemaDesignerAPI(CoreContainer coreContainer) {
this(coreContainer, SchemaDesignerAPI.newSchemaSuggester(coreContainer), SchemaDesignerAPI.newSampleDocumentsLoader(coreContainer));
}
SchemaDesignerAPI(CoreContainer coreContainer, SchemaSuggester schemaSuggester, SampleDocumentsLoader sampleDocLoader) {
this.coreContainer = coreContainer;
this.schemaSuggester = schemaSuggester;
this.sampleDocLoader = sampleDocLoader;
this.settingsDAO = new SchemaDesignerSettingsDAO(coreContainer);
this.configSetHelper = new SchemaDesignerConfigSetHelper(this.coreContainer, this.schemaSuggester);
}
public static SchemaSuggester newSchemaSuggester(CoreContainer coreContainer) {
DefaultSchemaSuggester suggester = new DefaultSchemaSuggester();
suggester.init(new NamedList<>());
return suggester;
}
public static SampleDocumentsLoader newSampleDocumentsLoader(CoreContainer coreContainer) {
SampleDocumentsLoader loader = new DefaultSampleDocumentsLoader();
loader.init(new NamedList<>());
return loader;
}
static String getConfigSetZkPath(final String configSet) {
return getConfigSetZkPath(configSet, null);
}
static String getConfigSetZkPath(final String configSet, final String childNode) {
String path = ZkConfigManager.CONFIGS_ZKNODE + "/" + configSet;
if (childNode != null) {
path += "/" + childNode;
}
return path;
}
static String getMutableId(final String configSet) {
return DESIGNER_PREFIX + configSet;
}
@EndPoint(method = GET, path = "/schema-designer/info", permission = CONFIG_READ_PERM)
@SuppressWarnings("unchecked")
public void getInfo(SolrQueryRequest req, SolrQueryResponse rsp) throws IOException {
final String configSet = getRequiredParam(CONFIG_SET_PARAM, req);
Map<String, Object> responseMap = new HashMap<>();
responseMap.put(CONFIG_SET_PARAM, configSet);
boolean exists = configExists(configSet);
responseMap.put("published", exists);
// mutable config may not exist yet as this is just an info check to gather some basic info the UI needs
String mutableId = getMutableId(configSet);
// tricky: if there's a mutable config, prefer the settings from that first but fallback to the original if not found
SolrConfig srcConfig = exists ? configSetHelper.loadSolrConfig(configSet) : null;
SolrConfig solrConfig = configExists(mutableId) ? configSetHelper.loadSolrConfig(mutableId) : srcConfig;
addSettingsToResponse(settingsDAO.getSettings(solrConfig), responseMap);
responseMap.put(SCHEMA_VERSION_PARAM, configSetHelper.getCurrentSchemaVersion(mutableId));
responseMap.put("collections", exists ? configSetHelper.listCollectionsForConfig(configSet) : Collections.emptyList());
// don't fail if loading sample docs fails
try {
responseMap.put("numDocs", configSetHelper.getStoredSampleDocs(configSet).size());
} catch (Exception exc) {
log.warn("Failed to load sample docs from blob store for {}", configSet, exc);
}
rsp.getValues().addAll(responseMap);
}
@EndPoint(method = POST, path = "/schema-designer/prep", permission = CONFIG_EDIT_PERM)
@SuppressWarnings("unchecked")
public void prepNewSchema(SolrQueryRequest req, SolrQueryResponse rsp) throws IOException, SolrServerException {
final String configSet = getRequiredParam(CONFIG_SET_PARAM, req);
validateNewConfigSetName(configSet);
final String copyFrom = req.getParams().get(COPY_FROM_PARAM, DEFAULT_CONFIGSET_NAME);
SchemaDesignerSettings settings = getMutableSchemaForConfigSet(configSet, -1, copyFrom);
ManagedIndexSchema schema = settings.getSchema();
String mutableId = getMutableId(configSet);
// make sure the temp collection for this analysis exists
if (!zkStateReader().getClusterState().hasCollection(mutableId)) {
indexedVersion.remove(mutableId);
configSetHelper.createCollection(mutableId, mutableId);
}
settingsDAO.persistIfChanged(mutableId, settings);
rsp.getValues().addAll(buildResponse(configSet, schema, settings, null));
}
@EndPoint(method = PUT, path = "/schema-designer/cleanup", permission = CONFIG_EDIT_PERM)
public void cleanupTemp(SolrQueryRequest req, SolrQueryResponse rsp) throws IOException, SolrServerException {
cleanupTemp(getRequiredParam(CONFIG_SET_PARAM, req));
}
@EndPoint(method = GET, path = "/schema-designer/file", permission = CONFIG_READ_PERM)
@SuppressWarnings("unchecked")
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, true);
} catch (KeeperException | InterruptedException e) {
throw new IOException("Error reading file: " + filePath, SolrZkClient.checkInterrupted(e));
}
String stringData = data != null && data.length > 0 ? new String(data, StandardCharsets.UTF_8) : "";
rsp.getValues().addAll(Collections.singletonMap(file, stringData));
}
@EndPoint(method = POST, path = "/schema-designer/file", permission = CONFIG_EDIT_PERM)
@SuppressWarnings("unchecked")
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);
if (!pathExistsInZk(zkPath)) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "File '" + file + "' not found in configSet: " + configSet);
}
byte[] data = DefaultSampleDocumentsLoader.streamAsBytes(extractSingleContentStream(req, true).getStream());
Exception updateFileError = null;
if (SOLR_CONFIG_XML.equals(file)) {
// verify the updated solrconfig.xml is valid before saving to ZK (to avoid things blowing up later)
try {
InMemoryResourceLoader loader = new InMemoryResourceLoader(coreContainer, mutableId, SOLR_CONFIG_XML, data);
SolrConfig.readFromResourceLoader(loader, SOLR_CONFIG_XML, true, null);
} catch (Exception exc) {
updateFileError = exc;
}
}
if (updateFileError != null) {
// solrconfig.xml update failed, but haven't impacted the configSet yet, so just return the error directly
Throwable causedBy = SolrException.getRootCause(updateFileError);
Map<String, Object> response = new HashMap<>();
response.put("updateFileError", causedBy.getMessage());
response.put(file, new String(data, StandardCharsets.UTF_8));
rsp.getValues().addAll(response);
return;
}
// apply the update and reload the temp collection / re-index sample docs
SolrZkClient zkClient = zkStateReader().getZkClient();
try {
zkClient.setData(zkPath, data, true);
} catch (KeeperException | InterruptedException e) {
throw new IOException("Failed to save data in ZK at path: " + zkPath, SolrZkClient.checkInterrupted(e));
}
configSetHelper.reloadTempCollection(mutableId, false);
ManagedIndexSchema schema = loadLatestSchema(mutableId);
Map<Object, Throwable> errorsDuringIndexing = null;
SolrException solrExc = null;
List<SolrInputDocument> docs = configSetHelper.getStoredSampleDocs(configSet);
String[] analysisErrorHolder = new String[1];
if (!docs.isEmpty()) {
String idField = schema.getUniqueKeyField().getName();
try {
errorsDuringIndexing =
indexSampleDocsWithRebuildOnAnalysisError(idField, docs, mutableId, true, analysisErrorHolder);
} catch (SolrException exc) {
solrExc = exc;
}
}
Map<String, Object> response = buildResponse(configSet, schema, null, docs);
if (analysisErrorHolder[0] != null) {
response.put(ANALYSIS_ERROR, analysisErrorHolder[0]);
}
addErrorToResponse(mutableId, solrExc, errorsDuringIndexing, response,
"Failed to re-index sample documents after update to the " + file + " file");
rsp.getValues().addAll(response);
}
@EndPoint(method = GET, path = "/schema-designer/sample", permission = CONFIG_READ_PERM)
@SuppressWarnings("unchecked")
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);
final List<SolrInputDocument> docs = configSetHelper.getStoredSampleDocs(configSet);
String textValue = null;
if (isEmpty(docId)) {
// no doc ID from client ... find the first doc with a non-empty string value for fieldName
Optional<SolrInputDocument> doc = docs.stream()
.filter(d -> d.getField(fieldName) != null && d.getField(fieldName).getFirstValue() != null && !d.getField(fieldName).getFirstValue().toString().isEmpty())
.findFirst();
if (doc.isPresent()) {
docId = doc.get().getFieldValue(idField).toString();
textValue = doc.get().getField(fieldName).getFirstValue().toString();
}
} else {
final String idFilter = docId;
Optional<SolrInputDocument> doc = docs.stream().filter(d -> idFilter.equals(d.getFieldValue(idField))).findFirst();
if (doc.isPresent()) {
SolrInputField field = doc.get().getField(fieldName);
textValue = field != null && field.getFirstValue() != null ? field.getFirstValue().toString() : "";
}
}
if (textValue != null) {
Map<String, Object> analysis = configSetHelper.analyzeField(configSet, fieldName, textValue);
rsp.getValues().addAll(makeMap(idField, docId, fieldName, textValue, "analysis", analysis));
}
}
@EndPoint(method = GET, path = "/schema-designer/collectionsForConfig", permission = CONFIG_READ_PERM)
@SuppressWarnings("unchecked")
public void listCollectionsForConfig(SolrQueryRequest req, SolrQueryResponse rsp) {
final String configSet = getRequiredParam(CONFIG_SET_PARAM, req);
rsp.getValues().addAll(Collections.singletonMap("collections", configSetHelper.listCollectionsForConfig(configSet)));
}
// CONFIG_EDIT_PERM is required here since this endpoint is used by the UI to determine if the user has access to the Schema Designer UI
@EndPoint(method = GET, path = "/schema-designer/configs", permission = CONFIG_EDIT_PERM)
@SuppressWarnings("unchecked")
public void listConfigs(SolrQueryRequest req, SolrQueryResponse rsp) throws IOException {
rsp.getValues().addAll(Collections.singletonMap("configSets", listEnabledConfigs()));
}
protected Map<String, Integer> listEnabledConfigs() throws IOException {
List<String> configsInZk = configSetHelper.listConfigsInZk();
final Map<String, Integer> configs = configsInZk.stream()
.filter(c -> !excludeConfigSetNames.contains(c) && !c.startsWith(DESIGNER_PREFIX))
.collect(Collectors.toMap(c -> c, c -> settingsDAO.isDesignerDisabled(c) ? 1 : 2));
// add the in-progress but drop the _designer prefix
configsInZk.stream().filter(c -> c.startsWith(DESIGNER_PREFIX))
.map(c -> c.substring(DESIGNER_PREFIX.length()))
.forEach(c -> configs.putIfAbsent(c, 0));
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), true)) {
if (zkClient.exists(getConfigSetZkPath(configSet, null), true)) {
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));
}
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)
@SuppressWarnings("unchecked")
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<String, Object> addJson = readJsonFromRequest(req);
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<String, Object> response = buildResponse(configSet, schema, null, configSetHelper.getStoredSampleDocs(configSet));
response.put(action, objectName);
rsp.getValues().addAll(response);
}
@EndPoint(method = PUT, path = "/schema-designer/update", permission = CONFIG_EDIT_PERM)
@SuppressWarnings("unchecked")
public void updateSchemaObject(SolrQueryRequest req, SolrQueryResponse rsp) throws IOException, SolrServerException {
final String configSet = getRequiredParam(CONFIG_SET_PARAM, req);
final String mutableId = checkMutable(configSet, req);
// Updated field definition is in the request body as JSON
Map<String, Object> updateField = readJsonFromRequest(req);
String name = (String) updateField.get("name");
if (isEmpty(name)) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
"Invalid update request! JSON payload is missing the required name property: " + updateField);
}
log.info("Updating schema object: configSet={}, mutableId={}, name={}, JSON={}", configSet, mutableId, name, updateField);
SchemaDesignerSettings settings = getMutableSchemaForConfigSet(configSet, -1, null);
ManagedIndexSchema schemaBeforeUpdate = settings.getSchema();
Map<String, Object> updateResult = configSetHelper.updateSchemaObject(configSet, updateField, schemaBeforeUpdate);
SolrException solrExc = (SolrException) updateResult.get("solrExc");
String updateError = (String) updateResult.get(UPDATE_ERROR);
String updateType = (String) updateResult.get("updateType");
boolean needsRebuild = (boolean) updateResult.get("rebuild");
// re-index the docs if no error to this point
final ManagedIndexSchema schema = loadLatestSchema(mutableId);
List<SolrInputDocument> docs = configSetHelper.getStoredSampleDocs(configSet);
Map<Object, Throwable> errorsDuringIndexing = null;
String[] analysisErrorHolder = new String[1];
if (solrExc == null && !docs.isEmpty()) {
try {
errorsDuringIndexing =
indexSampleDocsWithRebuildOnAnalysisError(schema.getUniqueKeyField().getName(), docs, mutableId, false, analysisErrorHolder);
} catch (SolrException exc) {
solrExc = exc;
updateError = "Failed to re-index sample documents after update to the " + name + " " + updateType + " due to: " + solrExc.getMessage();
}
}
Map<String, Object> response = buildResponse(configSet, schema, settings, docs);
response.put("updateType", updateType);
if (FIELD_PARAM.equals(updateType)) {
response.put(updateType, fieldToMap(schema.getField(name), schema));
} else if ("type".equals(updateType)) {
response.put(updateType, schema.getFieldTypeByName(name).getNamedPropertyValues(true));
}
if (analysisErrorHolder[0] != null) {
response.put(ANALYSIS_ERROR, analysisErrorHolder[0]);
}
addErrorToResponse(mutableId, solrExc, errorsDuringIndexing, response, updateError);
response.put("rebuild", needsRebuild);
rsp.getValues().addAll(response);
}
@EndPoint(method = PUT, path = "/schema-designer/publish", permission = CONFIG_EDIT_PERM)
@SuppressWarnings("unchecked")
public void publish(SolrQueryRequest req, SolrQueryResponse rsp) throws IOException, SolrServerException {
final String configSet = getRequiredParam(CONFIG_SET_PARAM, req);
final String mutableId = checkMutable(configSet, req);
// verify the configSet we're going to apply changes to has not changed since being loaded for editing by the schema designer
SchemaDesignerSettings settings = settingsDAO.getSettings(mutableId);
final Optional<Integer> publishedVersion = settings.getPublishedVersion();
if (publishedVersion.isPresent()) {
int currentVersionOfSrc = configSetHelper.getCurrentSchemaVersion(configSet);
if (publishedVersion.get() != currentVersionOfSrc) {
throw new SolrException(SolrException.ErrorCode.CONFLICT,
"Version mismatch for " + configSet + "! Expected version " + publishedVersion.get() + " but current is " + currentVersionOfSrc +
"; another user may have changed the published schema while you were making edits. " +
"Publishing your changes would result in losing the edits from the other user.");
}
}
String newCollection = req.getParams().get(NEW_COLLECTION_PARAM);
if (!isEmpty(newCollection) && zkStateReader().getClusterState().hasCollection(newCollection)) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Collection '" + newCollection + "' already exists!");
}
if (configExists(configSet)) {
SolrZkClient zkClient = coreContainer.getZkController().getZkClient();
try {
zkClient.zkTransfer(getConfigSetZkPath(mutableId), true, getConfigSetZkPath(configSet), true, true);
} catch (KeeperException | InterruptedException e) {
throw new IOException("Failed to copy config set: " + mutableId, SolrZkClient.checkInterrupted(e));
}
} else {
copyConfig(mutableId, configSet);
}
boolean reloadCollections = req.getParams().getBool(RELOAD_COLLECTIONS_PARAM, false);
if (reloadCollections) {
log.debug("Reloading collections after update to configSet: {}", configSet);
List<String> collectionsForConfig = configSetHelper.listCollectionsForConfig(configSet);
CloudSolrClient csc = cloudClient();
for (String next : collectionsForConfig) {
CollectionAdminRequest.reloadCollection(next).processAsync(csc);
}
}
// create new collection
Map<Object, Throwable> errorsDuringIndexing = null;
if (!isEmpty(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)) {
List<SolrInputDocument> docs = configSetHelper.getStoredSampleDocs(configSet);
if (!docs.isEmpty()) {
ManagedIndexSchema schema = loadLatestSchema(mutableId);
errorsDuringIndexing = indexSampleDocs(schema.getUniqueKeyField().getName(), docs, newCollection, true);
}
}
}
if (req.getParams().getBool(CLEANUP_TEMP_PARAM, true)) {
cleanupTemp(configSet);
}
settings.setDisabled(req.getParams().getBool(DISABLE_DESIGNER_PARAM, false));
settingsDAO.persistIfChanged(configSet, settings);
Map<String, Object> response = new HashMap<>();
response.put(CONFIG_SET_PARAM, configSet);
response.put(SCHEMA_VERSION_PARAM, configSetHelper.getCurrentSchemaVersion(configSet));
if (!isEmpty(newCollection)) {
response.put(NEW_COLLECTION_PARAM, newCollection);
}
addErrorToResponse(newCollection, null, errorsDuringIndexing, response, null);
rsp.getValues().addAll(response);
}
@EndPoint(method = POST, path = "/schema-designer/analyze", permission = CONFIG_EDIT_PERM)
@SuppressWarnings("unchecked")
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);
// don't let the user edit the _default configSet with the designer (for now)
if (DEFAULT_CONFIGSET_NAME.equals(configSet)) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
"'" + DEFAULT_CONFIGSET_NAME + "' is a reserved configSet name! Please choose a different name.");
}
validateNewConfigSetName(configSet);
// Get the sample documents to analyze, preferring those in the request but falling back to previously stored
SampleDocuments sampleDocuments = loadSampleDocuments(req, 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);
String mutableId = getMutableId(configSet);
// holds additional settings needed by the designer to maintain state
SchemaDesignerSettings settings = getMutableSchemaForConfigSet(configSet, schemaVersion, copyFrom);
ManagedIndexSchema schema = settings.getSchema();
String uniqueKeyFieldParam = req.getParams().get(UNIQUE_KEY_FIELD_PARAM);
if (!isEmpty(uniqueKeyFieldParam)) {
String uniqueKeyField = schema.getUniqueKeyField() != null ? schema.getUniqueKeyField().getName() : null;
if (!uniqueKeyFieldParam.equals(uniqueKeyField)) {
// The Schema API doesn't support changing the ID field so would have to use XML directly
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Changing the unique key field not supported yet!");
}
}
boolean langsUpdated = false;
String[] languages = req.getParams().getParams(LANGUAGES_PARAM);
List<String> langs;
if (languages != null) {
langs = languages.length == 0 || (languages.length == 1 && "*".equals(languages[0])) ? Collections.emptyList() : Arrays.asList(languages);
if (!langs.equals(settings.getLanguages())) {
settings.setLanguages(langs);
langsUpdated = true;
}
} else {
// nothing from client, go with what's stored in the settings ...
langs = settings.getLanguages();
}
boolean dynamicUpdated = false;
Boolean enableDynamicFields = req.getParams().getBool(ENABLE_DYNAMIC_FIELDS_PARAM);
if (enableDynamicFields != null && enableDynamicFields != settings.dynamicFieldsEnabled()) {
settings.setDynamicFieldsEnabled(enableDynamicFields);
dynamicUpdated = true;
}
if (langsUpdated) {
schema = configSetHelper.syncLanguageSpecificObjectsAndFiles(mutableId, schema, langs, settings.dynamicFieldsEnabled(), copyFrom);
}
if (dynamicUpdated) {
if (!enableDynamicFields) {
schema = configSetHelper.removeDynamicFields(schema);
} else {
schema = configSetHelper.restoreDynamicFields(schema, langs, copyFrom);
}
}
List<SolrInputDocument> docs = sampleDocuments.parsed;
if (!docs.isEmpty()) {
if (sampleDocLoader.ensureUniqueKey(schema.getUniqueKeyField(), docs)) {
configSetHelper.storeSampleDocs(configSet, docs);
}
schema = analyzeInputDocs(schemaSuggester.transposeDocs(docs), schema, langs);
}
// 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);
}
// make sure the temp collection for this analysis exists
if (!zkStateReader().getClusterState().hasCollection(mutableId)) {
configSetHelper.createCollection(mutableId, mutableId);
indexedVersion.remove(mutableId);
} else {
// already created in the prep step ... reload it to pull in the updated schema
CollectionAdminRequest.reloadCollection(mutableId).process(cloudClient());
}
// nested docs
Boolean enableNestedDocs = req.getParams().getBool(ENABLE_NESTED_DOCS_PARAM);
if (enableNestedDocs != null && enableNestedDocs != settings.nestedDocsEnabled()) {
settings.setNestedDocsEnabled(enableNestedDocs);
configSetHelper.toggleNestedDocsFields(schema, enableNestedDocs);
}
// index the sample docs using the suggested schema
String[] analysisErrorHolder = new String[1];
Map<Object, Throwable> errorsDuringIndexing = null;
if (!docs.isEmpty()) {
String idField = schema.getUniqueKeyField().getName();
errorsDuringIndexing =
indexSampleDocsWithRebuildOnAnalysisError(idField, docs, mutableId, false, analysisErrorHolder);
}
if (settingsDAO.persistIfChanged(mutableId, settings)) {
CollectionAdminRequest.reloadCollection(mutableId).process(cloudClient());
}
Map<String, Object> response = buildResponse(configSet, loadLatestSchema(mutableId), settings, docs);
response.put("sampleSource", sampleDocuments.getSource());
if (analysisErrorHolder[0] != null) {
response.put(ANALYSIS_ERROR, analysisErrorHolder[0]);
}
addErrorToResponse(mutableId, null, errorsDuringIndexing, response, null);
rsp.getValues().addAll(response);
}
@EndPoint(method = GET, path = "/schema-designer/query", permission = CONFIG_READ_PERM)
@SuppressWarnings("unchecked")
public void query(SolrQueryRequest req, SolrQueryResponse rsp) throws IOException, SolrServerException {
final String configSet = getRequiredParam(CONFIG_SET_PARAM, req);
String mutableId = getMutableId(configSet);
if (!configExists(mutableId)) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
mutableId + " configSet not found! Are you sure " + configSet + " was being edited by the schema designer?");
}
if (!zkStateReader().getClusterState().hasCollection(mutableId)) {
indexedVersion.remove(mutableId);
configSetHelper.createCollection(mutableId, mutableId);
}
// only re-index if current state of test collection is not up-to-date
int currentVersion = configSetHelper.getCurrentSchemaVersion(mutableId);
Integer version = indexedVersion.get(mutableId);
Map<Object, Throwable> errorsDuringIndexing = null;
if (version == null || version != currentVersion) {
log.debug("Schema for collection {} is stale ({} != {}), need to re-index sample docs", mutableId, version, currentVersion);
List<SolrInputDocument> docs = configSetHelper.getStoredSampleDocs(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);
indexedVersion.put(mutableId, currentVersion);
}
if (errorsDuringIndexing != null) {
Map<String, Object> response = new HashMap<>();
rsp.setException(new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Failed to re-index sample documents after schema updated."));
response.put(ERROR_DETAILS, errorsDuringIndexing);
rsp.getValues().addAll(response);
return;
}
// execute the user's query against the temp collection
QueryResponse qr = cloudClient().query(mutableId, req.getParams());
rsp.getValues().addAll(qr.getResponse());
}
/**
* 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)
@SuppressWarnings("unchecked")
public void getSchemaDiff(SolrQueryRequest req, SolrQueryResponse rsp) throws IOException {
final String configSet = getRequiredParam(CONFIG_SET_PARAM, req);
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<String, Object> response = new HashMap<>();
response.put("diff", ManagedSchemaDiff.diff(loadLatestSchema(sourceSchema), settings.getSchema()));
response.put("diff-source", sourceSchema);
addSettingsToResponse(settings, response);
rsp.getValues().addAll(response);
}
protected SampleDocuments loadSampleDocuments(SolrQueryRequest req, String configSet) throws IOException {
List<SolrInputDocument> docs = null;
ContentStream stream = extractSingleContentStream(req, false);
SampleDocuments sampleDocs = null;
if (stream != null && stream.getContentType() != null) {
sampleDocs = sampleDocLoader.parseDocsFromStream(req.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 these to the existing set
List<SolrInputDocument> stored = configSetHelper.getStoredSampleDocs(configSet);
if (!stored.isEmpty()) {
// keep the docs in the request as newest
ManagedIndexSchema latestSchema = loadLatestSchema(getMutableId(configSet));
docs = sampleDocs.appendDocs(latestSchema.getUniqueKeyField().getName(), stored, MAX_SAMPLE_DOCS);
}
// store in the blob store so we always have access to these docs
configSetHelper.storeSampleDocs(configSet, docs);
}
}
if (docs == null || docs.isEmpty()) {
// no sample docs in the request ... find in blob store (or fail if no docs previously stored)
docs = configSetHelper.getStoredSampleDocs(configSet);
// no docs? but if this schema has already been published, it's OK, we can skip the docs part
if (docs.isEmpty() && !configExists(configSet)) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
"No sample documents provided for analyzing schema! Only CSV/TSV, XML, JSON, and JSON lines supported.");
}
sampleDocs = new SampleDocuments(docs, "", "blob");
}
return sampleDocs;
}
protected ManagedIndexSchema analyzeInputDocs(final Map<String, List<Object>> docs, ManagedIndexSchema schema, List<String> langs) {
// collect the fields to add ... adding all fields at once is faster than one-at-a-time
List<SchemaField> fieldsToAdd = new LinkedList<>();
for (String field : docs.keySet()) {
List<Object> sampleValues = docs.getOrDefault(field, Collections.emptyList());
// Collapse all whitespace in fields to a single underscore
String normalizedField = field.trim().replaceAll("\\s+", "_");
if (schema.hasExplicitField(normalizedField)) {
SchemaField existing = schema.getField(normalizedField);
schema = schemaSuggester.adaptExistingFieldToData(existing, sampleValues, schema);
continue;
}
Optional<SchemaField> maybeSchemaField = schemaSuggester.suggestField(normalizedField, sampleValues, schema, langs);
maybeSchemaField.ifPresent(fieldsToAdd::add);
}
if (!fieldsToAdd.isEmpty()) {
schema = (ManagedIndexSchema) schema.addFields(fieldsToAdd);
}
return schema;
}
protected String getManagedSchemaZkPath(final String configSet) {
return getConfigSetZkPath(configSet, DEFAULT_MANAGED_SCHEMA_RESOURCE_NAME);
}
protected 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
final String mutableId = getMutableId(configSet);
ManagedIndexSchema schema;
// create new from the built-in "_default" configSet
int publishedVersion = -1;
boolean isNew = false;
if (!configExists(mutableId)) {
// are they opening a temp of an existing?
if (configExists(configSet)) {
if (settingsDAO.isDesignerDisabled(configSet)) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
"Schema '" + configSet + "' is locked for edits by the schema designer!");
}
publishedVersion = configSetHelper.getCurrentSchemaVersion(configSet);
// ignore the copyFrom as we're making a mutable temp copy of an already published configSet
copyConfig(configSet, mutableId);
copyFrom = null;
} else {
copyConfig(copyFrom, mutableId);
}
isNew = true;
}
SolrConfig solrConfig = configSetHelper.loadSolrConfig(mutableId);
schema = configSetHelper.loadLatestSchema(solrConfig);
if (!isNew) {
// schema is not new, so the provided version must match, otherwise, we're trying to edit dirty data
configSetHelper.checkSchemaVersion(mutableId, schemaVersion, schema.getSchemaZkVersion());
}
SchemaDesignerSettings settings = settingsDAO.getSettings(solrConfig);
if (isNew) {
if (!configSet.equals(copyFrom)) {
settings.setDisabled(false);
}
// remember where this new one came from
if (copyFrom != null) {
settings.setCopyFrom(copyFrom);
}
if (publishedVersion != -1) {
// keep track of the version of the configSet the mutable is derived from in case another user
// changes the derived from schema before we publish the mutable on top of it
settings.setPublishedVersion(publishedVersion);
}
if (!settings.nestedDocsEnabled()) {
schema = configSetHelper.deleteNestedDocsFieldsIfNeeded(schema, false);
}
if (!settings.dynamicFieldsEnabled()) {
schema = configSetHelper.removeDynamicFields(schema);
}
schema.persistManagedSchema(false);
}
settings.setSchema(schema);
return settings;
}
ManagedIndexSchema loadLatestSchema(String configSet) {
return configSetHelper.loadLatestSchema(configSet);
}
protected ContentStream extractSingleContentStream(final SolrQueryRequest req, boolean required) {
Iterable<ContentStream> streams = req.getContentStreams();
Iterator<ContentStream> iter = streams != null ? streams.iterator() : null;
ContentStream stream = iter != null && iter.hasNext() ? iter.next() : null;
if (required && stream == null)
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "No JSON content found in the request body!");
return stream;
}
protected CloudSolrClient cloudClient() {
return coreContainer.getSolrClientCache().getCloudSolrClient(coreContainer.getZkController().getZkServerAddress());
}
protected ZkStateReader zkStateReader() {
return coreContainer.getZkController().getZkStateReader();
}
protected Map<Object, Throwable> indexSampleDocsWithRebuildOnAnalysisError(String idField,
List<SolrInputDocument> docs,
final String collectionName,
boolean asBatch,
String[] analysisErrorHolder) throws IOException, SolrServerException {
Map<Object, Throwable> results;
try {
results = indexSampleDocs(idField, docs, collectionName, asBatch);
} catch (IllegalArgumentException analysisExc) {
String errMsg = SolrException.getRootCause(analysisExc).getMessage();
if (analysisErrorHolder != null) {
analysisErrorHolder[0] = errMsg;
}
log.warn("Rebuilding temp collection {} after low-level Lucene indexing issue: {}", collectionName, errMsg);
configSetHelper.reloadTempCollection(collectionName, true);
results = indexSampleDocs(idField, docs, collectionName, asBatch);
log.info("Re-index sample docs into {} after rebuild due to {} succeeded; results: {}", collectionName, errMsg, results);
}
return results;
}
protected Map<Object, Throwable> indexSampleDocs(String idField,
List<SolrInputDocument> docs,
final String collectionName,
boolean asBatch) throws IOException, SolrServerException {
Map<Object, Throwable> errorsDuringIndexing = new LinkedHashMap<>();
RTimer timer = new RTimer();
// load sample docs from blob store
CloudSolrClient cloudSolrClient = cloudClient();
cloudSolrClient.deleteByQuery(collectionName, "*:*", 1);
cloudSolrClient.optimize(collectionName, true, true, 1);
final int commitWithin = 100;
final int numDocs = docs.size();
int numAdded = 0;
if (asBatch) {
cloudSolrClient.add(collectionName, docs, commitWithin);
numAdded = docs.size();
} else {
int maxErrors = Math.min(100, Math.round(numDocs / 2f));
for (SolrInputDocument next : docs) {
try {
cloudSolrClient.add(collectionName, next, commitWithin);
++numAdded;
} catch (Exception exc) {
Throwable rootCause = SolrException.getRootCause(exc);
if (String.valueOf(rootCause.getMessage()).contains("possible analysis error")) {
throw new IllegalArgumentException(rootCause);
} else {
Object docId = next.getFieldValue(idField);
if (docId == null) {
throw exc;
}
errorsDuringIndexing.put(docId, rootCause);
// break if there are a lot of errors in indexing as something is very wrong if so ...
if (errorsDuringIndexing.size() > 20 && errorsDuringIndexing.size() >= maxErrors) {
break;
}
}
}
}
}
cloudSolrClient.commit(collectionName, true, true, true);
if (!errorsDuringIndexing.isEmpty()) {
return errorsDuringIndexing;
}
long numFound = waitToSeeSampleDocs(collectionName, numAdded);
double tookMs = timer.getTime();
log.debug("Indexed {} docs into collection {}, took {} ms", numFound, collectionName, tookMs);
return !errorsDuringIndexing.isEmpty() ? errorsDuringIndexing : null;
}
protected long waitToSeeSampleDocs(String collectionName, long numAdded) throws IOException, SolrServerException {
CloudSolrClient cloudSolrClient = cloudClient();
SolrQuery query = new SolrQuery("*:*");
query.setRows(0);
QueryResponse queryResponse = cloudSolrClient.query(collectionName, query);
long numFound = queryResponse.getResults().getNumFound();
if (numFound < numAdded) {
// wait up to 5 seconds for this to occur
final long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
do {
cloudSolrClient.commit(collectionName, true, true, true);
queryResponse = cloudSolrClient.query(collectionName, query);
numFound = queryResponse.getResults().getNumFound();
if (numFound >= numAdded) {
break;
}
try {
Thread.sleep(200); // little pause to avoid flooding the server with requests in this loop
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
} while (System.nanoTime() < deadline);
if (numFound < numAdded) {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
"Failed to index " + numAdded + " sample docs into temp collection: " + collectionName);
}
}
return numFound;
}
protected Map<String, Object> buildResponse(String configSet,
final ManagedIndexSchema schema,
SchemaDesignerSettings settings,
List<SolrInputDocument> docs) throws IOException {
String mutableId = getMutableId(configSet);
int currentVersion = configSetHelper.getCurrentSchemaVersion(mutableId);
indexedVersion.put(mutableId, currentVersion);
// response is a map of data structures to support the schema designer
Map<String, Object> response = new HashMap<>();
DocCollection coll = zkStateReader().getCollection(mutableId);
if (coll.getActiveSlicesArr().length > 0) {
String coreName = coll.getActiveSlicesArr()[0].getLeader().getCoreName();
response.put("core", coreName);
}
response.put(UNIQUE_KEY_FIELD_PARAM, schema.getUniqueKeyField().getName());
response.put(CONFIG_SET_PARAM, 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));
// 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", schema.getFields().values().stream()
.map(f -> fieldToMap(f, schema))
.sorted(Comparator.comparing(map -> ((String) map.get("name"))))
.collect(Collectors.toList()));
if (settings == null) {
settings = settingsDAO.getSettings(mutableId);
}
addSettingsToResponse(settings, response);
response.put("dynamicFields", Arrays.stream(schema.getDynamicFieldPrototypes())
.map(e -> e.getNamedPropertyValues(true))
.sorted(Comparator.comparing(map -> ((String) map.get("name"))))
.collect(Collectors.toList()));
response.put("fieldTypes", schema.getFieldTypes().values().stream()
.map(fieldType -> fieldType.getNamedPropertyValues(true))
.sorted(Comparator.comparing(map -> ((String) map.get("name"))))
.collect(Collectors.toList()));
// files
SolrZkClient zkClient = zkStateReader().getZkClient();
String configPathInZk = getConfigSetZkPath(mutableId);
final Set<String> files = new HashSet<>();
try {
ZkMaintenanceUtils.traverseZkTree(zkClient, configPathInZk, ZkMaintenanceUtils.VISIT_ORDER.VISIT_POST, files::add);
} catch (KeeperException | InterruptedException e) {
throw new IOException("Failed to traverse files under: " + configPathInZk, SolrZkClient.checkInterrupted(e));
}
files.remove(configPathInZk);
final String prefix = configPathInZk + "/";
final int prefixLen = prefix.length();
Set<String> stripPrefix = files.stream().map(f -> f.startsWith(prefix) ? f.substring(prefixLen) : f).collect(Collectors.toSet());
stripPrefix.remove(DEFAULT_MANAGED_SCHEMA_RESOURCE_NAME);
stripPrefix.remove("lang");
stripPrefix.remove(CONFIGOVERLAY_JSON); // treat this file as private
List<String> sortedFiles = new ArrayList<>(stripPrefix);
Collections.sort(sortedFiles);
response.put("files", sortedFiles);
// info about the sample docs
if (docs != null) {
final String uniqueKeyField = schema.getUniqueKeyField().getName();
response.put("docIds", docs.stream()
.map(d -> (String) d.getFieldValue(uniqueKeyField))
.filter(Objects::nonNull)
.limit(100)
.collect(Collectors.toList()));
}
response.put("numDocs", docs != null ? docs.size() : -1);
return response;
}
protected void addErrorToResponse(String collection,
SolrException solrExc,
Map<Object, Throwable> errorsDuringIndexing,
Map<String, Object> response,
String updateError) {
if (solrExc == null && (errorsDuringIndexing == null || errorsDuringIndexing.isEmpty())) {
return; // no errors
}
if (updateError != null) {
response.put(UPDATE_ERROR, updateError);
}
if (solrExc != null) {
response.put("updateErrorCode", solrExc.code());
response.putIfAbsent(UPDATE_ERROR, solrExc.getMessage());
}
response.putIfAbsent(UPDATE_ERROR, "Index sample documents into " + collection + " failed!");
response.putIfAbsent("updateErrorCode", 400);
if (errorsDuringIndexing != null) {
response.put(ERROR_DETAILS, errorsDuringIndexing);
}
}
protected SimpleOrderedMap<Object> fieldToMap(SchemaField f, ManagedIndexSchema schema) {
SimpleOrderedMap<Object> map = f.getNamedPropertyValues(true);
// add the copy field destination field names
List<String> copyFieldNames =
schema.getCopyFieldsList((String) map.get("name")).stream().map(c -> c.getDestination().getName()).collect(Collectors.toList());
map.add("copyDest", String.join(",", copyFieldNames));
return map;
}
@SuppressWarnings("unchecked")
protected Map<String, Object> readJsonFromRequest(SolrQueryRequest req) throws IOException {
ContentStream stream = extractSingleContentStream(req, true);
String contentType = stream.getContentType();
if (isEmpty(contentType) || !contentType.toLowerCase(Locale.ROOT).contains(JSON_MIME)) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Expected JSON in request!");
}
final Object json;
try (Reader reader = stream.getReader()) {
json = ObjectBuilder.getVal(new JSONParser(reader));
}
return (Map<String, Object>) json;
}
protected void addSettingsToResponse(SchemaDesignerSettings settings, final Map<String, Object> 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<Integer> publishedVersion = settings.getPublishedVersion();
publishedVersion.ifPresent(version -> response.put(PUBLISHED_VERSION, version));
String copyFrom = settings.getCopyFrom();
if (copyFrom != null) {
response.put(COPY_FROM_PARAM, copyFrom);
}
}
protected String checkMutable(String configSet, SolrQueryRequest req) throws IOException {
// an apply just copies over the temp config to the "live" location
String mutableId = getMutableId(configSet);
if (!configExists(mutableId)) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
mutableId + " configSet not found! Are you sure " + configSet + " was being edited by the schema designer?");
}
// check the versions agree
configSetHelper.checkSchemaVersion(mutableId, requireSchemaVersionFromClient(req), -1);
return mutableId;
}
protected int requireSchemaVersionFromClient(SolrQueryRequest req) {
final int schemaVersion = req.getParams().getInt(SCHEMA_VERSION_PARAM, -1);
if (schemaVersion == -1) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
SCHEMA_VERSION_PARAM + " is a required parameter for the " + req.getPath() + " endpoint");
}
return schemaVersion;
}
protected String getRequiredParam(final String param, final SolrQueryRequest req) {
final String paramValue = req.getParams().get(param);
if (isEmpty(paramValue)) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
param + " is a required parameter for the " + req.getPath() + " endpoint!");
}
return paramValue;
}
protected void cleanupTemp(String configSet) throws IOException, SolrServerException {
String mutableId = getMutableId(configSet);
indexedVersion.remove(mutableId);
CollectionAdminRequest.deleteCollection(mutableId).process(cloudClient());
configSetHelper.deleteStoredSampleDocs(configSet);
deleteConfig(mutableId);
}
private boolean configExists(String configSet) throws IOException {
return configSetHelper.checkConfigExists(configSet);
}
private void deleteConfig(String configSet) throws IOException {
configSetHelper.deleteConfig(configSet);
}
private void copyConfig(String from, String to) throws IOException {
configSetHelper.copyConfig(from, to);
}
private void validateNewConfigSetName(String configSet) {
if (configSet.length() > 50) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Schema name should be 50 characters or less");
}
if (configSet.contains(" ") || configSet.contains("/")) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Schema name should not contain spaces or forward slash");
}
}
private boolean pathExistsInZk(final String zkPath) throws IOException {
SolrZkClient zkClient = zkStateReader().getZkClient();
try {
return zkClient.exists(zkPath, true);
} catch (KeeperException | InterruptedException e) {
throw new IOException("Failed to check if path exists: " + zkPath, SolrZkClient.checkInterrupted(e));
}
}
private static class InMemoryResourceLoader extends SolrResourceLoader {
String resource;
byte[] data;
ZkSolrResourceLoader delegate;
public InMemoryResourceLoader(CoreContainer cc, String configSet, String resource, byte[] data) {
super(cc.getResourceLoader().getInstancePath(), cc.getResourceLoader().getClassLoader());
final SolrResourceLoader resourceLoader = cc.getResourceLoader();
this.delegate = new ZkSolrResourceLoader(resourceLoader.getInstancePath(), configSet, resourceLoader.getClassLoader(), new Properties(), cc.getZkController());
this.resource = resource;
this.data = data;
}
@Override
public InputStream openResource(String res) throws IOException {
return (this.resource.equals(res)) ? new ByteArrayInputStream(data) : delegate.openResource(res);
}
}
}