package com.hida.controller;
import com.hida.model.BadParameterException;
import com.hida.model.DefaultSetting;
import com.hida.model.IdGenerator;
import com.hida.model.Pid;
import com.hida.model.Token;
import com.hida.service.MinterService;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.Map;
import java.util.Set;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.locks.ReentrantLock;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
/**
* A Controller that handles the requests and responses between the user and the
* Minter.
*
* @author lruffin
*/
@RestController
@RequestMapping("/Minter")
public class MinterController {
/*
* Logger; logfile to be stored in resource folder
*/
private static final Logger LOGGER = LoggerFactory.getLogger(MinterController.class);
/**
* A fair lock used to synchronize access to the minter service.
*/
private static final ReentrantLock requestLock_ = new ReentrantLock(true);
/**
* The service to use to mint IDs.
*/
@Autowired
private MinterService minterService_;
/**
* Using values sent from the /administration end point, this method updates
* the DefaultSetting object, database, and the properties file. The names
* of the requested parameters are given by the name attributes in the
* administration panel.
*
* @param prepend String of characters that determine the domain of each Pid
* @param idprefix String of characters that prefixes each Pid
* @param cacheSize The size of the cache that'll be generated whenever the
* context is refreshed. If an empty String is returned to cacheSize then
* the cacheSize will not be updated.
* @param mintType Auto Generator is used if true, Custom Generator if false
* @param mintOrder Pids are generated randomly if true, ordered if false
* @param sansvowel Pids will contain vowels if true, false othwerise
* @param idlength the length of the pid's root name
* @param charmapping the mapping used when Custom Generator is selected
* @param digits digits is included if selected
* @param lowercase lowercase is included if selected
* @param uppercase uppercase is included if selected
* @param request HTTP request from the administration panel
* @param response HTTP response that redirects to the administration panel
* after updating the new settings.
* @throws Exception
*/
@RequestMapping(value = {"/administration"}, method = {RequestMethod.POST})
public void handleForm(@RequestParam(required = false) String prepend,
@RequestParam(required = false) String idprefix,
@RequestParam(required = false) String cacheSize,
@RequestParam String mintType,
@RequestParam String mintOrder,
@RequestParam(defaultValue = "true") boolean sansvowel,
@RequestParam(required = false, defaultValue = "-1") int idlength,
@RequestParam(required = false) String charmapping,
@RequestParam(required = false) boolean digits,
@RequestParam(required = false) boolean lowercase,
@RequestParam(required = false) boolean uppercase,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
// prevents other clients from accessing the database whenever the form is submitted
requestLock_.lock();
try {
DefaultSetting oldSetting = minterService_.getStoredSetting();
DefaultSetting newSetting;
LOGGER.info("in handleForm");
boolean auto = mintType.equals("auto");
boolean random = mintOrder.equals("random");
long size;
if (cacheSize.isEmpty()) {
size = oldSetting.getCacheSize();
}
else {
size = Long.parseLong(cacheSize);
}
// assign values based on which minter type was selected
if (auto) {
// gets the token value
Token tokenType;
if (digits && !lowercase && !uppercase) {
tokenType = Token.DIGIT;
}
else if (!digits && lowercase && !uppercase) {
tokenType = (sansvowel) ? Token.LOWER_CONSONANTS
: Token.LOWER_ALPHABET;
}
else if (!digits && !lowercase && uppercase) {
tokenType = (sansvowel) ? Token.UPPER_CONSONANTS
: Token.UPPER_ALPHABET;
}
else if (!digits && lowercase && uppercase) {
tokenType = (sansvowel) ? Token.MIXED_CONSONANTS
: Token.MIXED_ALPHABET;
}
else if (digits && lowercase && !uppercase) {
tokenType = (sansvowel) ? Token.LOWER_CONSONANTS_EXTENDED
: Token.LOWER_ALPHABET_EXTENDED;
}
else if (digits && !lowercase && uppercase) {
tokenType = (sansvowel) ? Token.UPPER_CONSONANTS_EXTENDED
: Token.UPPER_ALPHABET_EXTENDED;
}
else if (digits && lowercase && uppercase) {
tokenType = (sansvowel) ? Token.MIXED_CONSONANTS_EXTENDED
: Token.MIXED_ALPHABET_EXTENDED;
}
else {
throw new BadParameterException();
}
// create new defaultsetting object
newSetting = new DefaultSetting(prepend,
idprefix,
size,
tokenType,
oldSetting.getCharMap(),
idlength,
sansvowel,
auto,
random);
}
else {
// validate charmapping
if (charmapping == null || charmapping.isEmpty()) {
throw new BadParameterException();
}
// create new defaultsetting object
newSetting = new DefaultSetting(prepend,
idprefix,
size,
oldSetting.getTokenType(),
charmapping,
oldSetting.getRootLength(),
sansvowel,
auto,
random);
}
minterService_.updateCurrentSetting(newSetting);
}
finally {
// unlocks RequestLock and gives access to longest waiting thread
requestLock_.unlock();
LOGGER.warn("Request to update default settings finished, UNLOCKING MINTER");
}
// redirect to the administration panel
response.sendRedirect("administration");
}
/**
* Creates a path to mint ids. If parameters aren't given then mintPids will
* resort to using the default values found in DefaultSetting.properties
*
* @param requestedAmount requested number of ids to mint
* @param parameters parameters given by user to instill variety in ids
* @return paths user to mint.jsp
* @throws Exception catches all sorts of exceptions that may be thrown by
* any methods
*/
@ResponseStatus(code = HttpStatus.CREATED)
@RequestMapping(value = {"/mint/{requestedAmount}"},
method = {RequestMethod.GET},
produces = "application/json")
public Set<Pid> mintPids(@PathVariable long requestedAmount,
@RequestParam Map<String, String> parameters) throws Exception {
// ensure that only one thread access the minter at any given time
requestLock_.lock();
Set<Pid> pidSet;
try {
LOGGER.info("Request to Minter made, LOCKING MINTER");
// validate amount
validateAmount(requestedAmount);
// override default settings where applicable
DefaultSetting tempSetting = overrideDefaultSetting(parameters,
minterService_.getStoredSetting());
// create the set of ids
pidSet = minterService_.mint(requestedAmount, tempSetting);
}
finally {
// unlocks RequestLock and gives access to longest waiting thread
requestLock_.unlock();
LOGGER.info("Request to Minter Finished, UNLOCKING MINTER");
}
// return the generated set of Pids
return pidSet;
}
/**
* Maps to the administration panel on the administration path.
*
* @return name of the index page
* @throws Exception
*/
@RequestMapping(value = {"/administration"}, method = {RequestMethod.GET})
public ModelAndView displayAdministrationPanel() throws Exception {
ModelAndView model = new ModelAndView();
// retrieve default values stored in the database
DefaultSetting defaultSetting = minterService_.getStoredSetting();
// add the values to the settings page so that they can be displayed
LOGGER.info("index page called");
model.addObject("prepend", defaultSetting.getPrepend());
model.addObject("prefix", defaultSetting.getPrefix());
model.addObject("cacheSize", defaultSetting.getCacheSize());
model.addObject("charMap", defaultSetting.getCharMap());
model.addObject("tokenType", defaultSetting.getTokenType());
model.addObject("rootLength", defaultSetting.getRootLength());
model.addObject("isAuto", defaultSetting.isAuto());
model.addObject("isRandom", defaultSetting.isRandom());
model.addObject("sansVowel", defaultSetting.isSansVowels());
model.setViewName("settings");
return model;
}
/**
* Maps to the root of the application
*
* @return
*/
@RequestMapping(value = {""}, method = {RequestMethod.GET})
public String displayIndex() {
return "";
}
/**
* Handles any exception that may be caught within the program
*
* @param req the HTTP request
* @param exception the caught exception
* @return The view of the error message
*/
@ResponseStatus(code = HttpStatus.BAD_REQUEST)
@ExceptionHandler(Exception.class)
public ModelAndView handleGeneralError(HttpServletRequest req, Exception exception) {
ModelAndView mav = new ModelAndView();
mav.addObject("status", 500);
mav.addObject("exception", exception.getClass().getSimpleName());
mav.addObject("message", exception.getMessage());
LOGGER.error("General Error: {}", exception.getMessage());
StackTraceElement[] s = exception.getStackTrace();
String trace = "";
for (StackTraceElement g : s) {
trace += g + "\n";
}
mav.addObject("stacktrace", trace);
mav.setViewName("error");
return mav;
}
/**
* Overrides the default value of cached value with values given in the
* parameter. If the parameters do not contain any of the valid parameters,
* the default values are maintained.
*
* @param parameters List of parameters given by the client.
* @param entity
* @return The settings used for the particular session it was called.
* @throws BadParameterException
*/
private DefaultSetting overrideDefaultSetting(final Map<String, String> parameters,
final DefaultSetting entity) throws BadParameterException {
String prepend = (parameters.containsKey("prepend"))
? parameters.get("prepend")
: entity.getPrepend();
String prefix = (parameters.containsKey("prefix"))
? validatePrefix(parameters.get("prefix"))
: entity.getPrefix();
int rootLength = (parameters.containsKey("rootLength"))
? validateRootLength(Integer.parseInt(parameters.get("rootLength")))
: entity.getRootLength();
String charMap = (parameters.containsKey("charMap"))
? validateCharMap(parameters.get("charMap"))
: entity.getCharMap();
Token tokenType = (parameters.containsKey("tokenType"))
? Token.valueOf(parameters.get("tokenType"))
: entity.getTokenType();
boolean isAuto = (parameters.containsKey("auto"))
? convertBoolean(parameters.get("auto"), "auto")
: entity.isAuto();
boolean isSansVowels = (parameters.containsKey("sansVowels"))
? convertBoolean(parameters.get("sansVowels"), "sansVowels")
: entity.isSansVowels();
return new DefaultSetting(prepend,
prefix,
entity.getCacheSize(),
tokenType,
charMap,
rootLength,
isSansVowels,
isAuto,
entity.isRandom());
}
/**
* This method is used to check whether or not the given parameter is
* explicitly equivalent to "true" or "false" and returns them respectively.
*
* @param parameter the given string to convert.
* @param parameterType the type of the parameter.
* @throws BadParameterException Thrown whenever parameter is neither "true"
* nor "false"
* @return true if the parameter is "true", false if the parameter is
* "false"
*/
private boolean convertBoolean(String parameter, String parameterType)
throws BadParameterException {
if (parameter.equals("true")) {
return true;
}
else if (parameter.equals("false")) {
return false;
}
else {
throw new BadParameterException(parameter, parameterType);
}
}
/**
* Asserts the validity of a CharMap using the minter service.
*
* @param charMap A sequence of characters used to configure PIDs
* @return Returns the given charMap if nothing wrong was detected
* @throws BadParameterException Thrown whenever a bad parameter is
* detected.
*/
private String validateCharMap(String charMap) throws BadParameterException {
if (!IdGenerator.isValidCharMap(charMap)) {
throw new BadParameterException(charMap, "charMap");
}
return charMap;
}
/**
* Asserts the validity of an amount using the minter service. A valid
* amount is greater than or equal to 0.
*
* @param amount The number of PIDs to be created
* @throws BadParameterException Thrown whenever a bad parameter is
* detected.
*/
private void validateAmount(long amount) throws BadParameterException {
if (!IdGenerator.isValidAmount(amount)) {
throw new BadParameterException(amount, "amount");
}
}
/**
* Asserts the validity of a rootLength is valid.
*
* @param rootLength
* @return
* @throws BadParameterException
*/
private int validateRootLength(int rootLength) throws BadParameterException {
if (!IdGenerator.isValidRootLength(rootLength)) {
throw new BadParameterException(rootLength, "rootLength");
}
return rootLength;
}
/**
* Asserts the validity of prefix is valid.
*
* @param prefix A sequence of characters that appear in the beginning of
* PIDs
* @return Returns the given prefix if nothing wrong was detected
* @throws BadParameterException Thrown whenever a bad parameter is
* detected.
*/
private String validatePrefix(String prefix) throws BadParameterException {
if (!IdGenerator.isValidPrefix(prefix)) {
throw new BadParameterException(prefix, "prefix");
}
return prefix;
}
}