JDBCProductIndex.java

/*
 * JDBCProductIndex
 */
package gov.usgs.earthquake.indexer;

import gov.usgs.earthquake.product.ProductId;
import gov.usgs.earthquake.util.JDBCConnection;
import gov.usgs.util.Config;
import gov.usgs.util.JDBCUtils;
import gov.usgs.util.StreamUtils;
import gov.usgs.util.StringUtils;

import java.io.File;
import java.math.BigDecimal;
import java.net.URI;
import java.net.URL;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

/**
 * JDBC Implementation of {@link ProductIndex}.
 */
public class JDBCProductIndex extends JDBCConnection implements ProductIndex {

	/** Logging Utility **/
	private static final Logger LOGGER = Logger.getLogger(Indexer.class
			.getName());

	/** _____ First, set up some constants _____ */

	/**
	 * Default index file. Copied into file system as JDBC_DEFAULT_FILE if
	 * doesn't already exist.
	 */
	private static final String JDBC_DEFAULT_INDEX = "etc/schema/productIndex.db";

	private static final String JDBC_DEFAULT_DRIVER = JDBCUtils.SQLITE_DRIVER_CLASSNAME;

	/**
	 * Default index file. Created by copying JDBC_DEFAULT_INDEX out of Jar if
	 * doesn't already exist in file system
	 */
	public static final String JDBC_DEFAULT_FILE = "productIndex.db";

	/**
	 * Constant used to specify what the index file property should be called in
	 * to config file
	 */
	private static final String JDBC_FILE_PROPERTY = "indexfile";

	/** Prefix for connecting to a sqlite database */
	private static final String JDBC_CONNECTION_PREFIX = "jdbc:sqlite:";

	/** Variables to store the event and product column names */
	// private static final String EVENT_TABLE = "event";
	private static final String EVENT_TABLE_ALIAS = "e";
	// private static final String EVENT_INDEX_ID = "id";
	// private static final String EVENT_CREATED = "created";
	// private static final String EVENT_UPDATED = "updated";
	// private static final String EVENT_SOURCE = "source";
	// private static final String EVENT_SOURCE_CODE = "sourceCode";
	private static final String EVENT_TIME = "eventTime";
	private static final String EVENT_LATITUDE = "latitude";
	private static final String EVENT_LONGITUDE = "longitude";
	private static final String EVENT_DEPTH = "depth";
	private static final String EVENT_MAGNITUDE = "magnitude";
	// private static final String EVENT_STATUS = "status";

	private static final String EVENT_STATUS_UPDATE = "UPDATE";
	private static final String EVENT_STATUS_DELETE = "DELETE";

	private static final String SUMMARY_TABLE = "productSummary";
	private static final String SUMMARY_TABLE_ALIAS = "p";
	// private static final String SUMMARY_CREATED = "created";
	/** Public var for summary product index IDs */
	public static final String SUMMARY_PRODUCT_INDEX_ID = "id";
	private static final String SUMMARY_PRODUCT_ID = "productId";
	// private static final String SUMMARY_EVENT_ID = "eventId";
	private static final String SUMMARY_TYPE = "type";
	private static final String SUMMARY_SOURCE = "source";
	private static final String SUMMARY_CODE = "code";
	private static final String SUMMARY_UPDATE_TIME = "updateTime";
	private static final String SUMMARY_EVENT_SOURCE = "eventSource";
	private static final String SUMMARY_EVENT_SOURCE_CODE = "eventSourceCode";
	private static final String SUMMARY_EVENT_TIME = "eventTime";
	private static final String SUMMARY_EVENT_LATITUDE = "eventLatitude";
	private static final String SUMMARY_EVENT_LONGITUDE = "eventLongitude";
	private static final String SUMMARY_EVENT_DEPTH = "eventDepth";
	private static final String SUMMARY_EVENT_MAGNITUDE = "eventMagnitude";
	private static final String SUMMARY_VERSION = "version";
	private static final String SUMMARY_STATUS = "status";
	private static final String SUMMARY_TRACKER_URL = "trackerURL";
	private static final String SUMMARY_PREFERRED = "preferred";
	// private static final String SUMMARY_PROPERTY_TABLE = "productSummaryProperty";
	// private static final String SUMMARY_PROPERTY_ID = "productSummaryIndexId";
	// private static final String SUMMARY_PROPERTY_NAME = "name";
	// private static final String SUMMARY_PROPERTY_VALUE = "value";
	// private static final String SUMMARY_LINK_TABLE = "productSummaryLink";
	// private static final String SUMMARY_LINK_ID = "productSummaryIndexId";
	// private static final String SUMMARY_LINK_RELATION = "relation";
	// private static final String SUMMARY_LINK_URL = "url";

	private String index_file;

	/**
	 * Constructor. Sets index_file to the default value JDBC_DEFAULT_FILE
	 *
	 * @throws Exception if error occurs
	 */
	public JDBCProductIndex() throws Exception {
		// Default index file, so calling configure() isn't required
		index_file = JDBC_DEFAULT_FILE;
		setDriver(JDBC_DEFAULT_DRIVER);
	}

	/**
	 * Constructor. Uses custom index_file
	 * @param sqliteFileName String for sqlite file name
	 * @throws Exception if error occurs
	 */
	public JDBCProductIndex(final String sqliteFileName) throws Exception {
		index_file = sqliteFileName;
		setDriver(JDBC_DEFAULT_DRIVER);
	}

	// ____________________________________
	// Public Methods
	// ____________________________________

	/**
	 * Grab values from the Config object and put them into private variables.
	 *
	 * @param config
	 *            Configuration for the product index
	 */
	@Override
	public void configure(Config config) throws Exception {
		super.configure(config);
		index_file = config.getProperty(JDBC_FILE_PROPERTY);

		if (getDriver() == null) { setDriver(JDBC_DEFAULT_DRIVER); }
		if (index_file == null || "".equals(index_file)) {
			index_file = JDBC_DEFAULT_FILE;
		}
	}

