diff --git a/pom.xml b/pom.xml index edf50177..fa28fdd4 100644 --- a/pom.xml +++ b/pom.xml @@ -1,75 +1,189 @@ + - - 4.0.0 - - com.aquent - crud-app - war - 1.0 - - - org.springframework.boot - spring-boot-starter-parent - 2.1.6.RELEASE - - - - UTF-8 - 1.8 - - - - https://github.com/aquent/crud-app - scm:git:git://github.com/aquent/crud-app.git - scm:git:git@github.com:aquent/crud-app.git - - - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-jdbc - - - org.springframework.boot - spring-boot-starter-thymeleaf - - - com.h2database - h2 - - - javax.validation - validation-api - 1.1.0.Final - - - org.hibernate - hibernate-validator - 5.2.1.Final - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.3 - - ${java.version} - ${java.version} - - - + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 + + com.aquent + crud-app + 1.0-SNAPSHOT + crudapi + Aquent CRUD APP + + org.springframework.boot - spring-boot-maven-plugin - - - - + spring-boot-starter-parent + 2.5.1 + + + + + UTF-8 + 1.8 + 1.5.2.Final + + + + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.apache.tomcat + tomcat-jdbc + + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + + + + + + + + + + + + + + + javax.validation + validation-api + 1.1.0.Final + + + + org.hibernate + hibernate-validator + 5.2.1.Final + + + + mysql + mysql-connector-java + runtime + + + + + + + + + org.mapstruct + mapstruct + ${org.mapstruct.version} + + + + org.mapstruct + mapstruct-processor + ${org.mapstruct.version} + + + + com.google.code.gson + gson + 2.8.0 + + + + org.jboss.resteasy + resteasy-client + 3.5.1.Final + + + + org.jboss.resteasy + resteasy-jackson2-provider + 3.5.1.Final + + + + + + + + + + + + + + + + junit + junit + test + + + + org.testcontainers + mockserver + 1.17.2 + test + + + + org.projectlombok + lombok + true + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.version} + ${java.version} + + + org.mapstruct + mapstruct-processor + ${org.mapstruct.version} + + + + + + org.springframework.boot + spring-boot-maven-plugin + + false + true + + 2.5.1 + + + + + \ No newline at end of file diff --git a/src/main/java/com/aquent/crudapp/Application.java b/src/main/java/com/aquent/crudapp/Application.java index 6c88ed1c..935f7a73 100644 --- a/src/main/java/com/aquent/crudapp/Application.java +++ b/src/main/java/com/aquent/crudapp/Application.java @@ -2,8 +2,12 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; -@SpringBootApplication +@SpringBootApplication(scanBasePackages = {"com.aquent.crudapp"}) +@EnableJpaRepositories("com.aquent.crudapp") +@Configuration public class Application { public static void main(String[] args) { diff --git a/src/main/java/com/aquent/crudapp/controllers/BaseController.java b/src/main/java/com/aquent/crudapp/controllers/BaseController.java new file mode 100644 index 00000000..17deeaa5 --- /dev/null +++ b/src/main/java/com/aquent/crudapp/controllers/BaseController.java @@ -0,0 +1,79 @@ +package com.aquent.crudapp.controllers; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.net.URI; +import java.util.List; + +/** + * This class acts as base class for all the controllers + * @param + * */ +public abstract class BaseController { + + /** + * This method generates the no content response for read by id operation + * @param entity + * */ + protected ResponseEntity generateResourceGetByIdNoContentResponse(T entity) { + return new ResponseEntity<>(entity, HttpStatus.NO_CONTENT); + } + + /** + * This method generates the no content response for read all operation + * @param entities + * */ + protected ResponseEntity> generateResourceGetAllNoContentResponse(List entities) { + return new ResponseEntity<>(entities, HttpStatus.NO_CONTENT); + } + + /** + * This method generates the required response for read by id operation + * @param entity + * */ + protected ResponseEntity generateResourceGetByIdResponseOk(T entity) { + return new ResponseEntity<>(entity, HttpStatus.OK); + } + + /** + * This method generates the required response for read all operation + * @param entities + * */ + protected ResponseEntity> generateResourceGetAllResponseOk(List entities) { + return new ResponseEntity<>(entities, HttpStatus.OK); + } + + /** + * This method generates the required response for create operation + * @param id + * */ + protected ResponseEntity generateResourceCreatedResponse(Integer id) { + URI location = ServletUriComponentsBuilder + .fromCurrentRequest().path("/{id}") + .buildAndExpand(id).toUri(); + + return ResponseEntity.created(location).build(); + } + + protected ResponseEntity generateResourceCreatedResponse() { + return new ResponseEntity<>(HttpStatus.REQUEST_TIMEOUT); + } + + /** + * This method generates the errors response for create operation + * @param errors + * */ + protected ResponseEntity> generateResourceCreatedResponse(List errors) { + return new ResponseEntity<>(errors, HttpStatus.OK); + } + + /** + * This method generates the required response for delete operation + * @param message + * */ + protected ResponseEntity generateStringResponse(T message) { + return new ResponseEntity<>(message, HttpStatus.OK); + } +} diff --git a/src/main/java/com/aquent/crudapp/controllers/ClientController.java b/src/main/java/com/aquent/crudapp/controllers/ClientController.java new file mode 100644 index 00000000..4b4d9c80 --- /dev/null +++ b/src/main/java/com/aquent/crudapp/controllers/ClientController.java @@ -0,0 +1,76 @@ +package com.aquent.crudapp.controllers; + +import com.aquent.crudapp.dtos.ClientDTO; +import com.aquent.crudapp.services.ClientService; +import com.aquent.crudapp.errors.ResourceNotFoundException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +import static com.aquent.crudapp.utils.Constants.CLIENT_DELETED; +import static com.aquent.crudapp.utils.Constants.CLIENT_NOT_FOUND; + +/** + * Controller for handling basic person management operations. + */ +@RestController +@RequestMapping(path = "/client", produces = MediaType.APPLICATION_JSON_VALUE) +@CrossOrigin(origins = "*") +public class ClientController extends BaseController { + @Autowired + ClientService clientService; + + @GetMapping(path = "/list") + public ResponseEntity> getAllClients() { + List clients = clientService.listClients(); + if (null == clients || clients.isEmpty()) + return generateResourceGetAllNoContentResponse(clients); + else + return generateResourceGetAllResponseOk(clients); + } + + @GetMapping(path = "/{id}") + public ResponseEntity getOne(@Valid @PathVariable("id") Integer id) { + ClientDTO client = clientService.viewClient(id); + if (null == client) throw new ResourceNotFoundException(String.format(CLIENT_NOT_FOUND, id)); + return generateResourceGetByIdResponseOk(client); + } + + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity create(@Valid @RequestBody ClientDTO clientDTO) { + List errors = clientService.validateClient(clientDTO); + + if (null != errors && !errors.isEmpty()) + return generateResourceCreatedResponse(errors); + + ClientDTO savedClientDTO = clientService.createClient(clientDTO); + if (null == savedClientDTO) + return generateResourceCreatedResponse(); + + ResponseEntity response = generateResourceCreatedResponse(savedClientDTO.getId()); + return response; + } + + @PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity update(@Valid @RequestBody ClientDTO clientDTO) { + List errors = clientService.validateClient(clientDTO); + + if (null != errors && !errors.isEmpty()) + return generateResourceCreatedResponse(errors); + + ClientDTO updatedClient = clientService.updateClient(clientDTO); + return generateResourceCreatedResponse(updatedClient.getId()); + } + + @DeleteMapping(path = "/delete/{id}") + public ResponseEntity delete(@Valid @PathVariable("id") Integer id) { + clientService.deleteClient(id); + String message = String.format(CLIENT_DELETED, id); + return generateStringResponse(message); + } + +} diff --git a/src/main/java/com/aquent/crudapp/HomeController.java b/src/main/java/com/aquent/crudapp/controllers/HomeController.java similarity index 93% rename from src/main/java/com/aquent/crudapp/HomeController.java rename to src/main/java/com/aquent/crudapp/controllers/HomeController.java index d7600b71..9ddd0cb2 100644 --- a/src/main/java/com/aquent/crudapp/HomeController.java +++ b/src/main/java/com/aquent/crudapp/controllers/HomeController.java @@ -1,4 +1,4 @@ -package com.aquent.crudapp; +package com.aquent.crudapp.controllers; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; diff --git a/src/main/java/com/aquent/crudapp/controllers/PersonController.java b/src/main/java/com/aquent/crudapp/controllers/PersonController.java new file mode 100644 index 00000000..08a14dda --- /dev/null +++ b/src/main/java/com/aquent/crudapp/controllers/PersonController.java @@ -0,0 +1,90 @@ +package com.aquent.crudapp.controllers; + + +import com.aquent.crudapp.dtos.PersonDTO; +import com.aquent.crudapp.errors.ResourceNotFoundException; +import com.aquent.crudapp.models.Person; +import com.aquent.crudapp.services.PersonService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; + +import javax.validation.Valid; +import java.util.ArrayList; +import java.util.List; + +import static com.aquent.crudapp.utils.Constants.*; + +/** + * Controller for handling basic person management operations. + */ +@RestController +@RequestMapping(path = "/person", produces = MediaType.APPLICATION_JSON_VALUE) +@CrossOrigin(origins = "*") +public class PersonController extends BaseController { + + @Autowired + private PersonService personService; + + @GetMapping(value = "/list") + public ResponseEntity> getAllPeople() { + List people = personService.listPeople(); + if (null == people || people.isEmpty()) + return generateResourceGetAllNoContentResponse(people); + else + return generateResourceGetAllResponseOk(people); + } + +// @GetMapping(value = "/list/unemployed/{clientId}") +// public ResponseEntity> getAllUnployedPeople(@Valid @PathVariable("clientId") Integer clientId) { +// List people = personService.getUnemployedPeopleWithClientId(clientId); +// if (null == people || people.isEmpty()) +// return generateResourceGetAllNoContentResponse(people); +// else +// return generateResourceGetAllResponseOk(people); +// } + + @GetMapping(value = "create") + public ModelAndView create() { + ModelAndView mav = new ModelAndView("person/create"); + mav.addObject("person", new Person()); + mav.addObject("errors", new ArrayList()); + return mav; + } + + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity create(@Valid @RequestBody PersonDTO personDTO) { + List errors = personService.validatePerson(personDTO); + if (null != errors && !errors.isEmpty()) + return generateResourceCreatedResponse(errors); + + Integer personId = personService.createPerson(personDTO); + return generateResourceCreatedResponse(personId); + } + + @GetMapping(path = "/{id}") + public ResponseEntity getOne(@Valid @PathVariable("id") Integer id) { + PersonDTO personDTO = personService.readPerson(id); + if (null == personDTO) throw new ResourceNotFoundException(String.format(PERSON_NOT_FOUND, id)); + return generateResourceGetByIdResponseOk(personDTO); + } + + @PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity edit(@Valid @RequestBody PersonDTO personDTO) { + List errors = personService.validatePerson(personDTO); + if (null != errors && !errors.isEmpty()) + return generateResourceCreatedResponse(errors); + + PersonDTO addedPersonDTO = personService.updatePerson(personDTO); + return generateResourceCreatedResponse(addedPersonDTO.getId()); + } + + @DeleteMapping(path = "/delete/{id}") + public ResponseEntity delete(@Valid @PathVariable("id") Integer id) { + personService.deletePerson(id); + String message = String.format(PERSON_DELETED, id); + return generateStringResponse(message); + } +} diff --git a/src/main/java/com/aquent/crudapp/dtos/ClientDTO.java b/src/main/java/com/aquent/crudapp/dtos/ClientDTO.java new file mode 100644 index 00000000..3ce65b68 --- /dev/null +++ b/src/main/java/com/aquent/crudapp/dtos/ClientDTO.java @@ -0,0 +1,91 @@ +package com.aquent.crudapp.dtos; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +@Data +public class ClientDTO implements Serializable { + private Integer id; + private String companyName; + private String websiteURI; + private String phoneNumber; + private String streetAddress; + private String city; + private String state; + private String zipCode; + private List contacts; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getCompanyName() { + return companyName; + } + + public void setCompanyName(String companyName) { + this.companyName = companyName; + } + + public String getWebsiteURI() { + return websiteURI; + } + + public void setWebsiteURI(String websiteURI) { + this.websiteURI = websiteURI; + } + + public String getPhoneNumber() { + return phoneNumber; + } + + public void setPhoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber; + } + + public String getStreetAddress() { + return streetAddress; + } + + public void setStreetAddress(String streetAddress) { + this.streetAddress = streetAddress; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public String getZipCode() { + return zipCode; + } + + public void setZipCode(String zipCode) { + this.zipCode = zipCode; + } + + public List getContacts() { + return contacts; + } + + public void setContacts(List contacts) { + this.contacts = contacts; + } +} diff --git a/src/main/java/com/aquent/crudapp/dtos/PersonDTO.java b/src/main/java/com/aquent/crudapp/dtos/PersonDTO.java new file mode 100644 index 00000000..76f4f5cb --- /dev/null +++ b/src/main/java/com/aquent/crudapp/dtos/PersonDTO.java @@ -0,0 +1,102 @@ +package com.aquent.crudapp.dtos; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +@Data +public class PersonDTO implements Serializable { + private Integer id; + + @NotNull(message = "First name is required with maximum length of 50") + private String firstName; + private String lastName; + private String emailAddress; + private String streetAddress; + private String city; + private String state; + private String zipCode; + private Integer clientId; + private String clientName; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmailAddress() { + return emailAddress; + } + + public void setEmailAddress(String emailAddress) { + this.emailAddress = emailAddress; + } + + public String getStreetAddress() { + return streetAddress; + } + + public void setStreetAddress(String streetAddress) { + this.streetAddress = streetAddress; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public String getZipCode() { + return zipCode; + } + + public void setZipCode(String zipCode) { + this.zipCode = zipCode; + } + + public Integer getClientId() { + return clientId; + } + + public void setClientId(Integer clientId) { + this.clientId = clientId; + } + + public String getClientName() { + return clientName; + } + + public void setClientName(String clientName) { + this.clientName = clientName; + } +} diff --git a/src/main/java/com/aquent/crudapp/errors/InvalidRequestException.java b/src/main/java/com/aquent/crudapp/errors/InvalidRequestException.java new file mode 100644 index 00000000..436107a4 --- /dev/null +++ b/src/main/java/com/aquent/crudapp/errors/InvalidRequestException.java @@ -0,0 +1,11 @@ +package com.aquent.crudapp.errors; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(value = HttpStatus.BAD_REQUEST) +public class InvalidRequestException extends RuntimeException { + public InvalidRequestException(String message) { + super(message); + } +} diff --git a/src/main/java/com/aquent/crudapp/errors/ResourceNotFoundException.java b/src/main/java/com/aquent/crudapp/errors/ResourceNotFoundException.java new file mode 100644 index 00000000..27848814 --- /dev/null +++ b/src/main/java/com/aquent/crudapp/errors/ResourceNotFoundException.java @@ -0,0 +1,15 @@ +package com.aquent.crudapp.errors; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(value = HttpStatus.NOT_FOUND) +public class ResourceNotFoundException extends RuntimeException { + public ResourceNotFoundException(String message) { + super(message); + } + + public ResourceNotFoundException(String message, Exception exception) { + super(message, exception); + } +} diff --git a/src/main/java/com/aquent/crudapp/mappers/ClientMapper.java b/src/main/java/com/aquent/crudapp/mappers/ClientMapper.java new file mode 100644 index 00000000..80e88e85 --- /dev/null +++ b/src/main/java/com/aquent/crudapp/mappers/ClientMapper.java @@ -0,0 +1,30 @@ +package com.aquent.crudapp.mappers; + +import com.aquent.crudapp.dtos.ClientDTO; +import com.aquent.crudapp.models.Client; +import org.mapstruct.DecoratedWith; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.factory.Mappers; + +import java.util.List; + + +@Mapper(componentModel = "spring", uses = {Client.class}) +@DecoratedWith(ClientMapperDecorator.class) +public interface ClientMapper { + ClientMapper INSTANCE = Mappers.getMapper(ClientMapper.class); + + @Mappings({ + @Mapping(source = "contacts", target = "contacts") + }) + ClientDTO toDto(Client client); + + @Mappings({ + @Mapping(source = "contacts", target = "contacts") + }) + Client toEntity(ClientDTO clientDTO); + + List toDTOList(List clients); +} diff --git a/src/main/java/com/aquent/crudapp/mappers/ClientMapperDecorator.java b/src/main/java/com/aquent/crudapp/mappers/ClientMapperDecorator.java new file mode 100644 index 00000000..efd15e9f --- /dev/null +++ b/src/main/java/com/aquent/crudapp/mappers/ClientMapperDecorator.java @@ -0,0 +1,70 @@ +package com.aquent.crudapp.mappers; + +import com.aquent.crudapp.dtos.ClientDTO; +import com.aquent.crudapp.dtos.PersonDTO; +import com.aquent.crudapp.models.Client; +import com.aquent.crudapp.models.Person; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class ClientMapperDecorator implements ClientMapper { + + @Autowired + private ClientMapper clientMapper; + + @Autowired + private PersonMapper personMapper; + + @Override + public ClientDTO toDto(Client client) { + List contacts = client.getContacts(); + ClientDTO clientDTO = clientMapper.toDto(client); + if (null == contacts || contacts.isEmpty()) { + clientDTO.setContacts(new ArrayList<>()); + } else { + List contactDTOs = contacts.stream() + .map(personMapper::toDto) + .collect(Collectors.toList()); + clientDTO.setContacts(contactDTOs); + } + + return clientDTO; + } + + @Override + public Client toEntity(ClientDTO clientDTO) { + Client client = new Client(); + client.setId( clientDTO.getId() ); + client.setCompanyName( clientDTO.getCompanyName() ); + client.setWebsiteURI( clientDTO.getWebsiteURI() ); + client.setPhoneNumber( clientDTO.getPhoneNumber() ); + client.setStreetAddress( clientDTO.getStreetAddress() ); + client.setCity( clientDTO.getCity() ); + client.setState( clientDTO.getState() ); + client.setZipCode( clientDTO.getZipCode() ); + + List contactDTOs = clientDTO.getContacts(); + if (null == contactDTOs || contactDTOs.isEmpty()) { + client.setContacts(new ArrayList<>()); + } else { + List contacts = contactDTOs.stream() + .map(personMapper::toEntity) + .collect(Collectors.toList()); + client.setContacts(contacts); + } + + return client; + } + + @Override + public List toDTOList(List clients) { + if (null == clients || clients.isEmpty()) return new ArrayList<>(); + + return clients.stream() + .map(this::toDto) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/aquent/crudapp/mappers/PersonMapper.java b/src/main/java/com/aquent/crudapp/mappers/PersonMapper.java new file mode 100644 index 00000000..02280771 --- /dev/null +++ b/src/main/java/com/aquent/crudapp/mappers/PersonMapper.java @@ -0,0 +1,32 @@ +package com.aquent.crudapp.mappers; + +import com.aquent.crudapp.dtos.PersonDTO; +import com.aquent.crudapp.models.Client; +import com.aquent.crudapp.models.Person; +import org.mapstruct.DecoratedWith; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper(componentModel = "spring", uses = {Person.class, Client.class}) +@DecoratedWith(PersonMapperDecorator.class) +public interface PersonMapper { + PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class); + + @Mappings({ + @Mapping(target = "clientId", expression = "java(null != person.getClient() ? person.getClient().getId() : null)"), + @Mapping(target = "clientName", expression = "java(null != person.getClient() ? person.getClient().getCompanyName() : null)") + }) + PersonDTO toDto(Person person); + + @Mappings({ + @Mapping(target = "client.id", expression = "java(null != personDTO.getClientId() ? personDTO.getClientId() : null)"), + @Mapping(target = "client.companyName", expression = "java(null != personDTO.getClientName() ? personDTO.getClientName() : null)") + }) + Person toEntity(PersonDTO personDTO); + + List toDTOList(List contacts); +} diff --git a/src/main/java/com/aquent/crudapp/mappers/PersonMapperDecorator.java b/src/main/java/com/aquent/crudapp/mappers/PersonMapperDecorator.java new file mode 100644 index 00000000..a622d972 --- /dev/null +++ b/src/main/java/com/aquent/crudapp/mappers/PersonMapperDecorator.java @@ -0,0 +1,50 @@ +package com.aquent.crudapp.mappers; + +import com.aquent.crudapp.dtos.PersonDTO; +import com.aquent.crudapp.models.Client; +import com.aquent.crudapp.models.Person; +import com.aquent.crudapp.repositories.ClientRepository; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +public class PersonMapperDecorator implements PersonMapper { + + @Autowired + private PersonMapper personMapper; + + @Autowired + private ClientRepository clientRepository; + + @Override + public PersonDTO toDto(Person person) { + return personMapper.toDto(person); + } + + @Override + public Person toEntity(PersonDTO personDTO) { + Person person = new Person(); + person.setId( personDTO.getId() ); + person.setFirstName( personDTO.getFirstName() ); + person.setLastName( personDTO.getLastName() ); + person.setEmailAddress( personDTO.getEmailAddress() ); + person.setStreetAddress( personDTO.getStreetAddress() ); + person.setCity( personDTO.getCity() ); + person.setState( personDTO.getState() ); + person.setZipCode( personDTO.getZipCode() ); + + Integer clientId = personDTO.getClientId(); + if (null == clientId) { + person.setClient(null); + } else { + Client client = clientRepository.getById(clientId); + person.setClient(client); + } + return person; + } + + @Override + public List toDTOList(List contacts) { + return personMapper.toDTOList(contacts); + } +} diff --git a/src/main/java/com/aquent/crudapp/models/Client.java b/src/main/java/com/aquent/crudapp/models/Client.java new file mode 100644 index 00000000..2931d732 --- /dev/null +++ b/src/main/java/com/aquent/crudapp/models/Client.java @@ -0,0 +1,130 @@ +package com.aquent.crudapp.models; + +import lombok.*; +import org.springframework.stereotype.Component; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.util.List; + +@Entity +@Table(name = "client") +@Data +@Component +public class Client { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Integer id; + + @NotNull + @Column(name = "company_name") + @Size(min = 1, max = 50, message = "Company name is required with maximum length of 50") + private String companyName; + + @NotNull + @Column(name = "website_uri") + @Size(min = 1, max = 50, message = "Website URI is required with maximum length of 100") + private String websiteURI; + + @NotNull + @Column(name = "phone_number") + @Size(min = 1, max = 20, message = "Phone number is required with maximum length of 20") + private String phoneNumber; + + @NotNull + @Column(name = "street_address") + @Size(min = 1, max = 50, message = "Street address is required with maximum length of 50") + private String streetAddress; + + @NotNull + @Column(name = "city") + @Size(min = 1, max = 50, message = "City is required with maximum length of 50") + private String city; + + @NotNull + @Column(name = "state") + @Size(min = 2, max = 2, message = "State is required with length 2") + private String state; + + @NotNull + @Column(name = "zip_code") + @Size(min = 5, max = 5, message = "Zip code is required with length 5") + private String zipCode; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "client") + private List contacts; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getCompanyName() { + return companyName; + } + + public void setCompanyName(String companyName) { + this.companyName = companyName; + } + + public String getWebsiteURI() { + return websiteURI; + } + + public void setWebsiteURI(String websiteURI) { + this.websiteURI = websiteURI; + } + + public String getPhoneNumber() { + return phoneNumber; + } + + public void setPhoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber; + } + + public String getStreetAddress() { + return streetAddress; + } + + public void setStreetAddress(String streetAddress) { + this.streetAddress = streetAddress; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public String getZipCode() { + return zipCode; + } + + public void setZipCode(String zipCode) { + this.zipCode = zipCode; + } + + public List getContacts() { + return contacts; + } + + public void setContacts(List contacts) { + this.contacts = contacts; + } +} diff --git a/src/main/java/com/aquent/crudapp/person/Person.java b/src/main/java/com/aquent/crudapp/models/Person.java similarity index 72% rename from src/main/java/com/aquent/crudapp/person/Person.java rename to src/main/java/com/aquent/crudapp/models/Person.java index 03e938a2..c8aa993e 100644 --- a/src/main/java/com/aquent/crudapp/person/Person.java +++ b/src/main/java/com/aquent/crudapp/models/Person.java @@ -1,49 +1,71 @@ -package com.aquent.crudapp.person; +package com.aquent.crudapp.models; + +import lombok.Data; +import org.springframework.stereotype.Component; + +import javax.persistence.*; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; /** * The person entity corresponding to the "person" table in the database. */ +@Entity +@Table(name = "person") +@Data +@Component public class Person { - - private Integer personId; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Integer id; @NotNull + @Column(name = "first_name") @Size(min = 1, max = 50, message = "First name is required with maximum length of 50") private String firstName; @NotNull + @Column(name = "last_name") @Size(min = 1, max = 50, message = "Last name is required with maximum length of 50") private String lastName; @NotNull + @Column(name = "email_address") @Size(min = 1, max = 50, message = "Email address is required with maximum length of 50") private String emailAddress; @NotNull + @Column(name = "street_address") @Size(min = 1, max = 50, message = "Street address is required with maximum length of 50") private String streetAddress; @NotNull + @Column(name = "city") @Size(min = 1, max = 50, message = "City is required with maximum length of 50") private String city; @NotNull + @Column(name = "state") @Size(min = 2, max = 2, message = "State is required with length 2") private String state; @NotNull + @Column(name = "zip_code") @Size(min = 5, max = 5, message = "Zip code is required with length 5") private String zipCode; - public Integer getPersonId() { - return personId; + @ManyToOne + @JoinColumn(name = "client_id") + private Client client; + + public Integer getId() { + return id; } - public void setPersonId(Integer personId) { - this.personId = personId; + public void setId(Integer id) { + this.id = id; } public String getFirstName() { @@ -101,4 +123,12 @@ public String getZipCode() { public void setZipCode(String zipCode) { this.zipCode = zipCode; } + + public Client getClient() { + return client; + } + + public void setClient(Client client) { + this.client = client; + } } diff --git a/src/main/java/com/aquent/crudapp/person/DefaultPersonService.java b/src/main/java/com/aquent/crudapp/person/DefaultPersonService.java deleted file mode 100644 index 8f09607a..00000000 --- a/src/main/java/com/aquent/crudapp/person/DefaultPersonService.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.aquent.crudapp.person; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import javax.validation.ConstraintViolation; -import javax.validation.Validator; - -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; - -/** - * Default implementation of {@link PersonService}. - */ -@Component -public class DefaultPersonService implements PersonService { - - private final PersonDao personDao; - private final Validator validator; - - public DefaultPersonService(PersonDao personDao, Validator validator) { - this.personDao = personDao; - this.validator = validator; - } - - @Override - @Transactional(propagation = Propagation.SUPPORTS, readOnly = true) - public List listPeople() { - return personDao.listPeople(); - } - - @Override - @Transactional(propagation = Propagation.SUPPORTS, readOnly = true) - public Person readPerson(Integer id) { - return personDao.readPerson(id); - } - - @Override - @Transactional(propagation = Propagation.SUPPORTS, readOnly = false) - public Integer createPerson(Person person) { - return personDao.createPerson(person); - } - - @Override - @Transactional(propagation = Propagation.SUPPORTS, readOnly = false) - public void updatePerson(Person person) { - personDao.updatePerson(person); - } - - @Override - @Transactional(propagation = Propagation.SUPPORTS, readOnly = false) - public void deletePerson(Integer id) { - personDao.deletePerson(id); - } - - @Override - public List validatePerson(Person person) { - Set> violations = validator.validate(person); - List errors = new ArrayList(violations.size()); - for (ConstraintViolation violation : violations) { - errors.add(violation.getMessage()); - } - Collections.sort(errors); - return errors; - } -} diff --git a/src/main/java/com/aquent/crudapp/person/JdbcPersonDao.java b/src/main/java/com/aquent/crudapp/person/JdbcPersonDao.java deleted file mode 100644 index abe20753..00000000 --- a/src/main/java/com/aquent/crudapp/person/JdbcPersonDao.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.aquent.crudapp.person; - -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.Collections; -import java.util.List; - -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; - -/** - * Spring JDBC implementation of {@link PersonDao}. - */ -@Component -public class JdbcPersonDao implements PersonDao { - - private static final String SQL_LIST_PEOPLE = "SELECT * FROM person ORDER BY first_name, last_name, person_id"; - private static final String SQL_READ_PERSON = "SELECT * FROM person WHERE person_id = :personId"; - private static final String SQL_DELETE_PERSON = "DELETE FROM person WHERE person_id = :personId"; - private static final String SQL_UPDATE_PERSON = "UPDATE person SET (first_name, last_name, email_address, street_address, city, state, zip_code)" - + " = (:firstName, :lastName, :emailAddress, :streetAddress, :city, :state, :zipCode)" - + " WHERE person_id = :personId"; - private static final String SQL_CREATE_PERSON = "INSERT INTO person (first_name, last_name, email_address, street_address, city, state, zip_code)" - + " VALUES (:firstName, :lastName, :emailAddress, :streetAddress, :city, :state, :zipCode)"; - - private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; - - public JdbcPersonDao(NamedParameterJdbcTemplate namedParameterJdbcTemplate) { - this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; - } - - @Override - @Transactional(propagation = Propagation.SUPPORTS, readOnly = true) - public List listPeople() { - return namedParameterJdbcTemplate.getJdbcOperations().query(SQL_LIST_PEOPLE, new PersonRowMapper()); - } - - @Override - @Transactional(propagation = Propagation.SUPPORTS, readOnly = true) - public Person readPerson(Integer personId) { - return namedParameterJdbcTemplate.queryForObject(SQL_READ_PERSON, Collections.singletonMap("personId", personId), new PersonRowMapper()); - } - - @Override - @Transactional(propagation = Propagation.SUPPORTS, readOnly = false) - public void deletePerson(Integer personId) { - namedParameterJdbcTemplate.update(SQL_DELETE_PERSON, Collections.singletonMap("personId", personId)); - } - - @Override - @Transactional(propagation = Propagation.SUPPORTS, readOnly = false) - public void updatePerson(Person person) { - namedParameterJdbcTemplate.update(SQL_UPDATE_PERSON, new BeanPropertySqlParameterSource(person)); - } - - @Override - @Transactional(propagation = Propagation.SUPPORTS, readOnly = false) - public Integer createPerson(Person person) { - KeyHolder keyHolder = new GeneratedKeyHolder(); - namedParameterJdbcTemplate.update(SQL_CREATE_PERSON, new BeanPropertySqlParameterSource(person), keyHolder); - return keyHolder.getKey().intValue(); - } - - /** - * Row mapper for person records. - */ - private static final class PersonRowMapper implements RowMapper { - - @Override - public Person mapRow(ResultSet rs, int rowNum) throws SQLException { - Person person = new Person(); - person.setPersonId(rs.getInt("person_id")); - person.setFirstName(rs.getString("first_name")); - person.setLastName(rs.getString("last_name")); - person.setEmailAddress(rs.getString("email_address")); - person.setStreetAddress(rs.getString("street_address")); - person.setCity(rs.getString("city")); - person.setState(rs.getString("state")); - person.setZipCode(rs.getString("zip_code")); - return person; - } - } -} diff --git a/src/main/java/com/aquent/crudapp/person/PersonController.java b/src/main/java/com/aquent/crudapp/person/PersonController.java deleted file mode 100644 index 7b5d4249..00000000 --- a/src/main/java/com/aquent/crudapp/person/PersonController.java +++ /dev/null @@ -1,139 +0,0 @@ -package com.aquent.crudapp.person; - -import java.util.ArrayList; -import java.util.List; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.servlet.ModelAndView; - -/** - * Controller for handling basic person management operations. - */ -@Controller -@RequestMapping("person") -public class PersonController { - - public static final String COMMAND_DELETE = "Delete"; - - private final PersonService personService; - - public PersonController(PersonService personService) { - this.personService = personService; - } - - /** - * Renders the listing page. - * - * @return list view populated with the current list of people - */ - @GetMapping(value = "list") - public ModelAndView list() { - ModelAndView mav = new ModelAndView("person/list"); - mav.addObject("persons", personService.listPeople()); - return mav; - } - - /** - * Renders an empty form used to create a new person record. - * - * @return create view populated with an empty person - */ - @GetMapping(value = "create") - public ModelAndView create() { - ModelAndView mav = new ModelAndView("person/create"); - mav.addObject("person", new Person()); - mav.addObject("errors", new ArrayList()); - return mav; - } - - /** - * Validates and saves a new person. - * On success, the user is redirected to the listing page. - * On failure, the form is redisplayed with the validation errors. - * - * @param person populated form bean for the person - * @return redirect, or create view with errors - */ - @PostMapping(value = "create") - public ModelAndView create(Person person) { - List errors = personService.validatePerson(person); - if (errors.isEmpty()) { - personService.createPerson(person); - return new ModelAndView("redirect:/person/list"); - } else { - ModelAndView mav = new ModelAndView("person/create"); - mav.addObject("person", person); - mav.addObject("errors", errors); - return mav; - } - } - - /** - * Renders an edit form for an existing person record. - * - * @param personId the ID of the person to edit - * @return edit view populated from the person record - */ - @GetMapping(value = "edit/{personId}") - public ModelAndView edit(@PathVariable Integer personId) { - ModelAndView mav = new ModelAndView("person/edit"); - mav.addObject("person", personService.readPerson(personId)); - mav.addObject("errors", new ArrayList()); - return mav; - } - - /** - * Validates and saves an edited person. - * On success, the user is redirected to the listing page. - * On failure, the form is redisplayed with the validation errors. - * - * @param person populated form bean for the person - * @return redirect, or edit view with errors - */ - @PostMapping(value = "edit") - public ModelAndView edit(Person person) { - List errors = personService.validatePerson(person); - if (errors.isEmpty()) { - personService.updatePerson(person); - return new ModelAndView("redirect:/person/list"); - } else { - ModelAndView mav = new ModelAndView("person/edit"); - mav.addObject("person", person); - mav.addObject("errors", errors); - return mav; - } - } - - /** - * Renders the deletion confirmation page. - * - * @param personId the ID of the person to be deleted - * @return delete view populated from the person record - */ - @GetMapping(value = "delete/{personId}") - public ModelAndView delete(@PathVariable Integer personId) { - ModelAndView mav = new ModelAndView("person/delete"); - mav.addObject("person", personService.readPerson(personId)); - return mav; - } - - /** - * Handles person deletion or cancellation, redirecting to the listing page in either case. - * - * @param command the command field from the form - * @param personId the ID of the person to be deleted - * @return redirect to the listing page - */ - @PostMapping(value = "delete") - public String delete(@RequestParam String command, @RequestParam Integer personId) { - if (COMMAND_DELETE.equals(command)) { - personService.deletePerson(personId); - } - return "redirect:/person/list"; - } -} diff --git a/src/main/java/com/aquent/crudapp/person/PersonDao.java b/src/main/java/com/aquent/crudapp/person/PersonDao.java deleted file mode 100644 index 3c626c0d..00000000 --- a/src/main/java/com/aquent/crudapp/person/PersonDao.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.aquent.crudapp.person; - -import java.util.List; - -import org.springframework.stereotype.Repository; - -/** - * Operations on the "person" table. - */ -@Repository -public interface PersonDao { - - /** - * Retrieves all of the person records. - * - * @return list of person records - */ - List listPeople(); - - /** - * Creates a new person record. - * - * @param person the values to save - * @return the new person ID - */ - Integer createPerson(Person person); - - /** - * Retrieves a person record by ID. - * - * @param id the person ID - * @return the person record - */ - Person readPerson(Integer id); - - /** - * Updates an existing person record. - * - * @param person the new values to save - */ - void updatePerson(Person person); - - /** - * Deletes a person record by ID. - * - * @param id the person ID - */ - void deletePerson(Integer id); -} diff --git a/src/main/java/com/aquent/crudapp/repositories/ClientRepository.java b/src/main/java/com/aquent/crudapp/repositories/ClientRepository.java new file mode 100644 index 00000000..3af44780 --- /dev/null +++ b/src/main/java/com/aquent/crudapp/repositories/ClientRepository.java @@ -0,0 +1,17 @@ +package com.aquent.crudapp.repositories; + +import com.aquent.crudapp.models.Client; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/** + * Operations on the "client" table. + */ +@Repository +public interface ClientRepository extends JpaRepository { + Client getById(Integer clientId); + + Optional findById(Integer clientId); +} diff --git a/src/main/java/com/aquent/crudapp/repositories/PersonRepository.java b/src/main/java/com/aquent/crudapp/repositories/PersonRepository.java new file mode 100644 index 00000000..ff990970 --- /dev/null +++ b/src/main/java/com/aquent/crudapp/repositories/PersonRepository.java @@ -0,0 +1,24 @@ +package com.aquent.crudapp.repositories; + + +import com.aquent.crudapp.models.Person; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + + +/** + * Operations on the "person" table. + */ +@Repository +public interface PersonRepository extends JpaRepository { + Person getById(Integer id); + + @Query("select p from Person p where p.client.id = (:clientId) or p.client.id is null") + List findAllByClientIsNullOrClient_Id(@Param("clientId") Integer clientId); + + List findAllByClient_Id(Integer clientId); +} diff --git a/src/main/java/com/aquent/crudapp/services/ClientService.java b/src/main/java/com/aquent/crudapp/services/ClientService.java new file mode 100644 index 00000000..4e75ab38 --- /dev/null +++ b/src/main/java/com/aquent/crudapp/services/ClientService.java @@ -0,0 +1,28 @@ +package com.aquent.crudapp.services; + +import com.aquent.crudapp.dtos.ClientDTO; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public interface ClientService { + + List listClients(); + + ClientDTO createClient(ClientDTO clientDTO); + + ClientDTO viewClient(Integer clientId); + + ClientDTO updateClient(ClientDTO clientDTO); + + void deleteClient(Integer clientId); + + /** + * Validates populated client data. + * + * @param clientDTO the values to validate + * @return list of error messages + */ + List validateClient(ClientDTO clientDTO); +} diff --git a/src/main/java/com/aquent/crudapp/services/ClientServiceImpl.java b/src/main/java/com/aquent/crudapp/services/ClientServiceImpl.java new file mode 100644 index 00000000..d4b1b9c4 --- /dev/null +++ b/src/main/java/com/aquent/crudapp/services/ClientServiceImpl.java @@ -0,0 +1,137 @@ +package com.aquent.crudapp.services; + +import com.aquent.crudapp.dtos.ClientDTO; +import com.aquent.crudapp.dtos.PersonDTO; +import com.aquent.crudapp.errors.InvalidRequestException; +import com.aquent.crudapp.mappers.ClientMapper; +import com.aquent.crudapp.mappers.PersonMapper; +import com.aquent.crudapp.models.Client; +import com.aquent.crudapp.models.Person; +import com.aquent.crudapp.repositories.ClientRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.validation.ConstraintViolation; +import javax.validation.Validator; +import java.util.*; +import java.util.stream.Collectors; + +import static com.aquent.crudapp.utils.Constants.CLIENT_NOT_FOUND; + +@Service +public class ClientServiceImpl implements ClientService { + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + @Autowired + private ClientRepository clientRepository; + + @Autowired + private ClientMapper clientMapper; + + @Autowired + private Validator validator; + + @Autowired + private PersonService personService; + + @Autowired + private PersonMapper personMapper; + + @Override + public List listClients() { + List clients = new ArrayList<>(); + try { + clients = clientRepository.findAll(); + } catch (Exception exception) { + log.error("Error retrieving all the clients from the database: ", exception); + } + + if (clients.isEmpty()) return new ArrayList<>(); + + return clients + .stream() + .map(clientMapper::toDto) + .collect(Collectors.toList()); + } + + @Override + public ClientDTO createClient(ClientDTO clientDTO) { + Client client = clientMapper.toEntity(clientDTO); + + List contactDTOs = clientDTO.getContacts(); + if (null != contactDTOs && !contactDTOs.isEmpty()) { + List contacts = contactDTOs.stream() + .map(personMapper::toEntity) + .collect(Collectors.toList()); + client.setContacts(contacts); + } else { + client.setContacts(new ArrayList<>()); + } + try { + client = clientRepository.save(client); + } catch (Exception exception) { + log.error("Error saving the client {} to the database: ", client, exception); + return null; + } + log.info("client saved with id {}", client.getId()); + clientDTO = clientMapper.toDto(client); + log.info("clientDTO saved with id {}", clientDTO.getId()); + return clientDTO; + } + + @Override + public ClientDTO viewClient(Integer clientId) { + Client client = null; + try { + client = clientRepository.getById(clientId); + } catch (Exception exception) { + log.error("Error retrieving the client by ID [{}] from the database: ", clientId, exception); + } + + if (null == client) return null; + return clientMapper.toDto(client); + } + + @Override + public ClientDTO updateClient(ClientDTO clientDTO) { + List contactDTOs = clientDTO.getContacts(); + if (null != contactDTOs && !contactDTOs.isEmpty()) { + List updatedContactDTOs = personService.updateContacts(contactDTOs, clientDTO.getId(), clientDTO.getCompanyName()); + clientDTO.setContacts(updatedContactDTOs); + } + Client client = clientMapper.toEntity(clientDTO); + try { + client = clientRepository.save(client); + } catch (Exception exception) { + log.error("Error updating the existing client {} in the database: ", client, exception); + return null; + } + + + return clientMapper.toDto(client); + } + + @Override + public void deleteClient(Integer clientId) { + Client client = clientRepository.findById(clientId).orElse(null); + if (null == client) throw new InvalidRequestException(String.format(CLIENT_NOT_FOUND, clientId)); + try { + clientRepository.delete(client); + } catch (Exception exception) { + log.error("Error deleting the existing client with ID [{}] in the database: ", clientId, exception); + } + } + + @Override + public List validateClient(ClientDTO client) { + Set> violations = validator.validate(client); + List errors = new ArrayList<>(violations.size()); + for (ConstraintViolation violation : violations) { + errors.add(violation.getMessage()); + } + Collections.sort(errors); + return errors; + } +} diff --git a/src/main/java/com/aquent/crudapp/person/PersonService.java b/src/main/java/com/aquent/crudapp/services/PersonService.java similarity index 51% rename from src/main/java/com/aquent/crudapp/person/PersonService.java rename to src/main/java/com/aquent/crudapp/services/PersonService.java index 29572738..bb531cd4 100644 --- a/src/main/java/com/aquent/crudapp/person/PersonService.java +++ b/src/main/java/com/aquent/crudapp/services/PersonService.java @@ -1,29 +1,26 @@ -package com.aquent.crudapp.person; - -import java.util.List; +package com.aquent.crudapp.services; +import com.aquent.crudapp.dtos.PersonDTO; import org.springframework.stereotype.Service; +import java.util.List; + /** * Person operations. */ @Service public interface PersonService { - /** - * Retrieves all of the person records. - * - * @return list of person records - */ - List listPeople(); + List listPeople(); + + List getUnemployedPeopleWithClientId(Integer clientId); /** * Creates a new person record. * * @param person the values to save - * @return the new person ID */ - Integer createPerson(Person person); + Integer createPerson(PersonDTO person); /** * Retrieves a person record by ID. @@ -31,14 +28,14 @@ public interface PersonService { * @param id the person ID * @return the person record */ - Person readPerson(Integer id); + PersonDTO readPerson(Integer id); /** * Updates an existing person record. * - * @param person the new values to save + * @param personDTO the new values to save */ - void updatePerson(Person person); + PersonDTO updatePerson(PersonDTO personDTO); /** * Deletes a person record by ID. @@ -50,8 +47,12 @@ public interface PersonService { /** * Validates populated person data. * - * @param person the values to validate + * @param personDTO the values to validate * @return list of error messages */ - List validatePerson(Person person); + List validatePerson(PersonDTO personDTO); + + List updateContacts(List contactDTOs, Integer clientId, String clientName); + + List getContactsByClientId(Integer clientId); } diff --git a/src/main/java/com/aquent/crudapp/services/PersonServiceImpl.java b/src/main/java/com/aquent/crudapp/services/PersonServiceImpl.java new file mode 100644 index 00000000..9860ffe6 --- /dev/null +++ b/src/main/java/com/aquent/crudapp/services/PersonServiceImpl.java @@ -0,0 +1,163 @@ +package com.aquent.crudapp.services; + +import com.aquent.crudapp.dtos.PersonDTO; +import com.aquent.crudapp.errors.InvalidRequestException; +import com.aquent.crudapp.mappers.PersonMapper; +import com.aquent.crudapp.models.Person; +import com.aquent.crudapp.repositories.PersonRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.validation.ConstraintViolation; +import javax.validation.Validator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.aquent.crudapp.utils.Constants.PERSON_NOT_FOUND; + +@Service +public class PersonServiceImpl implements PersonService { + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + @Autowired + private PersonRepository personRepository; + + @Autowired + private PersonMapper personMapper; + + @Autowired + private Validator validator; + + @Override + public List listPeople() { + List people = new ArrayList<>(); + try { + people = personRepository.findAll(); + } catch (Exception exception) { + log.error("Error retrieving all the people from the database: ", exception); + } + + if (people.isEmpty()) return new ArrayList<>(); + return people.stream() + .map(personMapper::toDto) + .collect(Collectors.toList()); + } + + @Override + public List getUnemployedPeopleWithClientId(Integer clientId) { + List people = new ArrayList<>(); + try { + people = personRepository.findAllByClientIsNullOrClient_Id(clientId); + } catch (Exception exception) { + log.error("Error retrieving all the people from the database: ", exception); + } + + if (people.isEmpty()) return new ArrayList<>(); + return people.stream() + .map(personMapper::toDto) + .collect(Collectors.toList()); + } + + @Override + public Integer createPerson(PersonDTO personDTO) { + Person person = personMapper.toEntity(personDTO); + try { + person = personRepository.save(person); + } catch (Exception exception) { + log.error("Error saving the person {} to the database: ", person, exception); + } + return person.getId(); + } + + @Override + public PersonDTO readPerson(Integer id) { + Person person = null; + try { + person = personRepository.getById(id); + } catch (Exception exception) { + log.error("Error retrieving the person by ID [{}] from the database: ", id, exception); + } + if (null == person) return null; + return personMapper.toDto(person); + } + + @Override + public PersonDTO updatePerson(PersonDTO personDTO) { + Person person = personMapper.toEntity(personDTO); + try { + person = personRepository.save(person); + } catch (Exception exception) { + log.error("Error updating the existing person {} in the database: ", person, exception); + return null; + } + return personMapper.toDto(person); + } + + @Override + public void deletePerson(Integer id) { + Person person = personRepository.getById(id); + if (null == person) throw new InvalidRequestException(String.format(PERSON_NOT_FOUND, id)); + try { + personRepository.delete(person); + } catch (Exception exception) { + log.error("Error deleting the existing person with ID [{}] in the database: ", id, exception); + } + } + + @Override + public List validatePerson(PersonDTO personDTO) { + Set> violations = validator.validate(personDTO); + List errors = new ArrayList<>(violations.size()); + for (ConstraintViolation violation : violations) { + errors.add(violation.getMessage()); + } + Collections.sort(errors); + return errors; + } + + @Override + public List updateContacts(List contactDTOs, Integer clientId, String clientName) { + List peopleToUpdate = new ArrayList<>(); + List existingContactDTOs = getContactsByClientId(clientId); + List contactDTOIds = contactDTOs.stream() + .map(PersonDTO::getId) + .collect(Collectors.toList()); + + // add new contacts to client + contactDTOs.forEach(contactDTO -> { + // update contactDTO + contactDTO.setClientId(clientId); + contactDTO.setClientName(clientName); + + // map contactDTO to person and add to peopleToUpdate list + Person person = personMapper.toEntity(contactDTO); + peopleToUpdate.add(person); + }); + + // delete existing contacts from client + existingContactDTOs.forEach(existingContactDTO -> { + if (!contactDTOIds.contains(existingContactDTO.getId())) { + Person person = personMapper.toEntity(existingContactDTO); + person.setClient(null); + peopleToUpdate.add(person); + } + }); + + personRepository.saveAll(peopleToUpdate); + return contactDTOs; + } + + @Override + public List getContactsByClientId(Integer clientId) { + List contacts = personRepository.findAllByClient_Id(clientId); + if (contacts.isEmpty()) return new ArrayList<>(); + return contacts.stream() + .map(personMapper::toDto) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/aquent/crudapp/utils/Constants.java b/src/main/java/com/aquent/crudapp/utils/Constants.java new file mode 100644 index 00000000..d4777c61 --- /dev/null +++ b/src/main/java/com/aquent/crudapp/utils/Constants.java @@ -0,0 +1,10 @@ +package com.aquent.crudapp.utils; + +public class Constants { + private Constants() {} + + public static final String CLIENT_NOT_FOUND = "Client with ID = '%s' not found. "; + public static final String PERSON_NOT_FOUND = "Person with ID = '%s' not found. "; + public static final String CLIENT_DELETED = "Client with ID = '%s' deleted"; + public static final String PERSON_DELETED = "Person with ID = '%s' deleted"; +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index c6ae0b46..2ee6a5bd 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -7,7 +7,16 @@ spring.thymeleaf.enabled=true spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html -spring.datasource.driver-class-name=org.h2.Driver -spring.datasource.url=jdbc:h2:mem:bootapp;DB_CLOSE_DELAY=-1 -spring.datasource.username=sa -spring.datasource.password= +spring.datasource.url=jdbc:mysql://localhost/aquent?createDatabaseIfNotExist=true&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true +spring.datasource.username=aquent +spring.datasource.password=aquentPassword + +#spring.datasource.driver-class-name=org.h2.Driver +#spring.datasource.url=jdbc:h2:mem:bootapp;DB_CLOSE_DELAY=-1 +#spring.datasource.username=sa +#spring.datasource.password= + +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true +logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 180faa11..43c7a80b 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,25 +1,10 @@ -INSERT INTO person ( - first_name, - last_name, - email_address, - street_address, - city, - state, - zip_code -) VALUES ( - 'John', - 'Smith', - 'fake1@aquent.com', - '123 Any St.', - 'Asheville', - 'NC', - '28801' -), ( - 'Jane', - 'Smith', - 'fake2@aquent.com', - '123 Any St.', - 'Asheville', - 'NC', - '28801' -); +INSERT INTO client (company_name, website_uri, phone_number, street_address, city, state, zip_code) +VALUES ('Aquent', 'aquent', '800-123-4567', '123 Any St.', 'Asheville', 'NC', '28801'), + ('Company A', 'company-a', '800-321-9756', '123 Any St.', 'Asheville', 'NC', '28801'), + ('Company B', 'company-b', '800-456-977', '123 Any St.', 'Asheville', 'NC', '28801'), + ('Company C', 'company-c', '123-432-9876', '456 Any St.', 'Asheville', 'NC', '28812'); + +INSERT INTO person (first_name, last_name, email_address, street_address, city, state, zip_code, client_id) +VALUES ('John', 'Smith', 'fake1@aquent.com', '123 Any St.', 'Asheville', 'NC', '28801', 1), + ('Jane', 'Smith', 'fake2@aquent.com', '123 Any St.', 'Asheville', 'NC', '28801', 2), + ('Anna', 'William', 'fake3@aquent.com', '217 Any St.', 'Asheville', 'NC', '28802', null); \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 90e05818..0f94efb6 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,10 +1,26 @@ -CREATE TABLE person ( - person_id integer IDENTITY, +CREATE TABLE IF NOT EXISTS client ( + id int NOT NULL AUTO_INCREMENT, + company_name varchar(50) NOT NULL, + website_uri varchar(50) NOT NULL, + phone_number varchar(20) NOT NULL, + street_address varchar(50) NOT NULL, + city varchar(50) NOT NULL, + state varchar(2) NOT NULL, + zip_code varchar(5) NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS person ( + id int NOT NULL AUTO_INCREMENT, first_name varchar(50) NOT NULL, last_name varchar(50) NOT NULL, email_address varchar(50) NOT NULL, street_address varchar(50) NOT NULL, city varchar(50) NOT NULL, state varchar(2) NOT NULL, - zip_code varchar(5) NOT NULL + zip_code varchar(5) NOT NULL, + client_id int, + PRIMARY KEY (id), + CONSTRAINT fk_person_client_id_client_id + FOREIGN KEY (client_id) REFERENCES client(id) ); diff --git a/src/main/resources/templates/person/create.html b/src/main/resources/templates/person/create.html deleted file mode 100644 index 9fc3f624..00000000 --- a/src/main/resources/templates/person/create.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - Create Person - - -

