Java tutorial
/* * @(#)Scanner.java.java * * Copyright (C) 2008-2009 Scantegrity Project * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ package org.scantegrity.scanner; import java.awt.Rectangle; import java.awt.image.AffineTransformOp; import java.awt.image.BufferedImage; import java.beans.XMLDecoder; import java.beans.XMLEncoder; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.ListIterator; import java.util.Map; import java.util.TreeMap; import java.util.Vector; import java.util.logging.Level; import javax.imageio.ImageIO; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.Clip; import javax.sound.sampled.DataLine; import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.UnsupportedAudioFileException; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.apache.commons.cli.PosixParser; import org.apache.commons.io.FileUtils; import org.scantegrity.common.AffineCropper; import org.scantegrity.common.Ballot; import org.scantegrity.common.BallotStyle; import org.scantegrity.common.Contest; import org.scantegrity.common.DrunkDriver; import org.scantegrity.common.FindFile; import org.scantegrity.common.Logging; import org.scantegrity.common.RandomBallotStore; import org.scantegrity.common.SysBeep; import org.scantegrity.common.methods.TallyMethod; /** * @author John Conway * * This class is the main wrapper for the scanner. Running * Scanners main will load all necessary classes to run the * scanner on election day. */ public class Scanner { private static String c_errDir = "error"; private static Options c_opts; private static ScannerConfig c_config; private static Logging c_log; private static ScannerController c_scanner; private static RandomBallotStore[] c_store; private static Vector<Integer> c_ballotIds; private static Vector<String> c_outDirs; private static int c_myId = -1; private static MessageDigest c_hash; private static SecureRandom c_csprng; private static Integer c_scanCount; private static Integer c_errorCount; private static Integer c_ballotCount; private static String c_soundFileName = "/opt/scantegrity/sound/KenbeepLoud.wav"; //private static Thread c_audioThread = null; /** * Main Logic loop. * * @param args CLI flags */ public static void main(String[] args) { processCmdLine(args); //Create the locations we should output files too createOutputDirectories(); //register logging handlers if any c_myId = c_config.getPollID(); c_log = new Logging(c_outDirs, c_myId, Level.FINEST); c_log.log(Level.INFO, "Logging Intialized"); c_log.log(Level.INFO, "Scanner ID Number: " + c_myId); c_log.log(Level.INFO, "Initializing Cryptographic hash and rng."); try { c_hash = MessageDigest.getInstance("SHA1"); } catch (NoSuchAlgorithmException l_e) { l_e.printStackTrace(); c_hash = null; c_log.log(Level.SEVERE, "Unable to initialize hash. Reason: " + l_e.getMessage()); } try { c_csprng = SecureRandom.getInstance("SHA1PRNG"); } catch (NoSuchAlgorithmException l_e) { l_e.printStackTrace(); c_csprng = null; c_log.log(Level.SEVERE, "Unable to initialize RNG. Reason: " + l_e.getMessage()); } //init counters c_scanCount = new Integer(0); c_ballotCount = new Integer(0); c_errorCount = new Integer(0); checkForPreviousCounters(); writeCounters(); //init ballot storage c_ballotIds = new Vector<Integer>(); //TODO: Change this size to be variable... c_store = initializeBallotStore(c_outDirs, 100 * 1024 * 1024); //start the election c_log.log(Level.SEVERE, "Election Started"); playAudioClip(2); //main loop //TODO: terminating condition, button, or special ballot??? while (true) { BufferedImage l_ballotImg[] = null; Ballot l_ballot = null; //process image into ballot l_ballotImg = getBallotImages(); if (l_ballotImg == null || (l_ballotImg[0] == null && l_ballotImg[1] == null)) continue; //scan count c_scanCount++; writeCounters(); playAudioClip(0); for (int l_c = 0; l_c < l_ballotImg.length; l_c++) { //Ignore empties if (l_ballotImg[l_c] == null) { c_log.log(Level.WARNING, "Only 1 ballot object returned." + " Make sure the scanner supports duplex"); continue; } //Ignore blank pages if (DrunkDriver.isDrunk(l_ballotImg[l_c], 10)) continue; l_ballot = getBallot(l_ballotImg[l_c]); if (l_ballot == null) continue; //update ballot counter c_ballotCount++; writeCounters(); l_ballot.setScannerId(c_myId); if (isDuplicate(l_ballot)) { c_log.log(Level.WARNING, "Duplicate Ballot detected. ID : " + l_ballot.getId()); l_ballot.setCounted(false); l_ballot.addNote("Duplicate Ballot"); } //check if the ballot is a "starting ballot" //check if the ballot is a "closing ballot" //else saveBallot(l_ballot); } //resume scanning } //end election (ballot handler) //turn off storage //disconnect devices //turn off log //quit //endElection(); } /** * Process command line arguments, filling in any configuration settings * provided by the calling process. * @param args - CLI arguments. */ private static void processCmdLine(String args[]) { setOptions(); String l_args[] = null; CommandLine l_cmdLine = null; try { CommandLineParser l_parser = new PosixParser(); l_cmdLine = l_parser.parse(c_opts, args); l_args = l_cmdLine.getArgs(); } catch (ParseException l_e) { l_e.printStackTrace(); System.exit(-1); } //Invalid command line options. if (l_cmdLine == null || l_cmdLine.hasOption("help") || l_args == null) { printUsage(); System.exit(0); } //Custom configuration file. if (l_cmdLine.hasOption("config")) { c_config = getConfiguration(l_cmdLine.getOptionValue("config")); } else c_config = getConfiguration("ScannerConfig.xml"); //ScannerController input/output locations. String l_bin, l_in, l_out; if (l_cmdLine.hasOption("in")) l_in = l_cmdLine.getOptionValue("in"); else l_in = null; if (l_cmdLine.hasOption("out")) l_out = l_cmdLine.getOptionValue("out"); else l_out = null; if (l_cmdLine.hasOption("bin")) l_bin = l_cmdLine.getOptionValue("bin"); else l_bin = null; //The default error directory should be a subdirectory of the output //directory. if (l_out != null) { c_errDir = l_out + File.separator + c_errDir; } c_scanner = new ScannerController(c_log, l_bin, l_in, l_out, true); //Create error directory if it does not exist. try { File l_e = new File(c_errDir); if (!l_e.exists()) { FileUtils.forceMkdir(l_e); } else if (!l_e.isDirectory()) { FileUtils.forceMkdir(l_e); } } catch (Exception l_e) { //Try to save errors to local directory. c_errDir = ""; } } /** * Create options for this application. Currently there is only 1, and * that is if the user wants to include a contest information file. */ private static void setOptions() { c_opts = new Options(); Option l_help = new Option("help", "Print help message."); Option l_verb = new Option("v", "Unimplemented verbosity setting, prints more info."); Option l_in = new Option("in", "Input directory. This should be a " + " ramdisk mountpoint."); Option l_out = new Option("out", "Output directory. Output images will " + "be stored here. NOTE: This is not an " + "override option for the scanner config." + " This option backs up ballot images, " + "which is typically not necessary."); Option l_bin = new Option("bin", "Binaries directory. Where the " + "scanner scripts are stored."); Option l_config = new Option("config", "Configuration file path and name."); c_opts.addOption(l_help); c_opts.addOption(l_verb); c_opts.addOption(l_in); c_opts.addOption(l_out); c_opts.addOption(l_bin); c_opts.addOption(l_config); } /** * Prints the usage information for the application. */ private static void printUsage() { try { HelpFormatter l_form = new HelpFormatter(); l_form.printHelp(80, "scanner <OPTIONS>", "This is the scanner daemon for the scantegrity voting" + "system. If you do not provide a configuration, the system" + "will attempt to find one.\n" + "\nOPTIONS:", c_opts, "", false); } catch (Exception l_e) { l_e.printStackTrace(); System.exit(-1); } } /** * Get and set up the configuration file data. * * This file configures the election. * * @param p_configPath - The path. * @return */ private static ScannerConfig getConfiguration(String p_configPath) { ScannerConfig l_config = new ScannerConfig(); File c_loc = null; try { if (p_configPath == null) { c_loc = new FindFile(ScannerConstants.DEFAULT_CONFIG_NAME).find(); } else { c_loc = new File(p_configPath); } if (!c_loc.isFile()) { c_loc = new FindFile(ScannerConstants.DEFAULT_CONFIG_NAME).find(); System.err.println("Could not open file."); } } catch (NullPointerException e_npe) { System.err.println("Could not open file. File does not exist."); e_npe.printStackTrace(); criticalExit(5); } //TODO: make sure the file is found and is readable if (c_loc == null) { System.err.println("Critical Error: Could not open configuration " + "file. System Exiting."); criticalExit(10); } XMLDecoder e; try { e = new XMLDecoder(new BufferedInputStream(new FileInputStream(c_loc))); l_config = (ScannerConfig) e.readObject(); e.close(); } catch (Exception e_e) { System.err.println("Could not parse Configuration File!"); e_e.printStackTrace(); criticalExit(20); } return l_config; } /** * Reads all the output directories in the configuration, creates a list * of every directory, and sets up any output directories if needed. * * @return */ private static void createOutputDirectories() { Vector<String> l_locs = c_config.getOutputDirNames(); c_outDirs = new Vector<String>(); //WE want to avoid failures at all costs in this function. if (l_locs == null) { System.err.println("Invalid output directory option! Using" + " default /media."); l_locs = new Vector<String>(); l_locs.add("/media"); } //Go through each directory name. for (String l_loc : l_locs) { if (l_loc == null) continue; File l_d = new File(l_loc); int l_c = 0; try { if (l_d.exists() && l_d.canExecute() && l_d.canRead() && l_d.isDirectory()) { //Get a directory listing. File[] l_subds = l_d.listFiles(); for (File l_subd : l_subds) { try { if (l_subd.canExecute() && l_subd.canRead() && l_subd.canWrite() && l_subd.isDirectory()) { if (!(new File(l_subd.getAbsolutePath() + File.separator + "scantegrity-scanner") .exists())) { FileUtils.forceMkdir(new File( l_subd.getAbsolutePath() + File.separator + "scantegrity-scanner")); } if (!(new File(l_subd.getAbsolutePath() + File.separator + "scantegrity-scanner" + File.separator + c_errDir).exists())) { FileUtils.forceMkdir(new File(l_subd.getAbsolutePath() + File.separator + "scantegrity-scanner" + File.separator + c_errDir)); } c_outDirs.add(l_subd.getAbsolutePath() + File.separator + "scantegrity-scanner"); l_c++; } } catch (Exception l_e) { l_e.printStackTrace(); //ignore. } } if (l_c == 0 && l_d.canWrite()) { if (!(new File(l_d.getAbsolutePath() + File.separator + "scantegrity-scanner/").exists())) { FileUtils.forceMkdir( new File(l_d.getAbsolutePath() + File.separator + "scantegrity-scanner")); } if (!(new File(l_d.getAbsolutePath() + File.separator + "scantegrity-scanner" + File.separator + c_errDir).exists())) { FileUtils.forceMkdir(new File(l_d.getAbsolutePath() + File.separator + "scantegrity-scanner" + File.separator + c_errDir)); } c_outDirs.add(l_d.getAbsolutePath() + File.separator + "scantegrity-scanner"); } else if (l_c == 0) { System.err.println("Permissions error: could not use " + l_d.getAbsolutePath()); } } } catch (Exception l_e) { System.err.println("Could not read " + l_d.getAbsolutePath() + "\n Reason:" + l_e.getMessage()); } } //If all else fails.. if (c_outDirs.size() <= 0) { System.err.println("Unable to use an output directory!"); try { if (!(new File("scantegrity-scanner").exists())) { FileUtils.forceMkdir(new File("scantegrity-scanner")); } if (!(new File("scantegrity-scanner" + File.separator + c_errDir).exists())) { FileUtils.forceMkdir(new File("scantegrity-scanner" + File.separator + c_errDir)); } c_outDirs.add("scantegrity-scanner"); } catch (Exception l_e) { System.err.println("Exiting.. could not create an output directory!"); criticalExit(15); } } } private static RandomBallotStore[] initializeBallotStore(Vector<String> p_storeLocs, int p_size) { RandomBallotStore[] l_store = null; try { c_log.log(Level.INFO, "Initializing the Random Ballot Stores"); l_store = new RandomBallotStore[p_storeLocs.size()]; int l_ret = -1; for (int i = 0; i < p_storeLocs.size(); i++) { c_log.log(Level.INFO, "Creating Random Ballot Store : " + p_storeLocs.get(i)); l_store[i] = new RandomBallotStore(c_myId, p_size, 32 * 1024, p_storeLocs.get(i) + File.separator + "ballots.sbr", c_hash, c_csprng); l_ret = l_store[i].initializeStore(); c_ballotCount = Math.max(l_ret, c_ballotCount); if (l_ret < 0) { c_log.log(Level.SEVERE, "Failed to open random ballot store " + p_storeLocs.get(i)); System.err.println("Failed to open random ballot store " + p_storeLocs.get(i)); System.err.println("This error may prevent you from storing ballots!"); } else { c_log.log(Level.INFO, "Random Ballot Store Created."); if (i == 0) { c_ballotIds = l_store[0].getBallotIds(); if (c_ballotIds.size() > 0) { c_log.log(Level.WARNING, "There are " + c_ballotIds.size() + " ballots in the store!"); } } } } } catch (Exception e_e) { //Security Failed, log and quit c_log.log(Level.SEVERE, "Critical Failure: Could initialize random number generator. System Exiting. "); e_e.printStackTrace(); criticalExit(10); } if (l_store == null || l_store.length < 1) { //Security Failed, log and quit c_log.log(Level.SEVERE, "Critical Failure: Could initialize random number generator. System Exiting."); criticalExit(10); } return l_store; } private static BufferedImage[] getBallotImages() { BufferedImage l_ballotImgs[] = null; //get a ballot image try { c_log.log(Level.FINE, "Getting ballot image from scanner"); l_ballotImgs = c_scanner.getImagesFromScanner(); if (l_ballotImgs == null) { c_log.log(Level.FINE, "Invalid image object returned."); } return l_ballotImgs; } catch (Exception l_e) { c_log.log(Level.SEVERE, "Possibly Lost Ballot:" + l_e.getMessage()); } return null; } private static Ballot getBallot(BufferedImage p_ballotImg) { c_log.log(Level.INFO, "Converting Image to Ballot."); Ballot l_b = null; BallotReader l_reader = c_config.getReader(); Vector<BallotStyle> l_styles = c_config.getStyles(); String l_err = "Unknown"; try { //scan the ballot l_b = l_reader.scanBallot(l_styles, p_ballotImg); //as long as we have a ballot and an id return //the scanned ballot object if (l_b != null && l_b.getId() != null) { /* * check for errors like Overvotes and Undervotes */ if (checkForVotingErrors(l_b, p_ballotImg, l_reader)) { //we have an error condition. log it and save the //ballot image as an error c_log.log(Level.WARNING, "Errors found in contest. Saving ballot image " + "for ERM verification."); saveErrorImage(p_ballotImg); return l_b; } else { //the ballot is ok, return the ballot return l_b; } } } //couldn't find alignment marks or couldn't read serial number catch (Exception l_e) { l_err = l_e.getMessage(); } c_log.log(Level.WARNING, "Could not read (possible) ballot image." + "Reason: " + l_err); saveErrorImage(p_ballotImg); return null; } /** * This method will go through each contest in the ballot and verify * with the TallyMethod that there are no voting errors that will need * human verification in the ERM. If there are errors (for example Overvotes * or Undervotes in IVR) then we will have to save the ballot image have * the election judge manually process the ballot in the ERM like we do * with Write-Ins. * @param p_ballot_Img * @param c_log2 * * @param Ballot p_ballot: The scanned ballot object * * @return boolean: If an error was found */ private static boolean checkForVotingErrors(Ballot p_ballot, BufferedImage p_ballotImg, BallotReader l_reader) { TallyMethod l_method = null; boolean l_ret = false; //return value. This will be set to true if any error is found Vector<Contest> l_contests = c_config.getContests(); boolean l_error_found = false; TreeMap<Integer, Vector<String>> l_error_contests = new TreeMap<Integer, Vector<String>>(); //go through each contest and get it's tally method for (Map.Entry<Integer, Integer[][]> entry : p_ballot.getBallotData().entrySet()) { Integer l_contest_id = entry.getKey(); Integer[][] l_contest_data = entry.getValue(); //c_log.log(Level.INFO, "Checking contest " + l_contest_id.toString() + " for errors."); //TODO: testing //find the contest that matches the ballot contest we are working with. for (Contest l_contest : l_contests) { Vector<String> l_error_conditions = new Vector<String>(); if (l_contest.getId().equals(l_contest_id)) { //we have the contest, get the tally method and call the error checker l_method = l_contest.getMethod(); l_error_found = l_method.hasVotingErrors(l_contest_data, l_error_conditions, c_log); if (l_error_found) { //c_log.log(Level.INFO, "Contest " + l_contest_id + "has errors"); //TODO: testing l_ret = true; l_error_contests.put(l_contest_id, l_error_conditions); } } } } if (l_ret) { AffineTransformOp l_alignmentOp = l_reader.c_alignmentOp; //c_log.log(Level.INFO, "Ballot has errors: " + l_error_conditions); //TODO: testing //save in the ballot object which contests were marked as an error condition //and possibly which condition c_log.log(Level.INFO, "Error Contests Found: " + l_error_contests); BufferedImage l_errorImage = null; try { if (l_alignmentOp == null) throw new Exception("Unable to get alignment transformation."); l_errorImage = AffineCropper.cropUnscaled(p_ballotImg, l_alignmentOp, new Rectangle(0, 0, (int) l_reader.getDimension().getWidth(), (int) l_reader.getDimension().getHeight())); } catch (Exception e) { c_log.log(Level.WARNING, "Could not rotate error ballot image: " + e.getMessage()); } p_ballot.saveErrorImage(l_errorImage, l_error_contests); } return l_ret; } private static Boolean isDuplicate(Ballot p_ballot) { if (!c_ballotIds.contains(p_ballot.getId())) { c_ballotIds.add(p_ballot.getId()); return false; } return true; } private static void saveErrorImage(BufferedImage p_ballotImg) { //increment bad image count c_errorCount++; c_log.log(Level.SEVERE, "Bad Ballot " + c_errorCount + ". Saving to Error Directory."); writeCounters(); //Copy the bad image to the error directory try { ListIterator<String> it = c_outDirs.listIterator(); while (it.hasNext()) { ImageIO.write(p_ballotImg, "png", new File(it.next() + File.separator + c_errDir + File.separator + "scanerror" + c_errorCount + ".png")); } return; } catch (Exception e) { //do nothing... } c_log.log(Level.WARNING, "Could not save error ballot."); } private static void saveBallot(Ballot p_ballot) { //do some logging c_log.log(Level.INFO, "Saving Ballot to Random Ballot Store"); for (RandomBallotStore l_store : c_store) { try { c_log.log(Level.INFO, "Saving ballot " + c_ballotCount + " to store: " + l_store.getLocation()); l_store.addBallot(p_ballot); } catch (IOException e) { e.printStackTrace(); c_log.log(Level.SEVERE, "I/O Error. Unable to save ballot to " + l_store.getLocation()); } } } private static void writeCounters() { ListIterator<String> it = c_outDirs.listIterator(); while (it.hasNext()) { try { XMLEncoder l_countFile = new XMLEncoder( new BufferedOutputStream(new FileOutputStream(it.next() + File.separator + "count.xml"))); l_countFile.writeObject("Scan Count"); l_countFile.writeObject(c_scanCount); l_countFile.writeObject("Ballot Count"); l_countFile.writeObject(c_ballotCount); l_countFile.writeObject("Error Count"); l_countFile.writeObject(c_errorCount); l_countFile.close(); } catch (FileNotFoundException e) { c_log.log(Level.SEVERE, "Unable to create count.xml"); } } } private static void checkForPreviousCounters() { ListIterator<String> it = c_outDirs.listIterator(); while (it.hasNext()) { try { String l_path = it.next() + File.separator + "count.xml"; File l_file = new File(l_path); if (l_file.exists()) { c_log.log(Level.WARNING, "count.xml exists. Updating counters."); //copy the file try { FileUtils.copyFile(l_file, new File(l_path + "_bak"), true); } catch (IOException e) { c_log.log(Level.WARNING, "Could not backup previous counter file."); } XMLDecoder l_countFile = new XMLDecoder(new BufferedInputStream(new FileInputStream(l_path))); l_countFile.readObject(); c_scanCount = (Integer) l_countFile.readObject(); l_countFile.readObject(); c_ballotCount = (Integer) l_countFile.readObject(); l_countFile.readObject(); c_errorCount = (Integer) l_countFile.readObject(); l_countFile.close(); c_log.log(Level.WARNING, "Previous counts: ScanCount=" + c_scanCount + " BallotCount=" + c_ballotCount + " ErrorCount=" + c_errorCount); } } catch (FileNotFoundException e) { c_log.log(Level.SEVERE, "Unable to open count.xml"); } } } private static void playAudioClip(int p_numTimes) { /* * Threaded Code....sigsegv when run * / if(c_audioThread != null && c_audioThread.isAlive()) { try { c_audioThread.join(2000); } catch (InterruptedException e) { c_log.log(Level.SEVERE, "Could not wait for previous sound thread."); } } c_audioThread = new Thread(new AudioFile(c_soundFile, p_numTimes)); c_audioThread.start(); /* * End threaded Code */ AudioInputStream l_stream = null; try { l_stream = AudioSystem.getAudioInputStream(new File(c_soundFileName)); } catch (UnsupportedAudioFileException e_uaf) { c_log.log(Level.WARNING, "Unsupported Audio File"); return; } catch (IOException e1) { c_log.log(Level.WARNING, "Could not Open Audio File"); return; } AudioFormat l_format = l_stream.getFormat(); Clip l_dataLine = null; DataLine.Info l_info = new DataLine.Info(Clip.class, l_format); if (!AudioSystem.isLineSupported(l_info)) { c_log.log(Level.WARNING, "Audio Line is not supported"); } try { l_dataLine = (Clip) AudioSystem.getLine(l_info); l_dataLine.open(l_stream); } catch (LineUnavailableException ex) { c_log.log(Level.WARNING, "Audio Line is unavailable."); } catch (IOException e) { c_log.log(Level.WARNING, "Cannot playback Audio, IO Exception."); } l_dataLine.loop(p_numTimes); try { Thread.sleep(160 * (p_numTimes + 1)); } catch (InterruptedException e) { c_log.log(Level.WARNING, "Could not sleep the audio player thread."); } l_dataLine.close(); } /** * Beeps p_numBeeps times and then exits the system * * @param p_numBeeps the number of beeps */ private static void criticalExit(int p_numBeeps) { Thread l_th = new Thread(new SysBeep(p_numBeeps, 500)); l_th.start(); try { Thread.sleep(5000); } catch (InterruptedException e1) { //e1.printStackTrace(); } //TODO: Shutdown, but for now just exit System.exit(-1); } }