	/**
	 * Return a connection to the database.
	 *
	 * @return Connection object
	 * @throws Exception if error occurs
	 */
	@Override
	public Connection connect() throws Exception {
		// If they are using the sqlite driver, we need to try to create the
		// file
		if (getDriver().equals(JDBCUtils.SQLITE_DRIVER_CLASSNAME)) {
			// Make sure file exists or copy it out of the JAR
			File indexFile = new File(index_file);
			if (!indexFile.exists()) {
				// extract schema from jar
				URL schemaURL = JDBCProductIndex.class.getClassLoader()
						.getResource(JDBC_DEFAULT_INDEX);
				if (schemaURL != null) {
					StreamUtils.transferStream(schemaURL, indexFile);
				} else {
					// Failed. Probably because we're not in a Jar file
					File defaultIndex = new File(JDBC_DEFAULT_INDEX);
					StreamUtils.transferStream(defaultIndex, indexFile);
				}
			}
			indexFile = null;

			// Build the JDBC url
			setUrl(JDBC_CONNECTION_PREFIX + index_file);
		}
		return super.connect();
	}

	/**
	 * Return all events from the database that meet the parameters specified in
	 * the ProductIndexQuery object.
	 *
	 * @param query
	 *            A description of which events to retrieve.
	 * @return List of Event objects
	 */
	@Override
	public synchronized List<Event> getEvents(ProductIndexQuery query)
			throws Exception {
		if (query == null) {
			return new ArrayList<Event>();
		}
		// map of events (index id => event), so products can be added incrementally
		final Map<Long, Event> events = new HashMap<>();
		// all products for loading details
		ArrayList<ProductSummary> products = new ArrayList<>();

		// Build up our clause list like always
		// These clauses may only match certain products within events,
		// and are used to find a list of event ids
		List<String> clauses = buildProductClauses(query);

		// Build the SQL Query from our ProductIndexQuery object
		String sql = "SELECT DISTINCT ps2.*"
				+ " FROM productSummary ps2,"
				+ " (SELECT DISTINCT e.id FROM event e, productSummary p"
				+ " WHERE e.id=p.eventId";
		// Add all appropriate where clauses
		for (final String clause : clauses) {
			sql = sql + " AND " + clause;
		}
		sql = sql + ") eventids"
				+ " WHERE ps2.eventid=eventids.id";

		// add current clause to outer query
		if (query.getResultType() == ProductIndexQuery.RESULT_TYPE_CURRENT) {
			sql = sql + " AND NOT EXISTS ("
					+ " SELECT * FROM productSummary"
					+ " WHERE source=ps2.source"
					+ " AND type=ps2.type"
					+ " AND code=ps2.code"
					+ " AND updateTime>ps2.updateTime"
					+ ")";
		}

		// load event products
		try (
			final PreparedStatement statement = getConnection().prepareStatement(sql);
			final ResultSet results = statement.executeQuery();
		) {
			statement.setQueryTimeout(60);
			while (results.next()) {
				// eventid not part of product summary object,
				// so need to do this as products are parsed...
				final Long id = results.getLong("eventId");
				Event event = events.get(id);
				if (event == null) {
					// create event to hold products
					event = new Event();
					event.setIndexId(id);
					events.put(id, event);
				}
				final ProductSummary productSummary = parseProductSummary(results);
				event.addProduct(productSummary);
				products.add(productSummary);
			}
		}

		// load product details
		loadProductSummaries(products);

		return events.values().stream().collect(Collectors.toList());
	}

	/**
	 * Add an event to the database
	 *
	 * @param event
	 *            Event to store
	 * @return Event object with eventId set to the database id
	 */
	@Override
	public synchronized Event addEvent(Event event) throws Exception {
		Event e = null;

		final String sql = "INSERT INTO event (created) VALUES (?)";
		try (
			final PreparedStatement insertEvent =
					getConnection().prepareStatement(sql, new String[] {"id"});
		) {
			insertEvent.setQueryTimeout(60);
			// Add the values to the prepared statement
			JDBCUtils.setParameter(insertEvent, 1, new Date().getTime(), Types.BIGINT);

			// Execute the prepared statement
			int rows = insertEvent.executeUpdate();

			if (rows == 1) {
				long id = 0;
				try (final ResultSet keys = insertEvent.getGeneratedKeys()) {
					while (keys.next()) {
						id = keys.getLong(1);
					}
					e = new Event(event);
					e.setIndexId(id);
				}
				LOGGER.finest("Added event id=" + id);
			} else {
				LOGGER.log(Level.WARNING, "[" + getName()
						+ "] Exception when adding new event to database");
				throw new Exception("Error adding new event to database");
			}
		}
		LOGGER.log(Level.FINEST, "[" + getName() + "] Added event to Product Index");
		return e;
	}

	/**
	 * Delete an event from the database.
	 *
	 * @param event
	 *            Event to remove
	 * @return List containing all the ProductIds that were deleted by the
	 *         method call
	 */
	@Override
	public synchronized List<ProductId> removeEvent(Event event)
			throws Exception {

		Long id = event.getIndexId();
		// If there is no index id on the event, we can assume its
		// not in the database
		if (id == null) {
			return null;
		}

		// remove event products
		final List<ProductId> productIds = removeProductSummaries(event.getProductList());

		// and now remove event
		final String sql = "DELETE FROM event WHERE id=?";
		try (
			final PreparedStatement deleteEvent = getConnection().prepareStatement(sql);
		) {
			deleteEvent.setQueryTimeout(60);
			JDBCUtils.setParameter(deleteEvent, 1, id, Types.BIGINT);
			int rows = deleteEvent.executeUpdate();
			// If we didn't delete a row, or we deleted more than 1 row, throw an
			// exception
			if (rows != 1) {
				LOGGER.log(Level.WARNING, "[" + getName()
						+ "] Exception when deleting an event from the database");
				throw new Exception("Error deleting event from database");
			}

			LOGGER.finest("[" + getName() + "] Removed event id=" + id);
		}

		return productIds;
	}

	/**
	 * Return all products that aren't associated with an event.
	 *
	 * @param query
	 *            ProductIndexQuery used to further limit the results
	 * @return List of unassociated Products
	 * @throws IllegalArgumentException
	 *             when query event search type is SEARCH_EVENT_PREFERRED.
	 */
	@Override
	public synchronized List<ProductSummary> getUnassociatedProducts(
			ProductIndexQuery query) throws Exception {
		if (query.getEventSearchType() == ProductIndexQuery.SEARCH_EVENT_PREFERRED) {
			throw new IllegalArgumentException(
					"getUnassociatedProducts does not support SEARCH_EVENT_PREFERRED");
		}

		final ArrayList<ProductSummary> products = new ArrayList<ProductSummary>();

		final List<String> clauseList = buildProductClauses(query);
		// Add the unassociated quantifier to the clause list
		clauseList.add("eventId IS NULL");
		final String sql = buildProductQuery(query, clauseList);

		try (
			final PreparedStatement statement = getConnection().prepareStatement(sql);
		) {
			statement.setQueryTimeout(60);
			try (
				final ResultSet results = statement.executeQuery();
			) {
				// Now lets build product objects from each row in the result set
				while (results.next()) {
					products.add(parseProductSummary(results));
				}
			}
		}

		// load properties and links
		loadProductSummaries(products);

		return products;
	}

