org.semispace.semimeter.dao.SemiMeterDao.java Source code

Java tutorial

Introduction

Here is the source code for org.semispace.semimeter.dao.SemiMeterDao.java

Source

/*
 * Copyright 2009 Erlend Nossum
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.semispace.semimeter.dao;

import org.semispace.SemiEventRegistration;
import org.semispace.SemiSpace;
import org.semispace.SemiSpaceInterface;
import org.semispace.semimeter.bean.ArrayQuery;
import org.semispace.semimeter.bean.Item;
import org.semispace.semimeter.bean.JsonResults;
import org.semispace.semimeter.bean.ParameterizedQuery;
import org.semispace.semimeter.space.CounterHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils;
import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
import org.springframework.stereotype.Service;

import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

@Service("semimeterDao")
public class SemiMeterDao implements InitializingBean, DisposableBean {
    private static final Logger log = LoggerFactory.getLogger(SemiMeterDao.class);

    private SimpleJdbcTemplate jdbcTemplate;
    private SemiEventRegistration chRegistration;
    private SemiEventRegistration pqRegistration;
    private SemiEventRegistration aqRegistration;

    private ReadWriteLock rwl = new ReentrantReadWriteLock();
    private SemiSpaceInterface space;
    private static final int MAX_PATH_LENGTH = 2048;

    @Autowired
    @Qualifier("semiMeterDataSource")
    public void setDataSource(DataSource semiMeterDataSource) {
        this.jdbcTemplate = new SimpleJdbcTemplate(semiMeterDataSource);
    }

    /**
     * @return size of stalegroup table, or -1 if any errors occur.
     */
    public int size() {
        int result = -1;
        rwl.readLock().lock();
        try {
            result = this.jdbcTemplate.queryForInt("select count(*) from meter");
        } catch (DataAccessException e) {
            log.warn("Table probably not yet created. Got (intentionally masked) " + e.getMessage());
        } finally {
            rwl.readLock().unlock();
        }

        return result;
    }

    public boolean isAlive() {
        rwl.readLock().lock();
        try {
            int result = this.jdbcTemplate.queryForInt("select count(*) from meter where updated < 1");
            if (result >= 0) {
                return true;
            }
        } catch (DataAccessException e) {
            log.warn("Table probably not yet created. Got (intentionally masked) " + e.getMessage());
        } finally {
            rwl.readLock().unlock();
        }
        return false;
    }

    /**
     * Method called from Spring. Will (try to) create the table with the meter, if it does not already exist.
     */
    public void afterPropertiesSet() {
        log.debug("Retrieving semispace.");
        space = SemiSpace.retrieveSpace();
        log.debug("Registering listeners.");
        if (chRegistration != null || pqRegistration != null) {
            log.error("Did not expect any SemiSpace registration to exist already. Not registering again");
        } else {
            SpacePQListener spacePqListener = new SpacePQListener(space, this,
                    "Query listener - both parameterized and array queries");
            // Listen for events ten years
            chRegistration = space.notify(new CounterHolder(),
                    new Space2Dao(space, this, "CounterHolder which holds elements to be counted"),
                    SemiSpace.ONE_DAY * 3650);
            // Reusing
            pqRegistration = space.notify(new ParameterizedQuery(), spacePqListener, SemiSpace.ONE_DAY * 3650);
            aqRegistration = space.notify(new ArrayQuery(), spacePqListener, SemiSpace.ONE_DAY * 3650);

        }

        if (size() < 0) {
            log.info("Creating table meter");
            // The data type integer in the database is a long in the java world.
            try {
                jdbcTemplate.getJdbcOperations().execute(
                        "create table meter(updated bigint NOT NULL, counted integer NOT NULL, path varchar("
                                + MAX_PATH_LENGTH + ") NOT NULL)");
            } catch (Exception e) {
                try {
                    // Probably a different database
                    jdbcTemplate.getJdbcOperations().execute(
                            "create table meter(updated integer NOT NULL, counted integer NOT NULL, path varchar("
                                    + MAX_PATH_LENGTH + ") NOT NULL)");
                } catch (Exception e2) {
                    log.error("Did not manage to create table meter?! First exception is masked: " + e.getMessage(),
                            e2);
                }
            }
        }
        if (size() < 1) {
            try {
                // Need an initial default value
                jdbcTemplate.getJdbcOperations().execute(
                        "insert into meter(updated, counted, path) values (1, 0, '__disregarded needed default__')");
            } catch (Exception e) {
                log.error("Could not create default?!", e);
            }
        }
        try {
            log.debug("Creating indexes (even if they already exist)");
            // Create indexes
            jdbcTemplate.getJdbcOperations().execute("create index meter_updt_ix on meter( updated )");
            jdbcTemplate.getJdbcOperations().execute("create index meter_path_ix on meter( path )");
        } catch (Exception e) {
            log.error("Did not manage to create index on updated field. This is probably as it already exists. "
                    + "Ignoring this, as we ALWAYS try to create indexes after restart. Masked exception: " + e);
        }
        if (size() > 1) {
            // We don't need default any more if we have data
            jdbcTemplate.getJdbcOperations()
                    .execute("DELETE FROM meter where updated=1 and path like '__disregarded needed default__'");
        }
        if (isAlive()) {
            try {
                jdbcTemplate.getJdbcOperations().execute("select count(count) from meter meter");
                log.warn("Renaming field count to counted in table meter.");
                try {
                    jdbcTemplate.getJdbcOperations()
                            .execute("ALTER TABLE meter CHANGE COLUMN count counted integer NOT NULL;");
                } catch (Exception e) {
                    log.error(
                            "Could not rename column count to counted. Please drop table or rename column manually");
                }
            } catch (Exception e) {
                // Expected
            }
        }
    }

    protected void performInsertion(Collection<Item> items) {
        //log.debug("Performing batch insertion of "+items.size()+" items.");
        SqlParameterSource[] insertArgs = SqlParameterSourceUtils.createBatch(items.toArray());
        List<Object[]> updateArgs = new ArrayList<Object[]>();

        for (Item item : items) {
            // Original just called insert
            updateArgs.add(new Object[] { item.getAccessNumber(), item.getPath(), item.getWhen() });
        }
        rwl.writeLock().lock();
        try {
            try {
                //log.debug("INSERT INTO meter(updated, count, path) SELECT DISTINCT ?, 0, ? FROM meter WHERE NOT EXISTS ( SELECT * FROM meter WHERE updated=? AND path=?)");
                jdbcTemplate.batchUpdate(
                        "INSERT INTO meter(updated, counted, path) SELECT DISTINCT :when, 0, :path FROM meter WHERE NOT EXISTS ( SELECT * FROM meter WHERE updated=:when AND path=:path)",
                        insertArgs);
            } catch (Exception e) {
                log.warn(
                        "Unlikely event occurred - failure whilst inserting priming elements. This is not overly critical. Masked exception: "
                                + e);
            }
            jdbcTemplate.batchUpdate("update meter SET counted=counted+? WHERE path like ? and updated=?",
                    updateArgs);
        } catch (Exception e) {
            log.error("Could not update elements", e);
        } finally {
            rwl.writeLock().unlock();
        }
    }

    private void failed_rewrite_performInsertion(Collection<Item> items) {
        //log.debug("Performing batch insertion of "+items.size()+" items.");
        //List<Object[]> insertArgs = new ArrayList<Object[]>();
        //List<Object[]> updateArgs = new ArrayList<Object[]>();

        SqlParameterSource[] insertArgs = SqlParameterSourceUtils.createBatch(items.toArray());
        SqlParameterSource[] updateArgs = SqlParameterSourceUtils.createBatch(items.toArray());

        //for ( Item item : items ) {
        // Original just called insert
        //insertArgs.add( new Object[]{item.getWhen(), item.getPath(),item.getWhen(), item.getPath()});
        //updateArgs.add( new Object[] {item.getAccessNumber(), item.getPath(), item.getWhen()});
        //}
        rwl.writeLock().lock();
        try {
            try {
                //log.debug("INSERT INTO meter(updated, count, path) SELECT DISTINCT ?, 0, ? FROM meter WHERE NOT EXISTS ( SELECT * FROM meter WHERE updated=? AND path=?)");
                jdbcTemplate.batchUpdate(
                        "INSERT INTO meter(updated, counted, path) SELECT DISTINCT :when, 0, :path FROM meter WHERE NOT EXISTS ( SELECT * FROM meter WHERE updated=:when AND path=:path)",
                        insertArgs);
            } catch (Exception e) {
                log.warn(
                        "Unlikely event occurred - failure whilst inserting priming elements. This is not overly critical. Masked exception: "
                                + e);
            }
            jdbcTemplate.batchUpdate(
                    "update meter SET counted=counted+:accessNumber WHERE :path like :path and updated=:when",
                    updateArgs);
        } catch (Exception e) {
            log.error("Could not update elements", e);
        } finally {
            rwl.writeLock().unlock();
        }
    }

    /**
     * Insertion is performed like this: Try to insert item with count of zero if does not already
     * exist. Then update the count to the correct value. The candidate key for the element is
     * (when + path). This method is now legacy.
     *
     * Used in junit test for comparison purpose. For this reason is the method protected.
     *
     * @deprecated Do not use this code other places than test
     */
    protected void insert(Item item) {
        rwl.writeLock().lock();
        try {
            try {
                jdbcTemplate.update(
                        "INSERT INTO meter(updated, counted, path) SELECT DISTINCT ?, 0, ? FROM meter WHERE NOT EXISTS ( SELECT * FROM meter WHERE updated=? AND path=?)",
                        new Object[] { item.getWhen(), item.getPath(), item.getWhen(), item.getPath() });
            } catch (Exception e) {
                log.warn("Unlikely event occurred - failure whilst inserting priming element", e);
            }
            jdbcTemplate.update("update meter SET counted=counted+? WHERE path like ? and updated=?",
                    new Object[] { item.getAccessNumber(), item.getPath(), item.getWhen() });
        } catch (Exception e) {
            log.error("Could not insert or update", e);
        } finally {
            rwl.writeLock().unlock();
        }
    }

    @Override
    public void destroy() throws Exception {
        if (chRegistration != null) {
            chRegistration.getLease().cancel();
            chRegistration = null;
        }
        if (pqRegistration != null) {
            pqRegistration.getLease().cancel();
            pqRegistration = null;
        }
        if (aqRegistration != null) {
            aqRegistration.getLease().cancel();
            aqRegistration = null;
        }
    }

    public Long sumItems(long startAt, long endAt, String path) {
        Long result = Long.valueOf(-1);
        rwl.readLock().lock();
        try {
            final String sql = "select sum(counted) from meter " + "WHERE "
                    + "updated>? AND updated<=?  AND path like ?";
            //log.debug("Querying with ("+startAt+","+endAt+","+path+") : "+sql);
            Long sum = Long.valueOf(jdbcTemplate.queryForLong(sql,
                    new Object[] { Long.valueOf(startAt), Long.valueOf(endAt), path }));
            result = sum;
        } finally {
            rwl.readLock().unlock();
        }

        return result;
    }

    public JsonResults[] performParameterizedQuery(long startAt, long endAt, String path) {
        if (path.indexOf("$") == -1 || path.indexOf("$") != path.lastIndexOf("$")) {
            throw new RuntimeException("Expecting one and only one $");
        }
        List<JsonResults> jrs = new ArrayList<JsonResults>();
        rwl.readLock().lock();
        try {
            String prefix = path.substring(0, path.indexOf("$"));
            String postfix = path.substring(path.indexOf("$") + 1);
            List<String> variants = createStringListOfVariants(startAt, endAt, path);
            log.trace("Got variants: {}", variants);
            for (String s : variants.toArray(new String[0])) {
                Long sum = sumItems(startAt, endAt, prefix + s + postfix);
                JsonResults jr = new JsonResults();
                jr.setKey(s);
                jr.setValue(sum.toString());
                jrs.add(jr);
            }
        } finally {
            rwl.readLock().unlock();
        }

        return jrs.toArray(new JsonResults[0]);
    }

    private List<String> createStringListOfVariants(long startAt, long endAt, String path) {
        List<String> list = new ArrayList<String>();
        rwl.readLock().lock();
        try {
            String prefix = path.substring(0, path.indexOf("$"));
            String postfix = path.substring(path.indexOf("$") + 1);
            String sql = "SELECT distinct path AS path FROM meter WHERE path like ? "
                    + "AND path like ? AND updated>? AND updated<=? ORDER BY path";
            List<Map<String, Object>> result = jdbcTemplate.queryForList(sql,
                    new Object[] { prefix + "%", "%" + postfix + "%", Long.valueOf(startAt), Long.valueOf(endAt) });

            //log.debug("Got "+result.size()+" results when doing "+sql+" with regards to ("+prefix+","+postfix+", "+startAt+","+endAt+")");
            for (Map<String, Object> m : result) {
                String s = (String) m.get("path");
                s = s.substring(prefix.length(), s.length() - postfix.length());
                list.add(s);
            }
        } finally {
            rwl.readLock().unlock();
        }

        return list;
    }

    /**
     * @param numberOfSamples Presuming number to be positive
     */
    public JsonResults[] createTimeArray(String path, long endAt, long startAt, Integer numberOfSamples) {
        rwl.readLock().lock();
        List<Map<String, Object>> list = null;
        try {
            final String sql = "SELECT updated, counted FROM meter " + "WHERE "
                    + "updated>? AND updated<=?  AND path like ? ORDER BY updated";
            //log.debug("Querying with ("+startAt+","+endAt+","+path+") : "+sql);
            list = jdbcTemplate.queryForList(sql,
                    new Object[] { Long.valueOf(startAt), Long.valueOf(endAt), path });
        } finally {
            rwl.readLock().unlock();
        }
        // Need to add the start and stop in order to get the array correctly bounded.
        Map<String, Object> fake = new HashMap<String, Object>();
        fake.put("updated", "" + startAt);
        fake.put("counted", "0");
        list.add(0, fake);
        fake = new HashMap();
        fake.put("updated", "" + endAt);
        fake.put("counted", "0");
        list.add(fake);
        List<JsonResults> result = flatten(list, numberOfSamples.intValue());

        return result.toArray(new JsonResults[0]);
    }

    /**
     * Protected for the benefit of junit test
     */
    protected List<JsonResults> flatten(List<Map<String, Object>> list, int num) {
        // Using AtomicInteger just because it has nice adding features.
        List<AtomicInteger> res = new ArrayList<AtomicInteger>(num);
        for (int i = 0; i < num; i++) {
            // Prime
            res.add(new AtomicInteger());
        }
        if (list == null || list.isEmpty()) {
            return transformToJR(res);
        }
        long start = Long.valueOf("" + list.get(0).get("updated")).longValue();
        long end = Long.valueOf("" + list.get(list.size() - 1).get("updated")).longValue();
        long subrange = (end - start) / num;
        if (subrange < 1) {
            subrange = 1;
        }
        long mod = (end - start) / subrange;
        if (mod > 0) {
            log.trace("Having a rest which I need to retain. Just adding it to subrange. Rest is " + mod);
            subrange += mod;
        }
        int count = 0;
        for (int i = 0; i < num; i++) {
            start += subrange;
            //log.debug("Starting at "+start+" at iteration "+i+" having subrange "+subrange);
            // Still having more data, and current updated value less than where to start in scale
            while (count < list.size() && Long.valueOf("" + list.get(count).get("updated")).longValue() <= start) {
                //log.debug("Shall increment place "+i);
                res.get(i).getAndAdd(Integer.valueOf("" + list.get(count).get("counted")).intValue());
                count++;
            }
        }
        if (count < list.size()) {
            log.error(
                    "Sanity: Did not use all data!? This may happen if the data are not sorted in the updated field. Missing "
                            + (list.size() - count) + " elements");
        }

        return transformToJR(res);
    }

    private List<JsonResults> transformToJR(List<AtomicInteger> res) {
        List<JsonResults> list = new ArrayList<JsonResults>();
        for (int i = 0; i < res.size(); i++) {
            JsonResults jr = new JsonResults();
            jr.setKey("" + i);
            jr.setValue(res.get(i).toString());
            list.add(jr);
        }
        return list;
    }

    private String leaseInfo(SemiEventRegistration lease) {
        return "{id: " + lease.getId() + ", lease.holderId:" + lease.getLease().getHolderId() + "}";
    }

    /**
     * This method is intended to be used from a junit tests.
     *
     * @param whenStartedTest From inclusive when to delete item
     * @param path Path to delete, with percentage signs as applicable.
     */
    protected void deleteItemsFrom(long whenStartedTest, String path) {
        rwl.writeLock().lock();
        try {
            jdbcTemplate.update("DELETE FROM meter WHERE path like ? and updated>=?",
                    new Object[] { path, whenStartedTest });
        } catch (Exception e) {
            throw new RuntimeException("Could not delete items. From " + whenStartedTest + ", " + "path: " + path,
                    e);
        } finally {
            rwl.writeLock().unlock();
        }

    }

    /**
     * @param start Where to start, inclusive
     * @param end Where to end, inclusive
     */
    public void collate(long start, long end) {
        rwl.readLock().lock();
        // Find new elements
        List<Map<String, Object>> items = null;
        try {
            final String sql = "SELECT path AS path, sum(counted) AS counted, min(updated) AS updated FROM meter "
                    + "WHERE " + "updated>=? AND updated<=? GROUP BY path";
            log.debug("Querying with (" + start + "," + end + ") : " + sql);
            items = jdbcTemplate.queryForList(sql, new Object[] { Long.valueOf(start), Long.valueOf(end) });
        } finally {
            rwl.readLock().unlock();
        }
        // Remove old items
        rwl.writeLock().lock();
        try {
            for (Map<String, Object> item : items) {
                jdbcTemplate.update("DELETE FROM meter WHERE path like ? AND updated>=? AND updated<=?",
                        new Object[] { item.get("path"), start, end });
            }
        } finally {
            rwl.writeLock().unlock();
        }
        // Translate
        List<Item> replacements = new ArrayList<Item>();
        for (Map<String, Object> item : items) {
            Item i = new Item();
            i.setPath(item.get("path").toString());
            i.setAccessNumber(Integer.parseInt(item.get("counted").toString()));
            i.setWhen(Long.parseLong(item.get("updated").toString()));
            replacements.add(i);
        }
        // Insert
        performInsertion(replacements);
    }
}