diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..aff0668b3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,4 @@ +FROM openjdk:8 +ADD target/user-search.jar user-search.jar +EXPOSE 8086 +ENTRYPOINT ["java", "-jar", "user-search.jar"] \ No newline at end of file diff --git a/README.md b/README.md index 6fcb5a302..2ad302c99 100644 --- a/README.md +++ b/README.md @@ -33,3 +33,45 @@ Faça um ***Fork*** deste repositório e abra um ***Pull Request***, **com seu n - Ter um desempenho elevado num conjunto de dados muito grande - Utilizar o Docker +----- + +# Solução + +O backend foi desenvolvido com a utilização do framework Spring Data REST, com banco H2 embedded. +Uma UI simples foi construída em ReactJS para listar o resultado da busca com paginação. + +### UI + +A aplicação ReactJS utiliza webpack para compilar todos os arquivos num bundle. Usa rest.js para comunicação com a API REST. E usa babel para compilar ES6 para ES5. + +### Segurança + +A API está protegida por BASIC AUTHENTICATION, via Spring Security (usuário e senha: bruno). + +### Docker + +Um arquivo Dockerfile foi adicionado ao projeto permitindo a criação de uma imagem baseada no JDK8. +Para gerar a imagem, basta executar do diretório do projeto: + +`docker build . -t user-search` + +Depois de baixar a imagem, basta executá-la: + +`docker run image user-search` + +### Executando na máquina local + +Não é necessário ter npm e nodeJS na máquina local para executar a UI. +Basta executar: + +`mvn spring-boot:run` + +Esta operação demandará um bom tempo porque, além de instalar npm e nodeJS embedded, vai fazer o download do arquivo users.csv.gz, descompactá-lo e carregar o banco de dados H2. +Após a execução correta do comando acima, basta acessar a URL (`http://localhost:8086/`) no browser. +Caso queira acessar diretamente a API, basta acessar `http://localhost:8086/api/users` ou `http://localhost:8086/api/users/search/listUsers?nome=nome` + +## Melhorias + +- Otimizar o banco de dados H2 para melhor performance de LOAD e SEARCH +- Utilizar um banco de dados noSQL +- Criar mais unit tests e integration tests diff --git a/package.json b/package.json new file mode 100644 index 000000000..23b3b6776 --- /dev/null +++ b/package.json @@ -0,0 +1,44 @@ +{ + "name": "spring-data-rest-and-reactjs", + "version": "0.1.0", + "description": "Demo of ReactJS + Spring Data REST", + "repository": { + "type": "git", + "url": "git@github.com:spring-guides/tut-react-and-spring-data-rest.git" + }, + "keywords": [ + "rest", + "hateoas", + "spring", + "data", + "react" + ], + "author": "Greg L. Turnquist", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/spring-guides/tut-react-and-spring-data-rest/issues" + }, + "homepage": "https://github.com/spring-guides/tut-react-and-spring-data-rest", + "dependencies": { + "bootstrap": "^4.1.3", + "bootstrap-less": "^3.3.8", + "less": "^3.8.1", + "react": "^15.3.2", + "react-data-grid": "^4.0.8", + "react-dom": "^15.3.2", + "react-js-pagination": "^3.0.2", + "rest": "^1.3.1", + "webpack": "^1.12.2" + }, + "scripts": { + "watch": "webpack --watch -d" + }, + "devDependencies": { + "babel-core": "^6.18.2", + "babel-loader": "^6.2.7", + "babel-polyfill": "^6.16.0", + "babel-preset-es2015": "^6.18.0", + "babel-preset-react": "^6.16.0", + "less-loader": "^4.1.0" + } +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 000000000..4f10ec527 --- /dev/null +++ b/pom.xml @@ -0,0 +1,126 @@ + + + 4.0.0 + + com.picpay + trabalhe-conosco-backend-dev + 1.0-SNAPSHOT + jar + + PicPay User Manager Service + User Manager Service using Spring Boot + + + org.springframework.boot + spring-boot-starter-parent + 1.5.12.RELEASE + + + + + UTF-8 + 1.8 + + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-data-rest + + + org.springframework.boot + spring-boot-devtools + + + org.projectlombok + lombok + + + com.h2database + h2 + + + + mysql + mysql-connector-java + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-csv + + + org.springframework.boot + spring-boot-starter-batch + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + user-search + + + + + com.github.eirslett + frontend-maven-plugin + 1.2 + + target + + + + install node and npm + + install-node-and-npm + + + v4.4.5 + 3.9.2 + + + + npm install + + npm + + + install + + + + webpack build + + webpack + + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/picpay/ApplicationStartup.java b/src/main/java/com/picpay/ApplicationStartup.java new file mode 100644 index 000000000..53b9d901e --- /dev/null +++ b/src/main/java/com/picpay/ApplicationStartup.java @@ -0,0 +1,40 @@ +package com.picpay; + +import com.picpay.repositories.UserRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +import java.util.List; + + +/** + * @author Bruno Carreira + */ +// tag::code[] +@Component +public class ApplicationStartup implements CommandLineRunner { + @Autowired + private List listRelevancia1; + + @Autowired + private List listRelevancia2; + + @Autowired + private UserRepository repo; + + private static final Logger LOG = + LoggerFactory.getLogger(ApplicationStartup.class); + + @Override + public void run(String... strings) throws Exception { + LOG.info("Updating priority...."); + repo.updatePriorityByIds(listRelevancia1, 1); + repo.updatePriorityByIds(listRelevancia2, 2); + LOG.info("Priority updated!"); + } + +} +// end::code[] \ No newline at end of file diff --git a/src/main/java/com/picpay/DatasourceConfig.java b/src/main/java/com/picpay/DatasourceConfig.java new file mode 100644 index 000000000..46de77cf5 --- /dev/null +++ b/src/main/java/com/picpay/DatasourceConfig.java @@ -0,0 +1,58 @@ +package com.picpay; + + +import org.apache.tomcat.util.http.fileupload.IOUtils; +import org.h2.jdbcx.JdbcDataSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.sql.DataSource; +import java.io.File; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.zip.GZIPInputStream; + + +@Configuration +public class DatasourceConfig { + + @Value("${com.picpay.csv.database.url}") + private final String csvDatabaseUrl = null; + + private static final Logger LOG = + LoggerFactory.getLogger(DatasourceConfig.class); + + private static final String CSV_PATH = "src/main/resources/users.csv"; + + @Bean(name = "mainDataSource") + public DataSource createMainDataSource() throws Exception { + LOG.info("Importing and extracting CSV file..."); + + if (!java.nio.file.Files.exists(Paths.get(CSV_PATH))) { + + InputStream zipFileInputStream = new URL(csvDatabaseUrl).openStream(); + GZIPInputStream is = new GZIPInputStream(zipFileInputStream); + + File targetFile = new File(CSV_PATH); + + java.nio.file.Files.copy( + is, + targetFile.toPath(), + StandardCopyOption.REPLACE_EXISTING); + + IOUtils.closeQuietly(is); + LOG.info("CSV imported and extracted..."); + } + + JdbcDataSource ds = new JdbcDataSource(); + ds.setURL("jdbc:h2:file:~/testdb;LOG=0;CACHE_SIZE=65536;LOCK_MODE=0;UNDO_LOG=0"); + ds.setUser("sa"); + ds.setPassword(""); + return ds; + } +} \ No newline at end of file diff --git a/src/main/java/com/picpay/UserManagerApplication.java b/src/main/java/com/picpay/UserManagerApplication.java new file mode 100644 index 000000000..9166781ac --- /dev/null +++ b/src/main/java/com/picpay/UserManagerApplication.java @@ -0,0 +1,42 @@ +package com.picpay; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Bruno Carreira + */ +// tag::code[] +@SpringBootApplication +public class UserManagerApplication { + + @Value("${com.picpay.relevancia1}") + private String relevancia1_txt = null; + + @Value("${com.picpay.relevancia2}") + private String relevancia2_txt = null; + + public static void main(String[] args){ + SpringApplication.run(UserManagerApplication.class, args); + } + + @Bean + public List listRelevancia1() throws IOException { + return Files.lines(Paths.get(relevancia1_txt)).collect(Collectors.toList()); + } + + @Bean + public List listRelevancia2() throws IOException { + return Files.lines(Paths.get(relevancia2_txt)).collect(Collectors.toList()); + } + +} +// end::code[] \ No newline at end of file diff --git a/src/main/java/com/picpay/WebSecurityConfig.java b/src/main/java/com/picpay/WebSecurityConfig.java new file mode 100644 index 000000000..014d6a84e --- /dev/null +++ b/src/main/java/com/picpay/WebSecurityConfig.java @@ -0,0 +1,28 @@ +package com.picpay; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +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; + +@Configuration +@EnableWebSecurity +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + @Autowired + public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { + auth.inMemoryAuthentication() + .withUser("bruno").password("bruno") + .authorities("ROLE_USER"); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.authorizeRequests() + .antMatchers("/api/**").authenticated() + .and() + .httpBasic(); + } +} diff --git a/src/main/java/com/picpay/controller/HomeController.java b/src/main/java/com/picpay/controller/HomeController.java new file mode 100644 index 000000000..622e1a4ec --- /dev/null +++ b/src/main/java/com/picpay/controller/HomeController.java @@ -0,0 +1,19 @@ +package com.picpay.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * @author Bruno Carreira + */ +// tag::code[] +@Controller +public class HomeController { + + @RequestMapping(value = "/") + public String index() { + return "index"; + } + +} +// end::code[] \ No newline at end of file diff --git a/src/main/java/com/picpay/model/User.java b/src/main/java/com/picpay/model/User.java new file mode 100644 index 000000000..a30bcedc3 --- /dev/null +++ b/src/main/java/com/picpay/model/User.java @@ -0,0 +1,33 @@ +package com.picpay.model; + +import lombok.Data; +import org.springframework.data.rest.core.annotation.RestResource; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +/** + * @author Bruno Carreira + */ +// tag::code[] +@Data +@Entity +@Table(name = "tb_user") +public class User { + @Id + private String Id; + private String name; + private String username; + @RestResource(exported = false) + private Integer priority; + + private User() {} + + public User(String Id, String name, String username) { + this.Id = Id; + this.name = name; + this.username = username; + } +} +// end::code[] \ No newline at end of file diff --git a/src/main/java/com/picpay/repositories/UserRepository.java b/src/main/java/com/picpay/repositories/UserRepository.java new file mode 100644 index 000000000..eb5081952 --- /dev/null +++ b/src/main/java/com/picpay/repositories/UserRepository.java @@ -0,0 +1,27 @@ +package com.picpay.repositories; + +import com.picpay.model.User; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.data.rest.core.annotation.RestResource; + +import javax.transaction.Transactional; +import java.util.List; + +/** + * @author Bruno Carreira + */ + +@Transactional +public interface UserRepository extends PagingAndSortingRepository { + @RestResource(path = "usersbyname", rel = "usersbyname") + Page findByNameContainingOrUsernameContainingOrderByPriority(@Param("name") String nameText, @Param("username") String usernameText, Pageable pageable); + + @Modifying + @Query(value = "update User u set u.priority=:priority where u.id in (:ids)") + int updatePriorityByIds(@Param("ids") List id, @Param("priority") int priority); +} \ No newline at end of file diff --git a/src/main/js/api/uriListConverter.js b/src/main/js/api/uriListConverter.js new file mode 100644 index 000000000..1c2124eee --- /dev/null +++ b/src/main/js/api/uriListConverter.js @@ -0,0 +1,21 @@ +define(function() { + 'use strict'; + + /* Convert a single or array of resources into "URI1\nURI2\nURI3..." */ + return { + read: function(str /*, opts */) { + return str.split('\n'); + }, + write: function(obj /*, opts */) { + // If this is an Array, extract the self URI and then join using a newline + if (obj instanceof Array) { + return obj.map(function(resource) { + return resource._links.self.href; + }).join('\n'); + } else { // otherwise, just return the self URI + return obj._links.self.href; + } + } + }; + +}); diff --git a/src/main/js/api/uriTemplateInterceptor.js b/src/main/js/api/uriTemplateInterceptor.js new file mode 100644 index 000000000..269165f9e --- /dev/null +++ b/src/main/js/api/uriTemplateInterceptor.js @@ -0,0 +1,18 @@ +define(function(require) { + 'use strict'; + + var interceptor = require('rest/interceptor'); + + return interceptor({ + request: function (request /*, config, meta */) { + /* If the URI is a URI Template per RFC 6570 (http://tools.ietf.org/html/rfc6570), trim out the template part */ + if (request.path.indexOf('{') === -1) { + return request; + } else { + request.path = request.path.split('{')[0]; + return request; + } + } + }); + +}); \ No newline at end of file diff --git a/src/main/js/app.js b/src/main/js/app.js new file mode 100644 index 000000000..bc8c809ba --- /dev/null +++ b/src/main/js/app.js @@ -0,0 +1,232 @@ +'use strict'; + +const React = require('react'); +const ReactDOM = require('react-dom'); +const client = require('./client'); + +const follow = require('./follow'); // function to hop multiple links by "rel" + +const profileURL = 'http://localhost:8086/api/profile/users'; +const root = '/api/users/search'; +const loadURL = 'usersbyname'; + +class App extends React.Component { + + constructor(props) { + super(props); + this.state = {users: [], attributes: [], pageSize: 15, nameSearch: 'bruno', links: {}}; + this.updatePageSize = this.updatePageSize.bind(this); + this.updateNameSearch = this.updateNameSearch.bind(this); + this.onNavigate = this.onNavigate.bind(this); + } + + // tag::follow-2[] + loadFromServer(pageSize, nameSearch) { + console.log("loadFromServer pageSize "+pageSize+" nameSearch "+nameSearch); + follow(client, root, [ + {rel: loadURL, params: {size: pageSize, name: nameSearch, username: nameSearch, sort: "priority"}}] + ).then(userCollection => { + return client({ + method: 'GET', + path: profileURL, + headers: {'Accept': 'application/schema+json'} + }).then(schema => { + this.schema = schema.entity; + return userCollection; + }); + }).done(userCollection => { + this.setState({ + users: userCollection.entity._embedded.users, + attributes: Object.keys(this.schema.properties), + pageSize: pageSize, + nameSearch: nameSearch, + links: userCollection.entity._links}); + }); + } + // end::follow-2[] + + // tag::navigate[] + onNavigate(navUri) { + client({method: 'GET', path: navUri}).done(userCollection => { + this.setState({ + users: userCollection.entity._embedded.users, + attributes: this.state.attributes, + pageSize: this.state.pageSize, + nameSearch: this.state.nameSearch, + links: userCollection.entity._links + }); + }); + } + // end::navigate[] + + // tag::update-page-size[] + updatePageSize(pageSize) { + if (pageSize !== this.state.pageSize) { + this.loadFromServer(pageSize, this.state.nameSearch); + } + } + // end::update-page-size[] + + // tag::update-search-name[] + updateNameSearch(nameSearch) { + console.log("updateNameSearch"+nameSearch); + if (nameSearch !== this.state.nameSearch) { + this.loadFromServer(this.state.pageSize, nameSearch); + } + } + // end::update-search-name[] + + // tag::follow-1[] + componentDidMount() { + console.log("componentDidMount pageSize "+this.state.pageSize+" nameSearch "+this.state.nameSearch); + this.loadFromServer(this.state.pageSize, this.state.nameSearch); + } + // end::follow-1[] + + render() { + return ( + + + + ) + } +} + +class UserList extends React.Component { + + constructor(props) { + super(props); + this.handleNavFirst = this.handleNavFirst.bind(this); + this.handleNavPrev = this.handleNavPrev.bind(this); + this.handleNavNext = this.handleNavNext.bind(this); + this.handleNavLast = this.handleNavLast.bind(this); + this.handlePageSize = this.handlePageSize.bind(this); + this.handleNameSearch = this.handleNameSearch.bind(this); + } + + // tag::handle-page-size-updates[] + handlePageSize(e) { + e.preventDefault(); + const pageSize = ReactDOM.findDOMNode(this.refs.pageSize).value; + if (/^[0-9]+$/.test(pageSize)) { + this.props.updatePageSize(pageSize); + } else { + ReactDOM.findDOMNode(this.refs.pageSize).value = + pageSize.substring(0, pageSize.length - 1); + } + } + // end::handle-page-size-updates[] + + // tag::handle-name-search-updates[] + handleNameSearch(e) { + if (e.key == 'Enter'){ + e.preventDefault(); + const nameSearch = ReactDOM.findDOMNode(this.refs.nameSearch).value; + if (/^[a-zA-Z ]+$/.test(nameSearch)) { + this.props.updateNameSearch(nameSearch); + } else { + ReactDOM.findDOMNode(this.refs.nameSearch).value = + nameSearch.substring(0, nameSearch.length - 1); + } + } + } + // end::handle-name-search-updates[] + + // tag::handle-nav[] + handleNavFirst(e){ + e.preventDefault(); + this.props.onNavigate(this.props.links.first.href); + } + + handleNavPrev(e) { + e.preventDefault(); + this.props.onNavigate(this.props.links.prev.href); + } + + handleNavNext(e) { + e.preventDefault(); + this.props.onNavigate(this.props.links.next.href); + } + + handleNavLast(e) { + e.preventDefault(); + this.props.onNavigate(this.props.links.last.href); + } + // end::handle-nav[] + + // tag::user-list-render[] + render() { + const users = this.props.users.map(user => + + ); + + const navLinks = []; + if ("first" in this.props.links) { + navLinks.push(<<); + } + if ("prev" in this.props.links) { + navLinks.push(<); + } + if ("next" in this.props.links) { + navLinks.push(>); + } + if ("last" in this.props.links) { + navLinks.push(>>); + } + + return ( + + + Nome: + + + Tamanho página: + + + + + Id + Name + Username + + {users} + + + + {navLinks} + + + ) + } + // end::user-list-render[] +} + +// tag::user[] +class User extends React.Component { + + constructor(props) { + super(props); + } + + render() { + return ( + + {this.props.user.id} + {this.props.user.name} + {this.props.user.username} + + ) + } +} +// end::user[] + +ReactDOM.render( + , + document.getElementById('react') +) diff --git a/src/main/js/client.js b/src/main/js/client.js new file mode 100644 index 000000000..8e542c9e3 --- /dev/null +++ b/src/main/js/client.js @@ -0,0 +1,19 @@ +'use strict'; + +var rest = require('rest'); +var defaultRequest = require('rest/interceptor/defaultRequest'); +var mime = require('rest/interceptor/mime'); +var uriTemplateInterceptor = require('./api/uriTemplateInterceptor'); +var errorCode = require('rest/interceptor/errorCode'); +var baseRegistry = require('rest/mime/registry'); + +var registry = baseRegistry.child(); + +registry.register('text/uri-list', require('./api/uriListConverter')); +registry.register('application/hal+json', require('rest/mime/type/application/hal')); + +module.exports = rest + .wrap(mime, { registry: registry }) + .wrap(uriTemplateInterceptor) + .wrap(errorCode) + .wrap(defaultRequest, { headers: { 'Accept': 'application/hal+json' }}); diff --git a/src/main/js/follow.js b/src/main/js/follow.js new file mode 100644 index 000000000..fbadb8e2f --- /dev/null +++ b/src/main/js/follow.js @@ -0,0 +1,46 @@ +module.exports = function follow(api, rootPath, relArray) { + const root = api({ + method: 'GET', + path: rootPath + }); + + return relArray.reduce(function(root, arrayItem) { + const rel = typeof arrayItem === 'string' ? arrayItem : arrayItem.rel; + return traverseNext(root, rel, arrayItem); + }, root); + + function traverseNext (root, rel, arrayItem) { + //console.log("traverseNext root "+JSON.stringify(root)+" rel "+rel+" arrayItem "+JSON.stringify(arrayItem)); + return root.then(function (response) { + //console.log("response "+JSON.stringify(response)); + if (hasEmbeddedRel(response.entity, rel)) { + //console.log("hasEmbeddedRel response.entity "+JSON.stringify(response.entity)); + return response.entity._embedded[rel]; + } + + if(!response.entity._links) { + return []; + } + + if (typeof arrayItem === 'string') { + //console.log("arrayItem = string -> response.entity "+JSON.stringify(response.entity)); + return api({ + method: 'GET', + path: response.entity._links[rel].href + }); + } else { + //console.log("arrayItem != string -> response.entity "+JSON.stringify(response.entity)); + //console.log("arrayItem != string -> arrayItem.params "+JSON.stringify(arrayItem.params)); + return api({ + method: 'GET', + path: response.entity._links[rel].href, + params: arrayItem.params + }); + } + }); + } + + function hasEmbeddedRel (entity, rel) { + return entity._embedded && entity._embedded.hasOwnProperty(rel); + } +}; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 000000000..b55838601 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,50 @@ +spring.data.rest.base-path=/api + +spring.data.rest.default-page-size=15 + +com.picpay.csv.database.url=https://s3.amazonaws.com/careers-picpay/users.csv.gz +com.picpay.csv.database.filename=users.csv.gz +com.picpay.relevancia1=lista_relevancia_1.txt +com.picpay.relevancia2=lista_relevancia_2.txt + +# H2 CONFIG +# spring.datasource.url=jdbc:h2:mem:testdb +#spring.datasource.url=jdbc:h2:file:~/testdb;LOG=0;CACHE_SIZE=65536;LOCK_MODE=0;UNDO_LOG=0 +spring.datasource.driverClassName=org.h2.Driver +# spring.datasource.username=sa +# spring.datasource.password= +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.h2.console.enabled=true +# spring.h2.console.path=/h2-console + +# MYSQL CONFIG +# DataSource settings: set here your own configurations for the database +# connection. In this example we have "test" as database name and +# "root" as username and password. +#spring.datasource.url = jdbc:mysql://localhost:3306/test +# spring.datasource.url = jdbc:mysql://mysql-standalone:3306/test +#spring.datasource.username = sa +#spring.datasource.password = password +#spring.datasource.username = root +#spring.datasource.password = password +# Keep the connection alive if idle for a long time (needed in production) +#spring.datasource.testWhileIdle = true +#spring.datasource.validationQuery = SELECT 1 + +# Show or not log for each sql query +spring.jpa.show-sql = true + +# Hibernate ddl auto (create, create-drop, update) +spring.jpa.hibernate.ddl-auto = create + +# Naming strategy +spring.jpa.hibernate.naming-strategy = org.hibernate.cfg.ImprovedNamingStrategy + +# Use spring.jpa.properties.* for Hibernate native properties (the prefix is +# stripped before adding them to the entity manager) + +# The SQL dialect makes Hibernate generate better SQL for the chosen database +# spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect +#logging.level.org.springframework.security=DEBUG + +server.port=8086 \ No newline at end of file diff --git a/src/main/resources/import.sql b/src/main/resources/import.sql new file mode 100644 index 000000000..56904b139 --- /dev/null +++ b/src/main/resources/import.sql @@ -0,0 +1,7 @@ +--CREATE TABLE tb_user(id varchar(255), name varchar(255), username varchar(255)); +--LOAD DATA LOCAL INFILE '/home/bruno/IdeaProjects/trabalhe-conosco-backend-dev/src/main/resources/users.csv' INTO TABLE tb_user FIELDS TERMINATED BY ','; +DROP TABLE IF EXISTS tb_user; +CREATE TABLE tb_user(id varchar(255), name varchar(255), username varchar(255)) AS SELECT * FROM CSVREAD('classpath:/users.csv', 'id,name,username', null); +CREATE INDEX idxname ON tb_user(name, username); +ALTER TABLE tb_user ADD priority int DEFAULT 10; +CREATE INDEX idxpriority ON tb_user(priority); diff --git a/src/main/resources/static/main.css b/src/main/resources/static/main.css new file mode 100644 index 000000000..e59d00b3f --- /dev/null +++ b/src/main/resources/static/main.css @@ -0,0 +1,9 @@ +table { + border-collapse: collapse; +} + +td, th { + border: 1px solid #999; + padding: 0.5rem; + text-align: left; +} \ No newline at end of file diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html new file mode 100644 index 000000000..208e325d9 --- /dev/null +++ b/src/main/resources/templates/index.html @@ -0,0 +1,15 @@ + + + + + PicPay User Manager + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/com/picpay/repositories/UserRepositoryIT.java b/src/test/java/com/picpay/repositories/UserRepositoryIT.java new file mode 100644 index 000000000..667de6b57 --- /dev/null +++ b/src/test/java/com/picpay/repositories/UserRepositoryIT.java @@ -0,0 +1,40 @@ +package com.picpay.repositories; + +import com.picpay.UserManagerApplication; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +@RunWith(SpringRunner.class) +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = UserManagerApplication.class +) +@AutoConfigureMockMvc +@TestPropertySource(locations = "classpath:application-test.properties") +public class UserRepositoryIT { + + @Autowired + MockMvc mockMvc; + + @Test + public void contextLoads() throws Exception { + + MvcResult mvcResult = mockMvc.perform( + MockMvcRequestBuilders.get("/api/users") + .accept(MediaType.APPLICATION_JSON) + ).andReturn(); + + System.out.println(mvcResult.getResponse()); + + } + +} \ No newline at end of file diff --git a/src/test/java/com/picpay/repositories/UserRepositoryTest.java b/src/test/java/com/picpay/repositories/UserRepositoryTest.java new file mode 100644 index 000000000..fdfc79bcc --- /dev/null +++ b/src/test/java/com/picpay/repositories/UserRepositoryTest.java @@ -0,0 +1,56 @@ +package com.picpay.repositories; + +import com.picpay.model.User; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.data.domain.Page; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.Arrays; +import java.util.List; + +@RunWith(SpringRunner.class) +@DataJpaTest +public class UserRepositoryTest { + + @Autowired + private UserRepository userRepository; + + @Before + public void setUp(){ + userRepository.save(new User("id", "name", "username")); + userRepository.save(new User("id2", "name2", "username2")); + userRepository.save(new User("id3", "trash", "trash")); + } + + @Test + public void shouldReturnUserWithName() { + Page page = userRepository.findByNameContainingOrUsernameContainingOrderByPriority("name", "name", null); + Assert.assertTrue(page.getTotalElements() == 2); + } + + @Test + public void shouldUpdatePriority() { + int updated = userRepository.updatePriorityByIds(Arrays.asList("id", "id3"), 4); + Assert.assertTrue(updated == 2); + int updated2 = userRepository.updatePriorityByIds(Arrays.asList("id2"), 2); + Assert.assertTrue(updated2 == 1); + } + + @Test + public void shouldReturnUserWithNameOrdered() { + userRepository.updatePriorityByIds(Arrays.asList("id", "id3"), 10); + userRepository.updatePriorityByIds(Arrays.asList("id2"), 8); + + Page page = userRepository.findByNameContainingOrUsernameContainingOrderByPriority("a", "a", null); + Assert.assertTrue(page.getTotalElements() == 3); + List list = page.getContent(); + User user = list.get(0); + Assert.assertTrue(user.getId().equals("id2")); + } + +} \ No newline at end of file diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties new file mode 100644 index 000000000..1309731f4 --- /dev/null +++ b/src/test/resources/application-test.properties @@ -0,0 +1,2 @@ +spring.datasource.url=jdbc:h2:mem:test;DB_CLOSE_ON_EXIT=FALSE +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect \ No newline at end of file diff --git a/src/test/resources/import.sql b/src/test/resources/import.sql new file mode 100644 index 000000000..55ade54a1 --- /dev/null +++ b/src/test/resources/import.sql @@ -0,0 +1 @@ +INSERT INTO tb_user SELECT * FROM CSVREAD('classpath:/users-test.csv'); \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 000000000..48eaf0ad8 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,25 @@ +var path = require('path'); + +module.exports = { + entry: './src/main/js/app.js', + devtool: 'sourcemaps', + cache: true, + debug: true, + output: { + path: __dirname, + filename: './src/main/resources/static/built/bundle.js' + }, + module: { + loaders: [ + { + test: path.join(__dirname, '.'), + exclude: /(node_modules)/, + loader: 'babel-loader', + query: { + cacheDirectory: true, + presets: ['es2015', 'react'] + } + } + ] + } +}; \ No newline at end of file
+ Nome: +
+ Tamanho página: +