	/**
	 * Return all products that meet the parameters specified in the
	 * ProductIndexQuery object.
	 *
	 * @param query
	 *            A description of which products to retrieve.
	 * @return List of ProductSummary objects
	 * @throws IllegalArgumentException
	 *             when query event search type is SEARCH_EVENT_PREFERRED.
	 */
	@Override
	public synchronized List<ProductSummary> getProducts(ProductIndexQuery query)
			throws Exception {
		// load full product summaries by default
		return getProducts(query, true);
	}

	/**
	 * Load product summaries.
	 *
	 * @param query
	 *     product query
	 * @param loadDetails
	 *     whether to call {@link #loadProductSummaries(List)},
	 *     which loads links and properties with additional queries.
	 * @return
	 *     A list of loaded product summaries
	 * @throws Exception
	 *     if error occurs
	 */
	public synchronized List<ProductSummary> getProducts(ProductIndexQuery query, final boolean loadDetails)
			throws Exception {
		final String sql = buildProductQuery(query);

		final List<ProductSummary> products = new LinkedList<ProductSummary>();
		LOGGER.finer("Executing query " + sql);
		try (
			final PreparedStatement statement = getConnection().prepareStatement(sql);
		) {
			statement.setQueryTimeout(60);
			try (
				final ResultSet results = statement.executeQuery();
			) {
				// Now lets build product objects from each row in the result set
				while (results.next()) {
					products.add(parseProductSummary(results));
				}
			}
		}

		if (loadDetails) {
			// load properties and links
			loadProductSummaries(products);
		}

		return products;
	}

	/**
	 * Check whether product summary is in index.
	 *
	 * @param id
	 *     product to search.
	 */
	public synchronized boolean hasProduct(final ProductId id) throws Exception {
		final String sql = "SELECT id FROM productSummary"
				+ " WHERE source=? AND type=? AND code=? AND updateTime=?";
		try (
			final PreparedStatement statement = getConnection().prepareStatement(sql);
		) {
			statement.setQueryTimeout(60);
			statement.setString(1, id.getSource());
			statement.setString(2, id.getType());
			statement.setString(3, id.getCode());
			statement.setLong(4, id.getUpdateTime().getTime());

			try (
				final ResultSet results = statement.executeQuery();
			) {
				// return true if there is a matching row, false otherwise
				return results.next();
			}
		}
	}

	/**
	 * Add a product summary to the database
	 *
	 * @param summary
	 *            ProductSummary object to store. Must not be null.
	 * @return Copy of the product summary object with the indexId set to the
	 *         newly inserted id.
	 * @throws Exception if error occurs
	 */
	@Override
	public synchronized ProductSummary addProductSummary(ProductSummary summary)
			throws Exception {
		// Add values to the prepared statement
		long productId = 0;
		final ProductId sid = summary.getId();

		final String sql = "INSERT INTO productSummary"
				+ "(created, productId, type, source, code"
				+ ", updateTime, eventSource, eventSourceCode, eventTime"
				+ ", eventLatitude, eventLongitude, eventDepth, eventMagnitude"
				+ ", version, status, trackerURL, preferred"
				+ ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
		try (
			final PreparedStatement insertSummary =
					getConnection().prepareStatement(sql, new String[] {"id"});
		) {
			insertSummary.setQueryTimeout(60);
			// Set the created timestamp
			JDBCUtils.setParameter(insertSummary, 1, new Date().getTime(),
					Types.BIGINT);

			if (sid != null) {
				JDBCUtils.setParameter(insertSummary, 2, sid.toString(),
						Types.VARCHAR);
				JDBCUtils.setParameter(insertSummary, 3, sid.getType(),
						Types.VARCHAR);
				JDBCUtils.setParameter(insertSummary, 4, sid.getSource(),
						Types.VARCHAR);
				JDBCUtils.setParameter(insertSummary, 5, sid.getCode(),
						Types.VARCHAR);
				JDBCUtils.setParameter(insertSummary, 6,
						(sid.getUpdateTime() != null) ? sid.getUpdateTime()
								.getTime() : null, Types.BIGINT);
			} else {
				// Summary product id is null. Set all these parameter to null
				JDBCUtils.setParameter(insertSummary, 2, null, Types.VARCHAR);
				JDBCUtils.setParameter(insertSummary, 3, null, Types.VARCHAR);
				JDBCUtils.setParameter(insertSummary, 4, null, Types.VARCHAR);
				JDBCUtils.setParameter(insertSummary, 5, null, Types.VARCHAR);
				JDBCUtils.setParameter(insertSummary, 6, null, Types.BIGINT);
			}

			JDBCUtils.setParameter(insertSummary, 7, summary.getEventSource(),
					Types.VARCHAR);
			JDBCUtils.setParameter(insertSummary, 8, summary.getEventSourceCode(),
					Types.VARCHAR);

			Date eventTime = summary.getEventTime();
			JDBCUtils.setParameter(insertSummary, 9,
					(eventTime != null) ? eventTime.getTime() : null, Types.BIGINT);

			JDBCUtils
					.setParameter(insertSummary, 10,
							(summary.getEventLatitude() != null) ? summary
									.getEventLatitude().doubleValue() : null,
							Types.DECIMAL);
			JDBCUtils
					.setParameter(
							insertSummary,
							11,
							(summary.getEventLongitude() != null) ? normalizeLongitude(summary
									.getEventLongitude().doubleValue()) : null,
							Types.DECIMAL);
			JDBCUtils.setParameter(insertSummary, 12,
					(summary.getEventDepth() != null) ? summary.getEventDepth()
							.doubleValue() : null, Types.DECIMAL);
			JDBCUtils.setParameter(insertSummary, 13,
					(summary.getEventMagnitude() != null) ? summary
							.getEventMagnitude().doubleValue() : null,
					Types.DECIMAL);
			JDBCUtils.setParameter(insertSummary, 14, summary.getVersion(),
					Types.VARCHAR);
			JDBCUtils.setParameter(insertSummary, 15, summary.getStatus(),
					Types.VARCHAR);
			JDBCUtils.setParameter(insertSummary, 16,
					(summary.getTrackerURL() != null) ? summary.getTrackerURL()
							.toString() : null, Types.VARCHAR);
			JDBCUtils.setParameter(insertSummary, 17, summary.getPreferredWeight(),
					Types.BIGINT);

			// Execute the prepared statement
			insertSummary.executeUpdate();

			try (final ResultSet keys = insertSummary.getGeneratedKeys()) {
				while (keys.next()) {
					productId = keys.getLong(1);
				}
			}
		}
		// Now that the summary is stored, lets try to store the properties
		addProductProperties(productId, summary.getProperties());
		// And try to store the links
		addProductLinks(productId, summary.getLinks());

		ProductSummary p = new ProductSummary(summary);
		p.setIndexId(productId);

		if (LOGGER.isLoggable(Level.FINEST)) {
			LOGGER.finest("[" + getName() + "] Added productSummary " + sid
					+ ", indexid=" + productId + " to product index");
		}
		return p;
	}

