| /* |
| * 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); |
| } |
| }; |
| |
| } |
| } |
| |