/* * 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.util.Collection; import java.util.ArrayList; import java.util.List; import java.util.HashSet; import org.apache.solr.core.SolrCore; import org.apache.solr.util.plugin.SolrCoreAware; import org.apache.solr.common.util.NamedList; import org.apache.solr.common.SolrInputField; import org.apache.solr.common.SolrInputDocument; import org.apache.solr.common.SolrException; import static org.apache.solr.common.SolrException.ErrorCode.SERVER_ERROR; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.response.SolrQueryResponse; import org.apache.solr.update.AddUpdateCommand; import org.apache.solr.update.processor.FieldMutatingUpdateProcessorFactory.SelectorParams; import org.apache.solr.update.processor.FieldMutatingUpdateProcessor.FieldNameSelector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Clones the values found in any matching <code>source</code> field into * the configured <code>dest</code> field. * <p> * While the <code>dest</code> field must be a single <code><str></code>, * the <code>source</code> fields can be configured as either: * </p> * <ul> * <li>One or more <code><str></code></li> * <li>An <code><arr></code> of <code><str></code></li> * <li>A <code><lst></code> containing {@link FieldMutatingUpdateProcessorFactory FieldMutatingUpdateProcessorFactory style selector arguments}</li> * </ul> * <p> * If the <code>dest</code> field already exists in the document, then the * values from the <code>source</code> fields will be added to it. The * "boost" value associated with the <code>dest</code> will not be changed, * and any boost specified on the <code>source</code> fields will be ignored. * (If the <code>dest</code> field did not exist prior to this processor, the * newly created <code>dest</code> field will have the default boost of 1.0) * </p> * <p> * In the example below, the <code>category</code> field will be cloned * into the <code>category_s</code> field, both the <code>authors</code> and * <code>editors</code> fields will be cloned into the <code>contributors</code> * field, and any field with a name ending in <code>_price</code> -- except for * <code>list_price</code> -- will be cloned into the <code>all_prices</code> * field. * </p> * <!-- see solrconfig-update-processors-chains.xml for where this is tested --> * <pre class="prettyprint"> * <updateRequestProcessorChain name="multiple-clones"> * <processor class="solr.CloneFieldUpdateProcessorFactory"> * <str name="source">category</str> * <str name="dest">category_s</str> * </processor> * <processor class="solr.CloneFieldUpdateProcessorFactory"> * <arr name="source"> * <str>authors</str> * <str>editors</str> * </arr> * <str name="dest">contributors</str> * </processor> * <processor class="solr.CloneFieldUpdateProcessorFactory"> * <lst name="source"> * <str name="fieldRegex">.*_price</str> * <lst name="exclude"> * <str name="fieldName">list_price</str> * </lst> * </lst> * <str name="dest">all_prices</str> * </processor> * </updateRequestProcessorChain> * </pre> */ public class CloneFieldUpdateProcessorFactory extends UpdateRequestProcessorFactory implements SolrCoreAware { private final static Logger log = LoggerFactory.getLogger(CloneFieldUpdateProcessorFactory.class); public static final String SOURCE_PARAM = "source"; public static final String DEST_PARAM = "dest"; private SelectorParams srcInclusions = new SelectorParams(); private Collection<SelectorParams> srcExclusions = new ArrayList<>(); private FieldNameSelector srcSelector = null; private String dest = null; protected final FieldNameSelector getSourceSelector() { if (null != srcSelector) return srcSelector; throw new SolrException(SERVER_ERROR, "selector was never initialized, "+ " inform(SolrCore) never called???"); } @SuppressWarnings("unchecked") @Override public void init(NamedList args) { Object d = args.remove(DEST_PARAM); if (null == d) { throw new SolrException (SERVER_ERROR, "Init param '" + DEST_PARAM + "' must be specified"); } else if (! (d instanceof CharSequence) ) { throw new SolrException (SERVER_ERROR, "Init param '" + DEST_PARAM + "' must be a string (ie: 'str')"); } dest = d.toString(); List<Object> sources = args.getAll(SOURCE_PARAM); if (0 == sources.size()) { throw new SolrException (SERVER_ERROR, "Init param '" + SOURCE_PARAM + "' must be specified"); } if (1 == sources.size() && sources.get(0) instanceof NamedList) { // nested set of selector options NamedList selectorConfig = (NamedList) args.remove(SOURCE_PARAM); srcInclusions = parseSelectorParams(selectorConfig); List<Object> excList = selectorConfig.getAll("exclude"); for (Object excObj : excList) { if (null == excObj) { throw new SolrException (SERVER_ERROR, "Init param '" + SOURCE_PARAM + "' child 'exclude' can not be null"); } if (! (excObj instanceof NamedList) ) { throw new SolrException (SERVER_ERROR, "Init param '" + SOURCE_PARAM + "' child 'exclude' must be <lst/>"); } NamedList exc = (NamedList) excObj; srcExclusions.add(parseSelectorParams(exc)); if (0 < exc.size()) { throw new SolrException(SERVER_ERROR, "Init param '" + SOURCE_PARAM + "' has unexpected 'exclude' sub-param(s): '" + selectorConfig.getName(0) + "'"); } // call once per instance selectorConfig.remove("exclude"); } if (0 < selectorConfig.size()) { throw new SolrException(SERVER_ERROR, "Init param '" + SOURCE_PARAM + "' contains unexpected child param(s): '" + selectorConfig.getName(0) + "'"); } } else { // source better be one or more strings srcInclusions.fieldName = new HashSet<>(args.removeConfigArgs("source")); } if (0 < args.size()) { throw new SolrException(SERVER_ERROR, "Unexpected init param(s): '" + args.getName(0) + "'"); } super.init(args); } @Override public void inform(final SolrCore core) { srcSelector = FieldMutatingUpdateProcessor.createFieldNameSelector (core.getResourceLoader(), core, srcInclusions, FieldMutatingUpdateProcessor.SELECT_NO_FIELDS); for (SelectorParams exc : srcExclusions) { srcSelector = FieldMutatingUpdateProcessor.wrap (srcSelector, FieldMutatingUpdateProcessor.createFieldNameSelector (core.getResourceLoader(), core, exc, FieldMutatingUpdateProcessor.SELECT_NO_FIELDS)); } } @Override public final UpdateRequestProcessor getInstance(SolrQueryRequest req, SolrQueryResponse rsp, UpdateRequestProcessor next) { return new UpdateRequestProcessor(next) { @Override public void processAdd(AddUpdateCommand cmd) throws IOException { final SolrInputDocument doc = cmd.getSolrInputDocument(); // preserve initial values and boost (if any) SolrInputField destField = doc.containsKey(dest) ? doc.getField(dest) : new SolrInputField(dest); boolean modified = false; for (final String fname : doc.getFieldNames()) { if (! srcSelector.shouldMutate(fname)) continue; for (Object val : doc.getFieldValues(fname)) { // preserve existing dest boost (multiplicitive), ignore src boost destField.addValue(val, 1.0f); } modified=true; } if (modified) doc.put(dest, destField); super.processAdd(cmd); } }; } /** macro */ private static SelectorParams parseSelectorParams(NamedList args) { return FieldMutatingUpdateProcessorFactory.parseSelectorParams(args); } }