	/**
	 * Delete a product summary from the database If the summary doesn't have an
	 * indexId value set, throw an exception
	 *
	 * @param summary
	 *            ProductSummary object to delete
	 */
	@Override
	public synchronized ProductId removeProductSummary(ProductSummary summary)
			throws Exception {
		List<ProductId> removed = removeProductSummaries(Arrays.asList(summary));
		return removed.get(0);
	}

	/**
	 * Create an association between the given event and product summary. This
	 * assumes that both the event and the product are already stored in their
	 * respective tables.
	 *
	 * @param event Event to add association to
	 * @param summary ProductSummary to add association to
	 * @return Copy of event with summary added to the event's products list
	 */
	@Override
	public synchronized Event addAssociation(Event event, ProductSummary summary)
			throws Exception {

		if (event.getIndexId() == null || summary.getIndexId() == null) {
			throw new Exception(
					"["
							+ getName()
							+ "] Cannot add association between event or summary that are not already in index.");
		}

		final ProductId sid = summary.getId();
		final String sql = "UPDATE productSummary"
				+ " SET eventId=? WHERE source=? AND type=? AND code=?";
		try (
			final PreparedStatement addAssociation = getConnection().prepareStatement(sql);
		) {
			addAssociation.setQueryTimeout(60);
			JDBCUtils.setParameter(addAssociation, 1, event.getIndexId(), Types.BIGINT);
			// these will target EVERY version of the given product
			JDBCUtils.setParameter(addAssociation, 2, sid.getSource(), Types.VARCHAR);
			JDBCUtils.setParameter(addAssociation, 3, sid.getType(), Types.VARCHAR);
			JDBCUtils.setParameter(addAssociation, 4, sid.getCode(), Types.VARCHAR);

			addAssociation.executeUpdate();
		}

		final Event e = new Event(event);
		e.addProduct(summary);
		LOGGER.log(
				Level.FINER,
				"[" + getName() + "] Added associations event id="
						+ event.getIndexId() + ", productSummary source="
						+ sid.getSource() + ", type=" + sid.getType()
						+ ", code=" + sid.getCode() + " (id="
						+ summary.getIndexId() + ")");

		return e;
	}

	/**
	 * Delete the association, if it exists, between the given event and product
	 * summary.
	 *
	 * NOTE: this removes the association between the event and ALL versions of the product summary.
	 *
	 * @param event An event to remove an association with
	 * @param summary A ProductSummary to remove an association with
	 * @throws Exception if error occurs
	 */
	@Override
	public synchronized Event removeAssociation(Event event,
			ProductSummary summary) throws Exception {

		// Deleting the association is really just removing the foreign key
		// on the products table

		// First check that this summary and event are both in the database

		// What happens if runtime objects are set up, but not added to index.
		// This would return the event with the association in-tact. Is that
		// okay?

		Long eventIndexId = event.getIndexId();
		Long productIndexId = summary.getIndexId();
		if (eventIndexId == null || productIndexId == null) {
			return event;
		}

		final ProductId sid = summary.getId();
		final String sql = "UPDATE productSummary"
				+ " SET eventId=? WHERE source=? AND type=? AND code=?";
		try (
			final PreparedStatement removeAssociation = getConnection().prepareStatement(sql);
		) {
			removeAssociation.setQueryTimeout(60);
			// Now run the query
			JDBCUtils.setParameter(removeAssociation, 1, null, Types.BIGINT);
			// these will target EVERY version of the given product
			JDBCUtils.setParameter(removeAssociation, 2, summary.getId()
					.getSource(), Types.VARCHAR);
			JDBCUtils.setParameter(removeAssociation, 3, summary.getId().getType(),
					Types.VARCHAR);
			JDBCUtils.setParameter(removeAssociation, 4, summary.getId().getCode(),
					Types.VARCHAR);

			int rows = removeAssociation.executeUpdate();
			// Throw an exception if we didn't update any
			if (rows < 1) {
				LOGGER.log(Level.INFO, "[" + getName()
						+ "] Failed to remove an association in the Product Index");
				throw new Exception("Failed to remove association");
			}
		}

		LOGGER.finer("[" + getName() + "] Removed associations event id="
				+ eventIndexId + ", productSummary source=" + sid.getSource()
				+ ", type=" + sid.getType() + ", code=" + sid.getCode()
				+ " (id=" + productIndexId + ")");

		// Should this method remove the summary from the event's list? Yes.
		Event updatedEvent = new Event(event);
		List<ProductSummary> productsList = updatedEvent.getAllProducts().get(
				summary.getType());

		// pre 1.7.6 archive policies didn't always clean up after themselves
		// handle it gracefully
		if (productsList != null) {
			// remove all product with given source, type, and code
			Iterator<ProductSummary> iter = productsList.iterator();
			while (iter.hasNext()) {
				ProductId id = iter.next().getId();
				if (id.isSameProduct(summary.getId())) {
					iter.remove();
				}
			}
			if (productsList.size() == 0) {
				// if this was the last product of that type, remove the list
				// too
				updatedEvent.getAllProducts().remove(summary.getType());
			}
		} else {
			LOGGER.warning("Products list is empty for summary type "
					+ summary.getId().toString()
					+ ", when removing association");
		}
		return updatedEvent;
	}

	// ____________________________________
	// Protected Methods
	// ____________________________________

