package com.hida.service;
import com.hida.repositories.DefaultSettingRepository;
import com.hida.repositories.PidRepository;
import com.hida.model.Token;
import com.hida.repositories.UsedSettingRepository;
import com.hida.model.AutoIdGenerator;
import com.hida.model.CustomIdGenerator;
import com.hida.model.DefaultSetting;
import com.hida.model.Pid;
import com.hida.model.IdGenerator;
import com.hida.model.NotEnoughPermutationsException;
import com.hida.model.UsedSetting;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Properties;
import java.util.Set;
import java.util.LinkedHashSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* A service class that is used as a medium between the requests received by the
* controller and the transactions done by Hibernate.
*
* @author lruffin
*/
@Service("minterService")
@Transactional
public class MinterService {
/**
* Logger; logfile to be stored in resource folder
*/
private static final Logger LOGGER = LoggerFactory.getLogger(MinterService.class);
/**
* Default setting values stored in resources folder
*/
private String defaultSettingPath_ = "DefaultSetting.properties";
@Autowired
private PidRepository pidRepo_;
@Autowired
private UsedSettingRepository usedSettingRepo_;
@Autowired
private DefaultSettingRepository defaultSettingRepo_;
private ArrayList<Pid> cachedPid_;
private long lastSequentialAmount_;
/**
* Declares a Generator object to manage
*/
private IdGenerator generator_;
/**
* The setting used to store the values of the current request
*/
private DefaultSetting currentSetting_;
/**
* The default values that are currently stored in the properties file and
* in the database.
*/
private DefaultSetting storedSetting_;
/**
* No-arg constructor
*/
public MinterService() {
}
/**
* Returns the difference between the total permutations and the amount of
* Pids that were already created using the requested settings.
*
* @return The amount of permutations remaining
*/
private long getRemainingPermutations() {
LOGGER.info("in getRemainingPerumtations");
long totalPermutations = generator_.getMaxPermutation();
long amountCreated = getAmountCreated();
return totalPermutations - amountCreated;
}
/**
* Returns the amount of Pids that were created using the requested settings
*
* @return amount of Pids
*/
private long getAmountCreated() {
UsedSetting entity = findUsedSetting();
if (entity == null) {
return 0;
}
else {
return entity.getAmount();
}
}
/**
* Creates a generator to be used in accordance to the setting
*/
private void createGenerator() {
LOGGER.info("in createGenerator");
if (currentSetting_.isAuto()) {
generator_ = new AutoIdGenerator(
currentSetting_.getPrefix(),
currentSetting_.getTokenType(),
currentSetting_.getRootLength());
LOGGER.info("AutoGenerator created");
}
else {
generator_ = new CustomIdGenerator(
currentSetting_.getPrefix(),
currentSetting_.isSansVowels(),
currentSetting_.getCharMap());
LOGGER.info("CustomIdGenerator created");
}
}
/**
* Attempts to create a number of Pids and store them in database
*
* @param amount The number of PIDs to be created
* @param setting The desired setting used to create a Pid
* @return
* @throws IOException Thrown when the file cannot be found
*/
public Set<Pid> mint(long amount, DefaultSetting setting) throws IOException {
LOGGER.info("in mint");
// store the desired setting values
this.currentSetting_ = setting;
// create appropriate generator
createGenerator();
// calculate total number of permutations
long total = generator_.getMaxPermutation();
// determine remaining amount of permutations
long remaining = getRemainingPermutations();
// determine if its possible to create the requested amount of ids
if (remaining < amount) {
NotEnoughPermutationsException exception
= new NotEnoughPermutationsException(remaining, amount);
LOGGER.error("Exception caught;", exception);
throw exception;
}
LOGGER.info("request is valid");
/*
if the current setting is random, have the generator return a random set,
otherwise, have the generator return a sequential set
*/
Set<Pid> set;
if (currentSetting_.isRandom()) {
set = generator_.randomMint(amount);
}
else if (currentSetting_.equals(storedSetting_)) {
set = generator_.sequentialMint(amount, lastSequentialAmount_);
lastSequentialAmount_ = (lastSequentialAmount_ + amount) % total;
}
else {
set = generator_.sequentialMint(amount);
}
// check ids and increment them appropriately
set = rollPidSet(set, total, amount);
// add the set of ids to the id table in the database and their formats
addPidSet(set, amount);
// return the set of ids
return set;
}
/**
* Generates a list of Pids based on default settings.
*
* @throws IOException Thrown when the DEFAULT_SETTING_PATH cannot be found
*/
public void generateCache() throws IOException {
LOGGER.trace("in generateCache");
// get default settings
currentSetting_ = this.getStoredSetting();
// create the generator
createGenerator();
// get the maximum number of permutations
long maxPermutation = generator_.getMaxPermutation();
// create all possible permutations
Set<Pid> cache = generator_.sequentialMint(500);
// add each mmember of the set to CachedPid
ArrayList<Pid> list = new ArrayList<>();
Iterator<Pid> iter = cache.iterator();
while (iter.hasNext()) {
Pid pid = iter.next();
list.add(pid);
LOGGER.info("adding {}", pid);
}
cachedPid_ = list;
LOGGER.trace("cache generated");
}
/**
* Continuously increments a set of ids until the set is completely filled
* with unique ids.
*
* @param set the set of ids
* @param order determines whether or not the ids will be ordered
* @param isAuto determines whether or not the ids are AutoId or CustomId
* @param amount the amount of ids to be created.
* @return A set of unique ids database.
*/
private Set<Pid> rollPidSet(Set<Pid> set, long totalPermutations, long amount) {
LOGGER.info("in rollIdSet");
// Used to count the number of unique ids. Size methods aren't used because int is returned
long uniqueIdCounter = 0;
// Declares and initializes a list that holds unique values.
Set<Pid> uniqueList = new LinkedHashSet<>();
// iterate through every id
for (Pid currentId : set) {
// counts the number of times an id has been rejected
long counter = 0;
// continuously increments invalid or non-unique ids
while (!isValidPid(currentId) || uniqueList.contains(currentId)) {
/*
if counter exceeds totalPermutations, then id has iterated through every
possible permutation. Related format is updated as a quick look-up reference
with the number of ids that were inadvertedly been created using other formats.
NotEnoughPermutationsException is thrown stating remaining number of ids.
*/
if (counter > totalPermutations) {
NotEnoughPermutationsException exception
= new NotEnoughPermutationsException(uniqueIdCounter, amount);
LOGGER.error("Exception caught;", exception);
throw exception;
}
generator_.incrementPid(currentId);
counter++;
}
// unique ids are added to list and uniqueIdCounter is incremented.
// Size methods aren't used because int is returned
uniqueIdCounter++;
uniqueList.add(currentId);
}
return uniqueList;
}
/**
* Adds a requested amount of formatted ids to the database.
*
* @param list list of ids to check.
* @param amountCreated Holds the true size of the list as list.size method
* can only return the maximum possible value of an integer.
* @param prefix The string that will be at the front of every id.
* @param tokenType Designates what characters are contained in the id's
* root.
* @param sansVowel Designates whether or not the id's root contains vowels.
* @param rootLength Designates the length of the id's root.
*/
private void addPidSet(Set<Pid> list, long amountCreated) {
LOGGER.info("in addPidSet");
for (Pid pid : list) {
pidRepo_.save(pid);
}
LOGGER.info("Database Updated with new pids");
// update table format
recordSettings(amountCreated);
}
/**
* Attempts to find a UsedSetting based on the currently used DefaultSetting
*
* @return Returns a UsedSetting entity if found, null otherwise
*/
private UsedSetting findUsedSetting() {
LOGGER.info("in findUsedSetting");
return usedSettingRepo_.findUsedSetting(currentSetting_.getPrefix(),
currentSetting_.getTokenType(),
currentSetting_.getCharMap(),
currentSetting_.getRootLength(),
currentSetting_.isSansVowels());
}
/**
* Attempts to record the setting that were used to create the current set
* of Pids
*
* @param amount The number of PIDs that were created
*/
private void recordSettings(long amount) {
LOGGER.info("in recordSettings");
UsedSetting entity = findUsedSetting();
if (entity == null) {
entity = new UsedSetting(currentSetting_.getPrefix(),
currentSetting_.getTokenType(),
currentSetting_.getCharMap(),
currentSetting_.getRootLength(),
currentSetting_.isSansVowels(),
amount);
usedSettingRepo_.save(entity);
}
else {
long previousAmount = entity.getAmount();
entity.setAmount(previousAmount + amount);
}
}
/**
* Checks to see if a Pid already exists in the database.
*
* @param pid Pid to be checked
* @return Returns true if a Pid with the same doesn't exist, false
* otherwise
*/
private boolean isValidPid(Pid pid) {
LOGGER.info("in isValidId");
Pid entity = this.pidRepo_.findOne(pid.getName());
return entity == null;
}
/**
* Updates the CurrentDefaultSetting to match the values in the given
* DefaultSetting.
*
* @param newSetting A DefaultSetting object that contains newly requested
* values
* @throws IOException
*/
public void updateCurrentSetting(DefaultSetting newSetting) throws Exception {
LOGGER.info("in updateCurrentSetting");
currentSetting_ = defaultSettingRepo_.findCurrentDefaultSetting();
currentSetting_.setPrepend(newSetting.getPrepend());
currentSetting_.setPrefix(newSetting.getPrefix());
currentSetting_.setCharMap(newSetting.getCharMap());
currentSetting_.setRootLength(newSetting.getRootLength());
currentSetting_.setTokenType(newSetting.getTokenType());
currentSetting_.setAuto(newSetting.isAuto());
currentSetting_.setRandom(newSetting.isRandom());
currentSetting_.setSansVowels(newSetting.isSansVowels());
// record Default Setting values into properties file
writeToPropertiesFile(defaultSettingPath_, newSetting);
}
/**
* Initializes the storedSetting_ field by reading its value in the
* database. If its null, then it is given initial values.
*
* @throws IOException Thrown when the file cannot be found
*/
public void initializeStoredSetting() throws IOException {
storedSetting_ = defaultSettingRepo_.findCurrentDefaultSetting();
if (storedSetting_ == null) {
// read default values stored in properties file and save it
storedSetting_ = readPropertiesFile(defaultSettingPath_);
defaultSettingRepo_.save(storedSetting_);
}
}
public DefaultSetting getStoredSetting() {
return storedSetting_;
}
/**
* Read a given properties file and return its values in the form of a
* DefaultSetting object
*
* @return DefaultSetting object with read values
* @throws IOException Thrown when the file cannot be found
*/
private DefaultSetting readPropertiesFile(String filename) throws IOException {
Properties prop = new Properties();
ClassLoader loader = Thread.currentThread().getContextClassLoader();
InputStream input = loader.getResourceAsStream(filename);
DefaultSetting setting = new DefaultSetting();
// load a properties file
prop.load(input);
// get the property value, store it, and return it
setting.setPrepend(prop.getProperty("prepend"));
setting.setPrefix(prop.getProperty("prefix"));
setting.setCacheSize(Long.parseLong(prop.getProperty("cacheSize")));
setting.setCharMap(prop.getProperty("charMap"));
setting.setTokenType(Token.valueOf(prop.getProperty("tokenType")));
setting.setRootLength(Integer.parseInt(prop.getProperty("rootLength")));
setting.setSansVowels(Boolean.parseBoolean(prop.getProperty("sansVowel")));
setting.setAuto(Boolean.parseBoolean(prop.getProperty("auto")));
setting.setRandom(Boolean.parseBoolean(prop.getProperty("random")));
// close and return
input.close();
return setting;
}
/**
* Writes to a given properties file, updating its key-value pairs using the
* values stored in the setting parameter. If the file does not exist it is
* created.
*
* @param setting The setting whose value needs to be stored
* @throws Exception
*/
private void writeToPropertiesFile(String filename, DefaultSetting setting)
throws Exception {
Properties prop = new Properties();
ClassLoader loader = Thread.currentThread().getContextClassLoader();
URL url = loader.getResource(filename);
File file = new File(url.toURI());
OutputStream output = new FileOutputStream(file);
// set the properties value
prop.setProperty("prepend", setting.getPrepend());
prop.setProperty("prefix", setting.getPrefix());
prop.setProperty("cacheSize", setting.getCacheSize() + "");
prop.setProperty("charMap", setting.getCharMap());
prop.setProperty("rootLength", setting.getRootLength() + "");
prop.setProperty("tokenType", setting.getTokenType() + "");
prop.setProperty("sansVowel", setting.isSansVowels() + "");
prop.setProperty("auto", setting.isAuto() + "");
prop.setProperty("random", setting.isRandom() + "");
// save and close
prop.store(output, "");
output.close();
}
public long getLastSequentialAmount() {
return lastSequentialAmount_;
}
public String getDefaultSettingPath() {
return defaultSettingPath_;
}
public void setDefaultSettingPath(String DefaultSettingPath) {
this.defaultSettingPath_ = DefaultSettingPath;
}
}