/*
* Copyright 2007 Glencoe Software, Inc. All rights reserved.
* Use is subject to license terms supplied in LICENSE.txt
*/
package ome.services;
import java.io.Serializable;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import ome.annotations.RolesAllowed;
import ome.api.Search;
import ome.api.ServiceInterface;
import ome.conditions.ApiUsageException;
import ome.conditions.InternalException;
import ome.model.IObject;
import ome.model.annotations.Annotation;
import ome.model.internal.Details;
import ome.parameters.Parameters;
import ome.services.search.AnnotatedWith;
import ome.services.search.Complement;
import ome.services.search.FullText;
import ome.services.search.HqlQuery;
import ome.services.search.Intersection;
import ome.services.search.SearchAction;
import ome.services.search.SearchValues;
import ome.services.search.SimilarTerms;
import ome.services.search.SomeMustNone;
import ome.services.search.TagsAndGroups;
import ome.services.search.Union;
import ome.services.util.Executor;
import ome.system.SelfConfigurableService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.lucene.analysis.Analyzer;
import org.springframework.transaction.annotation.Transactional;
/**
* Implements the {@link Search} interface.
*
* @author Josh Moore, josh at glencoesoftware.com
* @since 3.0-Beta3
*/
@Transactional(readOnly = true)
public class SearchBean extends AbstractStatefulBean implements Search {
private final static long serialVersionUID = 59809384038000069L;
/** The logger for this class. */
private final static Logger log = LoggerFactory.getLogger(SearchBean.class);
private final ActionList actions = new ActionList();
private final SearchValues values = new SearchValues();
private final List<List<IObject>> results = new ArrayList<List<IObject>>();
private/* final */transient Executor executor;
private/* final */transient Class<? extends Analyzer> analyzer;
private/* final */transient Integer maxClauseCount;
public SearchBean(Executor executor, Class<? extends Analyzer> analyzer) {
this.executor = executor;
this.analyzer = analyzer;
}
public Class<? extends ServiceInterface> getServiceInterface() {
return Search.class;
}
/**
* Empty constructor required by EJB and
* {@link SelfConfigurableService self configuration}.
*/
public SearchBean() {
}
/**
* Injector used by Spring, currently, since
* {@link SelfConfigurableService#selfConfigure()} requires it.
*/
public void setExecutor(Executor executor) {
this.executor = executor;
}
/**
* Injector used by Spring.
*/
public void setAnalyzer(Class<? extends Analyzer> analyzer) {
this.analyzer = analyzer;
}
/**
* Injector used by Spring.
*/
public void setMaxClauseCount(Integer maxClauseCount) {
this.maxClauseCount = maxClauseCount;
}
// Lifecycle methods
// ===================================================
// See documentation on JobBean#passivate
@RolesAllowed("user")
@Transactional(readOnly = true)
public void passivate() {
// All state is passivatable.
}
// See documentation on JobBean#activate
@RolesAllowed("user")
@Transactional(readOnly = true)
public void activate() {
// State needs to be read back with synchronization.
}
@RolesAllowed("user")
@Transactional(readOnly = true)
public void close() {
// Could null state.
}
// Interface methods ~
// ============================================
//
// CREATE METHODS
//
@Transactional
@RolesAllowed("user")
public void byAnnotatedWith(Annotation... examples) {
SearchAction byAnnotatedWith;
synchronized (values) {
byAnnotatedWith = new AnnotatedWith(values, examples, false, false);
}
actions.add(byAnnotatedWith);
}
@Transactional
@RolesAllowed("user")
public void byFullText(String query) {
SearchAction byFullText;
synchronized (values) {
byFullText = new FullText(values, query, analyzer);
}
actions.add(byFullText);
}
@Transactional
@RolesAllowed("user")
public void byLuceneQueryBuilder(String fields, String from,
String to, String dateType, String query) {
SearchAction byFullText;
synchronized (values) {
byFullText = new FullText(values, fields, from,
to, dateType, query, analyzer);
}
actions.add(byFullText);
}
@Transactional
@RolesAllowed("user")
public void byHqlQuery(String query, Parameters p) {
SearchAction byHqlQuery;
synchronized (values) {
byHqlQuery = new HqlQuery(values, query, p);
}
actions.add(byHqlQuery);
}
@Transactional
@RolesAllowed("user")
public void bySomeMustNone(String[] some, String[] must, String[] none) {
SearchAction bySomeMustNone;
synchronized (values) {
bySomeMustNone = new SomeMustNone(values, some, must, none,
analyzer);
}
actions.add(bySomeMustNone);
}
@Transactional
@RolesAllowed("user")
public void bySimilarTerms(String...terms) {
SearchAction bySimilarTerms;
synchronized (values) {
bySimilarTerms = new SimilarTerms(values, terms);
}
actions.add(bySimilarTerms);
}
@Transactional
@RolesAllowed("user")
public void byGroupForTags(String group) {
SearchAction byTags;
synchronized (values) {
byTags = new TagsAndGroups(values, group, false);
}
actions.add(byTags);
}
@Transactional
@RolesAllowed("user")
public void byTagForGroups(String tag) {
SearchAction byTags;
synchronized (values) {
byTags = new TagsAndGroups(values, tag, true);
}
actions.add(byTags);
}
@Transactional
@RolesAllowed("user")
public void byUUID(String[] uuids) {
throw new UnsupportedOperationException();
}
// LOGICAL COMBINATIONS
@Transactional
@RolesAllowed("user")
public void or() {
actions.union();
}
@Transactional
@RolesAllowed("user")
public void and() {
actions.intersection();
}
@Transactional
@RolesAllowed("user")
public void not() {
actions.complement();
}
//
// FETCH METHODS
//
@Transactional
@RolesAllowed("user")
public boolean hasNext() {
while (results.size() > 0) {
List<IObject> first = results.get(0);
if (first == null || first.size() < 1) {
results.remove(0);
} else {
return true;
}
}
// There are no current results, we now need to execute an action
if (actions.size() == 0) {
return false;
}
SearchAction action = actions.popFirst();
List<IObject> list = (List<IObject>) executor.execute(null, action);
results.add(list);
return hasNext(); // recursive call
}
@Transactional
@RolesAllowed("user")
public IObject next() throws ApiUsageException {
if (!hasNext()) {
throw new ApiUsageException("No element. Please use hasNext().");
}
// Now we're guaranteed to have an element
return pop(results.get(0));
}
@Transactional
@RolesAllowed("user")
public Map<String, Annotation> currentMetadata() {
throw new UnsupportedOperationException();
}
@Transactional
@RolesAllowed("user")
public List<Map<String, Annotation>> currentMetadataList() {
throw new UnsupportedOperationException();
}
@Transactional
@RolesAllowed("user")
public <T extends IObject> List<T> results() {
if (!hasNext()) {
throw new ApiUsageException("No elements. Please use hasNext().");
}
// Now we're guaranteed to have an element
List<T> rv = new ArrayList<T>();
while (hasNext() && rv.size() < values.batchSize) {
List<IObject> current = results.get(0);
if (current.size() > 0) {
rv.add((T) pop(current));
} else {
// If batches aren't merged, we can exist now.
if (!values.mergedBatches) {
break;
}
}
}
return rv;
}
/**
* Wrapper method which should be called on all results for the user.
* Removes the value from the last list, and applies all requirements of
* {@link #values}.
*/
protected IObject pop(List<IObject> current) {
IObject obj = current.remove(0);
if (values.returnUnloaded) {
obj.unload();
}
return obj;
}
@Transactional
@RolesAllowed("user")
public void lastresultsAsWorkingGroup() {
throw new UnsupportedOperationException();
}
@Transactional
@RolesAllowed("user")
public void remove() throws UnsupportedOperationException {
throw new UnsupportedOperationException(
"Cannot remove via ome.api.Search");
}
//
// QUERY MANAGEMENT
//
@Transactional
@RolesAllowed("user")
public int activeQueries() {
return actions.size();
}
@Transactional
@RolesAllowed("user")
public void clearQueries() {
actions.clear();
}
//
// TEMPLATE STATE
//
@Transactional
@RolesAllowed("user")
public void resetDefaults() {
synchronized (values) {
values.copy(new SearchValues());
}
}
@Transactional
@RolesAllowed("user")
public void addOrderByAsc(String path) {
synchronized (values) {
values.orderBy.add("A" + path);
}
}
@Transactional
@RolesAllowed("user")
public void addOrderByDesc(String path) {
synchronized (values) {
values.orderBy.add("D" + path);
}
}
@Transactional
@RolesAllowed("user")
public void unordered() {
synchronized (values) {
values.orderBy.clear();
}
}
@Transactional
@RolesAllowed("user")
public <T extends IObject> void fetchAlso(Map<T, String> fetches) {
synchronized (values) {
throw new UnsupportedOperationException();
}
}
@Transactional
@RolesAllowed("user")
public void fetchAnnotations(Class... classes) {
synchronized (values) {
values.fetchAnnotations = new ArrayList();
for (Class k : classes) {
values.fetchAnnotations.add(k);
}
}
}
@Transactional
@RolesAllowed("user")
public int getBatchSize() {
synchronized (values) {
return values.batchSize;
}
}
@Transactional
@RolesAllowed("user")
public boolean isCaseSensitive() {
synchronized (values) {
return values.caseSensitive;
}
}
@Transactional
@RolesAllowed("user")
public boolean isMergedBatches() {
synchronized (values) {
return values.mergedBatches;
}
}
@Transactional
@RolesAllowed("user")
public void onlyAnnotatedBetween(Timestamp start, Timestamp stop) {
synchronized (values) {
values.annotatedStart = SearchValues.copyTimestamp(start);
values.annotatedStop = SearchValues.copyTimestamp(stop);
}
}
@Transactional
@RolesAllowed("user")
public void onlyAnnotatedBy(Details d) {
synchronized (values) {
values.annotatedBy = SearchValues.copyDetails(d);
}
}
@Transactional
@RolesAllowed("user")
public void notAnnotatedBy(Details d) {
synchronized (values) {
values.notAnnotatedBy = SearchValues.copyDetails(d);
}
}
@Transactional
@RolesAllowed("user")
public void onlyAnnotatedWith(Class... classes) {
synchronized (values) {
if (classes == null) {
values.onlyAnnotatedWith = null;
} else {
List<Class> list = Arrays.<Class> asList(classes);
values.onlyAnnotatedWith = SearchValues.copyList(list);
}
}
}
@Transactional
@RolesAllowed("user")
public void onlyCreatedBetween(Timestamp start, Timestamp stop) {
synchronized (values) {
values.createdStart = SearchValues.copyTimestamp(start);
values.createdStop = SearchValues.copyTimestamp(stop);
if (start != null && stop != null) {
if (stop.getTime() < start.getTime()) {
log.warn("FullText search created with "
+ "creation stop before start");
}
}
}
}
@Transactional
@RolesAllowed("user")
public void onlyOwnedBy(Details d) {
synchronized (values) {
values.ownedBy = SearchValues.copyDetails(d);
}
}
@Transactional
@RolesAllowed("user")
public void onlyIds(Long... ids) {
synchronized (values) {
if (ids == null) {
values.onlyIds = null;
} else {
values.onlyIds = Arrays.asList(ids);
}
}
}
@Transactional
@RolesAllowed("user")
public void notOwnedBy(Details d) {
synchronized (values) {
values.notOwnedBy = SearchValues.copyDetails(d);
}
}
@Transactional
@RolesAllowed("user")
public void allTypes() {
throw new UnsupportedOperationException();
}
@Transactional
@RolesAllowed("user")
@SuppressWarnings("all")
public <T extends IObject> void onlyType(Class<T> klass) {
onlyTypes(klass);
}
@Transactional
@RolesAllowed("user")
@SuppressWarnings("unchecked")
public <T extends IObject> void onlyTypes(Class<T>... classes) {
synchronized (values) {
values.onlyTypes = new ArrayList();
for (Class<T> k : classes) {
values.onlyTypes.add(k);
}
}
}
@Transactional
@RolesAllowed("user")
@SuppressWarnings("unchecked")
public void setAllowLeadingWildcard(boolean allowLeadingWildcard) {
synchronized (values) {
values.leadingWildcard = allowLeadingWildcard;
}
}
@Transactional
@RolesAllowed("user")
public void setBatchSize(int size) {
synchronized (values) {
values.batchSize = size;
}
}
@Transactional
@RolesAllowed("user")
public void setIdOnly() {
synchronized (values) {
values.idOnly = true;
}
}
@Transactional
@RolesAllowed("user")
public void setMergedBatches(boolean merge) {
synchronized (values) {
values.mergedBatches = merge;
}
}
@Transactional
@RolesAllowed("user")
public void fetchAlso(String... fetches) {
synchronized (values) {
values.fetches = Arrays.asList(fetches);
}
}
@Transactional
@RolesAllowed("user")
public boolean isAllowLeadingWildcard() {
synchronized (values) {
return values.leadingWildcard;
}
}
@Transactional
@RolesAllowed("user")
public boolean isReturnUnloaded() {
synchronized (values) {
return values.returnUnloaded;
}
}
@Transactional
@RolesAllowed("user")
public boolean isUseProjections() {
synchronized (values) {
return values.useProjections;
}
}
@Transactional
@RolesAllowed("user")
public void onlyModifiedBetween(Timestamp start, Timestamp stop) {
synchronized (values) {
values.modifiedStart = SearchValues.copyTimestamp(start);
values.modifiedStop = SearchValues.copyTimestamp(stop);
if (start != null && stop != null) {
if (stop.getTime() < start.getTime()) {
log.warn("FullText search created "
+ "with modification stop before start");
}
}
}
}
@Transactional
@RolesAllowed("user")
public void setCaseSentivice(boolean caseSensitive) {
synchronized (values) {
values.caseSensitive = caseSensitive;
}
}
@Transactional
@RolesAllowed("user")
public void setReturnUnloaded(boolean returnUnloaded) {
synchronized (values) {
values.returnUnloaded = returnUnloaded;
}
}
@Transactional
@RolesAllowed("user")
public void setUseProjections(boolean useProjections) {
throw new UnsupportedOperationException();
// Before activating, please test heavily.
// In fact, this may need to be removed,
// since much of the security in Lucene
// is based on the db.
// synchronized (values) {
// values.useProjections = useProjections;
// }
}
//
// LOCAL API (mostly for testing)
//
public void addAction(SearchAction action) {
if (action == null) {
throw new IllegalArgumentException("Action cannot be null");
}
synchronized (actions) {
actions.add(action);
}
}
public void addResult(List<IObject> result) {
synchronized (results) {
results.add(result); // Can be null as flag?
}
}
public void addParameters(Parameters params) {
synchronized (values) {
values.copy(params);
}
}
/**
* Synchronized helper collection for maintaining {@link SearchAction}
* instances. Also knows how to do logical joins (union, etc.)
*/
private static class ActionList implements Serializable {
private static final long serialVersionUID = 1L;
enum State {
normal, union, intersection, complement;
}
private State state = State.normal;
final private List<SearchAction> actions = new ArrayList<SearchAction>();
synchronized void union() {
state = State.union;
}
synchronized void intersection() {
state = State.intersection;
}
synchronized void complement() {
state = State.complement;
}
synchronized void add(SearchAction b) {
// Any call to "add" reset the state of the ActionList
State previousState = state;
this.state = State.normal;
SearchAction a;
switch (previousState) {
case normal:
actions.add(b);
break;
case union:
a = popLast();
actions.add(new Union(b.copyOfValues(), a, b));
break;
case intersection:
a = popLast();
actions.add(new Intersection(b.copyOfValues(), a, b));
break;
case complement:
a = popLast();
actions.add(new Complement(b.copyOfValues(), a, b));
break;
default:
throw new InternalException("Unknown state:" + state);
}
}
synchronized int size() {
return actions.size();
}
synchronized void clear() {
actions.clear();
}
synchronized SearchAction popFirst() {
assertNonZero();
return actions.remove(0);
}
synchronized SearchAction popLast() {
assertNonZero();
return actions.remove(actions.size() - 1);
}
synchronized void assertNonZero() {
if (actions.size() == 0) {
throw new ApiUsageException("There must be at least 1"
+ " active query for this operation.");
}
}
}
}