	/**
	 * Build a list of all the pieces of the WHERE clause relevant to the
	 * productSummary table. If the query doesn't set any properties, this
	 * method will return an empty list. It is up to the calling methods to
	 * check if the clause list is empty when they build their WHERE clause.
	 *
	 * @param query ProductIndexQuery
	 * @return list containing clauses in the form: column="value"
	 */
	protected List<String> buildProductClauses(ProductIndexQuery query) {
		List<String> clauseList = new ArrayList<String>();

		if (query == null) {
			return clauseList; /* No query = No clauses */
		}

		// If they only want current products make a clause that contains a
		// subquery
		if (query.getResultType() == ProductIndexQuery.RESULT_TYPE_CURRENT) {
			String queryCode,
					querySource,
					queryType;

			queryCode = query.getProductCode();
			querySource = query.getProductSource();
			queryType = query.getProductType();

			if (queryCode != null && querySource != null && queryType != null) {
				// Better sub-select when these properties are specified
				clauseList
						.add(String
								.format("%s.%s = (SELECT %s FROM %s ps WHERE ps.%s='%s' AND ps.%s='%s' AND ps.%s='%s' AND ps.%s <> 'DELETE' ORDER BY ps.%s DESC LIMIT 1)",
										SUMMARY_TABLE_ALIAS, SUMMARY_PRODUCT_INDEX_ID,
										SUMMARY_PRODUCT_INDEX_ID, SUMMARY_TABLE,
										SUMMARY_SOURCE, querySource,
										SUMMARY_TYPE, queryType,
										SUMMARY_CODE, queryCode,
										SUMMARY_STATUS,
										SUMMARY_UPDATE_TIME));
			} else {
				clauseList
						.add(String
								.format("NOT EXISTS (SELECT %s FROM %s ps WHERE ps.%s=p.%s AND ps.%s=p.%s AND ps.%s=p.%s AND ps.%s > p.%s AND ps.%s <> 'DELETE')",
										SUMMARY_PRODUCT_INDEX_ID, SUMMARY_TABLE,
										SUMMARY_TYPE, SUMMARY_TYPE, SUMMARY_SOURCE,
										SUMMARY_SOURCE, SUMMARY_CODE, SUMMARY_CODE,
										SUMMARY_UPDATE_TIME, SUMMARY_UPDATE_TIME,
										SUMMARY_STATUS));
			}
		}
		// If they only want superseded products, make a slightly different
		// clause that has a subquery
		else if (query.getResultType() == ProductIndexQuery.RESULT_TYPE_SUPERSEDED) {
			clauseList
					.add(String
							.format("EXISTS (SELECT %s FROM %s ps WHERE ps.%s=p.%s AND ps.%s=p.%s AND ps.%s=p.%s AND ps.%s > p.%s AND ps.%s <> 'DELETE')",
									SUMMARY_PRODUCT_INDEX_ID, SUMMARY_TABLE,
									SUMMARY_TYPE, SUMMARY_TYPE, SUMMARY_SOURCE,
									SUMMARY_SOURCE, SUMMARY_CODE, SUMMARY_CODE,
									SUMMARY_UPDATE_TIME, SUMMARY_UPDATE_TIME,
									SUMMARY_STATUS));
		}

		// Interested in "any" productId in the query.
		Iterator<ProductId> productIter = query.getProductIds().iterator();

		// If there are one or more productIds we should build this clause
		if (productIter.hasNext()) {
			// Begin an "IN" clause
			StringBuilder clause = new StringBuilder();
			clause.append(String.format("%s.%s IN ('%s", SUMMARY_TABLE_ALIAS,
					SUMMARY_PRODUCT_ID, productIter.next().toString()));

			// Loop over any remaining productIds and add them to clause
			while (productIter.hasNext()) {
				clause.append("', '");
				clause.append(productIter.next().toString());
			}

			// Finish off our clause and add it to our clauseList
			clause.append("')");
			clauseList.add(clause.toString());
		}

		// Build clauses for all specified columns
		String eventSource = query.getEventSource();
		if (eventSource != null) {
			clauseList.add(String.format("%s.%s='%s'", SUMMARY_TABLE_ALIAS,
					SUMMARY_EVENT_SOURCE, eventSource));
		}

		String eventSourceCode = query.getEventSourceCode();
		if (eventSourceCode != null) {
			clauseList.add(String.format("%s.%s='%s'", SUMMARY_TABLE_ALIAS,
					SUMMARY_EVENT_SOURCE_CODE, eventSourceCode));
		}

		String eventTimeColumn;
		String eventLatitudeColumn;
		String eventLongitudeColumn;
		String eventMagnitudeColumn;
		String eventDepthColumn;

		// which table is used for event properties
		if (query.getEventSearchType() == ProductIndexQuery.SEARCH_EVENT_PREFERRED) {
			// search preferred event parameters in event table
			eventTimeColumn = EVENT_TABLE_ALIAS + "." + EVENT_TIME;
			eventLatitudeColumn = EVENT_TABLE_ALIAS + "." + EVENT_LATITUDE;
			eventLongitudeColumn = EVENT_TABLE_ALIAS + "." + EVENT_LONGITUDE;
			eventMagnitudeColumn = EVENT_TABLE_ALIAS + "." + EVENT_MAGNITUDE;
			eventDepthColumn = EVENT_TABLE_ALIAS + "." + EVENT_DEPTH;
		} else {
			// search product summary parameters in summary table
			eventTimeColumn = SUMMARY_TABLE_ALIAS + "." + SUMMARY_EVENT_TIME;
			eventLatitudeColumn = SUMMARY_TABLE_ALIAS + "."
					+ SUMMARY_EVENT_LATITUDE;
			eventLongitudeColumn = SUMMARY_TABLE_ALIAS + "."
					+ SUMMARY_EVENT_LONGITUDE;
			eventMagnitudeColumn = SUMMARY_TABLE_ALIAS + "."
					+ SUMMARY_EVENT_MAGNITUDE;
			eventDepthColumn = SUMMARY_TABLE_ALIAS + "." + SUMMARY_EVENT_DEPTH;
		}

		Date minTime = query.getMinEventTime();
		if (minTime != null) {
			clauseList.add(String.format("%s>=%d", eventTimeColumn,
					minTime.getTime()));
		}
		Date maxTime = query.getMaxEventTime();
		if (maxTime != null) {
			clauseList.add(String.format("%s<=%d", eventTimeColumn,
					maxTime.getTime()));
		}

		BigDecimal minLat = query.getMinEventLatitude();
		if (minLat != null) {
			clauseList.add(String.format("%s>=%f", eventLatitudeColumn,
					minLat.doubleValue()));
		}
		BigDecimal maxLat = query.getMaxEventLatitude();
		if (maxLat != null) {
			clauseList.add(String.format("%s<=%f", eventLatitudeColumn,
					maxLat.doubleValue()));
		}

		BigDecimal minDepth = query.getMinEventDepth();
		if (minDepth != null) {
			clauseList.add(String.format("%s>=%f", eventDepthColumn,
					minDepth.doubleValue()));
		}
		BigDecimal maxDepth = query.getMaxEventDepth();
		if (maxDepth != null) {
			clauseList.add(String.format("%s<=%f", eventDepthColumn,
					maxDepth.doubleValue()));
		}

		BigDecimal minMag = query.getMinEventMagnitude();
		if (minMag != null) {
			clauseList.add(String.format("%s>=%f", eventMagnitudeColumn,
					minMag.doubleValue()));
		}
		BigDecimal maxMag = query.getMaxEventMagnitude();
		if (maxMag != null) {
			clauseList.add(String.format("%s<=%f", eventMagnitudeColumn,
					maxMag.doubleValue()));
		}

		Date minUpdateTime = query.getMinProductUpdateTime();
		if (minUpdateTime != null) {
			clauseList.add(String.format("%s>=%d", SUMMARY_UPDATE_TIME,
					minUpdateTime.getTime()));
		}
		Date maxUpdateTime = query.getMaxProductUpdateTime();
		if (maxUpdateTime != null) {
			clauseList.add(String.format("%s<=%d", SUMMARY_UPDATE_TIME,
					maxUpdateTime.getTime()));
		}

		String source = query.getProductSource();
		if (source != null) {
			clauseList.add(String.format("%s='%s'", SUMMARY_SOURCE, source));
		}

		String type = query.getProductType();
		if (type != null) {
			clauseList.add(String.format("%s='%s'", SUMMARY_TYPE, type));
		}

		String code = query.getProductCode();
		if (code != null) {
			clauseList.add(String.format("%s='%s'", SUMMARY_CODE, code));
		}

		String version = query.getProductVersion();
		if (version != null) {
			clauseList.add(String.format("%s='%s'", SUMMARY_VERSION, version));
		}

		String status = query.getProductStatus();
		if (status != null) {
			clauseList.add(String.format("%s='%s'", SUMMARY_STATUS, status));
		}

		Long minProductIndexId = query.getMinProductIndexId();
		if (minProductIndexId != null) {
			clauseList.add(String.format("%s>=%d", SUMMARY_PRODUCT_INDEX_ID, minProductIndexId));
		}

		BigDecimal minLon = query.getMinEventLongitude();
		BigDecimal maxLon = query.getMaxEventLongitude();
		// Normalize the longitudes between -180 and 180
		minLon = normalizeLongitude(minLon);
		maxLon = normalizeLongitude(maxLon);

		if (minLon != null && maxLon != null) {
			if (maxLon.doubleValue() < minLon.doubleValue()) {
				// If the normalized maxLon is less than the normalized minLon,
				// the
				// span crosses
				// the date line
				Double minLonDouble = minLon.doubleValue();
				Double maxLonDouble = maxLon.doubleValue();
				// If the range crosses the date line, split it into 2 clauses
				String lonClause = String.format(
						"((%s > %f AND %s <= 180) OR (%s < %f AND %s > -180))",
						eventLongitudeColumn, minLonDouble,
						eventLongitudeColumn, eventLongitudeColumn,
						maxLonDouble, eventLongitudeColumn);
				clauseList.add(lonClause);
			} else {
				clauseList.add(String.format("%s>=%f and %s<=%f",
						eventLongitudeColumn, minLon.doubleValue(),
						eventLongitudeColumn, maxLon.doubleValue()));
			}
		} else if (minLon != null) {
			clauseList.add(String.format("%s>=%f", eventLongitudeColumn,
					minLon.doubleValue()));
		} else if (maxLon != null) {
			clauseList.add(String.format("%s<=%f", eventLongitudeColumn,
					maxLon.doubleValue()));
		}
		return clauseList;
	}

