/****************************************************************************** * Copyright © 2013-2016 The Nxt Core Developers. * * * * See the AUTHORS.txt, DEVELOPER-AGREEMENT.txt and LICENSE.txt files at * * the top-level directory of this distribution for the individual copyright * * holder information and the developer policies on copyright and licensing. * * * * Unless otherwise agreed in a custom licensing agreement, no part of the * * Nxt software, including this file, may be copied, modified, propagated, * * or distributed except according to the terms contained in the LICENSE.txt * * file. * * * * Removal or modification of this copyright notice is prohibited. * * * ******************************************************************************/ package nxt; import nxt.db.DbClause; import nxt.db.DbIterator; import nxt.db.DbKey; import nxt.db.DbUtils; import nxt.db.EntityDbTable; import nxt.db.ValuesDbTable; import nxt.util.Logger; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; import java.util.Arrays; import java.util.List; public final class Poll extends AbstractPoll { private static final boolean isPollsProcessing = Nxt.getBooleanProperty("nxt.processPolls"); public static final class OptionResult { private long result; private long weight; private OptionResult(long result, long weight) { this.result = result; this.weight = weight; } public long getResult() { return result; } public long getWeight() { return weight; } private void add(long vote, long weight) { this.result += vote; this.weight += weight; } } private static final DbKey.LongKeyFactory<Poll> pollDbKeyFactory = new DbKey.LongKeyFactory<Poll>("id") { @Override public DbKey newKey(Poll poll) { return poll.dbKey; } }; private final static EntityDbTable<Poll> pollTable = new EntityDbTable<Poll>("poll", pollDbKeyFactory, "name,description") { @Override protected Poll load(Connection con, ResultSet rs) throws SQLException { return new Poll(rs); } @Override protected void save(Connection con, Poll poll) throws SQLException { poll.save(con); } }; private static final DbKey.LongKeyFactory<Poll> pollResultsDbKeyFactory = new DbKey.LongKeyFactory<Poll>("poll_id") { @Override public DbKey newKey(Poll poll) { return poll.dbKey; } }; private static final ValuesDbTable<Poll, OptionResult> pollResultsTable = new ValuesDbTable<Poll, OptionResult>("poll_result", pollResultsDbKeyFactory) { @Override protected OptionResult load(Connection con, ResultSet rs) throws SQLException { long weight = rs.getLong("weight"); return weight == 0 ? null : new OptionResult(rs.getLong("result"), weight); } @Override protected void save(Connection con, Poll poll, OptionResult optionResult) throws SQLException { try (PreparedStatement pstmt = con.prepareStatement("INSERT INTO poll_result (poll_id, " + "result, weight, height) VALUES (?, ?, ?, ?)")) { int i = 0; pstmt.setLong(++i, poll.getId()); if (optionResult != null) { pstmt.setLong(++i, optionResult.result); pstmt.setLong(++i, optionResult.weight); } else { pstmt.setNull(++i, Types.BIGINT); pstmt.setLong(++i, 0); } pstmt.setInt(++i, Nxt.getBlockchain().getHeight()); pstmt.executeUpdate(); } } }; public static Poll getPoll(long id) { return pollTable.get(pollDbKeyFactory.newKey(id)); } public static DbIterator<Poll> getPollsFinishingAtOrBefore(int height) { return pollTable.getManyBy(new DbClause.IntClause("finish_height", DbClause.Op.LTE, height), 0, Integer.MAX_VALUE); } public static DbIterator<Poll> getAllPolls(int from, int to) { return pollTable.getAll(from, to); } public static DbIterator<Poll> getActivePolls(int from, int to) { return pollTable.getManyBy(new DbClause.IntClause("finish_height", DbClause.Op.GT, Nxt.getBlockchain().getHeight()), from, to); } public static DbIterator<Poll> getPollsByAccount(long accountId, boolean includeFinished, int from, int to) { DbClause dbClause = new DbClause.LongClause("account_id", accountId); if (!includeFinished) { dbClause = dbClause.and(new DbClause.IntClause("finish_height", DbClause.Op.GT, Nxt.getBlockchain().getHeight())); } return pollTable.getManyBy(dbClause, from, to); } public static DbIterator<Poll> getPollsFinishingAt(int height) { return pollTable.getManyBy(new DbClause.IntClause("finish_height", height), 0, Integer.MAX_VALUE); } public static DbIterator<Poll> searchPolls(String query, boolean includeFinished, int from, int to) { DbClause dbClause = includeFinished ? DbClause.EMPTY_CLAUSE : new DbClause.IntClause("finish_height", DbClause.Op.GT, Nxt.getBlockchain().getHeight()); return pollTable.search(query, dbClause, from, to, " ORDER BY ft.score DESC, poll.height DESC, poll.db_id DESC "); } public static int getCount() { return pollTable.getCount(); } static void addPoll(Transaction transaction, Attachment.MessagingPollCreation attachment) { Poll poll = new Poll(transaction, attachment); pollTable.insert(poll); } static void init() {} static { if (Poll.isPollsProcessing) { Nxt.getBlockchainProcessor().addListener(block -> { int height = block.getHeight(); if (height >= Constants.VOTING_SYSTEM_BLOCK) { Poll.checkPolls(height); } }, BlockchainProcessor.Event.AFTER_BLOCK_APPLY); } } private static void checkPolls(int currentHeight) { try (DbIterator<Poll> polls = getPollsFinishingAt(currentHeight)) { for (Poll poll : polls) { try { List<OptionResult> results = poll.countResults(poll.getVoteWeighting(), currentHeight); pollResultsTable.insert(poll, results); Logger.logDebugMessage("Poll " + Long.toUnsignedString(poll.getId()) + " has been finished"); } catch (RuntimeException e) { Logger.logErrorMessage("Couldn't count votes for poll " + Long.toUnsignedString(poll.getId())); } } } } private final DbKey dbKey; private final String name; private final String description; private final String[] options; private final byte minNumberOfOptions; private final byte maxNumberOfOptions; private final byte minRangeValue; private final byte maxRangeValue; private final int timestamp; private Poll(Transaction transaction, Attachment.MessagingPollCreation attachment) { super(transaction.getId(), transaction.getSenderId(), attachment.getFinishHeight(), attachment.getVoteWeighting()); this.dbKey = pollDbKeyFactory.newKey(this.id); this.name = attachment.getPollName(); this.description = attachment.getPollDescription(); this.options = attachment.getPollOptions(); this.minNumberOfOptions = attachment.getMinNumberOfOptions(); this.maxNumberOfOptions = attachment.getMaxNumberOfOptions(); this.minRangeValue = attachment.getMinRangeValue(); this.maxRangeValue = attachment.getMaxRangeValue(); this.timestamp = Nxt.getBlockchain().getLastBlockTimestamp(); } private Poll(ResultSet rs) throws SQLException { super(rs); this.dbKey = pollDbKeyFactory.newKey(this.id); this.name = rs.getString("name"); this.description = rs.getString("description"); this.options = DbUtils.getArray(rs, "options", String[].class); this.minNumberOfOptions = rs.getByte("min_num_options"); this.maxNumberOfOptions = rs.getByte("max_num_options"); this.minRangeValue = rs.getByte("min_range_value"); this.maxRangeValue = rs.getByte("max_range_value"); this.timestamp = rs.getInt("timestamp"); } private void save(Connection con) throws SQLException { try (PreparedStatement pstmt = con.prepareStatement("INSERT INTO poll (id, account_id, " + "name, description, options, finish_height, voting_model, min_balance, min_balance_model, " + "holding_id, min_num_options, max_num_options, min_range_value, max_range_value, timestamp, height) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")) { int i = 0; pstmt.setLong(++i, id); pstmt.setLong(++i, accountId); pstmt.setString(++i, name); pstmt.setString(++i, description); DbUtils.setArray(pstmt, ++i, options); pstmt.setInt(++i, finishHeight); pstmt.setByte(++i, voteWeighting.getVotingModel().getCode()); DbUtils.setLongZeroToNull(pstmt, ++i, voteWeighting.getMinBalance()); pstmt.setByte(++i, voteWeighting.getMinBalanceModel().getCode()); DbUtils.setLongZeroToNull(pstmt, ++i, voteWeighting.getHoldingId()); pstmt.setByte(++i, minNumberOfOptions); pstmt.setByte(++i, maxNumberOfOptions); pstmt.setByte(++i, minRangeValue); pstmt.setByte(++i, maxRangeValue); pstmt.setInt(++i, timestamp); pstmt.setInt(++i, Nxt.getBlockchain().getHeight()); pstmt.executeUpdate(); } } public List<OptionResult> getResults(VoteWeighting voteWeighting) { if (this.voteWeighting.equals(voteWeighting)) { return getResults(); } else { return countResults(voteWeighting); } } public List<OptionResult> getResults() { if (Poll.isPollsProcessing && isFinished()) { return pollResultsTable.get(pollResultsDbKeyFactory.newKey(id)); } else { return countResults(voteWeighting); } } public DbIterator<Vote> getVotes(){ return Vote.getVotes(this.getId(), 0, -1); } public String getName() { return name; } public String getDescription() { return description; } public String[] getOptions() { return options; } public byte getMinNumberOfOptions() { return minNumberOfOptions; } public byte getMaxNumberOfOptions() { return maxNumberOfOptions; } public byte getMinRangeValue() { return minRangeValue; } public byte getMaxRangeValue() { return maxRangeValue; } public int getTimestamp() { return timestamp; } public boolean isFinished() { return finishHeight <= Nxt.getBlockchain().getHeight(); } private List<OptionResult> countResults(VoteWeighting voteWeighting) { int countHeight = Math.min(finishHeight, Nxt.getBlockchain().getHeight()); if (countHeight < Nxt.getBlockchainProcessor().getMinRollbackHeight()) { return null; } return countResults(voteWeighting, countHeight); } private List<OptionResult> countResults(VoteWeighting voteWeighting, int height) { final OptionResult[] result = new OptionResult[options.length]; VoteWeighting.VotingModel votingModel = voteWeighting.getVotingModel(); try (DbIterator<Vote> votes = Vote.getVotes(this.getId(), 0, -1)) { for (Vote vote : votes) { long weight = votingModel.calcWeight(voteWeighting, vote.getVoterId(), height); if (weight <= 0) { continue; } long[] partialResult = countVote(vote, weight); for (int i = 0; i < partialResult.length; i++) { if (partialResult[i] != Long.MIN_VALUE) { if (result[i] == null) { result[i] = new OptionResult(partialResult[i], weight); } else { result[i].add(partialResult[i], weight); } } } } } return Arrays.asList(result); } private long[] countVote(Vote vote, long weight) { final long[] partialResult = new long[options.length]; final byte[] optionValues = vote.getVoteBytes(); for (int i = 0; i < optionValues.length; i++) { if (optionValues[i] != Constants.NO_VOTE_VALUE) { partialResult[i] = (long) optionValues[i] * weight; } else { partialResult[i] = Long.MIN_VALUE; } } return partialResult; } }