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)"
+