	/**
	 * Create the full SELECT query for the products table using the default clauseList.
	 *
	 * @param query
	 *     Query to build.
	 * @return String containing the full SELECT query
	 * @see #buildProductClauses(ProductIndexQuery)
	 */
	protected String buildProductQuery(final ProductIndexQuery query) {

		final List<String> clauseList = buildProductClauses(query);
		return buildProductQuery(query, clauseList);
	}

	/**
	 * Create the full SELECT query for the products table using a custom clauseList.
	 *
	 * @param query
	 *     Query to build.
	 * @param clauseList List of clauses for WHERE
	 * @return String containing the full SELECT query
	 */
	protected String buildProductQuery(final ProductIndexQuery query, final List<String> clauseList) {
		final StringBuffer sql = new StringBuffer();

		sql.append("SELECT * FROM " + SUMMARY_TABLE + " p");

		// optional where
		if (clauseList.size() > 0) {
			sql.append(" WHERE ").append(String.join(" AND ", clauseList));
		}

		// optional order by
		String queryOrderBy = query.getOrderBy();
		if (queryOrderBy != null) {
			sql.append(" ORDER BY ").append(queryOrderBy);
		}

		// limit is after order by
		Integer queryLimit = query.getLimit();
		if (queryLimit != null) {
			sql.append(" LIMIT ").append(queryLimit);
		}

		return sql.toString();
	}