Create Person

- -

Please correct the following errors in your submission:

-
    - -
  • ${error}
  • -
    -
-
-
-
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- -
- - diff --git a/src/main/resources/templates/person/delete.html b/src/main/resources/templates/person/delete.html deleted file mode 100644 index b16879e8..00000000 --- a/src/main/resources/templates/person/delete.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - Delete Person - - -

Delete Person

-

You are about to delete the person : Are you sure?

-
- - - -
- - diff --git a/src/main/resources/templates/person/edit.html b/src/main/resources/templates/person/edit.html deleted file mode 100644 index 41ebd157..00000000 --- a/src/main/resources/templates/person/edit.html +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - Edit Person - - -

Edit Person

- -

Please correct the following errors in your submission:

-
    - -
  • ${error}
  • -
    -
-
-
- -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- -
- - diff --git a/src/main/resources/templates/person/list.html b/src/main/resources/templates/person/list.html deleted file mode 100644 index 10881e75..00000000 --- a/src/main/resources/templates/person/list.html +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - Person Listing - - -

Person Listing

-

Create New Person

- - - - - - - - - - - - - - - - - - - - -
First NameLast NameEmail AddressActions
FirstLastfirst.last@email.com - Edit Person - Delete Person -
-
- -

No results found.

-
- - diff --git a/src/test/java/com/aquent/crudapp/services/ClientServiceImplTest.java b/src/test/java/com/aquent/crudapp/services/ClientServiceImplTest.java new file mode 100644 index 00000000..6fb18d8b --- /dev/null +++ b/src/test/java/com/aquent/crudapp/services/ClientServiceImplTest.java @@ -0,0 +1,96 @@ +package com.aquent.crudapp.services; + +import com.aquent.crudapp.dtos.ClientDTO; +import com.aquent.crudapp.mappers.ClientMapper; +import com.aquent.crudapp.mappers.PersonMapper; +import com.aquent.crudapp.models.Client; +import com.aquent.crudapp.repositories.ClientRepository; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.junit4.SpringRunner; + +import javax.validation.Validator; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.*; + +@RunWith(SpringRunner.class) +public class ClientServiceImplTest { + @Mock + private ClientRepository clientRepository; + + @Mock + private ClientMapper clientMapper; + + @Mock + private Validator validator; + + @Mock + private PersonService personService; + + @Mock + private PersonMapper personMapper; + + @InjectMocks + private ClientServiceImpl clientService; + + private Client client; + private Client client2; + private List clients; + private ClientDTO clientDTO; + private ClientDTO clientDTO2; + private List expectedClientDTOList; + + @Before + public void init() { + MockitoAnnotations.openMocks(this.getClass()); + client = new Client(); + client.setId(10); + client.setCompanyName("Test 1"); + + client2 = new Client(); + client2.setId(11); + clients = Arrays.asList(client, client2); + + clientDTO = new ClientDTO(); + clientDTO.setId(10); + clientDTO.setCompanyName("Test 1"); + + clientDTO2 = new ClientDTO(); + clientDTO2.setId(11); + expectedClientDTOList = Arrays.asList(clientDTO, clientDTO2); + } + + @Test + public void test_listClients_return_emptyList() { + doReturn(new ArrayList<>()).when(clientRepository).findAll(); + List clientDTOList = clientService.listClients(); + assertTrue(clientDTOList.isEmpty()); + verify(clientRepository, times(1)).findAll(); + verifyNoMoreInteractions(clientRepository); + } + + @Test + public void test_listClients_return_list() { + doReturn(clients).when(clientRepository).findAll(); + doReturn(clientDTO).when(clientMapper).toDto(client); + doReturn(clientDTO2).when(clientMapper).toDto(client2); + List clientDTOList = clientService.listClients(); + assertEquals(expectedClientDTOList, clientDTOList); + assertEquals(2, clientDTOList.size()); + verify(clientRepository, times(1)).findAll(); + verify(clientMapper, times(2)).toDto(any()); + verifyNoMoreInteractions(clientRepository); + verifyNoMoreInteractions(clientMapper); + } + +}