diff --git a/homework-g596-ivanova/pom.xml b/homework-g596-ivanova/pom.xml index 7be63c0fc..00d589ff0 100644 --- a/homework-g596-ivanova/pom.xml +++ b/homework-g596-ivanova/pom.xml @@ -4,12 +4,17 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> mipt-java-2016 - ru.mipt.java2016 + + ru.mipt.java2016 1.0.0 4.0.0 homework-g596-ivanova + + + 1.4.2.RELEASE + ru.mipt.java2016 @@ -35,8 +40,66 @@ 3.21.0-GA jar + + com.zaxxer + HikariCP + RELEASE + + + com.fasterxml.jackson.core + jackson-annotations + RELEASE + + + 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 + + + + spring-releases + https://repo.spring.io/libs-release + + + + + + spring-releases + https://repo.spring.io/libs-release + + + diff --git a/homework-g596-ivanova/src/main/java/ru/mipt/java2016/homework/g596/ivanova/task4/BillingDao.java b/homework-g596-ivanova/src/main/java/ru/mipt/java2016/homework/g596/ivanova/task4/BillingDao.java new file mode 100644 index 000000000..865daea34 --- /dev/null +++ b/homework-g596-ivanova/src/main/java/ru/mipt/java2016/homework/g596/ivanova/task4/BillingDao.java @@ -0,0 +1,193 @@ +package ru.mipt.java2016.homework.g596.ivanova.task4; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +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 javax.annotation.PostConstruct; +import javax.sql.DataSource; +import java.sql.ResultSet; +import java.sql.SQLException; +import ru.mipt.java2016.homework.base.task1.ParsingException; + +@Repository +public class BillingDao { + private static final Logger LOG = LoggerFactory.getLogger(BillingDao.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("CREATE SCHEMA IF NOT EXISTS billing"); + jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS billing.users " + + "(username VARCHAR PRIMARY KEY, password VARCHAR, enabled BOOLEAN)"); + jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS billing.variables " + + "(username VARCHAR, name VARCHAR, value DOUBLE)"); + jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS billing.functions " + + "(username VARCHAR, name VARCHAR, arguments VARCHAR, expression VARCHAR)"); + addUserIfNotExists("username", "password", true); + } + + boolean addUserIfNotExists(String username, String password, boolean enabled) { + try { + loadUser(username); + return false; + } catch (EmptyResultDataAccessException e) { + jdbcTemplate.update("INSERT INTO billing.users VALUES (?, ?, ?)", + new Object[]{username, password, enabled}); + return true; + } + } + + public Double getVariable(String username, String variable) { + return jdbcTemplate.queryForObject( + "SELECT username, name, value FROM billing.variables WHERE username = ? AND name = ?", + new Object[]{username, variable}, + new RowMapper() { + @Override + public Double mapRow(ResultSet rs, int rowNum) throws SQLException { + return rs.getDouble("value"); + } + } + ); + } + + Map getVariables(String username) { + try { + return jdbcTemplate.queryForObject( + "SELECT username, name, value FROM billing.variables WHERE username = ?", + new Object[] {username}, (rs, rowNum) -> { + HashMap result = new HashMap<>(); + while (true) { + result.put(rs.getString("name"), rs.getDouble("value")); + if (!rs.next()) { + break; + } + } + return result; + }); + } catch (EmptyResultDataAccessException e) { + HashMap result = new HashMap<>(); + return result; + } + } + + boolean deleteVariable(String username, String name) throws ParsingException { + try { + getVariable(username, name); + jdbcTemplate.update("DELETE FROM billing.variables WHERE username = ? AND name = ?", + new Object[]{username, name}); + return true; + } catch (EmptyResultDataAccessException e) { + return false; + } + } + + void addVariable(String username, String name, Double value) throws ParsingException { + try { + getVariable(username, name); + jdbcTemplate.update("DELETE FROM billing.variables WHERE username = ? AND name = ?", + new Object[]{username, name}); + jdbcTemplate.update("INSERT INTO billing.variables VALUES (?, ?, ?)", + new Object[]{username, name, value}); + } catch (EmptyResultDataAccessException e) { + jdbcTemplate.update("INSERT INTO billing.variables VALUES (?, ?, ?)", + new Object[]{username, name, value}); + } + } + + public Function getFunction(String username, String function) { + return jdbcTemplate.queryForObject( + "SELECT username, name, arguments, expression FROM billing.functions WHERE username = ? AND name = ?", + new Object[]{username, function}, (rs, rowNum) -> { + String name = rs.getString("name"); + List arguments = Arrays.asList(rs.getString("arguments").split(" ")); + String expression = rs.getString("expression"); + return new Function(name, arguments, expression); + }); + } + + void addFunction(String username, String name, List arguments, String expression) throws ParsingException { + try { + getFunction(username, name); + jdbcTemplate.update("DELETE FROM billing.functions WHERE username = ? AND name = ?", + new Object[]{username, name}); + String stringArguments = String.join(" ", arguments); + jdbcTemplate.update("INSERT INTO billing.functions VALUES (?, ?, ?, ?)", + new Object[]{username, name, stringArguments, expression}); + } catch (EmptyResultDataAccessException e) { + String stringArguments = String.join(" ", arguments); + jdbcTemplate.update("INSERT INTO billing.functions VALUES (?, ?, ?, ?)", + new Object[]{username, name, stringArguments, expression}); + } + } + + Map getFunctions(String username) { + try { + return jdbcTemplate.queryForObject( + "SELECT username, name, arguments, expression FROM billing.functions WHERE username = ?", + new Object[]{username}, (rs, rowNum) -> { + HashMap result = new HashMap<>(); + while (true) { + String name = rs.getString("name"); + List arguments = Arrays.asList(rs.getString("arguments").split(" ")); + String expression = rs.getString("expression"); + result.put(name, new Function(name, arguments, expression)); + if (!rs.next()) { + break; + } + } + return result; + }); + } catch (EmptyResultDataAccessException e) { + HashMap result = new HashMap<>(); + return result; + } + } + + boolean deleteFunction(String username, String name) throws ParsingException { + try { + getFunction(username, name); + jdbcTemplate.update("DELETE FROM billing.variables WHERE username = ? AND name = ?", + new Object[]{username, name}); + return true; + } catch (EmptyResultDataAccessException e) { + return false; + } + } + + public BillingUser loadUser(String username) throws EmptyResultDataAccessException { + LOG.trace("Querying for user " + username); + return jdbcTemplate.queryForObject( + "SELECT username, password, enabled FROM billing.users WHERE username = ?", + new Object[]{username}, + new RowMapper() { + @Override + public BillingUser mapRow(ResultSet rs, int rowNum) throws SQLException { + return new BillingUser( + rs.getString("username"), + rs.getString("password"), + rs.getBoolean("enabled") + ); + } + } + ); + } +} \ No newline at end of file diff --git a/homework-g596-ivanova/src/main/java/ru/mipt/java2016/homework/g596/ivanova/task4/BillingDatabaseConfiguration.java b/homework-g596-ivanova/src/main/java/ru/mipt/java2016/homework/g596/ivanova/task4/BillingDatabaseConfiguration.java new file mode 100644 index 000000000..f1e6a0ef3 --- /dev/null +++ b/homework-g596-ivanova/src/main/java/ru/mipt/java2016/homework/g596/ivanova/task4/BillingDatabaseConfiguration.java @@ -0,0 +1,26 @@ +package ru.mipt.java2016.homework.g596.ivanova.task4; + +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 BillingDatabaseConfiguration { + @Bean + public DataSource billingDataSource( + @Value("${ru.mipt.java2016.homework.g596.ivanova.task4.jdbcUrl}") String jdbcUrl, + @Value("${ru.mipt.java2016.homework.g596.ivanova.task4.username:}") String username, + @Value("${ru.mipt.java2016.homework.g596.ivanova.task4.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-g596-ivanova/src/main/java/ru/mipt/java2016/homework/g596/ivanova/task4/BillingUser.java b/homework-g596-ivanova/src/main/java/ru/mipt/java2016/homework/g596/ivanova/task4/BillingUser.java new file mode 100644 index 000000000..6770fd0c5 --- /dev/null +++ b/homework-g596-ivanova/src/main/java/ru/mipt/java2016/homework/g596/ivanova/task4/BillingUser.java @@ -0,0 +1,69 @@ +package ru.mipt.java2016.homework.g596.ivanova.task4; + +public class BillingUser { + private final String username; + private final String password; + private final boolean enabled; + + public BillingUser(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 "BillingUser{" + + "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; + } + + BillingUser that = (BillingUser) o; + + if (enabled != that.enabled) { + return false; + } + if (!username.equals(that.username)) { + return false; + } + return 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-g596-ivanova/src/main/java/ru/mipt/java2016/homework/g596/ivanova/task4/Function.java b/homework-g596-ivanova/src/main/java/ru/mipt/java2016/homework/g596/ivanova/task4/Function.java new file mode 100644 index 000000000..23b1ede5d --- /dev/null +++ b/homework-g596-ivanova/src/main/java/ru/mipt/java2016/homework/g596/ivanova/task4/Function.java @@ -0,0 +1,23 @@ +package ru.mipt.java2016.homework.g596.ivanova.task4; + +import java.util.*; + +public class Function { + private List arguments = new ArrayList<>(); + private String name; + private String expression; + + public Function(String name, List arguments, String expression) { + this.name = name; + this.arguments = arguments; + this.expression = expression; + } + + public List getArguments() { + return arguments; + } + + public String getExpression() { + return expression; + } +} \ No newline at end of file diff --git a/homework-g596-ivanova/src/main/java/ru/mipt/java2016/homework/g596/ivanova/task4/RestCalculatorApplication.java b/homework-g596-ivanova/src/main/java/ru/mipt/java2016/homework/g596/ivanova/task4/RestCalculatorApplication.java new file mode 100644 index 000000000..38df3a88c --- /dev/null +++ b/homework-g596-ivanova/src/main/java/ru/mipt/java2016/homework/g596/ivanova/task4/RestCalculatorApplication.java @@ -0,0 +1,49 @@ +package ru.mipt.java2016.homework.g596.ivanova.task4; + +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.g596.ivanova.task1.BestCalculatorEver; + +@EnableAutoConfiguration +@Configuration +@ComponentScan(basePackageClasses = RestCalculatorApplication.class) +public class RestCalculatorApplication { + @Bean + public BestCalculatorEver calculator() { + return new BestCalculatorEver(); + } + + @Bean + public EmbeddedServletContainerCustomizer customizer( + @Value("${ru.mipt.java2016.homework.g596.ivanova.task4.httpPort:8080}") int port) { + return container -> container.setPort(port); + } + + public static void main(String[] args) { + SpringApplication application = new SpringApplication(RestCalculatorApplication.class); + application.setBannerMode(Banner.Mode.OFF); + application.run(args); + } +} + +/** + * Query template. + curl http://localhost:8080/calculate \ + -X POST \ + -H "Content-Type: text/plain" \ + -H "Authorization: Basic $(echo -n "username:password" | base64)" \ + --data-raw "" + + + curl http://localhost:8080/variable/x \ + -X GET \ + -H "Content-Type: text/plain" \ + -H "Authorization: Basic $(echo -n "username:password" | base64)" \ + --data-raw "" + */ \ No newline at end of file diff --git a/homework-g596-ivanova/src/main/java/ru/mipt/java2016/homework/g596/ivanova/task4/RestCalculatorController.java b/homework-g596-ivanova/src/main/java/ru/mipt/java2016/homework/g596/ivanova/task4/RestCalculatorController.java new file mode 100644 index 000000000..252a95a1a --- /dev/null +++ b/homework-g596-ivanova/src/main/java/ru/mipt/java2016/homework/g596/ivanova/task4/RestCalculatorController.java @@ -0,0 +1,106 @@ +package ru.mipt.java2016.homework.g596.ivanova.task4; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import ru.mipt.java2016.homework.base.task1.ParsingException; +import ru.mipt.java2016.homework.g596.ivanova.task1.BestCalculatorEver; + +@RestController +public class RestCalculatorController { + @Autowired + private BestCalculatorEver calculator; + + @Autowired + private BillingDao billingDao; + + @RequestMapping(path = "/variable/{name}", method = RequestMethod.GET, produces = "text/plain") + public String getVariable(Authentication authentication, @PathVariable(value = "name") String variable) + throws ParsingException { + String userName = authentication.getName(); + Double result = billingDao.getVariable(userName, variable); + return variable + " = " + result + "\n"; + } + + @RequestMapping(path = "/variable/{name}", method = RequestMethod.DELETE, produces = "text/plain") + public String deleteVariable(Authentication authentication, @PathVariable(value = "name") String variable) + throws ParsingException { + String userName = authentication.getName(); + if (billingDao.deleteVariable(userName, variable)) { + return variable + " deleted\n"; + } else { + return variable + " not exists\n"; + } + } + + @RequestMapping(path = "/variable/{name}", method = RequestMethod.PUT, + consumes = "text/plain", produces = "text/plain") + public String addVariable(Authentication authentication, + @PathVariable(value = "name") String variable, + @RequestBody String value) + throws ParsingException { + String username = authentication.getName(); + billingDao.addVariable(username, variable, Double.parseDouble(value)); + return "Variable " + variable + " added\n"; + } + + @RequestMapping(path = "/variable", method = RequestMethod.GET, produces = "text/plain") + public String getVariables(Authentication authentication) throws ParsingException { + String userName = authentication.getName(); + Map result = billingDao.getVariables(userName); + return String.join(", ", result.keySet()) + "\n" + ""; + } + + @RequestMapping(path = "/function/{name}", method = RequestMethod.GET, produces = "text/plain") + public String getFunction(Authentication authentication, @PathVariable(value = "name") String function) + throws ParsingException { + String userName = authentication.getName(); + Function result = billingDao.getFunction(userName, function); + return function + "(" + String.join(", ", result.getArguments()) + ")" + " = " + result.getExpression() + "\n"; + } + + @RequestMapping(path = "/function/{name}", method = RequestMethod.PUT, + consumes = "text/plain", produces = "text/plain") + public String addFunction(Authentication authentication, @PathVariable(value = "name") String function, + @RequestParam(value = "args") String args, + @RequestBody String expression) + throws ParsingException { + String userName = authentication.getName(); + List arguments = Arrays.asList(args.split(",")); + billingDao.addFunction(userName, function, arguments, expression); + return "Function " + function + " added\n"; + } + + @RequestMapping(path = "/function", method = RequestMethod.GET, produces = "text/plain") + public String getFunctions(Authentication authentication) throws ParsingException { + String userName = authentication.getName(); + Map result = billingDao.getFunctions(userName); + return String.join(", ", result.keySet()) + "\n"; + } + + @RequestMapping(path = "/function/{name}", method = RequestMethod.DELETE, produces = "text/plain") + public String deleteFunction(Authentication authentication, @PathVariable(value = "name") String function) + throws ParsingException { + String username = authentication.getName(); + if (billingDao.deleteFunction(username, function)) { + return function + " deleted\n"; + } else { + return function + " not exists\n"; + } + } + + @RequestMapping(path = "/calculate", method = RequestMethod.POST, + consumes = "text/plain", produces = "text/plain") + public String calculate(@RequestBody String expression) + throws ParsingException { + return calculator.calculate(expression) + "\n"; + } +} diff --git a/homework-g596-ivanova/src/main/java/ru/mipt/java2016/homework/g596/ivanova/task4/SecurityServiceConfiguration.java b/homework-g596-ivanova/src/main/java/ru/mipt/java2016/homework/g596/ivanova/task4/SecurityServiceConfiguration.java new file mode 100644 index 000000000..41edb3dab --- /dev/null +++ b/homework-g596-ivanova/src/main/java/ru/mipt/java2016/homework/g596/ivanova/task4/SecurityServiceConfiguration.java @@ -0,0 +1,55 @@ +package ru.mipt.java2016.homework.g596.ivanova.task4; + +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 BillingDao billingDao; + + @Override + protected void configure(HttpSecurity http) throws Exception { + LOG.info("Configuring security"); + http + .httpBasic().realmName("Calculator").and() + .formLogin().disable() + .logout().disable() + .csrf().disable() + .authorizeRequests() + .antMatchers("/calculate/**").authenticated() + .anyRequest().permitAll(); + } + + @Autowired + public void registerGlobalAuthentication(AuthenticationManagerBuilder auth) throws Exception { + LOG.info("Registering global user details service"); + auth.userDetailsService(username -> { + try { + BillingUser user = billingDao.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); + } + }); + } +} \ No newline at end of file