	/**
	 * Populate links and properties for provided product summaries.
	 *
	 * @param summaries List of ProductSummaries
	 * @throws Exception if error occurs
	 */
	protected synchronized void loadProductSummaries(final List<ProductSummary> summaries)
			throws Exception {
		if (summaries.size() == 0) {
			// nothing to load
			return;
		}

		// index by id
		final Map<Long, ProductSummary> summaryMap = new HashMap<>();
		for (final ProductSummary summary : summaries) {
			summaryMap.put(summary.getIndexId(), summary);
		}

		// load all links in one query
		final String linkSql = "SELECT productSummaryIndexId as id, relation, url"
				+ " FROM productSummaryLink"
				+ " WHERE productSummaryIndexId IN ("
				+ StringUtils.join(
						summaryMap.keySet().stream().collect(Collectors.toList()),
						",")
				+ ")";
		try (
			final PreparedStatement statement = getConnection().prepareStatement(linkSql);
		) {
			statement.setQueryTimeout(60);
			try (
				final ResultSet results = statement.executeQuery();
			) {
				while (results.next()) {
					Long id = results.getLong("id");
					String relation = results.getString("relation");
					String uri = results.getString("url");
					// add properties to existing objects
					summaryMap.get(id).addLink(relation, new URI(uri));
				}
			}
		}

		// load all properties in one query
		final String propertySql = "SELECT productSummaryIndexId as id, name, value"
				+ " FROM productSummaryProperty"
				+ " WHERE productSummaryIndexId IN ("
				+ StringUtils.join(
						summaryMap.keySet().stream().collect(Collectors.toList()),
						",")
				+ ")";
		try (
			final PreparedStatement statement =
					getConnection().prepareStatement(propertySql);
		) {
			statement.setQueryTimeout(60);
			try (
				final ResultSet results = statement.executeQuery();
			) {
				while (results.next()) {
					Long id = results.getLong("id");
					String name = results.getString("name");
					String value = results.getString("value");
					// add properties to existing objects
					summaryMap.get(id).getProperties().put(name, value);
				}
			}
		}
	}

	/**
	 * Parse ProductSummary without loading links or properties.
	 *
	 * @param results ResultSet to parse
	 * @return ProductSummary object without links or properties.
	 * @throws Exception if error occurs
	 */
	protected ProductSummary parseProductSummary(ResultSet results)
			throws Exception {
		ProductSummary p = new ProductSummary();
		p.setIndexId(results.getLong(SUMMARY_PRODUCT_INDEX_ID));
		ProductId pid = ProductId.parse(results.getString(SUMMARY_PRODUCT_ID));
		p.setId(pid);
		p.setEventSource(results.getString(SUMMARY_EVENT_SOURCE));
		p.setEventSourceCode(results.getString(SUMMARY_EVENT_SOURCE_CODE));
		try {
			p.setEventTime(new Date(results.getLong(SUMMARY_EVENT_TIME)));
		} catch (Exception e) {
			p.setEventTime(null);
		}

		// getDouble() returns 0 if the value was actually NULL. In this case,
		// we are going to set the value to null
		String latitude = results.getString(SUMMARY_EVENT_LATITUDE);
		if (latitude == null) {
			p.setEventLatitude(null);
		} else {
			p.setEventLatitude(new BigDecimal(latitude));
		}
		String longitude = results.getString(SUMMARY_EVENT_LONGITUDE);
		if (longitude == null) {
			p.setEventLongitude(null);
		} else {
			p.setEventLongitude(new BigDecimal(longitude));
		}
		String depth = results.getString(SUMMARY_EVENT_DEPTH);
		if (depth == null) {
			p.setEventDepth(null);
		} else {
			p.setEventDepth(new BigDecimal(depth));
		}
		String magnitude = results.getString(SUMMARY_EVENT_MAGNITUDE);
		if (magnitude == null) {
			p.setEventMagnitude(null);
		} else {
			p.setEventMagnitude(new BigDecimal(magnitude));
		}
		p.setVersion(results.getString(SUMMARY_VERSION));
		p.setStatus(results.getString(SUMMARY_STATUS));
		p.setTrackerURL((results.getString(SUMMARY_TRACKER_URL) != null) ? new URL(
				results.getString(SUMMARY_TRACKER_URL)) : null);
		p.setPreferredWeight(results.getLong(SUMMARY_PREFERRED));

		return p;
	}

	/**
	 *
	 * @param summaries List of product summaries to remove
	 * @return List of ProductIds that were removed
	 * @throws Exception if error occurs
	 */
	public synchronized List<ProductId> removeProductSummaries(
			final List<ProductSummary> summaries) throws Exception {
		// index by id
		final ArrayList<ProductId> ids = new ArrayList<>();
				// index by id
		final Map<Long, ProductSummary> summaryMap = new HashMap<>();
		for (final ProductSummary summary : summaries) {
			if (summary.getIndexId() == null) {
				LOGGER.log(Level.WARNING, "[" + getName()
						+ "] Could not delete product summary. Index id not found");
				throw new Exception("[" + getName()
						+ "] Could not delete summary. Index id not found.");
			}
			summaryMap.put(summary.getIndexId(), summary);
			ids.add(summary.getId());
		}

		if (summaries.size() == 0) {
			return ids;
		}

		// remove all products in one query
		// on delete cascade wasn't always set...
		final String[] sqls = {
			"DELETE FROM productSummaryLink WHERE productSummaryIndexId IN",
			"DELETE FROM productSummaryProperty WHERE productSummaryIndexId IN",
			"DELETE FROM productSummary WHERE id IN",
		};
		final String idsIn =" ("
				+ StringUtils.join(
						summaryMap.keySet().stream().collect(Collectors.toList()),
						",")
				+ ")";
		for (final String sql : sqls) {
			try (
				final PreparedStatement statement =
						verifyConnection().prepareStatement(sql + idsIn);
			) {
				statement.setQueryTimeout(60);
				int rows = statement.executeUpdate();
				LOGGER.log(Level.FINER, "[" + getName() + "] removed " + rows + " rows");
			}
		}

		return ids;
	}

	/**
	 * Save the properties in the database and associate them to the given
	 * productId
	 *
	 * @param productId long product ID to associate to
	 * @param properties Map of properties to save
	 * @throws SQLException if SQL error occurs
	 */
	protected synchronized void addProductProperties(final long productId,
			final Map<String, String> properties) throws SQLException {
		// Loop through the properties list and add them all to the database
		final String sql = "INSERT INTO productSummaryProperty"
				+ " (productSummaryIndexId, name, value) VALUES (?, ?, ?)";
		try (
			final PreparedStatement insertProperty = getConnection().prepareStatement(sql);
		) {
			insertProperty.setQueryTimeout(60);
			for (String key : properties.keySet()) {
				JDBCUtils.setParameter(insertProperty, 1, productId, Types.BIGINT);
				JDBCUtils.setParameter(insertProperty, 2, key, Types.VARCHAR);
				JDBCUtils.setParameter(insertProperty, 3, properties.get(key),
						Types.VARCHAR);
				insertProperty.addBatch();
				if (LOGGER.isLoggable(Level.FINEST)) {
					LOGGER.log(Level.FINEST, "[" + getName() + "] Added property "
							+ key + ":" + properties.get(key) + " for product "
							+ productId);
				}
			}
			insertProperty.executeBatch();
		}
	}

