SOLR-6476

git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1628734 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Noble Paul 2014-10-01 15:20:48 +00:00
parent e3329e08a6
commit 041ea20853
13 changed files with 1019 additions and 167 deletions

View File

@ -145,6 +145,8 @@ New Features
* SOLR-6565: SolrRequest support for query params (Gregory Chanan)
* SOLR-6476: Create a bulk mode for schema API (Noble Paul, Steve Rowe)
Bug Fixes
----------------------

View File

@ -70,7 +70,7 @@ abstract class BaseFieldTypeResource extends BaseSolrResource {
while (!success) {
try {
synchronized (oldSchema.getSchemaUpdateLock()) {
newSchema = oldSchema.addFieldTypes(newFieldTypes);
newSchema = oldSchema.addFieldTypes(newFieldTypes, true);
getSolrCore().setLatestSchema(newSchema);
success = true;
}

View File

@ -173,7 +173,7 @@ public class CopyFieldCollectionResource extends BaseFieldResource implements GE
while (!success) {
try {
synchronized (oldSchema.getSchemaUpdateLock()) {
newSchema = oldSchema.addCopyFields(fieldsToCopy);
newSchema = oldSchema.addCopyFields(fieldsToCopy,true);
if (null != newSchema) {
getSolrCore().setLatestSchema(newSchema);
success = true;

View File

@ -178,7 +178,7 @@ public class DynamicFieldCollectionResource extends BaseFieldResource implements
}
firstAttempt = false;
synchronized (oldSchema.getSchemaUpdateLock()) {
newSchema = oldSchema.addDynamicFields(newDynamicFields, copyFields);
newSchema = oldSchema.addDynamicFields(newDynamicFields, copyFields, true);
if (null != newSchema) {
getSolrCore().setLatestSchema(newSchema);
success = true;

View File

@ -32,9 +32,14 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.UnsupportedEncodingException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
/**
* This class responds to requests at /solr/(corename)/schema/dynamicfields/(pattern)
@ -142,12 +147,12 @@ public class DynamicFieldResource extends BaseFieldResource implements GETable,
} else {
ManagedIndexSchema oldSchema = (ManagedIndexSchema)getSchema();
Object copies = map.get(IndexSchema.COPY_FIELDS);
List<String> copyFieldNames = null;
Collection<String> copyFieldNames = null;
if (copies != null) {
if (copies instanceof List) {
copyFieldNames = (List<String>)copies;
} else if (copies instanceof String) {
copyFieldNames = Collections.singletonList(copies.toString());
copyFieldNames = singletonList(copies.toString());
} else {
String message = "Invalid '" + IndexSchema.COPY_FIELDS + "' type.";
log.error(message);
@ -163,7 +168,7 @@ public class DynamicFieldResource extends BaseFieldResource implements GETable,
try {
SchemaField newDynamicField = oldSchema.newDynamicField(fieldNamePattern, fieldType, map);
synchronized (oldSchema.getSchemaUpdateLock()) {
newSchema = oldSchema.addDynamicField(newDynamicField, copyFieldNames);
newSchema = oldSchema.addDynamicFields(singletonList(newDynamicField), singletonMap(newDynamicField.getName(), copyFieldNames), true);
if (null != newSchema) {
getSolrCore().setLatestSchema(newSchema);
success = true;

View File

@ -199,7 +199,7 @@ public class FieldCollectionResource extends BaseFieldResource implements GETabl
}
firstAttempt = false;
synchronized (oldSchema.getSchemaUpdateLock()) {
newSchema = oldSchema.addFields(newFields, copyFields);
newSchema = oldSchema.addFields(newFields, copyFields, true);
if (null != newSchema) {
getSolrCore().setLatestSchema(newSchema);
success = true;

View File

@ -16,18 +16,26 @@ package org.apache.solr.rest.schema;
* limitations under the License.
*/
import org.apache.solr.request.SolrRequestInfo;
import org.apache.solr.rest.BaseSolrResource;
import org.apache.solr.rest.GETable;
import org.apache.solr.rest.POSTable;
import org.apache.solr.schema.IndexSchema;
import org.apache.solr.schema.SchemaManager;
import org.restlet.representation.Representation;
import org.restlet.resource.ResourceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.StringReader;
import java.util.Collections;
import java.util.List;
/**
* This class responds to requests at /solr/(corename)/schema
*/
public class SchemaResource extends BaseSolrResource implements GETable {
public class SchemaResource extends BaseSolrResource implements GETable,POSTable {
private static final Logger log = LoggerFactory.getLogger(SchemaResource.class);
public SchemaResource() {
@ -50,4 +58,25 @@ public class SchemaResource extends BaseSolrResource implements GETable {
return new SolrOutputRepresentation();
}
@Override
public Representation post(Representation representation) {
SolrRequestInfo requestInfo = SolrRequestInfo.getRequestInfo();
List<String> errs = null;
try {
String text = representation.getText();
errs = new SchemaManager(requestInfo.getReq()).performOperations(new StringReader(text));
} catch (IOException e) {
requestInfo.getRsp().add("errors", Collections.singletonList("Error reading input String " + e.getMessage()));
requestInfo.getRsp().setException(e);
}
if(!errs.isEmpty()){
requestInfo.getRsp().add("errors", errs);
}
return new BaseSolrResource.SolrOutputRepresentation();
}
}

View File

@ -74,6 +74,9 @@ import java.util.TreeMap;
import java.util.TreeSet;
import java.util.regex.Pattern;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
/**
* <code>IndexSchema</code> contains information about the valid fields in an index
* and the types of those fields.
@ -1473,23 +1476,26 @@ public class IndexSchema {
}
/**
* Copies this schema, adds the given field to the copy, then persists the
* new schema. Requires synchronizing on the object returned by
* Copies this schema, adds the given field to the copy
* Requires synchronizing on the object returned by
* {@link #getSchemaUpdateLock()}.
*
* @param newField the SchemaField to add
* @param persist to persist the schema or not or not
* @return a new IndexSchema based on this schema with newField added
* @see #newField(String, String, Map)
*/
public IndexSchema addField(SchemaField newField, boolean persist) {
return addFields(Collections.singletonList(newField),Collections.EMPTY_MAP,persist );
}
public IndexSchema addField(SchemaField newField) {
String msg = "This IndexSchema is not mutable.";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
return addField(newField, true);
}
/**
* Copies this schema, adds the given field to the copy, then persists the
* new schema. Requires synchronizing on the object returned by
* Copies this schema, adds the given field to the copy
* Requires synchronizing on the object returned by
* {@link #getSchemaUpdateLock()}.
*
* @param newField the SchemaField to add
@ -1498,14 +1504,12 @@ public class IndexSchema {
* @see #newField(String, String, Map)
*/
public IndexSchema addField(SchemaField newField, Collection<String> copyFieldNames) {
String msg = "This IndexSchema is not mutable.";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
return addFields(singletonList(newField), singletonMap(newField.getName(), copyFieldNames), true);
}
/**
* Copies this schema, adds the given fields to the copy, then persists the
* new schema. Requires synchronizing on the object returned by
* Copies this schema, adds the given fields to the copy.
* Requires synchronizing on the object returned by
* {@link #getSchemaUpdateLock()}.
*
* @param newFields the SchemaFields to add
@ -1513,99 +1517,57 @@ public class IndexSchema {
* @see #newField(String, String, Map)
*/
public IndexSchema addFields(Collection<SchemaField> newFields) {
String msg = "This IndexSchema is not mutable.";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
return addFields(newFields, Collections.<String, Collection<String>>emptyMap(), true);
}
/**
* Copies this schema, adds the given fields to the copy, then persists the
* new schema. Requires synchronizing on the object returned by
* Copies this schema, adds the given fields to the copy
* Requires synchronizing on the object returned by
* {@link #getSchemaUpdateLock()}.
*
* @param newFields the SchemaFields to add
* @param copyFieldNames 0 or more names of targets to copy this field to. The target fields must already exist.
* @param persist Persist the schema or not
* @return a new IndexSchema based on this schema with newFields added
* @see #newField(String, String, Map)
*/
public IndexSchema addFields(Collection<SchemaField> newFields, Map<String, Collection<String>> copyFieldNames) {
public IndexSchema addFields(Collection<SchemaField> newFields, Map<String, Collection<String>> copyFieldNames, boolean persist) {
String msg = "This IndexSchema is not mutable.";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
/**
* Copies this schema, adds the given dynamic field to the copy, then persists the
* new schema. Requires synchronizing on the object returned by
* {@link #getSchemaUpdateLock()}.
*
* @param newDynamicField the SchemaField to add
* @return a new IndexSchema based on this schema with newField added
* @see #newDynamicField(String, String, Map)
*/
public IndexSchema addDynamicField(SchemaField newDynamicField) {
String msg = "This IndexSchema is not mutable.";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
/**
* Copies this schema, adds the given dynamic field to the copy, then persists the
* new schema. Requires synchronizing on the object returned by
* {@link #getSchemaUpdateLock()}.
*
* @param newDynamicField the SchemaField to add
* @param copyFieldNames 0 or more names of targets to copy this field to. The targets must already exist.
* @return a new IndexSchema based on this schema with newDynamicField added
* @see #newDynamicField(String, String, Map)
*/
public IndexSchema addDynamicField(SchemaField newDynamicField, Collection<String> copyFieldNames) {
String msg = "This IndexSchema is not mutable.";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
/**
* Copies this schema, adds the given dynamic fields to the copy, then persists the
* new schema. Requires synchronizing on the object returned by
* {@link #getSchemaUpdateLock()}.
*
* @param newDynamicFields the SchemaFields to add
* @return a new IndexSchema based on this schema with newDynamicFields added
* @see #newDynamicField(String, String, Map)
*/
public IndexSchema addDynamicFields(Collection<SchemaField> newDynamicFields) {
String msg = "This IndexSchema is not mutable.";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
/**
* Copies this schema, adds the given dynamic fields to the copy, then persists the
* new schema. Requires synchronizing on the object returned by
* Copies this schema, adds the given dynamic fields to the copy,
* Requires synchronizing on the object returned by
* {@link #getSchemaUpdateLock()}.
*
* @param newDynamicFields the SchemaFields to add
* @param copyFieldNames 0 or more names of targets to copy this field to. The target fields must already exist.
* @param persist to persist the schema or not or not
* @return a new IndexSchema based on this schema with newDynamicFields added
* @see #newDynamicField(String, String, Map)
*/
public IndexSchema addDynamicFields
(Collection<SchemaField> newDynamicFields, Map<String, Collection<String>> copyFieldNames) {
(Collection<SchemaField> newDynamicFields,
Map<String, Collection<String>> copyFieldNames,
boolean persist) {
String msg = "This IndexSchema is not mutable.";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
/**
* Copies this schema and adds the new copy fields to the copy, then
* persists the new schema. Requires synchronizing on the object returned by
* Copies this schema and adds the new copy fields to the copy
* Requires synchronizing on the object returned by
* {@link #getSchemaUpdateLock()}.
*
* @param copyFields Key is the name of the source field name, value is a collection of target field names. Fields must exist.
* @param persist to persist the schema or not or not
* @return The new Schema with the copy fields added
*/
public IndexSchema addCopyFields(Map<String, Collection<String>> copyFields){
public IndexSchema addCopyFields(Map<String, Collection<String>> copyFields, boolean persist){
String msg = "This IndexSchema is not mutable.";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
@ -1660,30 +1622,16 @@ public class IndexSchema {
}
/**
* Copies this schema, adds the given field type to the copy, then persists the
* new schema. Requires synchronizing on the object returned by
* {@link #getSchemaUpdateLock()}.
*
* @param fieldType the FieldType to add
* @return a new IndexSchema based on this schema with the new FieldType added
* @see #newFieldType(String, String, Map)
*/
public IndexSchema addFieldType(FieldType fieldType) {
String msg = "This IndexSchema is not mutable.";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
}
/**
* Copies this schema, adds the given field type to the copy, then persists the
* new schema. Requires synchronizing on the object returned by
* Copies this schema, adds the given field type to the copy,
* Requires synchronizing on the object returned by
* {@link #getSchemaUpdateLock()}.
*
* @param fieldTypeList a list of FieldTypes to add
* @param persist to persist the schema or not or not
* @return a new IndexSchema based on this schema with the new types added
* @see #newFieldType(String, String, Map)
*/
public IndexSchema addFieldTypes(List<FieldType> fieldTypeList) {
public IndexSchema addFieldTypes(List<FieldType> fieldTypeList, boolean persist) {
String msg = "This IndexSchema is not mutable.";
log.error(msg);
throw new SolrException(ErrorCode.SERVER_ERROR, msg);
@ -1692,13 +1640,13 @@ public class IndexSchema {
/**
* Returns a FieldType if the given typeName does not already
* exist in this schema. The resulting FieldType can be used in a call
* to {@link #addFieldType(FieldType)}.
* to {@link #addFieldTypes(java.util.List, boolean)}.
*
* @param typeName the name of the type to add
* @param className the name of the FieldType class
* @param options the options to use when creating the FieldType
* @return The created FieldType
* @see #addFieldType(FieldType)
* @see #addFieldTypes(java.util.List, boolean)
*/
public FieldType newFieldType(String typeName, String className, Map<String,?> options) {
String msg = "This IndexSchema is not mutable.";

View File

@ -80,6 +80,9 @@ import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
/** Solr-managed schema - non-user-editable, but can be mutable via internal and external REST API requests. */
public final class ManagedIndexSchema extends IndexSchema {
@ -386,23 +389,11 @@ public final class ManagedIndexSchema extends IndexSchema {
}
}
@Override
public ManagedIndexSchema addField(SchemaField newField) {
return addFields(Arrays.asList(newField));
}
@Override
public ManagedIndexSchema addField(SchemaField newField, Collection<String> copyFieldNames) {
return addFields(Arrays.asList(newField), Collections.singletonMap(newField.getName(), copyFieldNames));
}
@Override
public ManagedIndexSchema addFields(Collection<SchemaField> newFields) {
return addFields(newFields, Collections.<String, Collection<String>>emptyMap());
}
@Override
public ManagedIndexSchema addFields(Collection<SchemaField> newFields, Map<String, Collection<String>> copyFieldNames) {
public ManagedIndexSchema addFields(Collection<SchemaField> newFields,
Map<String, Collection<String>> copyFieldNames,
boolean persist) {
ManagedIndexSchema newSchema = null;
if (isMutable) {
boolean success = false;
@ -439,6 +430,8 @@ public final class ManagedIndexSchema extends IndexSchema {
aware.inform(newSchema);
}
newSchema.refreshAnalyzers();
if(persist) {
success = newSchema.persistManagedSchema(false); // don't just create - update it if it already exists
if (success) {
log.debug("Added field(s): {}", newFields);
@ -446,6 +439,7 @@ public final class ManagedIndexSchema extends IndexSchema {
log.error("Failed to add field(s): {}", newFields);
newSchema = null;
}
}
} else {
String msg = "This ManagedIndexSchema is not mutable.";
log.error(msg);
@ -454,25 +448,10 @@ public final class ManagedIndexSchema extends IndexSchema {
return newSchema;
}
@Override
public IndexSchema addDynamicField(SchemaField newDynamicField) {
return addDynamicFields(Arrays.asList(newDynamicField));
}
@Override
public IndexSchema addDynamicField(SchemaField newDynamicField, Collection<String> copyFieldNames) {
return addDynamicFields(Arrays.asList(newDynamicField),
Collections.singletonMap(newDynamicField.getName(), copyFieldNames));
}
@Override
public ManagedIndexSchema addDynamicFields(Collection<SchemaField> newDynamicFields) {
return addDynamicFields(newDynamicFields, Collections.<String,Collection<String>>emptyMap());
}
@Override
public ManagedIndexSchema addDynamicFields(Collection<SchemaField> newDynamicFields,
Map<String,Collection<String>> copyFieldNames) {
Map<String,Collection<String>> copyFieldNames, boolean persist) {
ManagedIndexSchema newSchema = null;
if (isMutable) {
boolean success = false;
@ -503,12 +482,14 @@ public final class ManagedIndexSchema extends IndexSchema {
aware.inform(newSchema);
}
newSchema.refreshAnalyzers();
if(persist) {
success = newSchema.persistManagedSchema(false); // don't just create - update it if it already exists
if (success) {
log.debug("Added dynamic field(s): {}", newDynamicFields);
} else {
log.error("Failed to add dynamic field(s): {}", newDynamicFields);
}
}
} else {
String msg = "This ManagedIndexSchema is not mutable.";
log.error(msg);
@ -518,7 +499,7 @@ public final class ManagedIndexSchema extends IndexSchema {
}
@Override
public ManagedIndexSchema addCopyFields(Map<String, Collection<String>> copyFields) {
public ManagedIndexSchema addCopyFields(Map<String, Collection<String>> copyFields, boolean persist) {
ManagedIndexSchema newSchema = null;
if (isMutable) {
boolean success = false;
@ -536,6 +517,7 @@ public final class ManagedIndexSchema extends IndexSchema {
aware.inform(newSchema);
}
newSchema.refreshAnalyzers();
if(persist) {
success = newSchema.persistManagedSchema(false); // don't just create - update it if it already exists
if (success) {
log.debug("Added copy fields for {} sources", copyFields.size());
@ -543,14 +525,11 @@ public final class ManagedIndexSchema extends IndexSchema {
log.error("Failed to add copy fields for {} sources", copyFields.size());
}
}
}
return newSchema;
}
public ManagedIndexSchema addFieldType(FieldType fieldType) {
return addFieldTypes(Collections.singletonList(fieldType));
}
public ManagedIndexSchema addFieldTypes(List<FieldType> fieldTypeList) {
public ManagedIndexSchema addFieldTypes(List<FieldType> fieldTypeList, boolean persist) {
if (!isMutable) {
String msg = "This ManagedIndexSchema is not mutable.";
log.error(msg);
@ -612,6 +591,7 @@ public final class ManagedIndexSchema extends IndexSchema {
newSchema.refreshAnalyzers();
if (persist) {
boolean success = newSchema.persistManagedSchema(false);
if (success) {
if (log.isDebugEnabled()) {
@ -628,6 +608,7 @@ public final class ManagedIndexSchema extends IndexSchema {
throw new SolrException(ErrorCode.SERVER_ERROR,
"Failed to persist updated schema due to underlying storage issue; check log for more details!");
}
}
return newSchema;
}
@ -834,7 +815,7 @@ public final class ManagedIndexSchema extends IndexSchema {
* are copied; otherwise, they are not.
* @return A shallow copy of this schema
*/
private ManagedIndexSchema shallowCopy(boolean includeFieldDataStructures) {
ManagedIndexSchema shallowCopy(boolean includeFieldDataStructures) {
ManagedIndexSchema newSchema = null;
try {
newSchema = new ManagedIndexSchema

View File

@ -0,0 +1,380 @@
package org.apache.solr.schema;
/*
* 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.
*/
import org.apache.solr.cloud.ZkSolrResourceLoader;
import org.apache.solr.common.SolrException;
import org.apache.solr.core.CoreDescriptor;
import org.apache.solr.core.SolrCore;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.rest.BaseSolrResource;
import org.noggit.JSONParser;
import org.noggit.ObjectBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static java.util.Collections.EMPTY_LIST;
import static java.util.Collections.EMPTY_MAP;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static org.apache.solr.common.cloud.ZkNodeProps.makeMap;
import static org.apache.solr.schema.FieldType.CLASS_NAME;
import static org.apache.solr.schema.IndexSchema.DESTINATION;
import static org.apache.solr.schema.IndexSchema.NAME;
import static org.apache.solr.schema.IndexSchema.SOURCE;
import static org.apache.solr.schema.IndexSchema.TYPE;
/**A utility class to manipulate schema using the bulk mode.
* This class takes in all the commands and process them completely. It is an all or none
* operation
*/
public class SchemaManager {
private static final Logger log = LoggerFactory.getLogger(SchemaManager.class);
final SolrQueryRequest req;
ManagedIndexSchema managedIndexSchema;
public static final String ADD_FIELD = "add-field";
public static final String ADD_COPY_FIELD = "add-copy-field";
public static final String ADD_DYNAMIC_FIELD = "add-dynamic-field";
public static final String ADD_FIELD_TYPE = "add-field-type";
private static final Set<String> KNOWN_OPS = new HashSet<>();
static {
KNOWN_OPS.add(ADD_COPY_FIELD);
KNOWN_OPS.add(ADD_FIELD);
KNOWN_OPS.add(ADD_DYNAMIC_FIELD);
KNOWN_OPS.add(ADD_FIELD_TYPE);
}
public SchemaManager(SolrQueryRequest req){
this.req = req;
}
/**Take in a JSON command set and execute them . It tries to capture as many errors
* as possible instead of failing at the frst error it encounters
* @param rdr The input as a Reader
* @return Lis of errors . If the List is empty then the operation is successful.
*/
public List performOperations(Reader rdr) {
List<Operation> ops = null;
try {
ops = SchemaManager.parse(rdr);
} catch (Exception e) {
String msg= "Error parsing schema operations ";
log.warn(msg ,e );
return Collections.singletonList(singletonMap(ERR_MSGS, msg + ":" + e.getMessage()));
}
List errs = captureErrors(ops);
if(!errs.isEmpty()) return errs;
IndexSchema schema = req.getCore().getLatestSchema();
if (!(schema instanceof ManagedIndexSchema)) {
return singletonList( singletonMap(ERR_MSGS,"schema is not editable"));
}
synchronized (schema.getSchemaUpdateLock()) {
return doOperations(ops);
}
}
private List<String> doOperations(List<Operation> operations){
int timeout = req.getParams().getInt(BaseSolrResource.UPDATE_TIMEOUT_SECS, -1);
long startTime = System.nanoTime();
long endTime = timeout >0 ? System.nanoTime()+ (timeout * 1000*1000) : Long.MAX_VALUE;
SolrCore core = req.getCore();
for(;System.nanoTime() < endTime ;) {
managedIndexSchema = (ManagedIndexSchema) core.getLatestSchema();
for (Operation op : operations) {
if (ADD_FIELD.equals(op.name) || ADD_DYNAMIC_FIELD.equals(op.name)) {
applyAddField(op);
} else if(ADD_COPY_FIELD.equals(op.name)) {
applyAddCopyField(op);
} else if(ADD_FIELD_TYPE.equals(op.name)) {
applyAddType(op);
} else {
op.addError("No such operation : " + op.name);
}
}
List errs = captureErrors(operations);
if (!errs.isEmpty()) return errs;
try {
managedIndexSchema.persistManagedSchema(false);
core.setLatestSchema(managedIndexSchema);
waitForOtherReplicasToUpdate(timeout, startTime);
return EMPTY_LIST;
} catch (ManagedIndexSchema.SchemaChangedInZkException e) {
String s = "Failed to update schema because schema is modified";
log.warn(s, e);
continue;
} catch (Exception e){
String s = "Exception persisting schema";
log.warn(s, e);
return singletonList(s + e.getMessage());
}
}
return singletonList("Unable to persist schema");
}
private void waitForOtherReplicasToUpdate(int timeout, long startTime) {
if(timeout > 0 && managedIndexSchema.getResourceLoader()instanceof ZkSolrResourceLoader){
CoreDescriptor cd = req.getCore().getCoreDescriptor();
String collection = cd.getCollectionName();
if (collection != null) {
ZkSolrResourceLoader zkLoader = (ZkSolrResourceLoader) managedIndexSchema.getResourceLoader();
long timeLeftSecs1 = timeout - ((System.nanoTime() - startTime) /1000000);
int secsLeft = (int) (timeLeftSecs1 > 0 ? timeLeftSecs1 : -1);
if(secsLeft<=0) throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Not enough time left to update replicas. However the schema is updated already");
long timeLeftSecs = timeout - ((System.nanoTime() - startTime) /1000000);
ManagedIndexSchema.waitForSchemaZkVersionAgreement(collection,
cd.getCloudDescriptor().getCoreNodeName(),
(managedIndexSchema).getSchemaZkVersion(),
zkLoader.getZkController(),
(int) (timeLeftSecs > 0 ? timeLeftSecs : -1));
}
}
}
private boolean applyAddType(Operation op) {
String name = op.getStr(NAME);
String clz = op.getStr(CLASS_NAME);
if(op.hasError())
return false;
try {
FieldType fieldType = managedIndexSchema.newFieldType(name, clz, (Map<String, ?>) op.commandData);
managedIndexSchema = managedIndexSchema.addFieldTypes(singletonList(fieldType), false);
return true;
} catch (Exception e) {
op.addError(getErrorStr(e));
return false;
}
}
private String getErrorStr(Exception e) {
StringBuilder sb = new StringBuilder();
Throwable cause= e;
for(int i =0;i<5;i++) {
sb.append(cause.getMessage()).append("\n");
if(cause.getCause() == null || cause.getCause() == cause) break;
cause = cause.getCause();
}
return sb.toString();
}
private boolean applyAddCopyField(Operation op) {
String src = op.getStr(SOURCE);
List<String> dest = op.getStrs(DESTINATION);
if(op.hasError())
return false;
try {
managedIndexSchema = managedIndexSchema.addCopyFields(Collections.<String,Collection<String>>singletonMap(src,dest), false);
return true;
} catch (Exception e) {
op.addError(getErrorStr(e));
return false;
}
}
private boolean applyAddField( Operation op) {
String name = op.getStr(NAME);
String type = op.getStr(TYPE);
if(op.hasError())
return false;
FieldType ft = managedIndexSchema.getFieldTypeByName(type);
if(ft==null){
op.addError("No such field type '"+type+"'");
return false;
}
try {
if(ADD_DYNAMIC_FIELD.equals(op.name)){
managedIndexSchema = managedIndexSchema.addDynamicFields(
singletonList(SchemaField.create(name, ft, op.getValuesExcluding(NAME, TYPE))),
EMPTY_MAP,false);
} else {
managedIndexSchema = managedIndexSchema.addFields(
singletonList( SchemaField.create(name, ft, op.getValuesExcluding(NAME, TYPE))),
EMPTY_MAP,
false);
}
} catch (Exception e) {
op.addError(getErrorStr(e));
return false;
}
return true;
}
public static class Operation {
public final String name;
private Object commandData;//this is most often a map
private List<String> errors = new ArrayList<>();
Operation(String operationName, Object metaData) {
commandData = metaData;
this.name = operationName;
if(!KNOWN_OPS.contains(this.name)) errors.add("Unknown Operation :"+this.name);
}
public String getStr(String key, String def){
String s = (String) getMapVal(key);
return s == null ? def : s;
}
private Object getMapVal(String key) {
if (commandData instanceof Map) {
Map metaData = (Map) commandData;
return metaData.get(key);
} else {
String msg= " value has to be an object for operation :"+name;
if(!errors.contains(msg)) errors.add(msg);
return null;
}
}
public List<String> getStrs(String key){
List<String> val = getStrs(key, null);
if(val == null) errors.add("'"+key + "' is a required field");
return val;
}
/**Get collection of values for a key. If only one val is present a
* single value collection is returned
*/
public List<String> getStrs(String key, List<String> def){
Object v = getMapVal(key);
if(v == null){
return def;
} else {
if (v instanceof List) {
ArrayList<String> l = new ArrayList<>();
for (Object o : (List)v) {
l.add(String.valueOf(o));
}
if(l.isEmpty()) return def;
return l;
} else {
return singletonList(String.valueOf(v));
}
}
}
/**Get a required field. If missing it adds to the errors
*/
public String getStr(String key){
String s = getStr(key,null);
if(s==null) errors.add("'"+key + "' is a required field");
return s;
}
private Map errorDetails(){
return makeMap(name, commandData, ERR_MSGS, errors);
}
public boolean hasError() {
return !errors.isEmpty();
}
public void addError(String s) {
errors.add(s);
}
/**Get all the values from the metadata for the command
* without the specified keys
*/
public Map getValuesExcluding(String... keys) {
getMapVal(null);
if(hasError()) return emptyMap();//just to verify the type is Map
LinkedHashMap<String, Object> cp = new LinkedHashMap<>((Map<String,?>) commandData);
if(keys == null) return cp;
for (String key : keys) {
cp.remove(key);
}
return cp;
}
public List<String> getErrors() {
return errors;
}
}
/**Parse the command operations into command objects
*/
static List<Operation> parse(Reader rdr ) throws IOException {
JSONParser parser = new JSONParser(rdr);
ObjectBuilder ob = new ObjectBuilder(parser);
if(parser.lastEvent() != JSONParser.OBJECT_START) {
throw new RuntimeException("The JSON must be an Object of the form {\"command\": {...},...");
}
List<Operation> operations = new ArrayList<>();
for(;;) {
int ev = parser.nextEvent();
if (ev==JSONParser.OBJECT_END) return operations;
Object key = ob.getKey();
ev = parser.nextEvent();
Object val = ob.getVal();
if (val instanceof List) {
List list = (List) val;
for (Object o : list) {
operations.add(new Operation(String.valueOf(key), o));
}
} else {
operations.add(new Operation(String.valueOf(key), val));
}
}
}
static List<Map> captureErrors(List<Operation> ops){
List<Map> errors = new ArrayList<>();
for (SchemaManager.Operation op : ops) {
if(op.hasError()) {
errors.add(op.errorDetails());
}
}
return errors;
}
public static final String ERR_MSGS = "errorMessages";
}

View File

@ -0,0 +1,236 @@
package org.apache.solr.rest.schema;
/*
* 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.
*/
import org.apache.commons.io.FileUtils;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.schema.SchemaManager;
import org.apache.solr.util.RestTestBase;
import org.apache.solr.util.RestTestHarness;
import org.eclipse.jetty.servlet.ServletHolder;
import org.junit.After;
import org.junit.Before;
import org.noggit.JSONParser;
import org.noggit.ObjectBuilder;
import org.restlet.ext.servlet.ServerServlet;
import java.io.File;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
public class TestBulkSchemaAPI extends RestTestBase {
private static File tmpSolrHome;
private static File tmpConfDir;
private static final String collection = "collection1";
private static final String confDir = collection + "/conf";
@Before
public void before() throws Exception {
tmpSolrHome = createTempDir().toFile();
tmpConfDir = new File(tmpSolrHome, confDir);
FileUtils.copyDirectory(new File(TEST_HOME()), tmpSolrHome.getAbsoluteFile());
final SortedMap<ServletHolder,String> extraServlets = new TreeMap<>();
final ServletHolder solrRestApi = new ServletHolder("SolrSchemaRestApi", ServerServlet.class);
solrRestApi.setInitParameter("org.restlet.application", "org.apache.solr.rest.SolrSchemaRestApi");
extraServlets.put(solrRestApi, "/schema/*"); // '/schema/*' matches '/schema', '/schema/', and '/schema/whatever...'
System.setProperty("managed.schema.mutable", "true");
System.setProperty("enable.update.log", "false");
createJettyAndHarness(tmpSolrHome.getAbsolutePath(), "solrconfig-managed-schema.xml", "schema-rest.xml",
"/solr", true, extraServlets);
}
@After
public void after() throws Exception {
if (jetty != null) {
jetty.stop();
jetty = null;
}
server = null;
restTestHarness = null;
}
public void testMultipleAddFieldWithErrors() throws Exception {
String payload = SolrTestCaseJ4.json( "{\n" +
" 'add-field' : {\n" +
" 'name':'a1',\n" +
" 'type': 'string1',\n" +
" 'stored':true,\n" +
" 'indexed':false\n" +
" },\n" +
" 'add-field' : {\n" +
" 'type': 'string',\n" +
" 'stored':true,\n" +
" 'indexed':true\n" +
" }\n" +
" \n" +
" }");
String response = restTestHarness.post("/schema?wt=json", payload);
Map map = (Map) ObjectBuilder.getVal(new JSONParser(new StringReader(response)));
List l = (List) map.get("errors");
List errorList = (List) ((Map) l.get(0)).get(SchemaManager.ERR_MSGS);
assertEquals(1, errorList.size());
assertTrue (((String)errorList.get(0)).contains("No such field type"));
errorList = (List) ((Map) l.get(1)).get(SchemaManager.ERR_MSGS);
assertEquals(1, errorList.size());
assertTrue (((String)errorList.get(0)).contains("is a required field"));
}
public void testMultipleCommands() throws Exception{
String payload = "{\n" +
" 'add-field' : {\n" +
" 'name':'a1',\n" +
" 'type': 'string',\n" +
" 'stored':true,\n" +
" 'indexed':false\n" +
" },\n" +
" 'add-field' : {\n" +
" 'name':'a2',\n" +
" 'type': 'string',\n" +
" 'stored':true,\n" +
" 'indexed':true\n" +
" },\n" +
" 'add-dynamic-field' : {\n" +
" 'name' :'*_lol',\n" +
" 'type':'string',\n" +
" 'stored':true,\n" +
" 'indexed':true\n" +
" },\n" +
" 'add-copy-field' : {\n" +
" 'source' :'a1',\n" +
" 'dest':['a2','hello_lol']\n" +
" },\n" +
" 'add-field-type' : {\n" +
" 'name' :'mystr',\n" +
" 'class' : 'solr.StrField',\n" +
" 'sortMissingLast':'true'\n" +
" },\n" +
" 'add-field-type' : {" +
" 'name' : 'myNewTxtField',\n" +
" 'class':'solr.TextField','positionIncrementGap':'100',\n" +
" 'analyzer' : {\n" +
" 'charFilters':[\n" +
" {'class':'solr.PatternReplaceCharFilterFactory','replacement':'$1$1','pattern':'([a-zA-Z])\\\\\\\\1+'}\n" +
" ],\n" +
" 'tokenizer':{'class':'solr.WhitespaceTokenizerFactory'},\n" +
" 'filters':[\n" +
" {'class':'solr.WordDelimiterFilterFactory','preserveOriginal':'0'},\n" +
" {'class':'solr.StopFilterFactory','words':'stopwords.txt','ignoreCase':'true'},\n" +
" {'class':'solr.LowerCaseFilterFactory'},\n" +
" {'class':'solr.ASCIIFoldingFilterFactory'},\n" +
" {'class':'solr.KStemFilterFactory'}\n" +
" ]\n" +
" }\n" +
" }"+
" }";
RestTestHarness harness = restTestHarness;
String response = harness.post("/schema?wt=json", SolrTestCaseJ4.json( payload));
Map map = (Map) ObjectBuilder.getVal(new JSONParser(new StringReader(response)));
assertNull(response, map.get("errors"));
Map m = getObj(harness, "a1", "fields");
assertNotNull("field a1 not created", m);
assertEquals("string", m.get("type"));
assertEquals(Boolean.TRUE, m.get("stored"));
assertEquals(Boolean.FALSE, m.get("indexed"));
m = getObj(harness,"a2", "fields");
assertNotNull("field a2 not created", m);
assertEquals("string", m.get("type"));
assertEquals(Boolean.TRUE, m.get("stored"));
assertEquals(Boolean.TRUE, m.get("indexed"));
m = getObj(harness,"*_lol", "dynamicFields");
assertNotNull("field *_lol not created",m );
assertEquals("string", m.get("type"));
assertEquals(Boolean.TRUE, m.get("stored"));
assertEquals(Boolean.TRUE, m.get("indexed"));
List l = getCopyFields(harness,"a1");
Set s =new HashSet();
assertEquals(2,l.size());
s.add(((Map) l.get(0)).get("dest"));
s.add(((Map) l.get(1)).get("dest"));
assertTrue(s.contains("hello_lol"));
assertTrue(s.contains("a2"));
m = getObj(harness,"mystr", "fieldTypes");
assertNotNull(m);
assertEquals("solr.StrField",m.get("class"));
assertEquals("true",String.valueOf(m.get("sortMissingLast")));
m = getObj(harness,"myNewTxtField", "fieldTypes");
assertNotNull(m);
}
public static Map getObj(RestTestHarness restHarness, String fld, String key) throws Exception {
Map map = getRespMap(restHarness);
List l = (List) ((Map)map.get("schema")).get(key);
for (Object o : l) {
Map m = (Map) o;
if(fld.equals(m.get("name"))) return m;
}
return null;
}
public static Map getRespMap(RestTestHarness restHarness) throws Exception {
String response = restHarness.query("/schema?wt=json");
return (Map) ObjectBuilder.getVal(new JSONParser(new StringReader(response)));
}
public static List getCopyFields(RestTestHarness harness, String src) throws Exception {
Map map = getRespMap(harness);
List l = (List) ((Map)map.get("schema")).get("copyFields");
List result = new ArrayList();
for (Object o : l) {
Map m = (Map) o;
if(src.equals(m.get("source"))) result.add(m);
}
return result;
}
}

View File

@ -0,0 +1,194 @@
package org.apache.solr.schema;
/*
* 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.
*/
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.client.solrj.SolrServer;
import org.apache.solr.client.solrj.impl.HttpSolrServer;
import org.apache.solr.cloud.AbstractFullDistribZkTestBase;
import org.apache.solr.common.cloud.ZkStateReader;
import org.apache.solr.util.RESTfulServerProvider;
import org.apache.solr.util.RestTestHarness;
import org.noggit.JSONParser;
import org.noggit.ObjectBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static java.text.MessageFormat.format;
import static org.apache.solr.rest.schema.TestBulkSchemaAPI.getCopyFields;
import static org.apache.solr.rest.schema.TestBulkSchemaAPI.getObj;
public class TestBulkSchemaConcurrent extends AbstractFullDistribZkTestBase {
static final Logger log = LoggerFactory.getLogger(TestBulkSchemaConcurrent.class);
private List<RestTestHarness> restTestHarnesses = new ArrayList<>();
private void setupHarnesses() {
for (final SolrServer client : clients) {
RestTestHarness harness = new RestTestHarness(new RESTfulServerProvider() {
@Override
public String getBaseURL() {
return ((HttpSolrServer)client).getBaseURL();
}
});
restTestHarnesses.add(harness);
}
}
@Override
public void doTest() throws Exception {
final int threadCount = 5;
setupHarnesses();
Thread[] threads = new Thread[threadCount];
final List<List> collectErrors = new ArrayList<>();
for(int i=0;i<threadCount;i++){
final int finalI = i;
threads[i] = new Thread(){
@Override
public void run() {
try {
ArrayList errs = new ArrayList();
collectErrors.add(errs);
invokeBulkCall(finalI,errs);
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
};
threads[i].start();
}
for (Thread thread : threads) thread.join();
boolean success = true;
for (List e : collectErrors) {
if(!e.isEmpty()){
success = false;
log.error(e.toString());
}
}
assertTrue(success);
}
private void invokeBulkCall(int seed, ArrayList<String> errs) throws Exception {
String payload = "{\n" +
" 'add-field' : {\n" +
" 'name':'replaceFieldA',\n" +
" 'type': 'string',\n" +
" 'stored':true,\n" +
" 'indexed':false\n" +
" },\n" +
" 'add-dynamic-field' : {\n" +
" 'name' :'replaceDynamicField',\n" +
" 'type':'string',\n" +
" 'stored':true,\n" +
" 'indexed':true\n" +
" },\n" +
" 'add-copy-field' : {\n" +
" 'source' :'replaceFieldA',\n" +
" 'dest':['replaceDynamicCopyFieldDest']\n" +
" },\n" +
" 'add-field-type' : {\n" +
" 'name' :'myNewFieldTypeName',\n" +
" 'class' : 'solr.StrField',\n" +
" 'sortMissingLast':'true'\n" +
" }\n" +
"\n" +
" }";
String aField = "a" + seed;
String dynamicFldName = "*_lol" + seed;
String dynamicCopyFldDest = "hello_lol"+seed;
String newFieldTypeName = "mystr" + seed;
RestTestHarness publisher = restTestHarnesses.get(r.nextInt(restTestHarnesses.size()));
payload = payload.replace("replaceFieldA1", aField);
payload = payload.replace("replaceDynamicField", dynamicFldName);
payload = payload.replace("dynamicFieldLol","lol"+seed);
payload = payload.replace("replaceDynamicCopyFieldDest",dynamicCopyFldDest);
payload = payload.replace("myNewFieldTypeName", newFieldTypeName);
String response = publisher.post("/schema?wt=json", SolrTestCaseJ4.json(payload));
Map map = (Map) ObjectBuilder.getVal(new JSONParser(new StringReader(response)));
Object errors = map.get("errors");
if(errors!= null){
errs.add(new String(ZkStateReader.toJSON(errors), StandardCharsets.UTF_8));
return;
}
//get another node
RestTestHarness harness = restTestHarnesses.get(r.nextInt(restTestHarnesses.size()));
long startTime = System.nanoTime();
boolean success = false;
long maxTimeoutMillis = 100000;
Set<String> errmessages = new HashSet<>();
while ( ! success
&& TimeUnit.MILLISECONDS.convert(System.nanoTime() - startTime, TimeUnit.NANOSECONDS) < maxTimeoutMillis) {
errmessages.clear();
Map m = getObj(harness, aField, "fields");
if(m== null) errmessages.add(format("field {0} not created", aField));
m = getObj(harness, dynamicFldName, "dynamicFields");
if(m== null) errmessages.add(format("dynamic field {0} not created", dynamicFldName));
List l = getCopyFields(harness, "a1");
if(!checkCopyField(l,aField,dynamicCopyFldDest))
errmessages.add(format("CopyField source={0},dest={1} not created" , aField,dynamicCopyFldDest));
m = getObj(harness, "mystr", "fieldTypes");
if(m == null) errmessages.add(format("new type {} not created" , newFieldTypeName));
Thread.sleep(10);
}
if(!errmessages.isEmpty()){
errs.addAll(errmessages);
}
}
private boolean checkCopyField(List<Map> l, String src, String dest) {
if(l == null) return false;
for (Map map : l) {
if(src.equals(map.get("source")) &&
dest.equals(map.get("dest"))) return true;
}
return false;
}
}

View File

@ -0,0 +1,77 @@
package org.apache.solr.schema;
/*
* 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.
*/
import org.apache.solr.SolrTestCaseJ4;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.IOException;
import java.io.StringReader;
import java.util.List;
public class TestSchemaManager extends SolrTestCaseJ4 {
@BeforeClass
public static void beforeClass() throws Exception {
initCore("solrconfig.xml","schema-tiny.xml");
}
@Test
public void testParsing() throws IOException {
String x = "{\n" +
" \"add-field\" : {\n" +
" \"name\":\"a\",\n" +
" \"type\": \"string\",\n" +
" \"stored\":true,\n" +
" \"indexed\":false\n" +
" },\n" +
" \"add-field\" : {\n" +
" \"name\":\"b\",\n" +
" \"type\": \"string\",\n" +
" \"stored\":true,\n" +
" \"indexed\":false\n" +
" }\n" +
"\n" +
"}";
List<SchemaManager.Operation> ops = SchemaManager.parse(new StringReader(x));
assertEquals(2,ops.size());
assertTrue( SchemaManager.captureErrors(ops).isEmpty());
x = " {\"add-field\" : [{\n" +
" \"name\":\"a1\",\n" +
" \"type\": \"string\",\n" +
" \"stored\":true,\n" +
" \"indexed\":false\n" +
" },\n" +
" {\n" +
" \"name\":\"a2\",\n" +
" \"type\": \"string\",\n" +
" \"stored\":true,\n" +
" \"indexed\":true\n" +
" }]\n" +
" }";
ops = SchemaManager.parse(new StringReader(x));
assertEquals(2,ops.size());
assertTrue( SchemaManager.captureErrors(ops).isEmpty());
}
}