blob: ed43b323f4ee0e3da9756cdcdf20a8d1f363aa44 [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.update.processor;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Collection;
import java.util.function.Function;
import java.util.regex.Pattern;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.SolrInputField;
import org.apache.solr.core.SolrCore;
import org.apache.solr.core.SolrResourceLoader;
import org.apache.solr.schema.FieldType;
import org.apache.solr.schema.IndexSchema;
import org.apache.solr.update.AddUpdateCommand;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.solr.common.SolrException.ErrorCode.BAD_REQUEST;
import static org.apache.solr.common.SolrException.ErrorCode.SERVER_ERROR;
import static org.apache.solr.update.processor.FieldMutatingUpdateProcessorFactory.SelectorParams;
/**
* Reusable base class for UpdateProcessors that will consider
* AddUpdateCommands and mutate the values associated with configured
* fields.
* <p>
* Subclasses should override the mutate method to specify how individual
* SolrInputFields identified by the selector associated with this instance
* will be mutated.
* </p>
*
* @see FieldMutatingUpdateProcessorFactory
* @see FieldValueMutatingUpdateProcessor
* @see FieldNameSelector
*/
public abstract class FieldMutatingUpdateProcessor
extends UpdateRequestProcessor {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private final FieldNameSelector selector;
public FieldMutatingUpdateProcessor(FieldNameSelector selector,
UpdateRequestProcessor next) {
super(next);
this.selector = selector;
}
/**
* Method for mutating SolrInputFields associated with fields identified
* by the FieldNameSelector associated with this processor
* @param src the SolrInputField to mutate, may be modified in place and
* returned
* @return the SolrInputField to use in replacing the original (src) value.
* If null the field will be removed.
*/
protected abstract SolrInputField mutate(final SolrInputField src);
/**
* Calls <code>mutate</code> on any fields identified by the selector
* before forwarding the command down the chain. Any SolrExceptions
* thrown by <code>mutate</code> will be logged with the Field name,
* wrapped and re-thrown.
*/
@Override
public void processAdd(AddUpdateCommand cmd) throws IOException {
final SolrInputDocument doc = cmd.getSolrInputDocument();
// make a copy we can iterate over while mutating the doc
final Collection<String> fieldNames
= new ArrayList<>(doc.getFieldNames());
for (final String fname : fieldNames) {
if (! selector.shouldMutate(fname)) continue;
final SolrInputField src = doc.get(fname);
SolrInputField dest = null;
try {
dest = mutate(src);
} catch (SolrException e) {
String msg = "Unable to mutate field '"+fname+"': "+e.getMessage();
SolrException.log(log, msg, e);
throw new SolrException(BAD_REQUEST, msg, e);
}
if (null == dest) {
doc.remove(fname);
} else {
// semantics of what happens if dest has diff name are hard
// we could treat it as a copy, or a rename
// for now, don't allow it.
if (! fname.equals(dest.getName()) ) {
throw new SolrException(SERVER_ERROR,
"mutate returned field with different name: "
+ fname + " => " + dest.getName());
}
doc.put(dest.getName(), dest);
}
}
super.processAdd(cmd);
}
/**
* Interface for identifying which fields should be mutated
*/
public interface FieldNameSelector {
boolean shouldMutate(final String fieldName);
}
/** Singleton indicating all fields should be mutated */
public static final FieldNameSelector SELECT_ALL_FIELDS = fieldName -> true;
/** Singleton indicating no fields should be mutated */
public static final FieldNameSelector SELECT_NO_FIELDS = fieldName -> false;
/**
* Wraps two FieldNameSelectors such that the FieldNameSelector
* returned matches all fields specified by the "includes" unless they
* are matched by "excludes"
* @param includes a selector identifying field names that should be selected
* @param excludes a selector identifying field names that should be
* <i>not</i> be selected, even if they are matched by the 'includes'
* selector
* @return Either a new FieldNameSelector or one of the input selecors
* if the combination lends itself to optimization.
*/
public static FieldNameSelector wrap(final FieldNameSelector includes,
final FieldNameSelector excludes) {
if (SELECT_NO_FIELDS == excludes) {
return includes;
}
if (SELECT_ALL_FIELDS == excludes) {
return SELECT_NO_FIELDS;
}
if (SELECT_ALL_FIELDS == includes) {
return fieldName -> ! excludes.shouldMutate(fieldName);
}
return fieldName -> (includes.shouldMutate(fieldName)
&& ! excludes.shouldMutate(fieldName));
}
/**
* Utility method that can be used to define a FieldNameSelector
* using the same types of rules as the FieldMutatingUpdateProcessor init
* code. This may be useful for Factories that wish to define default
* selectors in similar terms to what the configuration would look like.
* @lucene.internal
*/
public static FieldNameSelector createFieldNameSelector
(final SolrResourceLoader loader,
final SolrCore core,
final SelectorParams params,
final FieldNameSelector defSelector) {
if (params.noSelectorsSpecified()) {
return defSelector;
}
final ConfigurableFieldNameSelectorHelper helper =
new ConfigurableFieldNameSelectorHelper(loader, params);
return fieldName -> helper.shouldMutateBasedOnSchema(fieldName, core.getLatestSchema());
}
/**
* Utility method that can be used to define a FieldNameSelector
* using the same types of rules as the FieldMutatingUpdateProcessor init
* code. This may be useful for Factories that wish to define default
* selectors in similar terms to what the configuration would look like.
* Uses {@code schema} for checking field existence.
* @lucene.internal
*/
public static FieldNameSelector createFieldNameSelector
(final SolrResourceLoader loader,
final IndexSchema schema,
final SelectorParams params,
final FieldNameSelector defSelector) {
if (params.noSelectorsSpecified()) {
return defSelector;
}
final ConfigurableFieldNameSelectorHelper helper =
new ConfigurableFieldNameSelectorHelper(loader, params);
return fieldName -> helper.shouldMutateBasedOnSchema(fieldName, schema);
}
private static final class ConfigurableFieldNameSelectorHelper {
final SelectorParams params;
@SuppressWarnings({"rawtypes"})
final Collection<Class> classes;
private ConfigurableFieldNameSelectorHelper(final SolrResourceLoader loader,
final SelectorParams params) {
this.params = params;
@SuppressWarnings({"rawtypes"})
final Collection<Class> classes = new ArrayList<>(params.typeClass.size());
for (String t : params.typeClass) {
try {
classes.add(loader.findClass(t, Object.class));
} catch (Exception e) {
throw new SolrException(SERVER_ERROR, "Can't resolve typeClass: " + t, e);
}
}
this.classes = classes;
}
public boolean shouldMutateBasedOnSchema(final String fieldName, IndexSchema schema) {
// order of checks is based on what should be quicker
// (ie: set lookups faster the looping over instanceOf / matches tests
if ( ! (params.fieldName.isEmpty() || params.fieldName.contains(fieldName)) ) {
return false;
}
// do not consider it an error if the fieldName has no type
// there might be another processor dealing with it later
FieldType t = schema.getFieldTypeNoEx(fieldName);
final boolean fieldExists = (null != t);
if ( (null != params.fieldNameMatchesSchemaField) &&
(fieldExists != params.fieldNameMatchesSchemaField) ) {
return false;
}
if (fieldExists) {
if (! (params.typeName.isEmpty() || params.typeName.contains(t.getTypeName())) ) {
return false;
}
if (! (classes.isEmpty() || instanceOfAny(t, classes)) ) {
return false;
}
}
if (! (params.fieldRegex.isEmpty() || matchesAny(fieldName, params.fieldRegex)) ) {
return false;
}
return true;
}
/**
* returns true if the Object 'o' is an instance of any class in
* the Collection
*/
private static boolean instanceOfAny(Object o,
@SuppressWarnings({"rawtypes"})Collection<Class> classes) {
for (@SuppressWarnings({"rawtypes"})Class c : classes) {
if ( c.isInstance(o) ) return true;
}
return false;
}
/**
* returns true if the CharSequence 's' matches any Pattern in the
* Collection
*/
private static boolean matchesAny(CharSequence s,
Collection<Pattern> regexes) {
for (Pattern p : regexes) {
if (p.matcher(s).matches()) return true;
}
return false;
}
}
public static FieldMutatingUpdateProcessor mutator(FieldNameSelector selector,
UpdateRequestProcessor next,
Function<SolrInputField,SolrInputField> fun){
return new FieldMutatingUpdateProcessor(selector, next) {
@Override
protected SolrInputField mutate(SolrInputField src) {
return fun.apply(src);
}
};
}
}