/*
* JBoss, Home of Professional Open Source.
* Copyright 2012, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This 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 2.1 of
* the License, or (at your option) any later version.
*
* This software 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 this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.capedwarf.search;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import com.google.appengine.api.search.Cursor;
import com.google.appengine.api.search.Document;
import com.google.appengine.api.search.Field;
import com.google.appengine.api.search.GetRequest;
import com.google.appengine.api.search.GetResponse;
import com.google.appengine.api.search.Index;
import com.google.appengine.api.search.OperationResult;
import com.google.appengine.api.search.PutResponse;
import com.google.appengine.api.search.Query;
import com.google.appengine.api.search.QueryOptions;
import com.google.appengine.api.search.Results;
import com.google.appengine.api.search.Schema;
import com.google.appengine.api.search.ScoredDocument;
import com.google.appengine.api.search.StatusCode;
import com.google.common.base.Function;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.SetMultimap;
import org.hibernate.search.query.dsl.QueryBuilder;
import org.hibernate.search.query.dsl.RangeTerminationExcludable;
import org.infinispan.Cache;
import org.infinispan.query.CacheQuery;
import org.infinispan.query.Search;
import org.infinispan.query.SearchManager;
import org.jboss.capedwarf.common.async.Wrappers;
/**
* @author <a href="mailto:mluksa@redhat.com">Marko Luksa</a>
* @author <a href="mailto:ales.justin@jboss.org">Ales Justin</a>
*/
@SuppressWarnings("deprecation")
public class CapedwarfSearchIndex implements Index {
private final static Function<Document.Builder, Document> FN = new Function<Document.Builder, Document>() {
public Document apply(Document.Builder input) {
return input.build();
}
};
private String name;
private String namespace;
private Cache<CacheKey, CacheValue> cache;
private SearchManager searchManager;
private SchemaAdapter schemaAdapter;
public CapedwarfSearchIndex(String name, String namespace, Cache<CacheKey, CacheValue> cache) {
this.name = name;
this.namespace = namespace;
this.cache = cache;
this.searchManager = Search.getSearchManager(cache);
this.schemaAdapter = new CapedwarfSchema(getSchemaName(name, namespace));
}
private String getSchemaName(String name, String namespace) {
return "__Schema__#" + name + "#" + normalizeNamespace(namespace);
}
public String getName() {
return name;
}
public String getNamespace() {
return namespace;
}
public long getStorageUsage() {
return -1; // TODO?
}
public long getStorageLimit() {
return -1; // TODO?
}
public Future<Void> removeAsync(String... documentIds) {
return removeAsync(Arrays.asList(documentIds));
}
public Future<Void> removeAsync(final Iterable<String> documentIds) {
return Wrappers.future(new Callable<Void>() {
public Void call() throws Exception {
remove(documentIds);
return null;
}
});
}
public Future<Results<ScoredDocument>> searchAsync(String queryString) {
return searchAsync(Query.newBuilder().build(queryString));
}
public Future<Results<ScoredDocument>> searchAsync(final Query query) {
return Wrappers.future(new Callable<Results<ScoredDocument>>() {
public Results<ScoredDocument> call() throws Exception {
return search(query);
}
});
}
public void remove(String... documentIds) {
remove(Arrays.asList(documentIds));
}
public void remove(Iterable<String> documentIds) {
delete(documentIds);
}
public Document get(String documentId) {
CacheValue value = cache.get(getCacheKey(documentId));
return (value != null ? value.getDocument() : null);
}
private CacheKey getCacheKey(String documentId) {
return new CacheKey(getName(), getNamespace(), documentId);
}
private CacheValue getCacheValue(Document document) {
return new CacheValue(getName(), getNamespace(), document);
}
@SuppressWarnings("UnusedParameters")
private String generateId(Document document) {
return UUID.randomUUID().toString();
}
public Results<ScoredDocument> search(String queryString) {
return search(Query.newBuilder().build(queryString));
}
public Results<ScoredDocument> search(Query query) {
CacheQuery cacheQuery = searchManager.getQuery(
createQueryBuilder().bool()
.must(createIndexAndNamespaceQuery())
.must(createLuceneQuery(query))
.createQuery());
if (query.getOptions() != null) {
cacheQuery.firstResult(query.getOptions().getOffset());
if (query.getOptions().getLimit() > 0) {
cacheQuery.maxResults(query.getOptions().getLimit());
}
}
List<ScoredDocument> scoredDocuments = new ArrayList<>();
for (Object o : cacheQuery.list()) {
CacheValue cacheValue = (CacheValue) o;
scoredDocuments.add(createScoredDocument(cacheValue.getDocument(), query));
}
OperationResult operationResult = new OperationResult(StatusCode.OK, null);
return newResults(operationResult, scoredDocuments, cacheQuery.getResultSize(), scoredDocuments.size(), null);
}
private org.apache.lucene.search.Query createIndexAndNamespaceQuery() {
QueryBuilder queryBuilder = createQueryBuilder();
return queryBuilder.bool()
.must(queryBuilder.keyword().onField("indexName").matching(getName()).createQuery())
.must(queryBuilder.keyword().onField("namespace").matching(normalizeNamespace(getNamespace())).createQuery())
.createQuery();
}
private String normalizeNamespace(String namespace) {
return namespace.isEmpty() ? CacheValue.EMPTY_NAMESPACE : namespace;
}
private QueryBuilder createQueryBuilder() {
return searchManager.buildQueryBuilderForClass(CacheValue.class).get();
}
private ScoredDocument createScoredDocument(Document document, Query query) {
ScoredDocument.Builder builder = ScoredDocument.newBuilder();
copyPropertiesToBuilder(document, builder, query.getOptions());
return builder.build();
}
private Document createCopyWithId(Document document, String id) {
Document.Builder builder = Document.newBuilder();
builder.setId(id);
copyPropertiesToBuilder(document, builder, null);
return builder.build();
}
private void copyPropertiesToBuilder(Document document, Document.Builder builder, QueryOptions options) {
builder.setId(document.getId());
builder.setLocale(document.getLocale());
builder.setRank(document.getRank());
for (Field field : document.getFields()) {
if (options == null || (!options.isReturningIdsOnly() && (options.getFieldsToReturn().isEmpty() || options.getFieldsToReturn().contains(field.getName())))) {
builder.addField(field);
}
}
}
private org.apache.lucene.search.Query createLuceneQuery(Query query) {
QueryConverter queryConverter = new QueryConverter(CacheValue.ALL_FIELD_NAME) {
@Override
protected GAEQueryTreeVisitor createTreeVisitor(String allFieldName) {
return new MultiFieldGAEQueryTreeVisitor(allFieldName);
}
};
return queryConverter.convert(query.getQueryString());
}
@SuppressWarnings("unchecked")
private Results<ScoredDocument> newResults(OperationResult operationResult, Collection<ScoredDocument> results, long numberFound, int numberReturned, Cursor cursor) {
return new Results<ScoredDocument>(operationResult, results, numberFound, numberReturned, cursor){};
}
private CacheQuery createListDocumentsQuery(GetRequest request) {
CacheQuery query;
if (request.getStartId() == null) {
query = searchManager.getQuery(createIndexAndNamespaceQuery());
} else {
query = searchManager.getQuery(
createQueryBuilder().bool()
.must(createIndexAndNamespaceQuery())
.must(createStartingIdQuery(request))
.createQuery());
}
if (request.getLimit() > 0) {
query.maxResults(request.getLimit());
}
return query;
}
private org.apache.lucene.search.Query createStartingIdQuery(GetRequest request) {
RangeTerminationExcludable range = createQueryBuilder().range().onField(CacheValue.ID_FIELD_NAME).above(request.getStartId());
if (!request.isIncludeStart()) {
range = range.excludeLimit();
}
return range.createQuery();
}
public Schema getSchema() {
return schemaAdapter.buildSchema();
}
public Future<Void> deleteSchemaAsync() {
return Wrappers.future(new Callable<Void>() {
public Void call() throws Exception {
deleteSchema();
return null;
}
});
}
public void deleteSchema() {
schemaAdapter.deleteSchema();
}
public Future<Void> deleteAsync(String... strings) {
return deleteAsync(Arrays.asList(strings));
}
public Future<Void> deleteAsync(final Iterable<String> strings) {
return Wrappers.future(new Callable<Void>() {
public Void call() throws Exception {
delete(strings);
return null;
}
});
}
public Future<PutResponse> putAsync(Document... documents) {
return putAsync(Arrays.asList(documents));
}
public Future<PutResponse> putAsync(final Document.Builder... builders) {
return Wrappers.future(new Callable<PutResponse>() {
public PutResponse call() throws Exception {
return put(builders);
}
});
}
public Future<PutResponse> putAsync(final Iterable<Document> documents) {
return Wrappers.future(new Callable<PutResponse>() {
public PutResponse call() throws Exception {
return put(documents);
}
});
}
public Future<GetResponse<Document>> getRangeAsync(final GetRequest getRequest) {
return Wrappers.future(new Callable<GetResponse<Document>>() {
public GetResponse<Document> call() throws Exception {
return getRange(getRequest);
}
});
}
public Future<GetResponse<Document>> getRangeAsync(final GetRequest.Builder builder) {
return Wrappers.future(new Callable<GetResponse<Document>>() {
public GetResponse<Document> call() throws Exception {
return getRange(builder);
}
});
}
public void delete(String... strings) {
delete(Arrays.asList(strings));
}
public void delete(Iterable<String> documentIds) {
for (String documentId : documentIds) {
CacheValue cacheValue = cache.remove(getCacheKey(documentId));
if (cacheValue != null) {
Document document = cacheValue.getDocument();
schemaAdapter.removeFields(document.getFieldNames());
}
}
}
public PutResponse put(Document... documents) {
return put(Arrays.asList(documents));
}
public PutResponse put(Document.Builder... builders) {
return put(toDocuments(builders));
}
public PutResponse put(Iterable<Document> documents) {
final List<OperationResult> results = new ArrayList<OperationResult>();
final List<String> ids = new ArrayList<String>();
final SetMultimap<String, Field.FieldType> fields = HashMultimap.create();
for (Document document : documents) {
StatusCode status = StatusCode.OK;
String errorDetail = null;
String id = null;
try {
Document documentWithId = document;
if (document.getId() == null) {
documentWithId = createCopyWithId(document, generateId(document));
}
cache.put(getCacheKey(documentWithId.getId()), getCacheValue(documentWithId));
id = documentWithId.getId();
} catch (Exception e) {
// TODO -- check err
status = StatusCode.INTERNAL_ERROR;
errorDetail = e.getMessage();
}
results.add(new OperationResult(status, errorDetail));
ids.add(id);
for (Field field : document.getFields()) {
fields.put(field.getName(), field.getType());
}
}
schemaAdapter.addFields(fields);
return new PutResponse(results, ids){};
}
public GetResponse<Document> getRange(GetRequest request) {
final List<Document> documents = new ArrayList<Document>();
final CacheQuery cacheQuery = createListDocumentsQuery(request);
for (Object o : cacheQuery.list()) {
CacheValue cacheValue = (CacheValue) o;
documents.add(cacheValue.getDocument());
}
return new GetResponse<Document>(documents){};
}
public GetResponse<Document> getRange(GetRequest.Builder builder) {
return getRange(builder.build());
}
protected static List<Document> toDocuments(Document.Builder... builders) {
return Lists.transform(Lists.newArrayList(builders), FN);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CapedwarfSearchIndex that = (CapedwarfSearchIndex) o;
if (!name.equals(that.name)) return false;
if (namespace != null ? !namespace.equals(that.namespace) : that.namespace != null) return false;
return true;
}
@Override
public int hashCode() {
int result = name.hashCode();
result = 31 * result + (namespace != null ? namespace.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "CapedwarfSearchIndex{" +
"name='" + name + '\'' +
", namespace='" + namespace + '\'' +
'}';
}
}