package cds.utils;
/*
* This file is part of ADQLLibrary.
*
* ADQLLibrary is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ADQLLibrary is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with ADQLLibrary. If not, see <http://www.gnu.org/licenses/>.
*
* Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
* Astronomisches Rechen Institut (ARI)
*/
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
/**
* <p>A TextualSearchList is an {@link ArrayList} with a textual search capability.</p>
* <p>
* The interest of this class lies in the fact that objects can be searched with
* or without case sensitivity on their textual key thanks to {@link #get(String, boolean)}.
* </p>
* <p>
* The textual key is extracted by an object implementing the {@link KeyExtractor} instance.
* If no {@link KeyExtractor} instance is given at initialization, the string returned
* by the {@link Object#toString() toString()} function will be used as key.
* </p>
* <p><b><u>WARNING:</u> The extracted key MUST be CASE-SENSITIVE and UNIQUE !</b></p>
*
* @param <E> Type of object to manage in this list.
* @author Grégory Mantelet (CDS;ARI)
* @version 1.1 (11/2013)
*/
public class TextualSearchList< E > extends ArrayList<E> {
private static final long serialVersionUID = 1L;
/** Object to use to extract an unique textual string. */
public final KeyExtractor<E> keyExtractor;
/** Map which associates objects of type E with its textual string (case-sensitive). */
protected final HashMap<String,ArrayList<E>> csMap;
/** Map which associates objects of type E with their lower-case textual string. */
protected final HashMap<String,ArrayList<E>> ncsMap;
/* ************ */
/* CONSTRUCTORS */
/* ************ */
/**
* <p>Builds an empty TextualSearchList.</p>
*
* <p><i><u>Note:</u>
* the key of inserted objects will be the string returned by their {@link Object#toString() toString()} function.
* </i></p>
*
* @see #TextualSearchList(KeyExtractor)
*/
public TextualSearchList(){
this(new DefaultKeyExtractor<E>());
}
/**
* Builds an empty TextualSearchList.
*
* @param keyExtractor The object to use to extract a textual key from objects to insert.
*
* @see ArrayList#ArrayList()
*/
public TextualSearchList(final KeyExtractor<E> keyExtractor){
super();
this.keyExtractor = keyExtractor;
csMap = new HashMap<String,ArrayList<E>>();
ncsMap = new HashMap<String,ArrayList<E>>();
}
/**
* <p>Builds an empty TextualSearchList with an initial capacity.</p>
*
* <p><i><u>Note:</u>
* the key of inserted objects will be the string returned by their {@link Object#toString() toString()} function.
* </i></p>
*
* @param initialCapacity Initial capacity of this list.
*
* @see #TextualSearchList(int, KeyExtractor)
*/
public TextualSearchList(int initialCapacity){
this(initialCapacity, new DefaultKeyExtractor<E>());
}
/**
* Builds an empty TextualSearchList with an initial capacity.
*
* @param initialCapacity Initial capacity of this list.
* @param keyExtractor The object to use to extract a textual key from objects to insert.
*
* @see ArrayList#ArrayList(int)
*/
public TextualSearchList(final int initialCapacity, final KeyExtractor<E> keyExtractor){
super(initialCapacity);
this.keyExtractor = keyExtractor;
csMap = new HashMap<String,ArrayList<E>>(initialCapacity);
ncsMap = new HashMap<String,ArrayList<E>>(initialCapacity);
}
/**
* <p>Builds a TextualSearchList filled with the objects of the given collection.</p>
*
* <p><i><u>Note:</u>
* the key of inserted objects will be the string returned by their {@link Object#toString() toString()} function.
* </i></p>
*
* @param c Collection to copy into this list.
*/
public TextualSearchList(Collection<? extends E> c){
this(c, new DefaultKeyExtractor<E>());
}
/**
* Builds a TextualSearchList filled with the objects of the given collection.
*
* @param c Collection to copy into this list.
* @param keyExtractor The object object to use to extract a textual key from objects to insert.
*
* @see #addAll(Collection)
*/
public TextualSearchList(Collection<? extends E> c, final KeyExtractor<E> keyExtractor){
this.keyExtractor = keyExtractor;
csMap = new HashMap<String,ArrayList<E>>(c.size());
ncsMap = new HashMap<String,ArrayList<E>>(c.size());
addAll(c);
}
/**
* Returns true if this list contains the specified element.
* More formally, returns true if and only if this list contains at least one element
* e such that (keyExtractor.getKey(o).equals(keyExtractor.getKey(e))).
*
* @see java.util.ArrayList#contains(java.lang.Object)
* @see #getKey(Object)
*
* @since 1.1
*/
@SuppressWarnings("unchecked")
@Override
public boolean contains(Object o){
try{
if (o == null)
return false;
else{
E object = (E)o;
return !get(getKey(object)).isEmpty();
}
}catch(Exception e){
return false;
}
}
/**
* Searches (CASE-INSENSITIVE) the object which has the given key.
*
* @param key Textual key of the object to search.
*
* @return The corresponding object or <code>null</code>.
*/
public final ArrayList<E> get(final String key){
return get(key, false);
}
/**
* Searches of all the object which has the given key.
*
* @param key Textual key of the object to search.
* @param caseSensitive <i>true</i> to consider the case of the key, <i>false</i> otherwise.
*
* @return All the objects whose the key is the same as the given one.
*/
@SuppressWarnings("unchecked")
public ArrayList<E> get(final String key, final boolean caseSensitive){
if (key == null)
return new ArrayList<E>(0);
ArrayList<E> founds = caseSensitive ? csMap.get(key) : ncsMap.get(key.toLowerCase());
if (founds == null)
return new ArrayList<E>(0);
else
return (ArrayList<E>)founds.clone();
}
/**
* Generates and checks the key of the given object.
*
* @param value The object whose the key must be generated.
*
* @return Its corresponding key or <i>null</i> if this object already exist in this list.
*
* @throws NullPointerException If the given object or its extracted key is <code>null</code>.
* @throws IllegalArgumentException If the extracted key is already used by another object in this list.
*/
private final String getKey(final E value) throws NullPointerException, IllegalArgumentException{
String key = keyExtractor.getKey(value);
if (key == null)
throw new NullPointerException("Null keys are not allowed in a TextualSearchList !");
return key;
}
/**
* Adds the given object in the two maps with the given key.
*
* @param key The key with which the given object must be associated.
* @param value The object to add.
*/
private final void putIntoMaps(final String key, final E value){
// update the case-sensitive map:
putIntoMap(csMap, key, value);
// update the case-INsensitive map:
putIntoMap(ncsMap, key.toLowerCase(), value);
}
/**
* Adds the given object in the given map with the given key.
*
* @param map The map in which the given value must be added.
* @param key The key with which the given object must be associated.
* @param value The object to add.
*
* @param <E> The type of objects managed in the given map.
*/
private static final < E > void putIntoMap(final HashMap<String,ArrayList<E>> map, final String key, final E value){
ArrayList<E> lst = map.get(key);
if (lst == null){
lst = new ArrayList<E>();
lst.add(value);
map.put(key, lst);
}else
lst.add(value);
}
/**
* Adds the given object at the end of this list.
*
* @param obj Object to add (different from NULL).
*
* @throws NullPointerException If the given object or its extracted key is <code>null</code>.
* @throws IllegalArgumentException If the extracted key is already used by another object in this list.
*
* @see java.util.ArrayList#add(java.lang.Object)
*/
@Override
public boolean add(E obj) throws NullPointerException, IllegalArgumentException{
if (obj == null)
throw new NullPointerException("Null objects are not allowed in a TextualSearchList !");
String key = getKey(obj);
if (key == null)
return false;
if (super.add(obj)){
putIntoMaps(key, obj);
return true;
}else
return false;
}
/**
* Adds the given object at the given position in this list.
*
* @param index Index at which the given object must be added.
* @param obj Object to add (different from NULL).
*
* @throws NullPointerException If the given object or its extracted key is <code>null</code>.
* @throws IllegalArgumentException If the extracted key is already used by another object in this list.
* @throws IndexOutOfBoundsException If the given index is negative or greater than the size of this list.
*
* @see java.util.ArrayList#add(int, java.lang.Object)
*/
@Override
public void add(int index, E obj) throws NullPointerException, IllegalArgumentException, IndexOutOfBoundsException{
if (obj == null)
throw new NullPointerException("Null objects are not allowed in a TextualSearchList !");
String key = getKey(obj);
if (key == null)
return;
super.add(index, obj);
if (get(index).equals(obj))
putIntoMaps(key, obj);
}
/**
* Appends all the objects of the given collection in this list.
*
* @param c Collection of objects to add.
*
* @return <code>true</code> if this list changed as a result of the call, <code>false</code> otherwise.
*
* @throws NullPointerException If an object to add or its extracted key is <code>null</code>.
* @throws IllegalArgumentException If the extracted key is already used by another object in this list.
*
* @see java.util.ArrayList#addAll(java.util.Collection)
* @see #add(Object)
*/
@Override
public boolean addAll(Collection<? extends E> c) throws NullPointerException, IllegalArgumentException{
if (c == null)
return false;
boolean modified = false;
for(E obj : c)
modified = add(obj) || modified;
return modified;
}
/**
* Appends all the objects of the given collection in this list after the given position.
*
* @param index Position from which objects of the given collection must be added.
* @param c Collection of objects to add.
*
* @return <code>true</code> if this list changed as a result of the call, <code>false</code> otherwise.
*
* @throws NullPointerException If an object to add or its extracted key is <code>null</code>.
* @throws IllegalArgumentException If the extracted key is already used by another object in this list.
* @throws IndexOutOfBoundsException If the given index is negative or greater than the size of this list.
*
* @see java.util.ArrayList#addAll(int, java.util.Collection)
* @see #add(int, Object)
*/
@Override
public boolean addAll(int index, Collection<? extends E> c) throws NullPointerException, IllegalArgumentException, IndexOutOfBoundsException{
if (c == null)
return false;
boolean modified = false;
int ind = index;
for(E obj : c){
add(ind++, obj);
modified = get(ind).equals(obj);
}
return modified;
}
/**
* Replaces the element at the specified position in this list with the specified element.
*
* @param index Position of the object to replace.
* @param obj Object to be stored at the given position (different from NULL).
*
* @return Replaced object.
*
* @throws NullPointerException If the object to add or its extracted key is <code>null</code>.
* @throws IllegalArgumentException If the extracted key is already used by another object in this list.
* @throws IndexOutOfBoundsException If the given index is negative or greater than the size of this list.
*
* @see java.util.ArrayList#set(int, java.lang.Object)
*/
@Override
public E set(int index, E obj) throws NullPointerException, IllegalArgumentException{
if (obj == null)
throw new NullPointerException("Null objects are not allowed in a TextualSearchList !");
if (get(index).equals(obj))
return obj;
String key = getKey(obj);
E old = super.set(index, obj);
// Removes the old object from the index:
String oldKey = keyExtractor.getKey(old);
removeFromMaps(oldKey, old);
// Adds the new object into the index:
putIntoMaps(key, obj);
return old;
}
@Override
public void clear(){
super.clear();
csMap.clear();
ncsMap.clear();
}
/**
* Removes the given object associated with the given key from the two maps.
*
* @param key The key associated with the given object.
* @param value The object to remove.
*/
private final void removeFromMaps(final String key, final E value){
// update the case-sensitive map:
removeFromMap(csMap, key, value);
// update the case-insensitive map:
removeFromMap(ncsMap, key.toLowerCase(), value);
}
/**
* Removes the given object associated with the given key from the given map.
*
* @param map The map from which the given object must be removed.
* @param key The key associated with the given object.
* @param value The object to remove.
*
* @param <E> The type of objects managed in the given map.
*/
private static final < E > void removeFromMap(final HashMap<String,ArrayList<E>> map, final String key, final E value){
ArrayList<E> lst = map.get(key);
if (lst != null){
lst.remove(value);
if (lst.isEmpty())
map.remove(key);
}
}
@Override
public E remove(int index){
E removed = super.remove(index);
if (removed != null){
String key = keyExtractor.getKey(removed);
removeFromMaps(key, removed);
}
return removed;
}
@SuppressWarnings("unchecked")
@Override
public boolean remove(Object obj){
boolean removed = super.remove(obj);
if (removed){
String key = keyExtractor.getKey((E)obj);
removeFromMaps(key, (E)obj);
}
return removed;
}
@Override
protected void removeRange(int fromIndex, int toIndex) throws IndexOutOfBoundsException{
if (fromIndex < 0 || fromIndex >= size() || toIndex < 0 || toIndex >= size() || fromIndex > toIndex)
throw new IndexOutOfBoundsException("Incorrect range indexes: from " + fromIndex + " to " + toIndex + " !");
for(int i = fromIndex; i < toIndex; i++)
remove(i);
}
/* ************************************************ */
/* KEY_EXTRACTOR INTERFACE & DEFAULT IMPLEMENTATION */
/* ************************************************ */
/**
* Lets extract an unique textual key (case-sensitive) from a given type of object.
*
* @author Gégory Mantelet (CDS)
* @param <E> Type of object from which a textual key must be extracted.
* @see TextualSearchList
*/
public static interface KeyExtractor< E > {
/**
* Extracts an UNIQUE textual key (case-sensitive) from the given object.
* @param obj Object from which a textual key must be extracted.
* @return Its textual key (case-sensitive).
*/
public String getKey(final E obj);
}
/**
* Default implementation of {@link KeyExtractor}.
* The extracted key is the string returned by the {@link Object#toString() toString()} function.
*
* @author Grégory Mantelet (CDS)
* @param <E> Type of object from which a textual key must be extracted.
*/
protected static class DefaultKeyExtractor< E > implements KeyExtractor<E> {
@Override
public String getKey(final E obj){
return obj.toString();
}
}
}