	/**
	 * Save the links in the database and associate them to the given productId
	 *
	 * @param productId
	 *            Index id of the product to select
	 * @param links
	 *            Map of relations to URIs
	 * @throws SQLException if sql error occurs
	 */
	protected synchronized void addProductLinks(long productId,
			Map<String, List<URI>> links) throws SQLException {
		// Loop through the properties list and add them all to the database
		final String sql = "INSERT INTO productSummaryLink"
				+ " (productSummaryIndexId, relation, url) VALUES (?, ?, ?)";
		try (
			final PreparedStatement insertLink = getConnection().prepareStatement(sql);
		) {
			insertLink.setQueryTimeout(60);
			for (final String relation : links.keySet()) {
				for (final URI uri : links.get(relation)) {
					JDBCUtils.setParameter(insertLink, 1, productId, Types.BIGINT);
					JDBCUtils.setParameter(insertLink, 2, relation, Types.VARCHAR);
					JDBCUtils.setParameter(insertLink, 3, uri.toString(), Types.VARCHAR);
					insertLink.addBatch();
					LOGGER.log(Level.FINEST, "[" + getName() + "] Added link "
							+ relation + ":" + uri.toString() + " for product "
							+ productId);
				}
			}
			insertLink.executeBatch();
		}
	}

	/**
	 * Convert the given longitude to be between -180 and 180. If the given
	 * value is already in the range, this method just returns the value.
	 *
	 * @param lon Double longitude
	 * @return double normalized between -180 and 180
	 */
	protected double normalizeLongitude(double lon) {
		double normalizedLon = lon;

		if (normalizedLon <= 180 && normalizedLon > -180) {
			return normalizedLon;
		}

		// If the value is above 180, make it negative by subtracting 360
		if (normalizedLon > 180) {
			normalizedLon = normalizedLon % 360;
			normalizedLon = normalizedLon - 360;
			return normalizedLon;
		}

		// If the value is below 180, make it positive by adding 360
		if (normalizedLon <= -180) {
			normalizedLon = normalizedLon % 360;
			normalizedLon = normalizedLon + 360;
			return normalizedLon;
		}

		return normalizedLon;
	}

	/**
	 * Wrapper to normalize BigDecimal longitudes
	 *
	 * @param lon BigDecimal Longitude
	 * @return Normalized BigDecimal latitude
	 */
	protected BigDecimal normalizeLongitude(BigDecimal lon) {
		if (lon == null) {
			return null;
		}

		return BigDecimal.valueOf(normalizeLongitude(lon.doubleValue()));
	}

	/**
	 * Called when the indexer is done updating events after a product is
	 * processed. Stores the preferred attributes for each event in the list
	 *
	 * @param events
	 *            the events that have been updated.
	 */
	@Override
	public synchronized void eventsUpdated(List<Event> events) throws Exception {
		Long indexId = null;

		final String deletedSql = "UPDATE event SET status=? WHERE id=?";
		final String updatedSql = "UPDATE event"
				+ " SET updated=?, source=?, sourceCode=?, eventTime=?"
				+ " , latitude=?, longitude=?, depth=?, magnitude=?, status=?"
				+ " WHERE id=?";

		try (
			final PreparedStatement updateDeletedEvent =
					getConnection().prepareStatement(deletedSql);
			final PreparedStatement updateEvent =
					getConnection().prepareStatement(updatedSql);
		) {
			// big events take time...
			updateDeletedEvent.setQueryTimeout(300);
			updateEvent.setQueryTimeout(300);
			Iterator<Event> iter = events.iterator();
			while (iter.hasNext()) {
				Event updated = iter.next();

				indexId = updated.getIndexId();
				LOGGER.finer("[" + getName() + "] Updating event indexid=" + indexId);
				updated.log(LOGGER);

				try {
					if (updated.isDeleted()) {
						// only update status if event deleted, leave other
						// parameters intact
						JDBCUtils.setParameter(updateDeletedEvent, 1,
								EVENT_STATUS_DELETE, Types.VARCHAR);
						JDBCUtils.setParameter(updateDeletedEvent, 2, indexId,
								Types.BIGINT);

						updateDeletedEvent.executeUpdate();
					} else {
						EventSummary summary = updated.getEventSummary();

						// otherwise update event parameters
						JDBCUtils.setParameter(updateEvent, 1,
								new Date().getTime(), Types.BIGINT);
						JDBCUtils.setParameter(updateEvent, 2, summary.getSource(),
								Types.VARCHAR);
						JDBCUtils.setParameter(updateEvent, 3,
								summary.getSourceCode(), Types.VARCHAR);

						Long eventTime = null;
						if (summary.getTime() != null) {
							eventTime = summary.getTime().getTime();
						}
						JDBCUtils.setParameter(updateEvent, 4, eventTime,
								Types.BIGINT);

						Double latitude = null;
						if (summary.getLatitude() != null) {
							latitude = summary.getLatitude().doubleValue();
						}
						JDBCUtils.setParameter(updateEvent, 5, latitude,
								Types.DOUBLE);

						Double longitude = null;
						if (summary.getLongitude() != null) {
							longitude = summary.getLongitude().doubleValue();
						}
						JDBCUtils.setParameter(updateEvent, 6, longitude,
								Types.DOUBLE);

						// these may be null, handle carefully
						Double depth = null;
						if (summary.getDepth() != null) {
							depth = summary.getDepth().doubleValue();
						}
						JDBCUtils.setParameter(updateEvent, 7, depth, Types.DOUBLE);

						Double magnitude = null;
						if (summary.getMagnitude() != null) {
							magnitude = summary.getMagnitude().doubleValue();
						}
						JDBCUtils.setParameter(updateEvent, 8, magnitude,
								Types.DOUBLE);

						JDBCUtils.setParameter(updateEvent, 9, EVENT_STATUS_UPDATE,
								Types.VARCHAR);

						JDBCUtils.setParameter(updateEvent, 10, indexId,
								Types.BIGINT);

						updateEvent.executeUpdate();
					}

					LOGGER.log(Level.FINEST, "[" + getName()
							+ "] Updated event properties in Product Index");
				} catch (Exception e) {
					LOGGER.log(Level.WARNING, "[" + getName()
							+ "] Error updating event properties, eventid="
							+ indexId, e);
					// trigger a rollback
					throw e;
				}
			}
		}
	}

}