blob: 4be22f2413bf91c682b464870aae47ba7a04a808 [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.common.util;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import org.apache.solr.cluster.api.SimpleMap;
import org.apache.solr.common.MapWriter;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.MultiMapSolrParams;
import org.apache.solr.common.params.SolrParams;
/**
* A simple container class for modeling an ordered list of name/value pairs.
*
* <p>
* Unlike Maps:
* </p>
* <ul>
* <li>Names may be repeated</li>
* <li>Order of elements is maintained</li>
* <li>Elements may be accessed by numeric index</li>
* <li>Names and Values can both be null</li>
* </ul>
*
* <p>
* A NamedList provides fast access by element number, but not by name.
* </p>
* <p>
* When a NamedList is serialized, order is considered more important than access
* by key, so ResponseWriters that output to a format such as JSON will normally
* choose a data structure that allows order to be easily preserved in various
* clients (i.e. not a straight map).
* If access by key is more important for serialization, see {@link SimpleOrderedMap},
* or simply use a regular {@link Map}
* </p>
*
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public class NamedList<T> implements Cloneable, Serializable, Iterable<Map.Entry<String,T>> , MapWriter, SimpleMap<T> {
private static final long serialVersionUID = 1957981902839867821L;
protected final List<Object> nvPairs;
/** Creates an empty instance */
public NamedList() {
nvPairs = new ArrayList<>();
}
public NamedList(int sz) {
nvPairs = new ArrayList<>(sz<<1);
}
@Override
public void writeMap(EntryWriter ew) throws IOException {
for (int i = 0; i < nvPairs.size(); i+=2) {
ew.put((CharSequence) nvPairs.get(i), nvPairs.get(i + 1));
}
}
/**
* Creates a NamedList instance containing the "name,value" pairs contained in the
* Entry[].
*
* <p>
* Modifying the contents of the Entry[] after calling this constructor may change
* the NamedList (in future versions of Solr), but this is not guaranteed and should
* not be relied upon. To modify the NamedList, refer to {@link #add(String, Object)}
* or {@link #remove(String)}.
* </p>
*
* @param nameValuePairs the name value pairs
*/
public NamedList(Map.Entry<String, ? extends T>[] nameValuePairs) {
nvPairs = nameValueMapToList(nameValuePairs);
}
/**
* Creates a NamedList instance containing the "name,value" pairs contained in the
* Map.
*
* <p>
* Modifying the contents of the Map after calling this constructor may change
* the NamedList (in future versions of Solr), but this is not guaranteed and should
* not be relied upon. To modify the NamedList, refer to {@link #add(String, Object)}
* or {@link #remove(String)}.
* </p>
*
* @param nameValueMap the name value pairs
*/
public NamedList(Map<String,? extends T> nameValueMap) {
if (null == nameValueMap) {
nvPairs = new ArrayList<>();
} else {
nvPairs = new ArrayList<>(nameValueMap.size() << 1);
for (Map.Entry<String,? extends T> ent : nameValueMap.entrySet()) {
nvPairs.add(ent.getKey());
nvPairs.add(ent.getValue());
}
}
}
/**
* Creates an instance backed by an explicitly specified list of
* pairwise names/values.
*
* <p>
* When using this constructor, runtime type safety is only guaranteed if
* all even numbered elements of the input list are of type "T".
* </p>
* <p>
* This method is package protected and exists solely so SimpleOrderedMap and clone() can utilize it
* </p>
* <p>
* TODO: this method was formerly public, now that it's not we can change the impl details of
* this class to be based on a Map.Entry[]
* </p>
* @lucene.internal
* @see #nameValueMapToList
*/
NamedList(List<Object> nameValuePairs) {
nvPairs=nameValuePairs;
}
/**
* Method to serialize Map.Entry&lt;String, ?&gt; to a List in which the even
* indexed elements (0,2,4. ..etc) are Strings and odd elements (1,3,5,) are of
* the type "T".
*
* <p>
* NOTE: This a temporary placeholder method until the guts of the class
* are actually replaced by List&lt;String, ?&gt;.
* </p>
*
* @return Modified List as per the above description
* @see <a href="https://issues.apache.org/jira/browse/SOLR-912">SOLR-912</a>
*/
private List<Object> nameValueMapToList(Map.Entry<String, ? extends T>[] nameValuePairs) {
List<Object> result = new ArrayList<>(nameValuePairs.length << 1);
for (Map.Entry<String, ?> ent : nameValuePairs) {
result.add(ent.getKey());
result.add(ent.getValue());
}
return result;
}
/** The total number of name/value pairs */
public int size() {
return nvPairs.size() >> 1;
}
/**
* The name of the pair at the specified List index
*
* @return null if no name exists
*/
public String getName(int idx) {
return (String)nvPairs.get(idx << 1);
}
/**
* The value of the pair at the specified List index
*
* @return may be null
*/
@SuppressWarnings("unchecked")
public T getVal(int idx) {
return (T)nvPairs.get((idx << 1) + 1);
}
/**
* Adds a name/value pair to the end of the list.
*/
public void add(String name, T val) {
nvPairs.add(name);
nvPairs.add(val);
}
/**
* Modifies the name of the pair at the specified index.
*/
public void setName(int idx, String name) {
nvPairs.set(idx<<1, name);
}
/**
* Modifies the value of the pair at the specified index.
*
* @return the value that used to be at index
*/
public T setVal(int idx, T val) {
int index = (idx<<1)+1;
@SuppressWarnings("unchecked")
T old = (T)nvPairs.get( index );
nvPairs.set(index, val);
return old;
}
/**
* Removes the name/value pair at the specified index.
*
* @return the value at the index removed
*/
public T remove(int idx) {
int index = (idx<<1);
nvPairs.remove(index);
@SuppressWarnings("unchecked")
T result = (T)nvPairs.remove(index); // same index, as things shifted in previous remove
return result;
}
/**
* Scans the list sequentially beginning at the specified index and
* returns the index of the first pair with the specified name.
*
* @param name name to look for, may be null
* @param start index to begin searching from
* @return The index of the first matching pair, -1 if no match
*/
public int indexOf(String name, int start) {
int sz = size();
for (int i=start; i<sz; i++) {
String n = getName(i);
if (name==null) {
if (n==null) return i; // matched null
} else if (name.equals(n)) {
return i;
}
}
return -1;
}
/**
* Gets the value for the first instance of the specified name
* found.
* <p>
* NOTE: this runs in linear time (it scans starting at the
* beginning of the list until it finds the first pair with
* the specified name).
*
* @return null if not found or if the value stored was null.
* @see #indexOf
* @see #get(String,int)
*
*/
public T get(String name) {
return get(name,0);
}
/**
* Gets the value for the first instance of the specified name
* found starting at the specified index.
* <p>
* NOTE: this runs in linear time (it scans starting at the
* specified position until it finds the first pair with
* the specified name).
*
* @return null if not found or if the value stored was null.
* @see #indexOf
*/
public T get(String name, int start) {
int sz = size();
for (int i=start; i<sz; i++) {
String n = getName(i);
if (name==null) {
if (n==null) return getVal(i);
} else if (name.equals(n)) {
return getVal(i);
}
}
return null;
}
/**
* Gets the values for the the specified name
*
* @param name Name
* @return List of values
*/
public List<T> getAll(String name) {
List<T> result = new ArrayList<>();
int sz = size();
for (int i = 0; i < sz; i++) {
String n = getName(i);
if (Objects.equals(name, n)) {
result.add(getVal(i));
}
}
return result;
}
/**
* Removes all values matching the specified name
*
* @param name Name
*/
private void killAll(String name) {
int sz = size();
// Go through the list backwards, removing matches as found.
for (int i = sz - 1; i >= 0; i--) {
String n = getName(i);
if (Objects.equals(name, n)) {
remove(i);
}
}
}
/**
* Recursively parses the NamedList structure to arrive at a specific element.
* As you descend the NamedList tree, the last element can be any type,
* including NamedList, but the previous elements MUST be NamedList objects
* themselves. A null value is returned if the indicated hierarchy doesn't
* exist, but NamedList allows null values so that could be the actual value
* at the end of the path.
*
* This method is particularly useful for parsing the response from Solr's
* /admin/mbeans handler, but it also works for any complex structure.
*
* Explicitly casting the return value is recommended. An even safer option is
* to accept the return value as an object and then check its type.
*
* Usage examples:
*
* String coreName = (String) response.findRecursive
* ("solr-mbeans", "CORE", "core", "stats", "coreName");
* long numDoc = (long) response.findRecursive
* ("solr-mbeans", "CORE", "searcher", "stats", "numDocs");
*
* @param args
* One or more strings specifying the tree to navigate.
* @return the last entry in the given path hierarchy, null if not found.
*/
public Object findRecursive(String... args) {
NamedList<?> currentList = null;
Object value = null;
for (int i = 0; i < args.length; i++) {
String key = args[i];
/*
* The first time through the loop, the current list is null, so we assign
* it to this list. Then we retrieve the first key from this list and
* assign it to value.
*
* On the next loop, we check whether the retrieved value is a NamedList.
* If it is, then we drop down to that NamedList, grab the value of the
* next key, and start the loop over. If it is not a NamedList, then we
* assign the value to null and break out of the loop.
*
* Assigning the value to null and then breaking out of the loop seems
* like the wrong thing to do, but there's a very simple reason that it
* works: If we have reached the last key, then the loop ends naturally
* after we retrieve the value, and that code is never executed.
*/
if (currentList == null) {
currentList = this;
} else {
if (value instanceof NamedList) {
currentList = (NamedList<?>) value;
} else {
value = null;
break;
}
}
/*
* We do not need to do a null check on currentList for the following
* assignment. The instanceof check above will fail if the current list is
* null, and if that happens, the loop will end before this point.
*/
value = currentList.get(key, 0);
}
return value;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append('{');
int sz = size();
for (int i=0; i<sz; i++) {
if (i != 0) sb.append(',');
sb.append(getName(i));
sb.append('=');
sb.append(getVal(i));
}
sb.append('}');
return sb.toString();
}
public NamedList<T> getImmutableCopy() {
NamedList<T> copy = clone();
return new NamedList<>( Collections.unmodifiableList(copy.nvPairs));
}
public Map<String,T> asShallowMap() {
return asShallowMap(false);
}
public Map<String,T> asShallowMap(boolean allowDps) {
return new Map<String, T>() {
@Override
public int size() {
return NamedList.this.size();
}
@Override
public boolean isEmpty() {
return size() == 0;
}
public boolean containsKey(Object key) {
return NamedList.this.get((String) key) != null ;
}
@Override
public boolean containsValue(Object value) {
return false;
}
@Override
public T get(Object key) {
return NamedList.this.get((String) key);
}
@Override
public T put(String key, T value) {
if (allowDps) {
NamedList.this.add(key, value);
return null;
}
int idx = NamedList.this.indexOf(key, 0);
if (idx == -1) {
NamedList.this.add(key, value);
} else {
NamedList.this.setVal(idx, value);
}
return null;
}
@Override
public T remove(Object key) {
return NamedList.this.remove((String) key);
}
@Override
@SuppressWarnings({"unchecked"})
public void putAll(Map m) {
boolean isEmpty = isEmpty();
for (Object o : m.entrySet()) {
@SuppressWarnings({"rawtypes"})
Map.Entry e = (Entry) o;
if (isEmpty) {// we know that there are no duplicates
add((String) e.getKey(), (T) e.getValue());
} else {
put(e.getKey() == null ? null : e.getKey().toString(), (T) e.getValue());
}
}
}
@Override
public void clear() {
NamedList.this.clear();
}
@Override
@SuppressWarnings({"unchecked"})
public Set<String> keySet() {
//TODO implement more efficiently
return NamedList.this.asMap(1).keySet();
}
@Override
@SuppressWarnings({"unchecked", "rawtypes"})
public Collection values() {
//TODO implement more efficiently
return NamedList.this.asMap(1).values();
}
@Override
public Set<Entry<String,T>> entrySet() {
//TODO implement more efficiently
return NamedList.this.asMap(1).entrySet();
}
@Override
public void forEach(BiConsumer action) {
NamedList.this.forEachEntry(action);
}
};
}
public Map asMap(int maxDepth) {
LinkedHashMap result = new LinkedHashMap();
for(int i=0;i<size();i++){
Object val = getVal(i);
if (val instanceof NamedList && maxDepth> 0) {
//the maxDepth check is to avoid stack overflow due to infinite recursion
val = ((NamedList) val).asMap(maxDepth-1);
}
Object old = result.put(getName(i), val);
if(old!=null){
if (old instanceof List) {
List list = (List) old;
list.add(val);
result.put(getName(i),old);
} else {
ArrayList l = new ArrayList();
l.add(old);
l.add(val);
result.put(getName(i), l);
}
}
}
return result;
}
/**
* Create SolrParams from NamedList. Values must be {@code String[]} or {@code List}
* (with toString()-appropriate entries), or otherwise have a toString()-appropriate value.
* Nulls are retained as such in arrays/lists but otherwise will NPE.
*/
public SolrParams toSolrParams() {
HashMap<String,String[]> map = new HashMap<>();
for (int i=0; i<this.size(); i++) {
String name = this.getName(i);
Object val = this.getVal(i);
if (val instanceof String[]) {
MultiMapSolrParams.addParam(name, (String[]) val, map);
} else if (val instanceof List) {
List l = (List) val;
String[] s = new String[l.size()];
for (int j = 0; j < l.size(); j++) {
s[j] = l.get(j) == null ? null : l.get(j).toString();
}
MultiMapSolrParams.addParam(name, s, map);
} else {
//TODO: we NPE if val is null; yet we support val members above. A bug?
MultiMapSolrParams.addParam(name, val.toString(), map);
}
}
// always use MultiMap for easier processing further down the chain
return new MultiMapSolrParams(map);
}
/**
*
* Helper class implementing Map.Entry&lt;String, T&gt; to store the key-value
* relationship in NamedList (the keys of which are String-s)
*/
public static final class NamedListEntry<T> implements Map.Entry<String,T> {
public NamedListEntry() {
}
public NamedListEntry(String _key, T _value) {
key = _key;
value = _value;
}
@Override
public String getKey() {
return key;
}
@Override
public T getValue() {
return value;
}
@Override
public T setValue(T _value) {
T oldValue = value;
value = _value;
return oldValue;
}
private String key;
private T value;
}
/**
* Iterates over the Map and sequentially adds its key/value pairs
*/
public boolean addAll(Map<String,T> args) {
for (Map.Entry<String, T> entry : args.entrySet() ) {
add(entry.getKey(), entry.getValue());
}
return args.size()>0;
}
/** Appends the elements of the given NamedList to this one. */
public boolean addAll(NamedList<T> nl) {
nvPairs.addAll(nl.nvPairs);
return nl.size()>0;
}
/**
* Makes a <i>shallow copy</i> of the named list.
*/
@Override
public NamedList<T> clone() {
ArrayList<Object> newList = new ArrayList<>(nvPairs.size());
newList.addAll(nvPairs);
return new NamedList<>(newList);
}
//----------------------------------------------------------------------------
// Iterable interface
//----------------------------------------------------------------------------
/**
* Support the Iterable interface
*/
@Override
public Iterator<Map.Entry<String,T>> iterator() {
final NamedList<T> list = this;
Iterator<Map.Entry<String,T>> iter = new Iterator<Map.Entry<String,T>>() {
int idx = 0;
@Override
public boolean hasNext() {
return idx < list.size();
}
@Override
public Map.Entry<String,T> next() {
final int index = idx++;
Map.Entry<String,T> nv = new Map.Entry<String,T>() {
@Override
public String getKey() {
return list.getName( index );
}
@Override
public T getValue() {
return list.getVal( index );
}
@Override
public String toString() {
return getKey()+"="+getValue();
}
@Override
public T setValue(T value) {
return list.setVal(index, value);
}
};
return nv;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
return iter;
}
/**
* NOTE: this runs in linear time (it scans starting at the
* beginning of the list until it finds the first pair with
* the specified name).
*/
public T remove(String name) {
int idx = indexOf(name, 0);
if(idx != -1) return remove(idx);
return null;
}
/**
* Removes and returns all values for the specified name. Returns null if
* no matches found. This method will return all matching objects,
* regardless of data type. If you are parsing Solr config options, the
* {@link #removeConfigArgs(String)} or {@link #removeBooleanArg(String)}
* methods will probably work better.
*
* @param name Name
* @return List of values
*/
public List<T> removeAll(String name) {
List<T> result = getAll(name);
if (result.size() > 0 ) {
killAll(name);
return result;
}
return null;
}
/**
* Used for getting a boolean argument from a NamedList object. If the name
* is not present, returns null. If there is more than one value with that
* name, or if the value found is not a Boolean or a String, throws an
* exception. If there is only one value present and it is a Boolean or a
* String, the value is removed and returned as a Boolean. If an exception
* is thrown, the NamedList is not modified. See {@link #removeAll(String)}
* and {@link #removeConfigArgs(String)} for additional ways of gathering
* configuration information from a NamedList.
*
* @param name
* The key to look up in the NamedList.
* @return The boolean value found.
* @throws SolrException
* If multiple values are found for the name or the value found is
* not a Boolean or a String.
*/
public Boolean removeBooleanArg(final String name) {
Boolean bool = getBooleanArg(name);
if (null != bool) {
remove(name);
}
return bool;
}
/**
* Used for getting a boolean argument from a NamedList object. If the name
* is not present, returns null. If there is more than one value with that
* name, or if the value found is not a Boolean or a String, throws an
* exception. If there is only one value present and it is a Boolean or a
* String, the value is returned as a Boolean. The NamedList is not
* modified. See {@link #remove(String)}, {@link #removeAll(String)}
* and {@link #removeConfigArgs(String)} for additional ways of gathering
* configuration information from a NamedList.
*
* @param name The key to look up in the NamedList.
* @return The boolean value found.
* @throws SolrException
* If multiple values are found for the name or the value found is
* not a Boolean or a String.
*/
public Boolean getBooleanArg(final String name) {
Boolean bool;
List<T> values = getAll(name);
if (0 == values.size()) {
return null;
}
if (values.size() > 1) {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
"Only one '" + name + "' is allowed");
}
Object o = get(name);
if (o instanceof Boolean) {
bool = (Boolean)o;
} else if (o instanceof CharSequence) {
bool = Boolean.parseBoolean(o.toString());
} else {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
"'" + name + "' must have type Boolean or CharSequence; found " + o.getClass());
}
return bool;
}
/**
* Used for getting one or many arguments from NamedList objects that hold
* configuration parameters. Finds all entries in the NamedList that match
* the given name. If they are all strings or arrays of strings, remove them
* from the NamedList and return the individual elements as a {@link Collection}.
* Parameter order will be preserved if the returned collection is handled as
* an {@link ArrayList}. Throws SolrException if any of the values associated
* with the name are not strings or arrays of strings. If exception is
* thrown, the NamedList is not modified. Returns an empty collection if no
* matches found. If you need to remove and retrieve all matching items from
* the NamedList regardless of data type, use {@link #removeAll(String)} instead.
* The {@link #removeBooleanArg(String)} method can be used for retrieving a
* boolean argument.
*
* @param name
* The key to look up in the NamedList.
* @return A collection of the values found.
* @throws SolrException
* If values are found for the input key that are not strings or
* arrays of strings.
*/
public Collection<String> removeConfigArgs(final String name)
throws SolrException {
List<T> objects = getAll(name);
List<String> collection = new ArrayList<>(size() / 2);
final String err = "init arg '" + name + "' must be a string "
+ "(ie: 'str'), or an array (ie: 'arr') containing strings; found: ";
for (Object o : objects) {
if (o instanceof String) {
collection.add((String) o);
continue;
}
// If it's an array, convert to List (which is a Collection).
if (o instanceof Object[]) {
o = Arrays.asList((Object[]) o);
}
// If it's a Collection, collect each value.
if (o instanceof Collection) {
for (Object item : (Collection) o) {
if (!(item instanceof String)) {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, err + item.getClass());
}
collection.add((String) item);
}
continue;
}
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, err + o.getClass());
}
if (collection.size() > 0) {
killAll(name);
}
return collection;
}
public void clear() {
nvPairs.clear();
}
@Override
public int hashCode() {
return nvPairs.hashCode();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof NamedList)) return false;
NamedList<?> nl = (NamedList<?>) obj;
return this.nvPairs.equals(nl.nvPairs);
}
@Override
public void abortableForEach(BiFunction<String, ? super T, Boolean> fun) {
int sz = size();
for (int i = 0; i < sz; i++) {
if(!fun.apply(getName(i), getVal(i))) break;
}
}
@Override
public void abortableForEachKey(Function<String, Boolean> fun) {
int sz = size();
for (int i = 0; i < sz; i++) {
if(!fun.apply(getName(i))) break;
}
}
@Override
public void forEachKey(Consumer<String> fun) {
int sz = size();
for (int i = 0; i < sz; i++) {
fun.accept(getName(i));
}
}
public void forEach(BiConsumer<String, ? super T> action) {
int sz = size();
for (int i = 0; i < sz; i++) {
action.accept(getName(i), getVal(i));
}
}
@Override
public int _size() {
return size();
}
@Override
public void forEachEntry(BiConsumer<String, ? super T> fun) {
forEach(fun);
}
}