/* * 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.request.json; import java.io.IOException; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.apache.commons.io.IOUtils; import org.apache.solr.common.SolrException; import org.apache.solr.common.params.ModifiableSolrParams; import org.apache.solr.common.params.MultiMapSolrParams; import org.apache.solr.common.params.SolrParams; import org.apache.solr.common.util.ContentStream; import org.apache.solr.common.util.StrUtils; import org.apache.solr.handler.component.SearchHandler; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.request.SolrRequestHandler; import org.apache.solr.request.macro.MacroExpander; import org.noggit.JSONParser; import org.noggit.ObjectBuilder; import static org.apache.solr.common.params.CommonParams.JSON; import static org.apache.solr.common.params.CommonParams.SORT; public class RequestUtil { /** * Set default-ish params on a SolrQueryRequest as well as do standard macro processing and JSON request parsing. * * @param handler The search handler this is for (may be null if you don't want this method touching the content streams) * @param req The request whose params we are interested in * @param defaults values to be used if no values are specified in the request params * @param appends values to be appended to those from the request (or defaults) when dealing with multi-val params, or treated as another layer of defaults for singl-val params. * @param invariants values which will be used instead of any request, or default values, regardless of context. */ public static void processParams(SolrRequestHandler handler, SolrQueryRequest req, SolrParams defaults, SolrParams appends, SolrParams invariants) { boolean searchHandler = handler instanceof SearchHandler; SolrParams params = req.getParams(); // Handle JSON stream for search requests if (searchHandler && req.getContentStreams() != null) { Map<String,String[]> map = MultiMapSolrParams.asMultiMap(params, false); if (!(params instanceof MultiMapSolrParams || params instanceof ModifiableSolrParams)) { // need to set params on request since we weren't able to access the original map params = new MultiMapSolrParams(map); req.setParams(params); } String[] jsonFromParams = map.remove(JSON); // params from the query string should come after (and hence override) JSON content streams for (ContentStream cs : req.getContentStreams()) { String contentType = cs.getContentType(); if (contentType==null || !contentType.contains("/json")) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Bad contentType for search handler :" + contentType + " request="+req); } try { String jsonString = IOUtils.toString( cs.getReader() ); if (jsonString != null) { MultiMapSolrParams.addParam(JSON, jsonString, map); } } catch (IOException e) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Exception reading content stream for request:"+req, e); } } // append existing "json" params if (jsonFromParams != null) { for (String json : jsonFromParams) { MultiMapSolrParams.addParam(JSON, json, map); } } } String[] jsonS = params.getParams(JSON); boolean hasAdditions = defaults != null || invariants != null || appends != null || jsonS != null; // short circuit processing if (!hasAdditions && !params.getBool("expandMacros", true)) { return; // nothing to do... } boolean isShard = params.getBool("isShard", false); Map<String, String[]> newMap = MultiMapSolrParams.asMultiMap(params, hasAdditions); // see if the json has a "params" section // TODO: we should currently *not* do this if this is a leaf of a distributed search since it could overwrite parameters set by the top-level // The parameters we extract will be propagated anyway. if (jsonS != null && !isShard) { for (String json : jsonS) { getParamsFromJSON(newMap, json); } } // first populate defaults, etc.. if (defaults != null) { Map<String, String[]> defaultsMap = MultiMapSolrParams.asMultiMap(defaults); for (Map.Entry<String, String[]> entry : defaultsMap.entrySet()) { String key = entry.getKey(); if (!newMap.containsKey(key)) { newMap.put(key, entry.getValue()); } } } if (appends != null) { Map<String, String[]> appendsMap = MultiMapSolrParams.asMultiMap(appends); for (Map.Entry<String, String[]> entry : appendsMap.entrySet()) { String key = entry.getKey(); String[] arr = newMap.get(key); if (arr == null) { newMap.put(key, entry.getValue()); } else { String[] appendArr = entry.getValue(); String[] newArr = new String[arr.length + appendArr.length]; System.arraycopy(arr, 0, newArr, 0, arr.length); System.arraycopy(appendArr, 0, newArr, arr.length, appendArr.length); newMap.put(key, newArr); } } } if (invariants != null) { newMap.putAll( MultiMapSolrParams.asMultiMap(invariants) ); } if (!isShard) { // Don't expand macros in shard requests String[] doMacrosStr = newMap.get("expandMacros"); boolean doMacros = true; if (doMacrosStr != null) { doMacros = "true".equals(doMacrosStr[0]); } if (doMacros) { newMap = MacroExpander.expand(newMap); } } // Set these params as soon as possible so if there is an error processing later, things like // "wt=json" will take effect from the defaults. SolrParams newParams = new MultiMapSolrParams(newMap); // newMap may still change below, but that should be OK req.setParams(newParams); // Skip the rest of the processing (including json processing for now) if this isn't a search handler. // For example json.command started to be used in SOLR-6294, and that caused errors here. if (!searchHandler) return; Map<String, Object> json = null; // Handle JSON body first, so query params will always overlay on that jsonS = newMap.get(JSON); if (jsonS != null) { if (json == null) { json = new LinkedHashMap<>(); } mergeJSON(json, JSON, jsonS, new ObjectUtil.ConflictHandler()); } for (String key : newMap.keySet()) { // json.nl, json.wrf are existing query parameters if (key.startsWith("json.") && !("json.nl".equals(key) || "json.wrf".equals(key))) { if (json == null) { json = new LinkedHashMap<>(); } mergeJSON(json, key, newMap.get(key), new ObjectUtil.ConflictHandler()); } } // implement compat for existing components... if (json != null && !isShard) { for (Map.Entry<String,Object> entry : json.entrySet()) { String key = entry.getKey(); String out = null; boolean arr = false; if ("query".equals(key)) { out = "q"; } else if ("filter".equals(key)) { out = "fq"; arr = true; } else if ("fields".equals(key)) { out = "fl"; arr = true; } else if ("offset".equals(key)) { out = "start"; } else if ("limit".equals(key)) { out = "rows"; } else if (SORT.equals(key)) { out = SORT; } else if ("params".equals(key) || "facet".equals(key) ) { // handled elsewhere continue; } else { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Unknown top-level key in JSON request : " + key); } Object val = entry.getValue(); if (arr) { String[] existing = newMap.get(out); List lst = val instanceof List ? (List)val : null; int existingSize = existing==null ? 0 : existing.length; int jsonSize = lst==null ? 1 : lst.size(); String[] newval = new String[ existingSize + jsonSize ]; for (int i=0; i<existingSize; i++) { newval[i] = existing[i]; } if (lst != null) { for (int i = 0; i < jsonSize; i++) { Object v = lst.get(i); newval[existingSize + i] = v.toString(); } } else { newval[newval.length-1] = val.toString(); } newMap.put(out, newval); } else { newMap.put(out, new String[]{val.toString()}); } } } if (json != null) { req.setJSON(json); } } // queryParamName is something like json.facet or json.query, or just json... private static void mergeJSON(Map<String,Object> json, String queryParamName, String[] vals, ObjectUtil.ConflictHandler handler) { try { List<String> path = StrUtils.splitSmart(queryParamName, ".", true); path = path.subList(1, path.size()); for (String jsonStr : vals) { Object o = ObjectBuilder.fromJSON(jsonStr); // zero-length strings or comments can cause this to be null (and a zero-length string can result from a json content-type w/o a body) if (o != null) { ObjectUtil.mergeObjects(json, path, o, handler); } } } catch (IOException e) { // impossible } } private static void getParamsFromJSON(Map<String, String[]> params, String json) { if (json.indexOf("params") < 0) { return; } JSONParser parser = new JSONParser(json); try { JSONUtil.expect(parser, JSONParser.OBJECT_START); boolean found = JSONUtil.advanceToMapKey(parser, "params", false); if (!found) { return; } parser.nextEvent(); // advance to the value Object o = ObjectBuilder.getVal(parser); if (!(o instanceof Map)) return; Map<String,Object> map = (Map<String,Object>)o; // To make consistent with json.param handling, we should make query params come after json params (i.e. query params should // appear to overwrite json params. // Solr params are based on String though, so we need to convert for (Map.Entry<String, Object> entry : map.entrySet()) { String key = entry.getKey(); Object val = entry.getValue(); if (params.get(key) != null) { continue; } if (val == null) { params.remove(key); } else if (val instanceof List) { List lst = (List) val; String[] vals = new String[lst.size()]; for (int i = 0; i < vals.length; i++) { vals[i] = lst.get(i).toString(); } params.put(key, vals); } else { params.put(key, new String[]{val.toString()}); } } } catch (Exception e) { // ignore parse exceptions at this stage, they may be caused by incomplete macro expansions return; } } }