package net.sf.colossus.util; import java.io.File; import java.io.FileInputStream; import java.io.FilenameFilter; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Random; import java.util.logging.Level; import java.util.logging.Logger; import net.sf.colossus.common.WhatNextManager; public class RandomDotOrgFileReader { private static final Logger LOGGER = Logger .getLogger(RandomDotOrgFileReader.class.getName()); private static final String propertyName = "net.sf.colossus.randomDotOrgDirectory"; private final List<File> files = new ArrayList<File>(); private final ArrayList<File> unusedFiles = new ArrayList<File>(); private File currentFile = null; private FileInputStream randomByteStream = null; private final Random fallbackRandom = new Random(); public static boolean isPropertySet() { boolean set = false; // can throw try { String value = System.getProperty(propertyName); if (value != null) { set = true; } } catch (SecurityException e) { LOGGER.info("Checking whether randomDotOrg property is set caused" + " SecurityException - considering it as 'not set'."); } return set; } public RandomDotOrgFileReader() { if (isPropertySet()) { init(System.getProperty(propertyName)); } // otherwise it falls back to standard Random class anyway } public RandomDotOrgFileReader(String directoryPath) { init(directoryPath); } /** * Initializes the list of files, and sets up a FileInputStream that * reads from the first file. * currentFile and randomByteStream will be initialized to be ready * to read first byte from it. * * If something goes wrong, the list will be empty, currentFile * and randomByteStream will be null, so any call to nextRoll() * will fall back to the normal Java PRNG. */ private void init(String directoryPath) { try { File directory = new File(directoryPath); File[] filenames = directory.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.toLowerCase().endsWith(".bin"); } }); files.addAll(Arrays.asList(filenames)); } // NullPointerException, SecurityException catch (Exception e) { LOGGER.warning("Error reading files from directory '" + directoryPath + "'! Using PRNG as fallback."); files.clear(); return; } if (files.size() == 0) { LOGGER.info("Directory " + files.size() + " is empty."); } LOGGER.info("Directory contains " + files.size() + " 'random.org'-files."); unusedFiles.addAll(files); Collections.shuffle(unusedFiles); prepareNextFile(); discardSomeBytes(); } /** * After this returns, the randomByteInputStream is prepared for * that one can read bytes from it, OR, randomByteStream is null * so that reads fallback to PRNG. * */ private void prepareNextFile() { try { // close previous one, if necessary if (randomByteStream != null) { randomByteStream.close(); randomByteStream = null; currentFile = null; } while (randomByteStream == null) { if (files.isEmpty()) { LOGGER.severe("No usable files left in file list! " + "Falling back to PRNG."); randomByteStream = null; currentFile = null; return; } if (unusedFiles.isEmpty()) { LOGGER .warning("All random.org files used, starting from beginning."); unusedFiles.addAll(files); } currentFile = unusedFiles.remove(0); LOGGER.info("Opening file " + currentFile + " for reading random data."); randomByteStream = new FileInputStream(currentFile); if (randomByteStream.available() == 0) { LOGGER.warning("File " + currentFile.toString() + " seems to be empty? " + "Trying next file."); // Totally remove from files to consider files.remove(currentFile); currentFile = null; randomByteStream.close(); randomByteStream = null; } } } catch (Exception e) { LOGGER.warning("Exception while handling randomByteInputStream " + "files. Falling back to PRNG."); randomByteStream = null; currentFile = null; } } /** * Discard a random amount of bytes, so that even if we would have only * a single input files and all players would happen to use same file, * they get different stuff. * */ private void discardSomeBytes() { WhatNextManager.sleepFor(20); long now = new Date().getTime(); long count = now % 17 + now % 7; for (int i = 0; i < count; i++) { nextSingleByte(); } } private int oneByteFromFallback() { return fallbackRandom.nextInt(255); } /** * returns a 7 byte integer, or -1. * Returning -1 means, this input file is exhausted, next file * is already prepared to read from, but caller must call us again * to do so. * * @return */ private int nextSingleByte() { if (randomByteStream == null) { return oneByteFromFallback(); } int size = 1; byte[] bytes = new byte[1]; try { int got = randomByteStream.read(bytes); if (got == -1) { prepareNextFile(); return -1; } else if (got != size) { LOGGER.log(Level.WARNING, "Reading from random.org file " + currentFile.toString() + " got wrong amount of bytes!" + " Falling back to standard Java PRNG."); randomByteStream = null; return oneByteFromFallback(); } } catch (IOException ex) { LOGGER.log(Level.WARNING, "Exception during reading from random " + "file '" + currentFile.toString() + "'. " + " Falling back to standard Java PRNG."); randomByteStream = null; return oneByteFromFallback(); } int result = 0; result |= bytes[0]; return result; } /** * For dice rolls, which can be satisfied with a single byte, we * read the byte and handle the evaluation ourself, instead of having * Random blindly requesting 32 bits, of which most is then wasted. */ public int nextRoll() { int n = 6; int signedInt, result; // last complete block of six numbers (N*6 + (0 .. 5)) int upperBoundary = 251; do { signedInt = nextSingleByte(); // use it and do conversion anyway, otherwise compiler // complained about result possibly uninitialized result = (signedInt & 0xFF) % n; } while (signedInt == -1 || result > upperBoundary); return result + 1; } }