diff --git a/homework-g595-topilskiy/pom.xml b/homework-g595-topilskiy/pom.xml index d1dfd5ac0..95e74790c 100644 --- a/homework-g595-topilskiy/pom.xml +++ b/homework-g595-topilskiy/pom.xml @@ -10,8 +10,13 @@ 4.0.0 homework-g595-topilskiy + pom 1.0.0 + + 1.4.2.RELEASE + + ru.mipt.java2016 @@ -19,6 +24,43 @@ 1.0.0 + + net.sourceforge.jeval + jeval + 0.9.4 + + + + + org.springframework.boot + spring-boot-starter-web + ${spring.boot.version} + + + + org.springframework.boot + spring-boot-starter-jdbc + ${spring.boot.version} + + + + org.springframework.boot + spring-boot-starter-security + ${spring.boot.version} + + + + com.zaxxer + HikariCP + 2.5.1 + + + + com.h2database + h2 + 1.4.193 + + ru.mipt.java2016 homework-tests diff --git a/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/calculator/rest/IFunctionalCalculator.java b/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/calculator/rest/IFunctionalCalculator.java new file mode 100644 index 000000000..9ce167b2c --- /dev/null +++ b/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/calculator/rest/IFunctionalCalculator.java @@ -0,0 +1,69 @@ +package ru.mipt.java2016.homework.g595.topilskiy.task4.calculator.rest; + +import ru.mipt.java2016.homework.base.task1.ParsingException; +import ru.mipt.java2016.homework.g595.topilskiy.task4.calculator.rest.function.CalculatorFunctionObject; + +import java.util.List; + +/** + * Interface for a Calculator which can hold user-defined Functions and Variables + * + * @author Artem K. Topilskiy + * @since 16.12.16. + */ +public interface IFunctionalCalculator { + /** + * Methods of interacting with calculator VARIABLES + */ + /** + * @return the double value under the alias of variableAlias + */ + Double getVariable(String variableAlias); + + /** + * Make the alias of variableAlias reflect to the double value + */ + boolean putVariable(String variableAlias, Double value); + + /** + * Delete the alias of variableAlias and its held value + */ + boolean deleteVariable(String variableAlias); + + /** + * @return the list of aliases of variables in the calculator + */ + List getVariableList(); + + + /** + * Methods of interacting with calculator FUNCTIONS + */ + /** + * @return a CalculatorFunction object under the alias of functionAlias + * NOTE: predefined functions cannot be dealiased + */ + CalculatorFunctionObject getFunction(String functionAlias); + + /** + * Make the alias of functionAlias reflect to CalculatorFunction(expression, arguments) + */ + boolean putFunction(String functionAlias, String expression, List arguments); + + /** + * Delete the alias of functionAlias and its held function + */ + boolean deleteFunction(String functionAlias); + + /** + * @return the list of aliases of functions in the calculator + */ + List getFunctionList(); + + + /** + * Methods of CALCULATION of the value of expression + * (using the kept function and variable sets) + */ + Double calculate(String expression) throws ParsingException; +} diff --git a/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/calculator/rest/RESTCalculator.java b/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/calculator/rest/RESTCalculator.java new file mode 100644 index 000000000..ba5b47d48 --- /dev/null +++ b/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/calculator/rest/RESTCalculator.java @@ -0,0 +1,174 @@ +package ru.mipt.java2016.homework.g595.topilskiy.task4.calculator.rest; + +import ru.mipt.java2016.homework.base.task1.ParsingException; +import ru.mipt.java2016.homework.g595.topilskiy.task4.calculator.rest.function.CalculateableFunction; +import ru.mipt.java2016.homework.g595.topilskiy.task4.calculator.rest.function.CalculatorFunctionObject; +import ru.mipt.java2016.homework.g595.topilskiy.task4.calculator.rest.function.IEvaluateableFunction; +import ru.mipt.java2016.homework.g595.topilskiy.task4.calculator.rest.function.PredefinedFunction; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * REST-ready Calculator, which can work with user-defined functions/arguments + * + * @author Artem K. Topilskiy + * @since 16.12.16. + */ +public class RESTCalculator implements IFunctionalCalculator { + /** + * Stored Function and Variable Data + */ + private Map functions = new ConcurrentHashMap<>(); + private Map variables = new ConcurrentHashMap<>(); + + /** + * Initialization functions + */ + private void initializePredefinedFunctions() { + functions.put("sin", new PredefinedFunction(PredefinedFunction.PredefinedFunctionType.SIN)); + functions.put("cos", new PredefinedFunction(PredefinedFunction.PredefinedFunctionType.COS)); + functions.put("tg", new PredefinedFunction(PredefinedFunction.PredefinedFunctionType.TG)); + functions.put("sqrt", new PredefinedFunction(PredefinedFunction.PredefinedFunctionType.SQRT)); + functions.put("pow", new PredefinedFunction(PredefinedFunction.PredefinedFunctionType.POW)); + functions.put("abs", new PredefinedFunction(PredefinedFunction.PredefinedFunctionType.ABS)); + functions.put("sign", new PredefinedFunction(PredefinedFunction.PredefinedFunctionType.SIGN)); + functions.put("log", new PredefinedFunction(PredefinedFunction.PredefinedFunctionType.LOG)); + functions.put("log2", new PredefinedFunction(PredefinedFunction.PredefinedFunctionType.LOG2)); + functions.put("rnd", new PredefinedFunction(PredefinedFunction.PredefinedFunctionType.RND)); + functions.put("max", new PredefinedFunction(PredefinedFunction.PredefinedFunctionType.MAX)); + functions.put("min", new PredefinedFunction(PredefinedFunction.PredefinedFunctionType.MIN)); + } + + /** + * CONSTRUCTOR + */ + public RESTCalculator() { + initializePredefinedFunctions(); + } + + /** + * OVERRIDE + */ + + /** + * Methods of interacting with calculator VARIABLES + */ + /** + * @return the double value under the alias of variableAlias + */ + @Override + public Double getVariable(String variableAlias) { + return variables.get(variableAlias); + } + + /** + * Make the alias of variableAlias reflect to the double value + */ + @Override + public boolean putVariable(String variableAlias, Double value) { + if (functions.containsKey(variableAlias)) { + return false; + } else { + variables.put(variableAlias, value); + return true; + } + } + + /** + * Delete the alias of variableAlias and its held value + */ + @Override + public boolean deleteVariable(String variableAlias) { + if (variables.containsKey(variableAlias)) { + variables.remove(variableAlias); + return true; + } + + return false; + } + + /** + * @return the list of aliases of variables in the calculator + */ + @Override + public List getVariableList() { + return variables.keySet().stream().collect(Collectors.toList()); + } + + + /** + * Methods of interacting with calculator FUNCTIONS + */ + /** + * @return a CalculatorFunction object under the alias of functionAlias + * NOTE: predefined functions cannot be dealiased + */ + @Override + public CalculatorFunctionObject getFunction(String functionAlias) { + if (!functions.containsKey(functionAlias) || functions.get(functionAlias).isPredefined()) { + return null; + } + + CalculateableFunction function = (CalculateableFunction) functions.get(functionAlias); + return new CalculatorFunctionObject(function.getFunctionExpression(), function.getParameterList()); + } + + /** + * Make the alias of functionAlias reflect to CalculatorFunction(expression, arguments) + */ + @Override + public boolean putFunction(String functionAlias, String expression, List arguments) { + if (variables.containsKey(functionAlias) || + (functions.containsKey(functionAlias) && functions.get(functionAlias).isPredefined())) { + return false; + } + + try { + functions.put(functionAlias, new CalculateableFunction(expression, arguments, functions, variables)); + } catch (ParsingException e) { + return false; + } + + return true; + } + + /** + * Delete the alias of functionAlias and its held function + */ + @Override + public boolean deleteFunction(String functionAlias) { + if (functions.containsKey(functionAlias)) { + IEvaluateableFunction function = functions.get(functionAlias); + if (function == null || function.isPredefined()) { + return false; + } else { + functions.remove(functionAlias); + return true; + } + } + + return false; + } + + /** + * @return the list of aliases of functions in the calculator + */ + @Override + public List getFunctionList() { + return functions.keySet().stream().collect(Collectors.toList()); + } + + + /** + * Methods of CALCULATION of the value of expression + * (using the kept function and variable sets) + */ + @Override + public Double calculate(String expression) throws ParsingException { + return new CalculateableFunction(expression, new ArrayList<>(), functions, variables).evaluate(); + } +} diff --git a/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/calculator/rest/function/CalculateableFunction.java b/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/calculator/rest/function/CalculateableFunction.java new file mode 100644 index 000000000..34f85c620 --- /dev/null +++ b/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/calculator/rest/function/CalculateableFunction.java @@ -0,0 +1,369 @@ +package ru.mipt.java2016.homework.g595.topilskiy.task4.calculator.rest.function; + +import ru.mipt.java2016.homework.base.task1.ParsingException; +import ru.mipt.java2016.homework.g595.topilskiy.task4.calculator.vanilla.Token; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Calculator functions which can be evaluated in runtime + * + * Expression is defined as the following (in extended Backus-Naur Form) + * expression = multiple, { ('+' | '-') multiple } + * multiple = braced_expression, { ('*' | '/') braced_expression } + * braced_expression = '(' expression ')' | combined_expression + * combined_expression = '-' braced_expression | double_number | function_call | variable + * function_call = function_name '(' function_parameters_called ')' + * function_parameters_called = expression { ',' expression } + * + * double_number = '[1-9]' { '[0-9]' } [ '.' ] { '[0-9]' }+ + * variable = '[a-zA-Z]' { '[a-zA-Z0-9]' } + * function_definition = function_name '(' function_parameters ')' + * function_parameters = variable { ',' variable } + * + * @author Artem K. Topilskiy + * @since 16.12.16. + */ +public class CalculateableFunction implements IEvaluateableFunction { + /** + * Global Function Data + */ + private Map functions; + private Map variables; + + /** + * Function Data + */ + private final ArrayList tokens = new ArrayList<>(); + private int tokensIndex = 0; + private String functionExpression; + + /** + * Function Parameter Data + */ + private Map parameterNumberToParameterString = new HashMap<>(); + private Map parameters = new HashMap<>(); + + /** + * CONSTRUCTOR + */ + public CalculateableFunction(String functionExpression, List functionParameters, + Map functions, + Map variables) throws ParsingException { + if (functionParameters != null) { + for (int i = 0; i < functionParameters.size(); ++i) { + parameterNumberToParameterString.put(i, functionParameters.get(i)); + } + } + + this.functionExpression = functionExpression; + this.functions = functions; + this.variables = variables; + + parse(functionExpression); + } + + /** + * Private Token-generate Functions + */ + private void parse(String expression) throws ParsingException { + if (expression == null) { + throw new ParsingException("Expression is null."); + } + + for (int expressionIndex = 0; expressionIndex < expression.length(); ++expressionIndex) { + Token currentToken; + + if (Character.isWhitespace(expression.charAt(expressionIndex)) || + Character.isSpaceChar(expression.charAt(expressionIndex))) { + continue; + } + + switch (expression.charAt(expressionIndex)) { + case '+': + currentToken = new Token(Token.TokenType.PLUS); + break; + + case '-': + currentToken = new Token(Token.TokenType.MINUS); + break; + + case '*': + currentToken = new Token(Token.TokenType.MULTIPLY); + break; + + case '/': + currentToken = new Token(Token.TokenType.DIVIDE); + break; + + case '(': + currentToken = new Token(Token.TokenType.LEFT_BRACE); + break; + + case ')': + currentToken = new Token(Token.TokenType.RIGHT_BRACE); + break; + + case ',': + currentToken = new Token(Token.TokenType.COMMA); + break; + + default: + if (Character.isDigit(expression.charAt(expressionIndex))) { + boolean readDot = false; + int numberStartIndex = expressionIndex; + for (; expressionIndex < expression.length(); ++expressionIndex) { + Character currentCharacter = expression.charAt(expressionIndex); + if (currentCharacter == '.' && !readDot) { + readDot = true; + } else if (!Character.isDigit(currentCharacter)) { + break; + } + } + + Double currentNumber = + Double.parseDouble(expression.substring(numberStartIndex, expressionIndex)); + --expressionIndex; + currentToken = new Token(currentNumber); + + } else if (Character.isAlphabetic(expression.charAt(expressionIndex))) { + int nameStartIndex = expressionIndex; + for (; expressionIndex < expression.length(); ++expressionIndex) { + Character currentCharacter = expression.charAt(expressionIndex); + if (!Character.isAlphabetic(currentCharacter) && + !Character.isDigit(currentCharacter) && + currentCharacter != '_') { + break; + } + } + + currentToken = new Token(functionExpression.substring(nameStartIndex, expressionIndex)); + --expressionIndex; + } else { + throw new ParsingException(String.format("Unexpected symbol at %d", expressionIndex)); + } + break; + } + + tokens.add(currentToken); + } + } + + + /** + * Private Token-read Functions + */ + private void regressTokensIndex() { + --tokensIndex; + } + + private Token progressTokens() { + if (tokensIndex >= tokens.size()) { + return null; + } + + return tokens.get(tokensIndex++); + } + + + /** + * Private Token-calculate Functions + */ + private Double expression() throws ParsingException { + Double result = multiple(); + + for (Token token = progressTokens(); token != null; token = progressTokens()) { + if (token.getType() == Token.TokenType.PLUS) { + result += multiple(); + } else if (token.getType() == Token.TokenType.MINUS) { + result -= multiple(); + } else { + regressTokensIndex(); + break; + } + } + + return result; + } + + private Double multiple() throws ParsingException { + Double result = bracedExpression(); + + for (Token token = progressTokens(); token != null; token = progressTokens()) { + if (token.getType() == Token.TokenType.MULTIPLY) { + result *= bracedExpression(); + } else if (token.getType() == Token.TokenType.DIVIDE) { + result /= bracedExpression(); + } else { + regressTokensIndex(); + break; + } + } + + return result; + } + + private Double bracedExpression() throws ParsingException { + Double result; + + Token token = progressTokens(); + if (token == null) { + throw new ParsingException("Invalid amount of numbers."); + } + + if (token.getType() == Token.TokenType.LEFT_BRACE) { + result = expression(); + token = progressTokens(); + + if (token == null || token.getType() != Token.TokenType.RIGHT_BRACE) { + throw new ParsingException("Wrong number of left/right braces"); + } + } else { + regressTokensIndex(); + result = combinedExpression(); + } + + return result; + } + + private Double combinedExpression() throws ParsingException { + Double result; + + Token token = progressTokens(); + if (token == null) { + throw new ParsingException("Invalid amount of numbers"); + } + + if (token.getType() == Token.TokenType.MINUS) { + result = -bracedExpression(); + } else if (token.getType() == Token.TokenType.NUMBER) { + result = token.getNumber(); + } else if (token.getType() == Token.TokenType.NAME) { + regressTokensIndex(); + result = namedExpression(); + } else { + throw new ParsingException("Invalid order of operations"); + } + + return result; + } + + private Double namedExpression() throws ParsingException { + Double result; + + Token token = progressTokens(); + if (token == null) { + throw new ParsingException("Invalid amount of numbers"); + } + String tokenName = token.getName(); + + Token nextToken = progressTokens(); + if (nextToken != null && nextToken.getType() == Token.TokenType.LEFT_BRACE) { + if (!functions.containsKey(tokenName)) { + throw new ParsingException("Unexpected symbol. Function call impossible."); + } + + result = functionCall(tokenName); + } else { + if (nextToken != null) { + regressTokensIndex(); + } + + if (parameters.containsKey(tokenName)) { + result = parameters.get(tokenName); + } else { + if (!variables.containsKey(tokenName)) { + throw new ParsingException("Unexpected symbol. No such variable."); + } + result = variables.get(tokenName); + } + } + + return result; + } + + private Double functionCall(String functionName) throws ParsingException { + Double result; + + IEvaluateableFunction function = functions.get(functionName); + List functionArguments = new ArrayList<>(); + + Token token; + do { + token = progressTokens(); + + if (token == null) { + throw new ParsingException("Unexpected end of function expression."); + } else { + if (token.getType() == Token.TokenType.COMMA) { + throw new ParsingException("Unexpected comma - no parameter."); + } else if (token.getType() == Token.TokenType.RIGHT_BRACE) { + break; + } else { + regressTokensIndex(); + } + } + + functionArguments.add(expression()); + + token = progressTokens(); + if (token == null || + (token.getType() != Token.TokenType.COMMA && token.getType() != Token.TokenType.RIGHT_BRACE)) { + throw new ParsingException("Unexpected end of function expression."); + } + } while (token.getType() != Token.TokenType.RIGHT_BRACE); + + function.setArguments(functionArguments); + result = function.evaluate(); + return result; + } + + + /** + * OVERRIDE + */ + @Override + public Double evaluate() throws ParsingException { + tokensIndex = 0; + Double result = expression(); + + if (tokensIndex != tokens.size()) { + throw new ParsingException("Invalid number of tokens (too many)."); + } + + return result; + } + + @Override + public void setArguments(List arguments) throws ParsingException { + if (arguments.size() != parameterNumberToParameterString.size()) { + throw new ParsingException("Number of arguments to be set is invalid."); + } + for (int argumentsIndex = 0; argumentsIndex < arguments.size(); ++argumentsIndex) { + parameters.put(parameterNumberToParameterString.get(argumentsIndex), arguments.get(argumentsIndex)); + } + } + + /** + * GETTERS + */ + public String getFunctionExpression() { + return functionExpression; + } + + public List getParameterList() { + List result = new ArrayList<>(); + + List> entries = + parameterNumberToParameterString.entrySet() + .stream() + .sorted(Comparator.comparingInt(Map.Entry::getKey)) + .collect(Collectors.toList()); + + for (Map.Entry entry : entries) { + result.add(entry.getValue()); + } + return result; + } +} diff --git a/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/calculator/rest/function/CalculatorFunctionObject.java b/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/calculator/rest/function/CalculatorFunctionObject.java new file mode 100644 index 000000000..44b2c2c81 --- /dev/null +++ b/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/calculator/rest/function/CalculatorFunctionObject.java @@ -0,0 +1,77 @@ +package ru.mipt.java2016.homework.g595.topilskiy.task4.calculator.rest.function; + +import java.util.ArrayList; +import java.util.List; + +/** + * The Function-Objects held in FunctionalCalculators + * which hold the argument list and the expression of the function + * + * @author Artem K. Topilskiy + * @since 16.12.16. + */ +public class CalculatorFunctionObject { + private String expression; + private List arguments; + + /** + * CONSTRUCTORS + */ + public CalculatorFunctionObject() { + expression = ""; + arguments = new ArrayList<>(); + } + + public CalculatorFunctionObject(String expression, List arguments) { + this.expression = expression; + this.arguments = arguments; + } + + + /** + * GETTERS + */ + public String getExpression() { + return expression; + } + + public List getArguments() { + return arguments; + } + + + /** + * SETTERS + */ + public void setExpression(String expression) { + this.expression = expression; + } + + public void setArguments(List arguments) { + this.arguments = arguments; + } + + /** + * OVERRIDDEN + */ + @Override + public String toString() { + return "Expression: " + expression + '\n' + + "Arguments: " + String.join(", ", arguments); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof CalculatorFunctionObject)) { + return false; + } + CalculatorFunctionObject comparedCalculatorFunction = (CalculatorFunctionObject) obj; + return expression.equals(comparedCalculatorFunction.expression) && + arguments.equals(comparedCalculatorFunction.arguments); + } + + @Override + public int hashCode() { + return expression.hashCode() * 31 + arguments.hashCode(); + } +} diff --git a/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/calculator/rest/function/IEvaluateableFunction.java b/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/calculator/rest/function/IEvaluateableFunction.java new file mode 100644 index 000000000..a97cd8bcc --- /dev/null +++ b/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/calculator/rest/function/IEvaluateableFunction.java @@ -0,0 +1,30 @@ +package ru.mipt.java2016.homework.g595.topilskiy.task4.calculator.rest.function; + +import ru.mipt.java2016.homework.base.task1.ParsingException; + +import java.util.List; + +/** + * Interface for a Function which can be evaluated on a set of set arguments + * + * @author Artem K. Topilskiy + * @since 16.12.16. + */ +public interface IEvaluateableFunction { + /** + * @return the result of the function evalueated on the given set of arguments + */ + Double evaluate() throws ParsingException; + + /** + * Sets the arguments up for the evaluation of the Function + */ + void setArguments(List arguments) throws ParsingException; + + /** + * @return whether the current function is predefined + */ + default boolean isPredefined() { + return false; + } +} diff --git a/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/calculator/rest/function/PredefinedFunction.java b/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/calculator/rest/function/PredefinedFunction.java new file mode 100644 index 000000000..f84d2cf61 --- /dev/null +++ b/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/calculator/rest/function/PredefinedFunction.java @@ -0,0 +1,134 @@ +package ru.mipt.java2016.homework.g595.topilskiy.task4.calculator.rest.function; + +import ru.mipt.java2016.homework.base.task1.ParsingException; + +import java.util.*; + +/** + * Calculator functions that are agreed upon to be predefined + * + * @author Artem K. Topilskiy + * @since 16.12.16. + */ +public class PredefinedFunction implements IEvaluateableFunction { + /** + * PUBLIC STATIC DATA + */ + public enum PredefinedFunctionType { + SIN, COS, TG, SQRT, POW, ABS, SIGN, LOG, LOG2, RND, MAX, MIN + } + + public static final Map PREDEFINED_FUNCTION_TYPE_NUM_ARGUMENTS_MAP; + + static { + Map predefinedFunctionTypeNumArguments = new HashMap<>(); + predefinedFunctionTypeNumArguments.put(PredefinedFunctionType.SIN, 1); + predefinedFunctionTypeNumArguments.put(PredefinedFunctionType.COS, 1); + predefinedFunctionTypeNumArguments.put(PredefinedFunctionType.TG, 1); + predefinedFunctionTypeNumArguments.put(PredefinedFunctionType.SQRT, 1); + predefinedFunctionTypeNumArguments.put(PredefinedFunctionType.POW, 2); + predefinedFunctionTypeNumArguments.put(PredefinedFunctionType.ABS, 1); + predefinedFunctionTypeNumArguments.put(PredefinedFunctionType.SIGN, 1); + predefinedFunctionTypeNumArguments.put(PredefinedFunctionType.LOG, 1); + predefinedFunctionTypeNumArguments.put(PredefinedFunctionType.LOG2, 1); + predefinedFunctionTypeNumArguments.put(PredefinedFunctionType.RND, 0); + predefinedFunctionTypeNumArguments.put(PredefinedFunctionType.MAX, 2); + predefinedFunctionTypeNumArguments.put(PredefinedFunctionType.MIN, 2); + PREDEFINED_FUNCTION_TYPE_NUM_ARGUMENTS_MAP = + Collections.unmodifiableMap(predefinedFunctionTypeNumArguments); + } + + /** + * PRIVATE STATIC DATA + */ + private static final Random RANDOM_NUMBER_GENERATOR = new Random(1337); + + + /** + * DATA + */ + private final PredefinedFunctionType functionType; + private final List arguments = new ArrayList<>(); + + + /** + * CONSTRUCTOR + */ + public PredefinedFunction(PredefinedFunctionType functionType) { + this.functionType = functionType; + for (int numArgumentsCounter = 0; + numArgumentsCounter < PREDEFINED_FUNCTION_TYPE_NUM_ARGUMENTS_MAP.get(functionType); + ++numArgumentsCounter) { + arguments.add(0.0); + } + } + + + /** + * INTERFACE: IEvaluateableFunction + */ + @Override + public Double evaluate() throws ParsingException { + Double result = 0.0; + + switch (functionType) { + case SIN: + result = Math.sin(arguments.get(0)); + break; + case COS: + result = Math.cos(arguments.get(0)); + break; + case TG: + result = Math.tan(arguments.get(0)); + break; + case SQRT: + result = Math.sqrt(arguments.get(0)); + break; + case POW: + result = Math.pow(arguments.get(0), arguments.get(1)); + break; + case ABS: + result = Math.abs(arguments.get(0)); + break; + case SIGN: + result = Math.signum(arguments.get(0)); + break; + case LOG: + result = Math.log(arguments.get(0)); + break; + case LOG2: + result = Math.log(arguments.get(0)) / Math.log(2); + break; + case RND: + synchronized (RANDOM_NUMBER_GENERATOR) { + result = RANDOM_NUMBER_GENERATOR.nextDouble(); + } + break; + case MAX: + result = Math.max(arguments.get(0), arguments.get(1)); + break; + case MIN: + result = Math.min(arguments.get(0), arguments.get(1)); + break; + default: + break; + } + + return result; + } + + @Override + public void setArguments(List arguments) throws ParsingException { + if (this.arguments.size() != arguments.size()) { + throw new ParsingException("Number of arguments to be set is invalid."); + } + for (int argumentsIndex = 0; argumentsIndex < arguments.size(); ++argumentsIndex) { + this.arguments.set(argumentsIndex, arguments.get(argumentsIndex)); + } + } + + @Override + public boolean isPredefined() { + return true; + } +} diff --git a/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/calculator/vanilla/Token.java b/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/calculator/vanilla/Token.java new file mode 100644 index 000000000..4193e6ae1 --- /dev/null +++ b/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/calculator/vanilla/Token.java @@ -0,0 +1,64 @@ +package ru.mipt.java2016.homework.g595.topilskiy.task4.calculator.vanilla; + +/** + * Token class for parsing the expression in TokenCalculator + * + * @author Artem K. Topilskiy + * @since 16.12.16. + */ +public class Token { + /** + * Enum to describe the type of Data in Token + */ + public enum TokenType { + PLUS, MINUS, MULTIPLY, DIVIDE, + NUMBER, NAME, + LEFT_BRACE, RIGHT_BRACE, COMMA, + UNKNOWN + } + + /** + * Data + */ + private final TokenType type; + private Double number = null; + private String name = null; + + /** + * Constructors + */ + public Token(TokenType type) { + this.type = type; + } + + public Token(String name) { + this.name = name; + this.type = TokenType.NAME; + } + + public Token(Double number) { + this.number = number; + this.type = TokenType.NUMBER; + } + + + /** + * Getters + */ + public TokenType getType() { + return type; + } + + public Double getNumber() { + return number; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return String.format("Type: %s, Number: %s, Name: %s \n", type, number, name); + } +} diff --git a/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/calculator/vanilla/TokenCalculator.java b/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/calculator/vanilla/TokenCalculator.java new file mode 100644 index 000000000..b373cd196 --- /dev/null +++ b/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/calculator/vanilla/TokenCalculator.java @@ -0,0 +1,214 @@ +package ru.mipt.java2016.homework.g595.topilskiy.task4.calculator.vanilla; + +import ru.mipt.java2016.homework.base.task1.Calculator; +import ru.mipt.java2016.homework.base.task1.ParsingException; + +import java.util.ArrayList; + +/** + * Calculator that evaluates the expression by the way of tokens + * + * Expression is defined as the following (in extended Backus-Naur Form) + * expression = multiple, { ('+' | '-') multiple } + * multiple = braced_expression, { ('*' | '/') braced_expression } + * braced_expression = '(' expression ')' | number_expression + * number_expression = '-' braced_expression | double_number + * double_number = '[1-9]' { '[0-9]' } [ '.' ] { '[0-9]' }+ + * + * @author Artem K. Topilskiy + * @since 16.12.16. + */ +public class TokenCalculator implements Calculator { + /** + * Data + */ + private final ArrayList tokens = new ArrayList<>(); + private int tokensIndex = 0; + + + /** + * Private Token-generate Functions + */ + private void parse(String expression) throws ParsingException { + if (expression == null) { + throw new ParsingException("Expression is null."); + } + + for (int expressionIndex = 0; expressionIndex < expression.length(); ++expressionIndex) { + Token.TokenType currentTokenType; + Double currentNumber = null; + + if (Character.isWhitespace(expression.charAt(expressionIndex)) || + Character.isSpaceChar(expression.charAt(expressionIndex))) { + continue; + } + + switch (expression.charAt(expressionIndex)) { + case '+': + currentTokenType = Token.TokenType.PLUS; + break; + + case '-': + currentTokenType = Token.TokenType.MINUS; + break; + + case '*': + currentTokenType = Token.TokenType.MULTIPLY; + break; + + case '/': + currentTokenType = Token.TokenType.DIVIDE; + break; + + case '(': + currentTokenType = Token.TokenType.LEFT_BRACE; + break; + + case ')': + currentTokenType = Token.TokenType.RIGHT_BRACE; + break; + + default: + if (!Character.isDigit(expression.charAt(expressionIndex))) { + throw new ParsingException(String.format("Unexpected symbol at %d", expressionIndex)); + } + + boolean readDot = false; + int numberStartIndex = expressionIndex; + for (; expressionIndex < expression.length(); ++expressionIndex) { + Character currentCharacter = expression.charAt(expressionIndex); + if (currentCharacter == '.' && !readDot) { + readDot = true; + } else if (!Character.isDigit(currentCharacter)) { + break; + } + } + + currentNumber = Double.parseDouble(expression.substring(numberStartIndex, expressionIndex)); + --expressionIndex; + currentTokenType = Token.TokenType.NUMBER; + break; + } + + if (currentTokenType != Token.TokenType.NUMBER) { + tokens.add(new Token(currentTokenType)); + } else { + tokens.add(new Token(currentNumber)); + } + } + } + + + /** + * Private Token-read Functions + */ + private void regressTokensIndex() { + --tokensIndex; + } + + private Token progressTokens() { + if (tokensIndex >= tokens.size()) { + return null; + } + + return tokens.get(tokensIndex++); + } + + + /** + * Private Token-calculate Functions + */ + private Double expression() throws ParsingException { + Double result = multiple(); + + for (Token token = progressTokens(); token != null; token = progressTokens()) { + if (token.getType() == Token.TokenType.PLUS) { + result += multiple(); + } else if (token.getType() == Token.TokenType.MINUS) { + result -= multiple(); + } else { + regressTokensIndex(); + break; + } + } + + return result; + } + + private Double multiple() throws ParsingException { + Double result = bracedExpression(); + + for (Token token = progressTokens(); token != null; token = progressTokens()) { + if (token.getType() == Token.TokenType.MULTIPLY) { + result *= bracedExpression(); + } else if (token.getType() == Token.TokenType.DIVIDE) { + result /= bracedExpression(); + } else { + regressTokensIndex(); + break; + } + } + + return result; + } + + private Double bracedExpression() throws ParsingException { + Double result; + + Token token = progressTokens(); + if (token == null) { + throw new ParsingException("Invalid amount of numbers."); + } + + if (token.getType() == Token.TokenType.LEFT_BRACE) { + result = expression(); + token = progressTokens(); + + if (token == null || token.getType() != Token.TokenType.RIGHT_BRACE) { + throw new ParsingException("Wrong number of left/right braces"); + } + } else { + regressTokensIndex(); + result = numberExpression(); + } + + return result; + } + + private Double numberExpression() throws ParsingException { + Double result; + + Token token = progressTokens(); + if (token == null) { + throw new ParsingException("Invalid amount of numbers"); + } + + if (token.getType() == Token.TokenType.MINUS) { + result = -expression(); + } else if (token.getType() == Token.TokenType.NUMBER) { + result = token.getNumber(); + } else { + throw new ParsingException("Invalid order of operations"); + } + + return result; + } + + + /** + * Public calculate interface + */ + public double calculate(String expression) throws ParsingException { + tokens.clear(); + tokensIndex = 0; + + parse(expression); + Double result = expression(); + + if (tokensIndex != tokens.size()) { + throw new ParsingException("Invalid number of tokens (too many)."); + } + + return result; + } +} diff --git a/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/server/CalculatorApplication.java b/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/server/CalculatorApplication.java new file mode 100644 index 000000000..1a3bade0f --- /dev/null +++ b/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/server/CalculatorApplication.java @@ -0,0 +1,43 @@ +package ru.mipt.java2016.homework.g595.topilskiy.task4.server; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.Banner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +import ru.mipt.java2016.homework.g595.topilskiy.task4.calculator.rest.IFunctionalCalculator; +import ru.mipt.java2016.homework.g595.topilskiy.task4.calculator.rest.RESTCalculator; + +/** + * curl http://localhost:9001/eval \ + * -X POST \ + * -H "Content-Type: text/plain" \ + * -H "Authorization: Basic $(echo -n "supersanic:gottagofast" | base64)" \ + * --data-raw "44*3+2" + */ +@EnableAutoConfiguration +@Configuration +@ComponentScan(basePackageClasses = CalculatorApplication.class) +public class CalculatorApplication { + + @Bean + public IFunctionalCalculator calculator() { + return new RESTCalculator(); + } + + @Bean + public EmbeddedServletContainerCustomizer customizer( + @Value("${ru.mipt.java2016.homework.g595.topilskiy.task4.server.httpPort:9001}") int port) { + return container -> container.setPort(port); + } + + public static void main(String[] args) { + SpringApplication application = new SpringApplication(CalculatorApplication.class); + application.setBannerMode(Banner.Mode.OFF); + application.run(args); + } +} diff --git a/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/server/CalculatorController.java b/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/server/CalculatorController.java new file mode 100644 index 000000000..514b2ec65 --- /dev/null +++ b/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/server/CalculatorController.java @@ -0,0 +1,298 @@ +package ru.mipt.java2016.homework.g595.topilskiy.task4.server; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +import ru.mipt.java2016.homework.base.task1.ParsingException; +import ru.mipt.java2016.homework.g595.topilskiy.task4.calculator.rest.IFunctionalCalculator; +import ru.mipt.java2016.homework.g595.topilskiy.task4.calculator.rest.RESTCalculator; +import ru.mipt.java2016.homework.g595.topilskiy.task4.calculator.rest.function.CalculatorFunctionObject; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static ru.mipt.java2016.homework.g595.topilskiy.task4.server.CalculatorDao.ADMIN_USERNAME; + +@RestController +public class CalculatorController implements IFunctionalCalculator { + private static final Logger LOG = LoggerFactory.getLogger(CalculatorController.class); + + @Autowired + private CalculatorDao calculatorDao; + @Autowired + private static Map userCalculators = new HashMap<>(); + + static { + userCalculators.put(ADMIN_USERNAME, new RESTCalculator()); + } + + + /** + * STANDARD Functions + */ + @RequestMapping(path = "/ping", method = RequestMethod.GET, produces = "text/plain") + public String echo() { + return "OK\n"; + } + + @RequestMapping(path = "/", method = RequestMethod.GET, produces = "text/html") + public String main(@RequestParam(required = false) String name) { + if (name == null) { + name = "world"; + } + + return "" + + "CalculatorApp" + + "

Hello, " + name + "!

" + + ""; + } + + + /** + * REST Functions (OVERRIDE) + */ + + /** + * Methods of interacting with calculator VARIABLES + */ + /** + * @return the double value under the alias of variableAlias + */ + @Override + public Double getVariable(String variableAlias) { + return userCalculators.get(ADMIN_USERNAME).getVariable(variableAlias); + } + + @RequestMapping(path = "/variable/{variableAlias}", method = RequestMethod.GET, produces = "text/plain") + public String getVariableServer(Authentication authentication, + @PathVariable String variableAlias) { + String requesterUsername = authentication.getName(); + + LOG.debug("Attempting to get variable: " + variableAlias); + Double result = userCalculators.get(requesterUsername).getVariable(variableAlias); + if (result == null) { + LOG.debug("No such variable: " + variableAlias); + return "No such variable: " + variableAlias + '\n'; + } else { + LOG.debug("Variable: " + variableAlias + " = " + result); + return "Variable: " + variableAlias + " = " + result + '\n'; + } + } + + /** + * Make the alias of variableAlias reflect to the double value + */ + @Override + public boolean putVariable(String variableAlias, Double value) { + return userCalculators.get(ADMIN_USERNAME).putVariable(variableAlias, value); + } + + @RequestMapping(path = "/variable/{variableAlias}", method = RequestMethod.PUT) + public String putVariableServer(Authentication authentication, + @PathVariable String variableAlias, + @RequestBody String value) { + String requesterUsername = authentication.getName(); + LOG.debug("Attempting to put variable: " + variableAlias + " = " + value); + + Double variable = 0.0; + try { + variable = userCalculators.get(requesterUsername).calculate(value); + } catch (ParsingException e) { + LOG.debug("Failed to decode into double the value given: " + value + '\n' + e.getMessage()); + return "Failed to decode into double the value given: " + value + '\n' + e.getMessage(); + } + + if (userCalculators.get(requesterUsername).putVariable(variableAlias, variable)) { + LOG.debug("Variable put successfully: " + variableAlias + " = " + variable); + return "Variable put successfully: " + variableAlias + " = " + variable + '\n'; + } else { + LOG.debug("Failed to put successfully: " + variableAlias); + return "Failed to put successfully: " + variableAlias + '\n'; + } + } + + /** + * Delete the alias of variableAlias and its held value + */ + @Override + public boolean deleteVariable(String variableAlias) { + return userCalculators.get(ADMIN_USERNAME).deleteVariable(variableAlias); + } + + @RequestMapping(path = "/variable/{variableAlias}", method = RequestMethod.DELETE) + public String deleteVariableServer(Authentication authentication, @PathVariable String variableAlias) { + String requesterUsername = authentication.getName(); + + LOG.debug("Attempting to delete variable: " + variableAlias); + if (userCalculators.get(requesterUsername).deleteVariable(variableAlias)) { + LOG.debug("Variable deleted successfully: " + variableAlias); + return "Variable deleted successfully: " + variableAlias + '\n'; + } else { + LOG.debug("Failed to delete successfully: " + variableAlias); + return "Failed to delete successfully: " + variableAlias + '\n'; + } + } + + /** + * @return the list of aliases of variables in the calculator + */ + @Override + public List getVariableList() { + return userCalculators.get(ADMIN_USERNAME).getVariableList(); + } + + @RequestMapping(path = "/variable", method = RequestMethod.GET) + public String getVariableServer(Authentication authentication) { + String requesterUsername = authentication.getName(); + String result = String.join(", ", userCalculators.get(requesterUsername).getVariableList()); + LOG.trace("Output VariableList: " + result); + return "Currently defined variables:\n" + result + '\n'; + } + + + /** + * Methods of interacting with calculator FUNCTIONS + */ + /** + * @return a CalculatorFunction object under the alias of functionAlias + * NOTE: predefined functions cannot be dealiased + */ + @Override + public CalculatorFunctionObject getFunction(@PathVariable String functionAlias) { + return userCalculators.get(ADMIN_USERNAME).getFunction(functionAlias); + } + + @RequestMapping(path = "/function/{functionAlias}", method = RequestMethod.GET) + public String getFunctionServer(Authentication authentication, + @PathVariable String functionAlias) { + String requesterUsername = authentication.getName(); + LOG.debug("Attempting to get function: " + functionAlias); + CalculatorFunctionObject function = userCalculators.get(requesterUsername).getFunction(functionAlias); + if (function == null) { + LOG.debug("No such function: " + functionAlias); + return "No such function: " + functionAlias + '\n'; + } else { + LOG.debug("Function: " + functionAlias + '\n' + function.toString()); + return "Function: " + functionAlias + '\n' + function.toString() + '\n'; + } + } + + /** + * Make the alias of functionAlias reflect to CalculatorFunction(expression, arguments) + */ + @Override + public boolean putFunction(String functionAlias, String expression, List arguments) { + return userCalculators.get(ADMIN_USERNAME).putFunction(functionAlias, expression, arguments); + } + + @RequestMapping(path = "/function/{functionAlias}", method = RequestMethod.PUT) + public String putFunctionServer(Authentication authentication, + @PathVariable String functionAlias, + @RequestBody String expression, + @RequestParam(value = "args") List arguments) { + String requesterUsername = authentication.getName(); + + LOG.debug("Attempting to put function: " + functionAlias); + if (userCalculators.get(requesterUsername).putFunction(functionAlias, expression, arguments)) { + LOG.debug("Function put successfully: " + functionAlias); + return "Function put successfully: " + functionAlias + '\n' + + userCalculators.get(requesterUsername).getFunction(functionAlias).toString() + '\n'; + } else { + LOG.debug("Failed to put successfully: " + functionAlias); + return "Failed to put successfully: " + functionAlias + '\n'; + } + } + + /** + * Delete the alias of functionAlias and its held function + */ + @Override + public boolean deleteFunction(String functionAlias) { + return userCalculators.get(ADMIN_USERNAME).deleteFunction(functionAlias); + } + + @RequestMapping(path = "/function/{functionAlias}", method = RequestMethod.DELETE) + public String deleteFunctionServer(Authentication authentication, + @PathVariable String functionAlias) { + String requesterUsername = authentication.getName(); + LOG.debug("Attempting to delete function: " + functionAlias); + if (userCalculators.get(requesterUsername).deleteFunction(functionAlias)) { + LOG.debug("Function deleted successfully: " + functionAlias); + return "Function deleted successfully: " + functionAlias + '\n'; + } else { + LOG.debug("Failed to delete successfully: " + functionAlias); + return "Failed to delete successfully: " + functionAlias + '\n'; + } + } + + /** + * @return the list of aliases of functions in the calculator + */ + @Override + public List getFunctionList() { + return userCalculators.get(ADMIN_USERNAME).getFunctionList(); + } + + @RequestMapping(path = "/function", method = RequestMethod.GET) + @ResponseBody + public String getFunctionListServer(Authentication authentication) { + String requesterUsername = authentication.getName(); + + String result = String.join(", ", userCalculators.get(requesterUsername).getFunctionList()); + LOG.trace("Output FunctionList: " + result); + return "Currently defined functions:\n" + result + '\n'; + } + + + /** + * Methods of CALCULATION of the value of expression + * (using the kept function and variable sets) + */ + @Override + public Double calculate(String expression) throws ParsingException { + return userCalculators.get(ADMIN_USERNAME).calculate(expression); + } + + @RequestMapping(path = "/eval", method = RequestMethod.POST, consumes = "text/plain", produces = "text/plain") + public String eval(Authentication authentication, @RequestBody String expression) { + String requesterUsername = authentication.getName(); + + try { + LOG.debug("Evaluation request: [" + expression + "]"); + Double result = userCalculators.get(requesterUsername).calculate(expression); + LOG.trace("Result: " + result); + return "Result: " + result + '\n'; + } catch (ParsingException e) { + LOG.trace("Evaluation Failed: " + e.getMessage()); + return "Evaluation Failed: " + e.getMessage() + '\n'; + } + } + + + /** + * User interactions + */ + @RequestMapping(path = "/user/add/{username}", method = RequestMethod.PUT) + public String addUser(Authentication authentication, + @PathVariable String username, + @RequestParam String password) { + String requesterUsername = authentication.getName(); + + if (!requesterUsername.equals(requesterUsername)) { + LOG.trace("User " + requesterUsername + " is not an admin."); + return "You are not an admin. Cannot add users." + '\n'; + } else { + LOG.debug("Attempting to userAdd: " + "[" + username + "," + password + "]"); + if (calculatorDao.addUserDao(username, password, true)) { + userCalculators.put(username, new RESTCalculator()); + return "User " + username + " successfully created." + '\n'; + } else { + return "User " + username + " already exists." + '\n'; + } + } + } +} diff --git a/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/server/CalculatorDao.java b/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/server/CalculatorDao.java new file mode 100644 index 000000000..018299abc --- /dev/null +++ b/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/server/CalculatorDao.java @@ -0,0 +1,83 @@ +package ru.mipt.java2016.homework.g595.topilskiy.task4.server; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; +import ru.mipt.java2016.homework.g595.topilskiy.task4.calculator.rest.IFunctionalCalculator; +import ru.mipt.java2016.homework.g595.topilskiy.task4.calculator.rest.RESTCalculator; + +import javax.annotation.PostConstruct; +import javax.sql.DataSource; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; + +@Repository +public class CalculatorDao { + public static final String ADMIN_USERNAME = "supersanic"; + public static final String ADMIN_PASSWORD = "gottagofast"; + + private static final Logger LOG = LoggerFactory.getLogger(CalculatorDao.class); + + @Autowired + private DataSource dataSource; + + private JdbcTemplate jdbcTemplate; + + @PostConstruct + public void postConstruct() { + jdbcTemplate = new JdbcTemplate(dataSource, false); + initSchema(); + } + + public void initSchema() { + LOG.trace("Initializing schema"); + jdbcTemplate.execute("DROP SCHEMA calculator"); + jdbcTemplate.execute("CREATE SCHEMA IF NOT EXISTS calculator"); + jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS calculator.users " + + "(username VARCHAR PRIMARY KEY, password VARCHAR, enabled BOOLEAN)"); + addUserDao(ADMIN_USERNAME, ADMIN_PASSWORD, true); + } + + public boolean addUserDao(String username, String password, Boolean isEnabled) { + try { + loadUser(username); + LOG.debug("User " + username + " already exists."); + return false; + } catch (EmptyResultDataAccessException e) { + jdbcTemplate.update("INSERT INTO calculator.users VALUES (?, ?, ?)", + new Object[]{username, password, isEnabled}); + LOG.debug("User " + username + " successfully created."); + return true; + } + } + + public HashMap getUserCalculators() { + HashMap userCalculators = new HashMap<>(); + userCalculators.put(ADMIN_USERNAME, new RESTCalculator()); + return userCalculators; + } + + public CalculatorUser loadUser(String username) throws EmptyResultDataAccessException { + LOG.trace("Querying for user " + username); + return jdbcTemplate.queryForObject( + "SELECT username, password, enabled FROM calculator.users WHERE username = ?", + new Object[]{username}, + new RowMapper() { + @Override + public CalculatorUser mapRow(ResultSet rs, int rowNum) throws SQLException { + return new CalculatorUser( + rs.getString("username"), + rs.getString("password"), + rs.getBoolean("enabled") + ); + } + } + ); + } +} diff --git a/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/server/CalculatorDatabaseConfiguration.java b/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/server/CalculatorDatabaseConfiguration.java new file mode 100644 index 000000000..0843bcbf4 --- /dev/null +++ b/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/server/CalculatorDatabaseConfiguration.java @@ -0,0 +1,26 @@ +package ru.mipt.java2016.homework.g595.topilskiy.task4.server; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.sql.DataSource; + +@Configuration +public class CalculatorDatabaseConfiguration { + @Bean + public DataSource billingDataSource( + @Value("${ru.mipt.java2016.homework.g595.topilskiy.task4.server.jdbcUrl}") String jdbcUrl, + @Value("${ru.mipt.java2016.homework.g595.topilskiy.task4.server.username:}") String username, + @Value("${ru.mipt.java2016.homework.g595.topilskiy.task4.server.password:}") String password + ) { + HikariConfig config = new HikariConfig(); + config.setDriverClassName(org.h2.Driver.class.getName()); + config.setJdbcUrl(jdbcUrl); + config.setUsername(username); + config.setPassword(password); + return new HikariDataSource(config); + } +} diff --git a/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/server/CalculatorUser.java b/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/server/CalculatorUser.java new file mode 100644 index 000000000..6ee5f9194 --- /dev/null +++ b/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/server/CalculatorUser.java @@ -0,0 +1,65 @@ +package ru.mipt.java2016.homework.g595.topilskiy.task4.server; + +public class CalculatorUser { + private final String username; + private final String password; + private final boolean enabled; + + public CalculatorUser(String username, String password, boolean enabled) { + if (username == null) { + throw new IllegalArgumentException("Null username is not allowed"); + } + if (password == null) { + throw new IllegalArgumentException("Null password is not allowed"); + } + this.username = username; + this.password = password; + this.enabled = enabled; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public boolean isEnabled() { + return enabled; + } + + @Override + public String toString() { + return "CalculatorUser{" + + "username='" + username + '\'' + + ", password='" + password + '\'' + + ", enabled=" + enabled + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + CalculatorUser that = (CalculatorUser) o; + + return enabled == that.enabled && + username.equals(that.username) && + password.equals(that.password); + + } + + @Override + public int hashCode() { + int result = username.hashCode(); + result = 31 * result + password.hashCode(); + result = 31 * result + (enabled ? 1 : 0); + return result; + } +} diff --git a/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/server/SecurityServiceConfiguration.java b/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/server/SecurityServiceConfiguration.java new file mode 100644 index 000000000..254b965a0 --- /dev/null +++ b/homework-g595-topilskiy/src/main/java/ru/mipt/java2016/homework/g595/topilskiy/task4/server/SecurityServiceConfiguration.java @@ -0,0 +1,54 @@ +package ru.mipt.java2016.homework.g595.topilskiy.task4.server; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +import java.util.Collections; + +@Configuration +@EnableWebSecurity +public class SecurityServiceConfiguration extends WebSecurityConfigurerAdapter { + private static final Logger LOG = LoggerFactory.getLogger(SecurityServiceConfiguration.class); + + @Autowired + private CalculatorDao calculatorDao; + + @Override + protected void configure(HttpSecurity http) throws Exception { + LOG.info("Configuring security"); + http + .httpBasic().realmName("Calculator").and() + .formLogin().disable() + .logout().disable() + .csrf().disable() + .authorizeRequests() + .anyRequest().authenticated(); + } + + @Autowired + public void registerGlobalAuthentication(AuthenticationManagerBuilder auth) throws Exception { + LOG.info("Registering global user details service"); + auth.userDetailsService(username -> { + try { + CalculatorUser user = calculatorDao.loadUser(username); + return new User( + user.getUsername(), + user.getPassword(), + Collections.singletonList(() -> "AUTH") + ); + } catch (EmptyResultDataAccessException e) { + LOG.warn("No such user: " + username); + throw new UsernameNotFoundException(username); + } + }); + } +} diff --git a/homework-g595-topilskiy/src/main/resources/logback.xml b/homework-g595-topilskiy/src/main/resources/logback.xml new file mode 100644 index 000000000..9c97742d2 --- /dev/null +++ b/homework-g595-topilskiy/src/main/resources/logback.xml @@ -0,0 +1,13 @@ + + + + %date %-5p %20.20c{20} %msg%n + + + + + + + + + \ No newline at end of file diff --git a/homework-g595-topilskiy/src/test/java/ru/mipt/java2016/homework/g595/topilskiy/task3/OptimisedByteKeyValueStoragePerformanceTest.java b/homework-g595-topilskiy/src/test/java/ru/mipt/java2016/homework/g595/topilskiy/task3/OptimisedByteKeyValueStoragePerformanceTest.java index 035212023..99af68671 100644 --- a/homework-g595-topilskiy/src/test/java/ru/mipt/java2016/homework/g595/topilskiy/task3/OptimisedByteKeyValueStoragePerformanceTest.java +++ b/homework-g595-topilskiy/src/test/java/ru/mipt/java2016/homework/g595/topilskiy/task3/OptimisedByteKeyValueStoragePerformanceTest.java @@ -12,6 +12,7 @@ import ru.mipt.java2016.homework.g595.topilskiy.task2.Serializer.*; import org.junit.Test; +import org.apache.commons.io.FileUtils; import java.io.File; import java.io.IOException; @@ -40,6 +41,27 @@ public void testAdler32() throws IOException { } } + @Test + public void testLockingStorageFile() throws IOException { + final String TEST_DIR_PATH = "/tmp/java_test"; + + File test_directory = new File(TEST_DIR_PATH); + test_directory.mkdir(); + + Runnable task = () -> { + OptimisedByteKeyValueStorage storage = + new OptimisedByteKeyValueStorage<>( + TEST_DIR_PATH, + IntegerSerializerSingleton.getInstance(), + IntegerSerializerSingleton.getInstance()); + }; + + new Thread(task).start(); + new Thread(task).start(); + + FileUtils.deleteDirectory(test_directory); + } + @Override protected KeyValueStorage buildStringsStorage(String path) throws MalformedDataException { return new OptimisedByteKeyValueStorage<>(path, StringSerializerSingleton.getInstance(), diff --git a/homework-g595-topilskiy/src/test/java/ru/mipt/java2016/homework/g595/topilskiy/task4/CURL_COMMANDS.txt b/homework-g595-topilskiy/src/test/java/ru/mipt/java2016/homework/g595/topilskiy/task4/CURL_COMMANDS.txt new file mode 100644 index 000000000..f31b8acf9 --- /dev/null +++ b/homework-g595-topilskiy/src/test/java/ru/mipt/java2016/homework/g595/topilskiy/task4/CURL_COMMANDS.txt @@ -0,0 +1,26 @@ +curl http://localhost:9001/eval -X POST -H "Content-Type: text/plain" -H "Authorization: Basic $(echo -n "supersanic:gottagofast" | base64)" --data-raw "44*3+2" + +curl http://localhost:9001/user/add/krusher99?password=mlgpro -X PUT -H "Content-Type: text/plain" -H "Authorization: Basic $(echo -n "supersanic:gottagofast" | base64)" + +curl http://localhost:9001/eval -X POST -H "Content-Type: text/plain" -H "Authorization: Basic $(echo -n "krusher99:mlgpro" | base64)" --data-raw "44*3+2" + +curl http://localhost:9001/variable/mlgrank -X PUT -H "Content-Type: text/plain" -H "Authorization: Basic $(echo -n "krusher99:mlgpro" | base64)" --data-raw "1" + +curl http://localhost:9001/variable/mlgrank -X PUT -H "Content-Type: text/plain" -H "Authorization: Basic $(echo -n "supersanic:gottagofast" | base64)" --data-raw "99999" + +curl http://localhost:9001/variable/mlgrank -X GET -H "Authorization: Basic $(echo -n "krusher99:mlgpro" | base64)" + +curl http://localhost:9001/variable/mlgrank -X GET -H "Authorization: Basic $(echo -n "supersanic:gottagofast" | base64)" + +curl http://localhost:9001/variable/ml -X PUT -H "Content-Type: text/plain" -H "Authorization: Basic $(echo -n "supersanic:gottagofast" | base64)" --data-raw "max(1.010, 1.009)" + +curl http://localhost:9001/function/myfunc?args=x&args=y -X PUT -H "Content-Type: text/plain" -H "Authorization: Basic $(echo -n "krusher99:mlgpro" | base64)" --data-raw "pow(2, x)" + +curl http://localhost:9001/function/myfunc1?args=x,y -X PUT -H "Content-Type: text/plain" -H "Authorization: Basic $(echo -n "krusher99:mlgpro" | base64)" --data-raw "log2(x) + y" + +curl http://localhost:9001/eval -X POST -H "Content-Type: text/plain" -H "Authorization: Basic $(echo -n "krusher99:mlgpro" | base64)" --data-raw "15 + myfunc1(64, 4)" + +curl http://localhost:9001/eval -X POST -H "Content-Type: text/plain" -H "Authorization: Basic $(echo -n "supersanic:gottagofast" | base64)" --data-raw "15 + myfunc1(64, 4)" + +curl http://localhost:9001/function/myfunc1 -X GET -H "Content-Type: text/plain" -H "Authorization: Basic $(echo -n "supersanic:gottagofast" | base64)" +