package com.miguelfonseca.completely;
import com.miguelfonseca.completely.data.Indexable;
import com.miguelfonseca.completely.data.ScoredObject;
import com.miguelfonseca.completely.text.analyze.Analyzer;
import com.miguelfonseca.completely.text.analyze.ChainedAnalyzer;
import com.miguelfonseca.completely.text.index.Index;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import javax.annotation.Nullable;
import static com.miguelfonseca.completely.common.Precondition.checkArgument;
import static com.miguelfonseca.completely.common.Precondition.checkPointer;
/**
* Facade for indexing and searching {@link Indexable} elements.
*/
public final class AutocompleteEngine<T extends Indexable>
{
private final Analyzer analyzer;
private final Comparator<ScoredObject<T>> comparator;
private final IndexAdapter<T> index;
private final Lock read;
private final Lock write;
private AutocompleteEngine(Builder<T> builder)
{
assert builder != null;
this.analyzer = builder.analyzer;
this.comparator = builder.comparator;
this.index = builder.index;
ReadWriteLock lock = new ReentrantReadWriteLock();
this.read = lock.readLock();
this.write = lock.writeLock();
}
/**
* Indexes a single element.
*
* @throws NullPointerException if {@code element} is null;
*/
public boolean add(T element)
{
return addAll(Arrays.asList(element));
}
/**
* Indexes a collection of elements.
*
* @throws NullPointerException if {@code elements} is null or contains a null element;
*/
public boolean addAll(Collection<T> elements)
{
checkPointer(elements != null);
boolean result = false;
for (T element : elements)
{
checkPointer(element != null);
write.lock();
try
{
for (String field : element.getFields())
{
for (String token : analyzer.apply(field))
{
result |= index.put(token, element);
}
}
}
finally
{
write.unlock();
}
}
return result;
}
/**
* Removes a single element.
*
* @throws NullPointerException if {@code element} is null;
*/
public boolean remove(T element)
{
return removeAll(Arrays.asList(element));
}
/**
* Removes a collection of elements.
*
* @throws NullPointerException if {@code elements} is null or contains a null element;
*/
public boolean removeAll(Collection<T> elements)
{
checkPointer(elements != null);
boolean result = false;
for (T element : elements)
{
checkPointer(element != null);
write.lock();
try
{
result |= index.remove(element);
}
finally
{
write.unlock();
}
}
return result;
}
/**
* Returns a {@link List} of all elements that match a query, sorted
* according to the default comparator.
*
* @throws NullPointerException if {@code query} is null;
*/
public List<T> search(String query)
{
checkPointer(query != null);
read.lock();
try
{
Aggregator<T> aggregator = new Aggregator<>(comparator);
Iterator<String> tokens = analyzer.apply(query).iterator();
if (tokens.hasNext())
{
aggregator.addAll(index.get(tokens.next()));
}
while (tokens.hasNext())
{
if (aggregator.isEmpty())
{
break;
}
aggregator.retainAll(index.get(tokens.next()));
}
return aggregator.values();
}
finally
{
read.unlock();
}
}
/**
* Returns a {@link List} of the top elements that match a query, sorted
* according to the default comparator.
*
* @throws NullPointerException if {@code query} is null;
* @throws IllegalArgumentException if {@code limit} is negative;
*/
public List<T> search(String query, int limit)
{
checkArgument(limit >= 0);
List<T> result = search(query);
if (result.size() > limit)
{
return result.subList(0, limit);
}
return result;
}
/**
* Builder for constructing {@link AutocompleteEngine} instances.
*/
public static class Builder<T extends Indexable>
{
private Analyzer analyzer;
private Comparator<ScoredObject<T>> comparator;
private IndexAdapter<T> index;
/**
* Constructs a new {@link AutocompleteEngine.Builder}.
*/
public Builder()
{
this.analyzer = new Analyzer()
{
@Override
public Collection<String> apply(Collection<String> input)
{
return new ArrayList<>(input);
}
};
}
/**
* Set the analyzer.
*/
public Builder<T> setAnalyzer(Analyzer analyzer)
{
this.analyzer = analyzer;
return this;
}
/**
* Set the analyzer.
*/
public Builder<T> setAnalyzers(Analyzer... analyzers)
{
this.analyzer = new ChainedAnalyzer(analyzers);
return this;
}
/**
* Set the comparator.
*/
public Builder<T> setComparator(@Nullable Comparator<ScoredObject<T>> comparator)
{
this.comparator = comparator;
return this;
}
/**
* Set the index.
*
* @throws NullPointerException if {@code index} is null;
*/
public Builder<T> setIndex(final Index<T> index)
{
checkPointer(index != null);
return setIndex(new IndexAdapter<T>()
{
@Override
public Collection<ScoredObject<T>> get(String token)
{
List<ScoredObject<T>> result = new LinkedList<>();
for (T element : index.getAll(token))
{
result.add(new ScoredObject<>(element, 0));
}
return result;
}
@Override
public boolean put(String token, T value)
{
return index.put(token, value);
}
@Override
public boolean remove(T value)
{
return index.remove(value);
}
});
}
/**
* Set the index.
*/
public Builder<T> setIndex(IndexAdapter<T> index)
{
this.index = index;
return this;
}
/**
* Returns a new {@link AutocompleteEngine} parameterized according to
* the builder.
*
* @throws NullPointerException if {@code analyzer} or {@code index} are null;
*/
public AutocompleteEngine<T> build()
{
checkPointer(analyzer != null);
checkPointer(index != null);
return new AutocompleteEngine<>(this);
}
}
}