/* * $Id$ * * Copyright 2008 Glencoe Software, Inc. All rights reserved. * Use is subject to license terms supplied in LICENSE.txt */ package ome.services.eventlogs; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; import java.util.concurrent.atomic.AtomicBoolean; import ome.api.IQuery; import ome.model.IObject; import ome.model.meta.EventLog; import ome.parameters.Parameters; import ome.services.fulltext.FullTextIndexer; import ome.services.messages.ReindexMessage; import ome.tools.hibernate.QueryBuilder; import ome.util.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; /** * Data access object for the {@link FullTextIndexer} which provides an * {@link Iterator} interface for {@link EventLog} instances to be properly * indexed. Also supports the concept of batches. After {@link #batchSize} * queries, * * * @author Josh Moore, josh at glencoesoftware.com * @since 3.0-Beta3 */ public abstract class EventLogLoader implements Iterator<EventLog>, Iterable<EventLog>, ApplicationListener { protected final Logger log = LoggerFactory.getLogger(getClass()); /** * Currently 100. */ public final static int DEFAULT_BATCH_SIZE = 100; protected int batchSize = DEFAULT_BATCH_SIZE; /** * Set the number of {@link EventLog} instances will be loaded in a single * run. If not set, {@link #DEFAULT_BATCH_SIZE} will be used. * * @param batchSize */ public void setBatchSize(int batchSize) { this.batchSize = batchSize; } public int getBatchSize() { return this.batchSize; } /** * The number of objects which have been returned via {@link #next()}. If * {@link #count} is -1, then {@link #hasNext()} will temporarily return * null. This signals the end of a batch. A call to {@link #more()} will * test whether or not other batches are available. */ private int count = 0; /** * {@link List} of {@link EventLog} instances which will be consumed before * making use of the {@link #query()} method. Used to implement the default * {@link #rollback(EventLog)} mechanism. */ private final EventBacklog backlog = new EventBacklog(); /** * Marker set when {@link #stop()} is called in order to stop execution * after which {@link #hasNext()} will always return false. */ final private AtomicBoolean stop = new AtomicBoolean(false); private EventLog eventLog; /** * Array of class types which will get excluded from indexing. */ protected List<String> excludes = Collections.emptyList(); /** * Query string to be kept in sync with {@link #excludes}. * @see #initQueryString() */ protected String query; /** * Spring injector */ public void setExcludes(String[] excludes) { this.excludes = Collections.unmodifiableList(Arrays.asList(excludes)); initQueryString(); } /** * Build a query string based on the current {@link #excludes} {@link List}. * The query expects a single :id parameter to be set on execution. The * {@link #excludes} list is used to filter out unwanted {@link EventLog} * instances. */ private void initQueryString() { List<String> copy = excludes; // Instead of synchronizing QueryBuilder qb = new QueryBuilder(); qb.select("el"); qb.from("EventLog", "el"); qb.where(); qb.and("el.id > :id"); if (copy != null) { for (String exclude : copy) { qb.and("el.entityType != '" + exclude + "'"); } } qb.order("id", true); query = qb.queryString(); } protected IQuery queryService; /** * Spring injector */ public void setQueryService(IQuery queryService) { this.queryService = queryService; } /** * Tests for available objects. If {@link #count} is -1, then this batch has * ended (set in {@link #next()}) and false will be returned, * {@link EventBacklog} will be ready to be switched over to an "adding" * state if empty, and{@link #count} is also reset so further calls can * finish normally; otherwise {@link #query()} is called to load a new * {@link #eventLog}. Otherwise, just tests that field for null. */ public boolean hasNext() { if (stop.get()) { return false; } // If we have an event log, we always return true so that we always // have a clean slate. (And it's simply being honest) if (eventLog != null) { return true; } // If this is the first call in a batch, give the backlog a chance // to make this a backlog (removing-only) batch; if (count == 0) { backlog.flipState(); } // If we've done this enough, then bail out. if (count == batchSize) { count = 0; return false; } count++; // Do what we can to load an event log if (backlog.removingOnly()) { eventLog = backlog.remove(); } else { eventLog = query(); } boolean endBatch = eventLog == null; if (endBatch) { count = 0; } return !endBatch; } /** * Returns the current {@link #log} instance which may be loaded by a call * to {@link #hasNext()} if necessary. If {@link #hasNext()} returns false, * a {@link NoSuchElementException} will be thrown. */ public EventLog next() { // Consumer should have checked with hasNext if (!hasNext()) { throw new NoSuchElementException(); } // already loaded by call to hasNext() above EventLog rv = eventLog; eventLog = null; return rv; } public final void remove() { throw new UnsupportedOperationException("Cannot remove EventLogs"); } public void rollback(EventLog el) { if (excludes.contains(el.getEntityType())) { if (log.isDebugEnabled()) { log.debug("Skipping rollback of " + el.getEntityType()); } } backlog.add(el); } protected abstract EventLog query(); public Iterator<EventLog> iterator() { return this; } /** * Should return an estimate of how many more {@link EventLog} instances are * available for processing. Some implementations may attempt to take extra * measures if the number is too large. Use 1 for a constant rather than * {@link Long#MAX_VALUE}. Use 0 to stop execution. */ public abstract long more(); /** * Returns the {@link EventLog} with the next id after the given argument or * null if none exists. This method will only return "true" {@link EventLog} * instances, with a valid id. */ public final EventLog nextEventLog(long id) { if (query == null) { initQueryString(); } Parameters params = new Parameters().page(0, 1).addId(id); return queryService.findByQuery(query, params); } public final EventLog lastEventLog() { return queryService.findByQuery( "select el from EventLog el order by id desc", new Parameters().page(0, 1)); } // Re-Indexing // ========================================================================= /** * Adds an {@link EventLog} for the given {@link Class} and id to the * backlog. */ public boolean addEventLog(Class<? extends IObject> cls, long id) { if (excludes.contains(cls.getName())) { if (log.isDebugEnabled()) { log.debug("Skipping addition of " + cls.getName()); return false; } } EventLog el = new EventLog(); el.setEntityId(id); el.setEntityType(cls.getName()); el.setAction("INSERT"); return backlog.add(el); } @SuppressWarnings("unchecked") public void onApplicationEvent(ApplicationEvent arg0) { if (arg0 instanceof ReindexMessage) { ReindexMessage<? extends IObject> rm = (ReindexMessage<? extends IObject>) arg0; for (IObject obj : rm.objects) { Class trueClass = Utils.trueClass(obj.getClass()); addEventLog(trueClass, obj.getId()); } } } /** * Returns true if the stop flag has been set on this instance. */ public boolean isStopSet() { return this.stop.get(); } /** * Called by controlling objects (a worker thread) in order * to free up the thread. */ public void setStop(boolean stop) { if (stop) { log.info("Shutting down EventLogLoader"); } else { log.info("Restarting EventLogLoader"); } this.stop.set(stop); } }