JDBCProductIndex.java

  1. /*
  2.  * JDBCProductIndex
  3.  */
  4. package gov.usgs.earthquake.indexer;

  5. import gov.usgs.earthquake.product.ProductId;
  6. import gov.usgs.earthquake.util.JDBCConnection;
  7. import gov.usgs.util.Config;
  8. import gov.usgs.util.JDBCUtils;
  9. import gov.usgs.util.StreamUtils;
  10. import gov.usgs.util.StringUtils;

  11. import java.io.File;
  12. import java.math.BigDecimal;
  13. import java.net.URI;
  14. import java.net.URL;
  15. import java.sql.Connection;
  16. import java.sql.PreparedStatement;
  17. import java.sql.ResultSet;
  18. import java.sql.SQLException;
  19. import java.sql.Types;
  20. import java.util.ArrayList;
  21. import java.util.Arrays;
  22. import java.util.Date;
  23. import java.util.HashMap;
  24. import java.util.Iterator;
  25. import java.util.LinkedList;
  26. import java.util.List;
  27. import java.util.Map;
  28. import java.util.logging.Level;
  29. import java.util.logging.Logger;
  30. import java.util.stream.Collectors;

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

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

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

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

  44.     private static final String JDBC_DEFAULT_DRIVER = JDBCUtils.SQLITE_DRIVER_CLASSNAME;

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

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

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

  57.     /** Variables to store the event and product column names */
  58.     // private static final String EVENT_TABLE = "event";
  59.     private static final String EVENT_TABLE_ALIAS = "e";
  60.     // private static final String EVENT_INDEX_ID = "id";
  61.     // private static final String EVENT_CREATED = "created";
  62.     // private static final String EVENT_UPDATED = "updated";
  63.     // private static final String EVENT_SOURCE = "source";
  64.     // private static final String EVENT_SOURCE_CODE = "sourceCode";
  65.     private static final String EVENT_TIME = "eventTime";
  66.     private static final String EVENT_LATITUDE = "latitude";
  67.     private static final String EVENT_LONGITUDE = "longitude";
  68.     private static final String EVENT_DEPTH = "depth";
  69.     private static final String EVENT_MAGNITUDE = "magnitude";
  70.     // private static final String EVENT_STATUS = "status";

  71.     private static final String EVENT_STATUS_UPDATE = "UPDATE";
  72.     private static final String EVENT_STATUS_DELETE = "DELETE";

  73.     private static final String SUMMARY_TABLE = "productSummary";
  74.     private static final String SUMMARY_TABLE_ALIAS = "p";
  75.     // private static final String SUMMARY_CREATED = "created";
  76.     /** Public var for summary product index IDs */
  77.     public static final String SUMMARY_PRODUCT_INDEX_ID = "id";
  78.     private static final String SUMMARY_PRODUCT_ID = "productId";
  79.     // private static final String SUMMARY_EVENT_ID = "eventId";
  80.     private static final String SUMMARY_TYPE = "type";
  81.     private static final String SUMMARY_SOURCE = "source";
  82.     private static final String SUMMARY_CODE = "code";
  83.     private static final String SUMMARY_UPDATE_TIME = "updateTime";
  84.     private static final String SUMMARY_EVENT_SOURCE = "eventSource";
  85.     private static final String SUMMARY_EVENT_SOURCE_CODE = "eventSourceCode";
  86.     private static final String SUMMARY_EVENT_TIME = "eventTime";
  87.     private static final String SUMMARY_EVENT_LATITUDE = "eventLatitude";
  88.     private static final String SUMMARY_EVENT_LONGITUDE = "eventLongitude";
  89.     private static final String SUMMARY_EVENT_DEPTH = "eventDepth";
  90.     private static final String SUMMARY_EVENT_MAGNITUDE = "eventMagnitude";
  91.     private static final String SUMMARY_VERSION = "version";
  92.     private static final String SUMMARY_STATUS = "status";
  93.     private static final String SUMMARY_TRACKER_URL = "trackerURL";
  94.     private static final String SUMMARY_PREFERRED = "preferred";
  95.     // private static final String SUMMARY_PROPERTY_TABLE = "productSummaryProperty";
  96.     // private static final String SUMMARY_PROPERTY_ID = "productSummaryIndexId";
  97.     // private static final String SUMMARY_PROPERTY_NAME = "name";
  98.     // private static final String SUMMARY_PROPERTY_VALUE = "value";
  99.     // private static final String SUMMARY_LINK_TABLE = "productSummaryLink";
  100.     // private static final String SUMMARY_LINK_ID = "productSummaryIndexId";
  101.     // private static final String SUMMARY_LINK_RELATION = "relation";
  102.     // private static final String SUMMARY_LINK_URL = "url";

  103.     private String index_file;

  104.     /**
  105.      * Constructor. Sets index_file to the default value JDBC_DEFAULT_FILE
  106.      *
  107.      * @throws Exception if error occurs
  108.      */
  109.     public JDBCProductIndex() throws Exception {
  110.         // Default index file, so calling configure() isn't required
  111.         index_file = JDBC_DEFAULT_FILE;
  112.         setDriver(JDBC_DEFAULT_DRIVER);
  113.     }

  114.     /**
  115.      * Constructor. Uses custom index_file
  116.      * @param sqliteFileName String for sqlite file name
  117.      * @throws Exception if error occurs
  118.      */
  119.     public JDBCProductIndex(final String sqliteFileName) throws Exception {
  120.         index_file = sqliteFileName;
  121.         setDriver(JDBC_DEFAULT_DRIVER);
  122.     }

  123.     // ____________________________________
  124.     // Public Methods
  125.     // ____________________________________

  126.     /**
  127.      * Grab values from the Config object and put them into private variables.
  128.      *
  129.      * @param config
  130.      *            Configuration for the product index
  131.      */
  132.     @Override
  133.     public void configure(Config config) throws Exception {
  134.         super.configure(config);
  135.         index_file = config.getProperty(JDBC_FILE_PROPERTY);

  136.         if (getDriver() == null) { setDriver(JDBC_DEFAULT_DRIVER); }
  137.         if (index_file == null || "".equals(index_file)) {
  138.             index_file = JDBC_DEFAULT_FILE;
  139.         }
  140.     }

  141.     /**
  142.      * Return a connection to the database.
  143.      *
  144.      * @return Connection object
  145.      * @throws Exception if error occurs
  146.      */
  147.     @Override
  148.     public Connection connect() throws Exception {
  149.         // If they are using the sqlite driver, we need to try to create the
  150.         // file
  151.         if (getDriver().equals(JDBCUtils.SQLITE_DRIVER_CLASSNAME)) {
  152.             // Make sure file exists or copy it out of the JAR
  153.             File indexFile = new File(index_file);
  154.             if (!indexFile.exists()) {
  155.                 // extract schema from jar
  156.                 URL schemaURL = JDBCProductIndex.class.getClassLoader()
  157.                         .getResource(JDBC_DEFAULT_INDEX);
  158.                 if (schemaURL != null) {
  159.                     StreamUtils.transferStream(schemaURL, indexFile);
  160.                 } else {
  161.                     // Failed. Probably because we're not in a Jar file
  162.                     File defaultIndex = new File(JDBC_DEFAULT_INDEX);
  163.                     StreamUtils.transferStream(defaultIndex, indexFile);
  164.                 }
  165.             }
  166.             indexFile = null;

  167.             // Build the JDBC url
  168.             setUrl(JDBC_CONNECTION_PREFIX + index_file);
  169.         }
  170.         return super.connect();
  171.     }

  172.     /**
  173.      * Return all events from the database that meet the parameters specified in
  174.      * the ProductIndexQuery object.
  175.      *
  176.      * @param query
  177.      *            A description of which events to retrieve.
  178.      * @return List of Event objects
  179.      */
  180.     @Override
  181.     public synchronized List<Event> getEvents(ProductIndexQuery query)
  182.             throws Exception {
  183.         if (query == null) {
  184.             return new ArrayList<Event>();
  185.         }
  186.         // map of events (index id => event), so products can be added incrementally
  187.         final Map<Long, Event> events = new HashMap<>();
  188.         // all products for loading details
  189.         ArrayList<ProductSummary> products = new ArrayList<>();

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

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

  205.         // add current clause to outer query
  206.         if (query.getResultType() == ProductIndexQuery.RESULT_TYPE_CURRENT) {
  207.             sql = sql + " AND NOT EXISTS ("
  208.                     + " SELECT * FROM productSummary"
  209.                     + " WHERE source=ps2.source"
  210.                     + " AND type=ps2.type"
  211.                     + " AND code=ps2.code"
  212.                     + " AND updateTime>ps2.updateTime"
  213.                     + ")";
  214.         }

  215.         // load event products
  216.         try (
  217.             final PreparedStatement statement = getConnection().prepareStatement(sql);
  218.             final ResultSet results = statement.executeQuery();
  219.         ) {
  220.             statement.setQueryTimeout(60);
  221.             while (results.next()) {
  222.                 // eventid not part of product summary object,
  223.                 // so need to do this as products are parsed...
  224.                 final Long id = results.getLong("eventId");
  225.                 Event event = events.get(id);
  226.                 if (event == null) {
  227.                     // create event to hold products
  228.                     event = new Event();
  229.                     event.setIndexId(id);
  230.                     events.put(id, event);
  231.                 }
  232.                 final ProductSummary productSummary = parseProductSummary(results);
  233.                 event.addProduct(productSummary);
  234.                 products.add(productSummary);
  235.             }
  236.         }

  237.         // load product details
  238.         loadProductSummaries(products);

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

  241.     /**
  242.      * Add an event to the database
  243.      *
  244.      * @param event
  245.      *            Event to store
  246.      * @return Event object with eventId set to the database id
  247.      */
  248.     @Override
  249.     public synchronized Event addEvent(Event event) throws Exception {
  250.         Event e = null;

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

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

  261.             if (rows == 1) {
  262.                 long id = 0;
  263.                 try (final ResultSet keys = insertEvent.getGeneratedKeys()) {
  264.                     while (keys.next()) {
  265.                         id = keys.getLong(1);
  266.                     }
  267.                     e = new Event(event);
  268.                     e.setIndexId(id);
  269.                 }
  270.                 LOGGER.finest("Added event id=" + id);
  271.             } else {
  272.                 LOGGER.log(Level.WARNING, "[" + getName()
  273.                         + "] Exception when adding new event to database");
  274.                 throw new Exception("Error adding new event to database");
  275.             }
  276.         }
  277.         LOGGER.log(Level.FINEST, "[" + getName() + "] Added event to Product Index");
  278.         return e;
  279.     }

  280.     /**
  281.      * Delete an event from the database.
  282.      *
  283.      * @param event
  284.      *            Event to remove
  285.      * @return List containing all the ProductIds that were deleted by the
  286.      *         method call
  287.      */
  288.     @Override
  289.     public synchronized List<ProductId> removeEvent(Event event)
  290.             throws Exception {

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

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

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

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

  316.         return productIds;
  317.     }

  318.     /**
  319.      * Return all products that aren't associated with an event.
  320.      *
  321.      * @param query
  322.      *            ProductIndexQuery used to further limit the results
  323.      * @return List of unassociated Products
  324.      * @throws IllegalArgumentException
  325.      *             when query event search type is SEARCH_EVENT_PREFERRED.
  326.      */
  327.     @Override
  328.     public synchronized List<ProductSummary> getUnassociatedProducts(
  329.             ProductIndexQuery query) throws Exception {
  330.         if (query.getEventSearchType() == ProductIndexQuery.SEARCH_EVENT_PREFERRED) {
  331.             throw new IllegalArgumentException(
  332.                     "getUnassociatedProducts does not support SEARCH_EVENT_PREFERRED");
  333.         }

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

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

  339.         try (
  340.             final PreparedStatement statement = getConnection().prepareStatement(sql);
  341.         ) {
  342.             statement.setQueryTimeout(60);
  343.             try (
  344.                 final ResultSet results = statement.executeQuery();
  345.             ) {
  346.                 // Now lets build product objects from each row in the result set
  347.                 while (results.next()) {
  348.                     products.add(parseProductSummary(results));
  349.                 }
  350.             }
  351.         }

  352.         // load properties and links
  353.         loadProductSummaries(products);

  354.         return products;
  355.     }

  356.     /**
  357.      * Return all products that meet the parameters specified in the
  358.      * ProductIndexQuery object.
  359.      *
  360.      * @param query
  361.      *            A description of which products to retrieve.
  362.      * @return List of ProductSummary objects
  363.      * @throws IllegalArgumentException
  364.      *             when query event search type is SEARCH_EVENT_PREFERRED.
  365.      */
  366.     @Override
  367.     public synchronized List<ProductSummary> getProducts(ProductIndexQuery query)
  368.             throws Exception {
  369.         // load full product summaries by default
  370.         return getProducts(query, true);
  371.     }

  372.     /**
  373.      * Load product summaries.
  374.      *
  375.      * @param query
  376.      *     product query
  377.      * @param loadDetails
  378.      *     whether to call {@link #loadProductSummaries(List)},
  379.      *     which loads links and properties with additional queries.
  380.      * @return
  381.      *     A list of loaded product summaries
  382.      * @throws Exception
  383.      *     if error occurs
  384.      */
  385.     public synchronized List<ProductSummary> getProducts(ProductIndexQuery query, final boolean loadDetails)
  386.             throws Exception {
  387.         final String sql = buildProductQuery(query);

  388.         final List<ProductSummary> products = new LinkedList<ProductSummary>();
  389.         LOGGER.finer("Executing query " + sql);
  390.         try (
  391.             final PreparedStatement statement = getConnection().prepareStatement(sql);
  392.         ) {
  393.             statement.setQueryTimeout(60);
  394.             try (
  395.                 final ResultSet results = statement.executeQuery();
  396.             ) {
  397.                 // Now lets build product objects from each row in the result set
  398.                 while (results.next()) {
  399.                     products.add(parseProductSummary(results));
  400.                 }
  401.             }
  402.         }

  403.         if (loadDetails) {
  404.             // load properties and links
  405.             loadProductSummaries(products);
  406.         }

  407.         return products;
  408.     }

  409.     /**
  410.      * Check whether product summary is in index.
  411.      *
  412.      * @param id
  413.      *     product to search.
  414.      */
  415.     public synchronized boolean hasProduct(final ProductId id) throws Exception {
  416.         final String sql = "SELECT id FROM productSummary"
  417.                 + " WHERE source=? AND type=? AND code=? AND updateTime=?";
  418.         try (
  419.             final PreparedStatement statement = getConnection().prepareStatement(sql);
  420.         ) {
  421.             statement.setQueryTimeout(60);
  422.             statement.setString(1, id.getSource());
  423.             statement.setString(2, id.getType());
  424.             statement.setString(3, id.getCode());
  425.             statement.setLong(4, id.getUpdateTime().getTime());

  426.             try (
  427.                 final ResultSet results = statement.executeQuery();
  428.             ) {
  429.                 // return true if there is a matching row, false otherwise
  430.                 return results.next();
  431.             }
  432.         }
  433.     }

  434.     /**
  435.      * Add a product summary to the database
  436.      *
  437.      * @param summary
  438.      *            ProductSummary object to store. Must not be null.
  439.      * @return Copy of the product summary object with the indexId set to the
  440.      *         newly inserted id.
  441.      * @throws Exception if error occurs
  442.      */
  443.     @Override
  444.     public synchronized ProductSummary addProductSummary(ProductSummary summary)
  445.             throws Exception {
  446.         // Add values to the prepared statement
  447.         long productId = 0;
  448.         final ProductId sid = summary.getId();

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

  463.             if (sid != null) {
  464.                 JDBCUtils.setParameter(insertSummary, 2, sid.toString(),
  465.                         Types.VARCHAR);
  466.                 JDBCUtils.setParameter(insertSummary, 3, sid.getType(),
  467.                         Types.VARCHAR);
  468.                 JDBCUtils.setParameter(insertSummary, 4, sid.getSource(),
  469.                         Types.VARCHAR);
  470.                 JDBCUtils.setParameter(insertSummary, 5, sid.getCode(),
  471.                         Types.VARCHAR);
  472.                 JDBCUtils.setParameter(insertSummary, 6,
  473.                         (sid.getUpdateTime() != null) ? sid.getUpdateTime()
  474.                                 .getTime() : null, Types.BIGINT);
  475.             } else {
  476.                 // Summary product id is null. Set all these parameter to null
  477.                 JDBCUtils.setParameter(insertSummary, 2, null, Types.VARCHAR);
  478.                 JDBCUtils.setParameter(insertSummary, 3, null, Types.VARCHAR);
  479.                 JDBCUtils.setParameter(insertSummary, 4, null, Types.VARCHAR);
  480.                 JDBCUtils.setParameter(insertSummary, 5, null, Types.VARCHAR);
  481.                 JDBCUtils.setParameter(insertSummary, 6, null, Types.BIGINT);
  482.             }

  483.             JDBCUtils.setParameter(insertSummary, 7, summary.getEventSource(),
  484.                     Types.VARCHAR);
  485.             JDBCUtils.setParameter(insertSummary, 8, summary.getEventSourceCode(),
  486.                     Types.VARCHAR);

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

  490.             JDBCUtils
  491.                     .setParameter(insertSummary, 10,
  492.                             (summary.getEventLatitude() != null) ? summary
  493.                                     .getEventLatitude().doubleValue() : null,
  494.                             Types.DECIMAL);
  495.             JDBCUtils
  496.                     .setParameter(
  497.                             insertSummary,
  498.                             11,
  499.                             (summary.getEventLongitude() != null) ? normalizeLongitude(summary
  500.                                     .getEventLongitude().doubleValue()) : null,
  501.                             Types.DECIMAL);
  502.             JDBCUtils.setParameter(insertSummary, 12,
  503.                     (summary.getEventDepth() != null) ? summary.getEventDepth()
  504.                             .doubleValue() : null, Types.DECIMAL);
  505.             JDBCUtils.setParameter(insertSummary, 13,
  506.                     (summary.getEventMagnitude() != null) ? summary
  507.                             .getEventMagnitude().doubleValue() : null,
  508.                     Types.DECIMAL);
  509.             JDBCUtils.setParameter(insertSummary, 14, summary.getVersion(),
  510.                     Types.VARCHAR);
  511.             JDBCUtils.setParameter(insertSummary, 15, summary.getStatus(),
  512.                     Types.VARCHAR);
  513.             JDBCUtils.setParameter(insertSummary, 16,
  514.                     (summary.getTrackerURL() != null) ? summary.getTrackerURL()
  515.                             .toString() : null, Types.VARCHAR);
  516.             JDBCUtils.setParameter(insertSummary, 17, summary.getPreferredWeight(),
  517.                     Types.BIGINT);

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

  520.             try (final ResultSet keys = insertSummary.getGeneratedKeys()) {
  521.                 while (keys.next()) {
  522.                     productId = keys.getLong(1);
  523.                 }
  524.             }
  525.         }
  526.         // Now that the summary is stored, lets try to store the properties
  527.         addProductProperties(productId, summary.getProperties());
  528.         // And try to store the links
  529.         addProductLinks(productId, summary.getLinks());

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

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

  538.     /**
  539.      * Delete a product summary from the database If the summary doesn't have an
  540.      * indexId value set, throw an exception
  541.      *
  542.      * @param summary
  543.      *            ProductSummary object to delete
  544.      */
  545.     @Override
  546.     public synchronized ProductId removeProductSummary(ProductSummary summary)
  547.             throws Exception {
  548.         List<ProductId> removed = removeProductSummaries(Arrays.asList(summary));
  549.         return removed.get(0);
  550.     }

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

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

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

  581.             addAssociation.executeUpdate();
  582.         }

  583.         final Event e = new Event(event);
  584.         e.addProduct(summary);
  585.         LOGGER.log(
  586.                 Level.FINER,
  587.                 "[" + getName() + "] Added associations event id="
  588.                         + event.getIndexId() + ", productSummary source="
  589.                         + sid.getSource() + ", type=" + sid.getType()
  590.                         + ", code=" + sid.getCode() + " (id="
  591.                         + summary.getIndexId() + ")");

  592.         return e;
  593.     }

  594.     /**
  595.      * Delete the association, if it exists, between the given event and product
  596.      * summary.
  597.      *
  598.      * NOTE: this removes the association between the event and ALL versions of the product summary.
  599.      *
  600.      * @param event An event to remove an association with
  601.      * @param summary A ProductSummary to remove an association with
  602.      * @throws Exception if error occurs
  603.      */
  604.     @Override
  605.     public synchronized Event removeAssociation(Event event,
  606.             ProductSummary summary) throws Exception {

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

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

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

  613.         Long eventIndexId = event.getIndexId();
  614.         Long productIndexId = summary.getIndexId();
  615.         if (eventIndexId == null || productIndexId == null) {
  616.             return event;
  617.         }

  618.         final ProductId sid = summary.getId();
  619.         final String sql = "UPDATE productSummary"
  620.                 + " SET eventId=? WHERE source=? AND type=? AND code=?";
  621.         try (
  622.             final PreparedStatement removeAssociation = getConnection().prepareStatement(sql);
  623.         ) {
  624.             removeAssociation.setQueryTimeout(60);
  625.             // Now run the query
  626.             JDBCUtils.setParameter(removeAssociation, 1, null, Types.BIGINT);
  627.             // these will target EVERY version of the given product
  628.             JDBCUtils.setParameter(removeAssociation, 2, summary.getId()
  629.                     .getSource(), Types.VARCHAR);
  630.             JDBCUtils.setParameter(removeAssociation, 3, summary.getId().getType(),
  631.                     Types.VARCHAR);
  632.             JDBCUtils.setParameter(removeAssociation, 4, summary.getId().getCode(),
  633.                     Types.VARCHAR);

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

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

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

  650.         // pre 1.7.6 archive policies didn't always clean up after themselves
  651.         // handle it gracefully
  652.         if (productsList != null) {
  653.             // remove all product with given source, type, and code
  654.             Iterator<ProductSummary> iter = productsList.iterator();
  655.             while (iter.hasNext()) {
  656.                 ProductId id = iter.next().getId();
  657.                 if (id.isSameProduct(summary.getId())) {
  658.                     iter.remove();
  659.                 }
  660.             }
  661.             if (productsList.size() == 0) {
  662.                 // if this was the last product of that type, remove the list
  663.                 // too
  664.                 updatedEvent.getAllProducts().remove(summary.getType());
  665.             }
  666.         } else {
  667.             LOGGER.warning("Products list is empty for summary type "
  668.                     + summary.getId().toString()
  669.                     + ", when removing association");
  670.         }
  671.         return updatedEvent;
  672.     }

  673.     // ____________________________________
  674.     // Protected Methods
  675.     // ____________________________________

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

  687.         if (query == null) {
  688.             return clauseList; /* No query = No clauses */
  689.         }

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

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

  699.             if (queryCode != null && querySource != null && queryType != null) {
  700.                 // Better sub-select when these properties are specified
  701.                 clauseList
  702.                         .add(String
  703.                                 .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)",
  704.                                         SUMMARY_TABLE_ALIAS, SUMMARY_PRODUCT_INDEX_ID,
  705.                                         SUMMARY_PRODUCT_INDEX_ID, SUMMARY_TABLE,
  706.                                         SUMMARY_SOURCE, querySource,
  707.                                         SUMMARY_TYPE, queryType,
  708.                                         SUMMARY_CODE, queryCode,
  709.                                         SUMMARY_STATUS,
  710.                                         SUMMARY_UPDATE_TIME));
  711.             } else {
  712.                 clauseList
  713.                         .add(String
  714.                                 .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')",
  715.                                         SUMMARY_PRODUCT_INDEX_ID, SUMMARY_TABLE,
  716.                                         SUMMARY_TYPE, SUMMARY_TYPE, SUMMARY_SOURCE,
  717.                                         SUMMARY_SOURCE, SUMMARY_CODE, SUMMARY_CODE,
  718.                                         SUMMARY_UPDATE_TIME, SUMMARY_UPDATE_TIME,
  719.                                         SUMMARY_STATUS));
  720.             }
  721.         }
  722.         // If they only want superseded products, make a slightly different
  723.         // clause that has a subquery
  724.         else if (query.getResultType() == ProductIndexQuery.RESULT_TYPE_SUPERSEDED) {
  725.             clauseList
  726.                     .add(String
  727.                             .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')",
  728.                                     SUMMARY_PRODUCT_INDEX_ID, SUMMARY_TABLE,
  729.                                     SUMMARY_TYPE, SUMMARY_TYPE, SUMMARY_SOURCE,
  730.                                     SUMMARY_SOURCE, SUMMARY_CODE, SUMMARY_CODE,
  731.                                     SUMMARY_UPDATE_TIME, SUMMARY_UPDATE_TIME,
  732.                                     SUMMARY_STATUS));
  733.         }

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

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

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

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

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

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

  762.         String eventTimeColumn;
  763.         String eventLatitudeColumn;
  764.         String eventLongitudeColumn;
  765.         String eventMagnitudeColumn;
  766.         String eventDepthColumn;

  767.         // which table is used for event properties
  768.         if (query.getEventSearchType() == ProductIndexQuery.SEARCH_EVENT_PREFERRED) {
  769.             // search preferred event parameters in event table
  770.             eventTimeColumn = EVENT_TABLE_ALIAS + "." + EVENT_TIME;
  771.             eventLatitudeColumn = EVENT_TABLE_ALIAS + "." + EVENT_LATITUDE;
  772.             eventLongitudeColumn = EVENT_TABLE_ALIAS + "." + EVENT_LONGITUDE;
  773.             eventMagnitudeColumn = EVENT_TABLE_ALIAS + "." + EVENT_MAGNITUDE;
  774.             eventDepthColumn = EVENT_TABLE_ALIAS + "." + EVENT_DEPTH;
  775.         } else {
  776.             // search product summary parameters in summary table
  777.             eventTimeColumn = SUMMARY_TABLE_ALIAS + "." + SUMMARY_EVENT_TIME;
  778.             eventLatitudeColumn = SUMMARY_TABLE_ALIAS + "."
  779.                     + SUMMARY_EVENT_LATITUDE;
  780.             eventLongitudeColumn = SUMMARY_TABLE_ALIAS + "."
  781.                     + SUMMARY_EVENT_LONGITUDE;
  782.             eventMagnitudeColumn = SUMMARY_TABLE_ALIAS + "."
  783.                     + SUMMARY_EVENT_MAGNITUDE;
  784.             eventDepthColumn = SUMMARY_TABLE_ALIAS + "." + SUMMARY_EVENT_DEPTH;
  785.         }

  786.         Date minTime = query.getMinEventTime();
  787.         if (minTime != null) {
  788.             clauseList.add(String.format("%s>=%d", eventTimeColumn,
  789.                     minTime.getTime()));
  790.         }
  791.         Date maxTime = query.getMaxEventTime();
  792.         if (maxTime != null) {
  793.             clauseList.add(String.format("%s<=%d", eventTimeColumn,
  794.                     maxTime.getTime()));
  795.         }

  796.         BigDecimal minLat = query.getMinEventLatitude();
  797.         if (minLat != null) {
  798.             clauseList.add(String.format("%s>=%f", eventLatitudeColumn,
  799.                     minLat.doubleValue()));
  800.         }
  801.         BigDecimal maxLat = query.getMaxEventLatitude();
  802.         if (maxLat != null) {
  803.             clauseList.add(String.format("%s<=%f", eventLatitudeColumn,
  804.                     maxLat.doubleValue()));
  805.         }

  806.         BigDecimal minDepth = query.getMinEventDepth();
  807.         if (minDepth != null) {
  808.             clauseList.add(String.format("%s>=%f", eventDepthColumn,
  809.                     minDepth.doubleValue()));
  810.         }
  811.         BigDecimal maxDepth = query.getMaxEventDepth();
  812.         if (maxDepth != null) {
  813.             clauseList.add(String.format("%s<=%f", eventDepthColumn,
  814.                     maxDepth.doubleValue()));
  815.         }

  816.         BigDecimal minMag = query.getMinEventMagnitude();
  817.         if (minMag != null) {
  818.             clauseList.add(String.format("%s>=%f", eventMagnitudeColumn,
  819.                     minMag.doubleValue()));
  820.         }
  821.         BigDecimal maxMag = query.getMaxEventMagnitude();
  822.         if (maxMag != null) {
  823.             clauseList.add(String.format("%s<=%f", eventMagnitudeColumn,
  824.                     maxMag.doubleValue()));
  825.         }

  826.         Date minUpdateTime = query.getMinProductUpdateTime();
  827.         if (minUpdateTime != null) {
  828.             clauseList.add(String.format("%s>=%d", SUMMARY_UPDATE_TIME,
  829.                     minUpdateTime.getTime()));
  830.         }
  831.         Date maxUpdateTime = query.getMaxProductUpdateTime();
  832.         if (maxUpdateTime != null) {
  833.             clauseList.add(String.format("%s<=%d", SUMMARY_UPDATE_TIME,
  834.                     maxUpdateTime.getTime()));
  835.         }

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

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

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

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

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

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

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

  865.         if (minLon != null && maxLon != null) {
  866.             if (maxLon.doubleValue() < minLon.doubleValue()) {
  867.                 // If the normalized maxLon is less than the normalized minLon,
  868.                 // the
  869.                 // span crosses
  870.                 // the date line
  871.                 Double minLonDouble = minLon.doubleValue();
  872.                 Double maxLonDouble = maxLon.doubleValue();
  873.                 // If the range crosses the date line, split it into 2 clauses
  874.                 String lonClause = String.format(
  875.                         "((%s > %f AND %s <= 180) OR (%s < %f AND %s > -180))",
  876.                         eventLongitudeColumn, minLonDouble,
  877.                         eventLongitudeColumn, eventLongitudeColumn,
  878.                         maxLonDouble, eventLongitudeColumn);
  879.                 clauseList.add(lonClause);
  880.             } else {
  881.                 clauseList.add(String.format("%s>=%f and %s<=%f",
  882.                         eventLongitudeColumn, minLon.doubleValue(),
  883.                         eventLongitudeColumn, maxLon.doubleValue()));
  884.             }
  885.         } else if (minLon != null) {
  886.             clauseList.add(String.format("%s>=%f", eventLongitudeColumn,
  887.                     minLon.doubleValue()));
  888.         } else if (maxLon != null) {
  889.             clauseList.add(String.format("%s<=%f", eventLongitudeColumn,
  890.                     maxLon.doubleValue()));
  891.         }
  892.         return clauseList;
  893.     }

  894.     /**
  895.      * Create the full SELECT query for the products table using the default clauseList.
  896.      *
  897.      * @param query
  898.      *     Query to build.
  899.      * @return String containing the full SELECT query
  900.      * @see #buildProductClauses(ProductIndexQuery)
  901.      */
  902.     protected String buildProductQuery(final ProductIndexQuery query) {

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

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

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

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

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

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

  931.         return sql.toString();
  932.     }

  933.     /**
  934.      * Populate links and properties for provided product summaries.
  935.      *
  936.      * @param summaries List of ProductSummaries
  937.      * @throws Exception if error occurs
  938.      */
  939.     protected synchronized void loadProductSummaries(final List<ProductSummary> summaries)
  940.             throws Exception {
  941.         if (summaries.size() == 0) {
  942.             // nothing to load
  943.             return;
  944.         }

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

  950.         // load all links in one query
  951.         final String linkSql = "SELECT productSummaryIndexId as id, relation, url"
  952.                 + " FROM productSummaryLink"
  953.                 + " WHERE productSummaryIndexId IN ("
  954.                 + StringUtils.join(
  955.                         summaryMap.keySet().stream().collect(Collectors.toList()),
  956.                         ",")
  957.                 + ")";
  958.         try (
  959.             final PreparedStatement statement = getConnection().prepareStatement(linkSql);
  960.         ) {
  961.             statement.setQueryTimeout(60);
  962.             try (
  963.                 final ResultSet results = statement.executeQuery();
  964.             ) {
  965.                 while (results.next()) {
  966.                     Long id = results.getLong("id");
  967.                     String relation = results.getString("relation");
  968.                     String uri = results.getString("url");
  969.                     // add properties to existing objects
  970.                     summaryMap.get(id).addLink(relation, new URI(uri));
  971.                 }
  972.             }
  973.         }

  974.         // load all properties in one query
  975.         final String propertySql = "SELECT productSummaryIndexId as id, name, value"
  976.                 + " FROM productSummaryProperty"
  977.                 + " WHERE productSummaryIndexId IN ("
  978.                 + StringUtils.join(
  979.                         summaryMap.keySet().stream().collect(Collectors.toList()),
  980.                         ",")
  981.                 + ")";
  982.         try (
  983.             final PreparedStatement statement =
  984.                     getConnection().prepareStatement(propertySql);
  985.         ) {
  986.             statement.setQueryTimeout(60);
  987.             try (
  988.                 final ResultSet results = statement.executeQuery();
  989.             ) {
  990.                 while (results.next()) {
  991.                     Long id = results.getLong("id");
  992.                     String name = results.getString("name");
  993.                     String value = results.getString("value");
  994.                     // add properties to existing objects
  995.                     summaryMap.get(id).getProperties().put(name, value);
  996.                 }
  997.             }
  998.         }
  999.     }

  1000.     /**
  1001.      * Parse ProductSummary without loading links or properties.
  1002.      *
  1003.      * @param results ResultSet to parse
  1004.      * @return ProductSummary object without links or properties.
  1005.      * @throws Exception if error occurs
  1006.      */
  1007.     protected ProductSummary parseProductSummary(ResultSet results)
  1008.             throws Exception {
  1009.         ProductSummary p = new ProductSummary();
  1010.         p.setIndexId(results.getLong(SUMMARY_PRODUCT_INDEX_ID));
  1011.         ProductId pid = ProductId.parse(results.getString(SUMMARY_PRODUCT_ID));
  1012.         p.setId(pid);
  1013.         p.setEventSource(results.getString(SUMMARY_EVENT_SOURCE));
  1014.         p.setEventSourceCode(results.getString(SUMMARY_EVENT_SOURCE_CODE));
  1015.         try {
  1016.             p.setEventTime(new Date(results.getLong(SUMMARY_EVENT_TIME)));
  1017.         } catch (Exception e) {
  1018.             p.setEventTime(null);
  1019.         }

  1020.         // getDouble() returns 0 if the value was actually NULL. In this case,
  1021.         // we are going to set the value to null
  1022.         String latitude = results.getString(SUMMARY_EVENT_LATITUDE);
  1023.         if (latitude == null) {
  1024.             p.setEventLatitude(null);
  1025.         } else {
  1026.             p.setEventLatitude(new BigDecimal(latitude));
  1027.         }
  1028.         String longitude = results.getString(SUMMARY_EVENT_LONGITUDE);
  1029.         if (longitude == null) {
  1030.             p.setEventLongitude(null);
  1031.         } else {
  1032.             p.setEventLongitude(new BigDecimal(longitude));
  1033.         }
  1034.         String depth = results.getString(SUMMARY_EVENT_DEPTH);
  1035.         if (depth == null) {
  1036.             p.setEventDepth(null);
  1037.         } else {
  1038.             p.setEventDepth(new BigDecimal(depth));
  1039.         }
  1040.         String magnitude = results.getString(SUMMARY_EVENT_MAGNITUDE);
  1041.         if (magnitude == null) {
  1042.             p.setEventMagnitude(null);
  1043.         } else {
  1044.             p.setEventMagnitude(new BigDecimal(magnitude));
  1045.         }
  1046.         p.setVersion(results.getString(SUMMARY_VERSION));
  1047.         p.setStatus(results.getString(SUMMARY_STATUS));
  1048.         p.setTrackerURL((results.getString(SUMMARY_TRACKER_URL) != null) ? new URL(
  1049.                 results.getString(SUMMARY_TRACKER_URL)) : null);
  1050.         p.setPreferredWeight(results.getLong(SUMMARY_PREFERRED));

  1051.         return p;
  1052.     }

  1053.     /**
  1054.      *
  1055.      * @param summaries List of product summaries to remove
  1056.      * @return List of ProductIds that were removed
  1057.      * @throws Exception if error occurs
  1058.      */
  1059.     public synchronized List<ProductId> removeProductSummaries(
  1060.             final List<ProductSummary> summaries) throws Exception {
  1061.         // index by id
  1062.         final ArrayList<ProductId> ids = new ArrayList<>();
  1063.                 // index by id
  1064.         final Map<Long, ProductSummary> summaryMap = new HashMap<>();
  1065.         for (final ProductSummary summary : summaries) {
  1066.             if (summary.getIndexId() == null) {
  1067.                 LOGGER.log(Level.WARNING, "[" + getName()
  1068.                         + "] Could not delete product summary. Index id not found");
  1069.                 throw new Exception("[" + getName()
  1070.                         + "] Could not delete summary. Index id not found.");
  1071.             }
  1072.             summaryMap.put(summary.getIndexId(), summary);
  1073.             ids.add(summary.getId());
  1074.         }

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

  1078.         // remove all products in one query
  1079.         // on delete cascade wasn't always set...
  1080.         final String[] sqls = {
  1081.             "DELETE FROM productSummaryLink WHERE productSummaryIndexId IN",
  1082.             "DELETE FROM productSummaryProperty WHERE productSummaryIndexId IN",
  1083.             "DELETE FROM productSummary WHERE id IN",
  1084.         };
  1085.         final String idsIn =" ("
  1086.                 + StringUtils.join(
  1087.                         summaryMap.keySet().stream().collect(Collectors.toList()),
  1088.                         ",")
  1089.                 + ")";
  1090.         for (final String sql : sqls) {
  1091.             try (
  1092.                 final PreparedStatement statement =
  1093.                         verifyConnection().prepareStatement(sql + idsIn);
  1094.             ) {
  1095.                 statement.setQueryTimeout(60);
  1096.                 int rows = statement.executeUpdate();
  1097.                 LOGGER.log(Level.FINER, "[" + getName() + "] removed " + rows + " rows");
  1098.             }
  1099.         }

  1100.         return ids;
  1101.     }

  1102.     /**
  1103.      * Save the properties in the database and associate them to the given
  1104.      * productId
  1105.      *
  1106.      * @param productId long product ID to associate to
  1107.      * @param properties Map of properties to save
  1108.      * @throws SQLException if SQL error occurs
  1109.      */
  1110.     protected synchronized void addProductProperties(final long productId,
  1111.             final Map<String, String> properties) throws SQLException {
  1112.         // Loop through the properties list and add them all to the database
  1113.         final String sql = "INSERT INTO productSummaryProperty"
  1114.                 + " (productSummaryIndexId, name, value) VALUES (?, ?, ?)";
  1115.         try (
  1116.             final PreparedStatement insertProperty = getConnection().prepareStatement(sql);
  1117.         ) {
  1118.             insertProperty.setQueryTimeout(60);
  1119.             for (String key : properties.keySet()) {
  1120.                 JDBCUtils.setParameter(insertProperty, 1, productId, Types.BIGINT);
  1121.                 JDBCUtils.setParameter(insertProperty, 2, key, Types.VARCHAR);
  1122.                 JDBCUtils.setParameter(insertProperty, 3, properties.get(key),
  1123.                         Types.VARCHAR);
  1124.                 insertProperty.addBatch();
  1125.                 if (LOGGER.isLoggable(Level.FINEST)) {
  1126.                     LOGGER.log(Level.FINEST, "[" + getName() + "] Added property "
  1127.                             + key + ":" + properties.get(key) + " for product "
  1128.                             + productId);
  1129.                 }
  1130.             }
  1131.             insertProperty.executeBatch();
  1132.         }
  1133.     }

  1134.     /**
  1135.      * Save the links in the database and associate them to the given productId
  1136.      *
  1137.      * @param productId
  1138.      *            Index id of the product to select
  1139.      * @param links
  1140.      *            Map of relations to URIs
  1141.      * @throws SQLException if sql error occurs
  1142.      */
  1143.     protected synchronized void addProductLinks(long productId,
  1144.             Map<String, List<URI>> links) throws SQLException {
  1145.         // Loop through the properties list and add them all to the database
  1146.         final String sql = "INSERT INTO productSummaryLink"
  1147.                 + " (productSummaryIndexId, relation, url) VALUES (?, ?, ?)";
  1148.         try (
  1149.             final PreparedStatement insertLink = getConnection().prepareStatement(sql);
  1150.         ) {
  1151.             insertLink.setQueryTimeout(60);
  1152.             for (final String relation : links.keySet()) {
  1153.                 for (final URI uri : links.get(relation)) {
  1154.                     JDBCUtils.setParameter(insertLink, 1, productId, Types.BIGINT);
  1155.                     JDBCUtils.setParameter(insertLink, 2, relation, Types.VARCHAR);
  1156.                     JDBCUtils.setParameter(insertLink, 3, uri.toString(), Types.VARCHAR);
  1157.                     insertLink.addBatch();
  1158.                     LOGGER.log(Level.FINEST, "[" + getName() + "] Added link "
  1159.                             + relation + ":" + uri.toString() + " for product "
  1160.                             + productId);
  1161.                 }
  1162.             }
  1163.             insertLink.executeBatch();
  1164.         }
  1165.     }

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

  1175.         if (normalizedLon <= 180 && normalizedLon > -180) {
  1176.             return normalizedLon;
  1177.         }

  1178.         // If the value is above 180, make it negative by subtracting 360
  1179.         if (normalizedLon > 180) {
  1180.             normalizedLon = normalizedLon % 360;
  1181.             normalizedLon = normalizedLon - 360;
  1182.             return normalizedLon;
  1183.         }

  1184.         // If the value is below 180, make it positive by adding 360
  1185.         if (normalizedLon <= -180) {
  1186.             normalizedLon = normalizedLon % 360;
  1187.             normalizedLon = normalizedLon + 360;
  1188.             return normalizedLon;
  1189.         }

  1190.         return normalizedLon;
  1191.     }

  1192.     /**
  1193.      * Wrapper to normalize BigDecimal longitudes
  1194.      *
  1195.      * @param lon BigDecimal Longitude
  1196.      * @return Normalized BigDecimal latitude
  1197.      */
  1198.     protected BigDecimal normalizeLongitude(BigDecimal lon) {
  1199.         if (lon == null) {
  1200.             return null;
  1201.         }

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

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

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

  1219.         try (
  1220.             final PreparedStatement updateDeletedEvent =
  1221.                     getConnection().prepareStatement(deletedSql);
  1222.             final PreparedStatement updateEvent =
  1223.                     getConnection().prepareStatement(updatedSql);
  1224.         ) {
  1225.             // big events take time...
  1226.             updateDeletedEvent.setQueryTimeout(300);
  1227.             updateEvent.setQueryTimeout(300);
  1228.             Iterator<Event> iter = events.iterator();
  1229.             while (iter.hasNext()) {
  1230.                 Event updated = iter.next();

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

  1234.                 try {
  1235.                     if (updated.isDeleted()) {
  1236.                         // only update status if event deleted, leave other
  1237.                         // parameters intact
  1238.                         JDBCUtils.setParameter(updateDeletedEvent, 1,
  1239.                                 EVENT_STATUS_DELETE, Types.VARCHAR);
  1240.                         JDBCUtils.setParameter(updateDeletedEvent, 2, indexId,
  1241.                                 Types.BIGINT);

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

  1245.                         // otherwise update event parameters
  1246.                         JDBCUtils.setParameter(updateEvent, 1,
  1247.                                 new Date().getTime(), Types.BIGINT);
  1248.                         JDBCUtils.setParameter(updateEvent, 2, summary.getSource(),
  1249.                                 Types.VARCHAR);
  1250.                         JDBCUtils.setParameter(updateEvent, 3,
  1251.                                 summary.getSourceCode(), Types.VARCHAR);

  1252.                         Long eventTime = null;
  1253.                         if (summary.getTime() != null) {
  1254.                             eventTime = summary.getTime().getTime();
  1255.                         }
  1256.                         JDBCUtils.setParameter(updateEvent, 4, eventTime,
  1257.                                 Types.BIGINT);

  1258.                         Double latitude = null;
  1259.                         if (summary.getLatitude() != null) {
  1260.                             latitude = summary.getLatitude().doubleValue();
  1261.                         }
  1262.                         JDBCUtils.setParameter(updateEvent, 5, latitude,
  1263.                                 Types.DOUBLE);

  1264.                         Double longitude = null;
  1265.                         if (summary.getLongitude() != null) {
  1266.                             longitude = summary.getLongitude().doubleValue();
  1267.                         }
  1268.                         JDBCUtils.setParameter(updateEvent, 6, longitude,
  1269.                                 Types.DOUBLE);

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

  1276.                         Double magnitude = null;
  1277.                         if (summary.getMagnitude() != null) {
  1278.                             magnitude = summary.getMagnitude().doubleValue();
  1279.                         }
  1280.                         JDBCUtils.setParameter(updateEvent, 8, magnitude,
  1281.                                 Types.DOUBLE);

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

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

  1286.                         updateEvent.executeUpdate();
  1287.                     }

  1288.                     LOGGER.log(Level.FINEST, "[" + getName()
  1289.                             + "] Updated event properties in Product Index");
  1290.                 } catch (Exception e) {
  1291.                     LOGGER.log(Level.WARNING, "[" + getName()
  1292.                             + "] Error updating event properties, eventid="
  1293.                             + indexId, e);
  1294.                     // trigger a rollback
  1295.                     throw e;
  1296.                 }
  1297.             }
  1298.         }
  1299.     }

  1300. }