feat: added notification pattern (#2629)

Co-authored-by: Ilkka Seppälä <iluwatar@users.noreply.github.com>
This commit is contained in:
sugavanesh
2024-03-09 18:16:46 +05:30
committed by GitHub
parent b2ca49a4e5
commit 249efd1e71
17 changed files with 849 additions and 0 deletions
@@ -0,0 +1,26 @@
package com.iluwatar;
import java.time.LocalDate;
/**
* The notification pattern captures information passed between layers, validates the information, and returns
* any errors to the presentation layer if needed.
*
* <p>In this code, this pattern is implemented through the example of a form being submitted to register
* a worker. The worker inputs their name, occupation, and date of birth to the RegisterWorkerForm (which acts
* as our presentation layer), and passes it to the RegisterWorker class (our domain layer) which validates it.
* Any errors caught by the domain layer are then passed back to the presentation layer through the
* RegisterWorkerDto.</p>
*/
public class App {
private static final String NAME = "";
private static final String OCCUPATION = "";
private static final LocalDate DATE_OF_BIRTH = LocalDate.of(2016, 7, 13);
public static void main(String[] args) {
var form = new RegisterWorkerForm(NAME, OCCUPATION, DATE_OF_BIRTH);
form.submit();
}
}
@@ -0,0 +1,16 @@
package com.iluwatar;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* Layer super type for all Data Transfer Objects.
* Also contains code for accessing our notification.
*/
@Getter
@NoArgsConstructor
public class DataTransferObject {
private final Notification notification = new Notification();
}
@@ -0,0 +1,26 @@
package com.iluwatar;
import java.util.ArrayList;
import java.util.List;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* The notification. Used for storing errors and any other methods
* that may be necessary for when we send information back to the
* presentation layer.
*/
@Getter
@NoArgsConstructor
public class Notification {
private final List<NotificationError> errors = new ArrayList<>();
public boolean hasErrors() {
return !this.errors.isEmpty();
}
public void addError(NotificationError error) {
this.errors.add(error);
}
}
@@ -0,0 +1,20 @@
package com.iluwatar;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* Error class for storing information on the error.
* Error ID is not necessary, but may be useful for serialisation.
*/
@Getter
@AllArgsConstructor
public class NotificationError {
private int errorId;
private String errorMessage;
@Override
public String toString() {
return "Error " + errorId + ": " + errorMessage;
}
}
@@ -0,0 +1,79 @@
package com.iluwatar;
import java.time.LocalDate;
import java.time.Period;
import lombok.extern.slf4j.Slf4j;
/**
* Class which handles actual internal logic and validation for worker registration.
* Part of the domain layer which collects information and sends it back to the presentation.
*/
@Slf4j
public class RegisterWorker extends ServerCommand {
static final int LEGAL_AGE = 18;
protected RegisterWorker(RegisterWorkerDto worker) {
super(worker);
}
/**
* Validates the data provided and adds it to the database in the backend.
*/
public void run() {
validate();
if (!super.getNotification().hasErrors()) {
LOGGER.info("Register worker in backend system");
}
}
/**
* Validates our data. Checks for any errors and if found, adds to notification.
*/
private void validate() {
var ourData = ((RegisterWorkerDto) this.data);
//check if any of submitted data is not given
// passing for empty value validation
fail(isNullOrBlank(ourData.getName()), RegisterWorkerDto.MISSING_NAME);
fail(isNullOrBlank(ourData.getOccupation()), RegisterWorkerDto.MISSING_OCCUPATION);
fail(isNullOrBlank(ourData.getDateOfBirth()), RegisterWorkerDto.MISSING_DOB);
if (isNullOrBlank(ourData.getDateOfBirth())) {
// If DOB is null or empty
fail(true, RegisterWorkerDto.MISSING_DOB);
} else {
// Validating age ( should be greater than or equal to 18 )
Period age = Period.between(ourData.getDateOfBirth(), LocalDate.now());
fail(age.getYears() < LEGAL_AGE, RegisterWorkerDto.DOB_TOO_SOON);
}
}
/**
* Validates for null/empty value.
*
* @param obj any object
* @return boolean
*/
protected boolean isNullOrBlank(Object obj) {
if (obj == null) {
return true;
}
if (obj instanceof String) {
return ((String) obj).trim().isEmpty();
}
return false;
}
/**
* If a condition is met, adds the error to our notification.
*
* @param condition condition to check for.
* @param error error to add if condition met.
*/
protected void fail(boolean condition, NotificationError error) {
if (condition) {
super.getNotification().addError(error);
}
}
}
@@ -0,0 +1,59 @@
package com.iluwatar;
import java.time.LocalDate;
import lombok.Getter;
import lombok.Setter;
/**
* Data transfer object which stores information about the worker. This is carried between
* objects and layers to reduce the number of method calls made.
*/
@Getter
@Setter
public class RegisterWorkerDto extends DataTransferObject {
private String name;
private String occupation;
private LocalDate dateOfBirth;
/**
* Error for when name field is blank or missing.
*/
public static final NotificationError MISSING_NAME =
new NotificationError(1, "Name is missing");
/**
* Error for when occupation field is blank or missing.
*/
public static final NotificationError MISSING_OCCUPATION =
new NotificationError(2, "Occupation is missing");
/**
* Error for when date of birth field is blank or missing.
*/
public static final NotificationError MISSING_DOB =
new NotificationError(3, "Date of birth is missing");
/**
* Error for when date of birth is less than 18 years ago.
*/
public static final NotificationError DOB_TOO_SOON =
new NotificationError(4, "Worker registered must be over 18");
protected RegisterWorkerDto() {
super();
}
/**
* Simple set up function for capturing our worker information.
*
* @param name Name of the worker
* @param occupation occupation of the worker
* @param dateOfBirth Date of Birth of the worker
*/
public void setupWorkerDto(String name, String occupation, LocalDate dateOfBirth) {
this.name = name;
this.occupation = occupation;
this.dateOfBirth = dateOfBirth;
}
}
@@ -0,0 +1,66 @@
package com.iluwatar;
import java.time.LocalDate;
import lombok.extern.slf4j.Slf4j;
/**
* The form submitted by the user, part of the presentation layer,
* linked to the domain layer through a data transfer object and
* linked to the service layer directly.
*/
@Slf4j
public class RegisterWorkerForm {
String name;
String occupation;
LocalDate dateOfBirth;
RegisterWorkerDto worker;
RegisterWorkerService service = new RegisterWorkerService();
/**
* Constructor.
*
* @param name Name of the worker
* @param occupation occupation of the worker
* @param dateOfBirth Date of Birth of the worker
*/
public RegisterWorkerForm(String name, String occupation, LocalDate dateOfBirth) {
this.name = name;
this.occupation = occupation;
this.dateOfBirth = dateOfBirth;
}
/**
* Attempts to submit the form for registering a worker.
*/
public void submit() {
//Transmit information to our transfer object to communicate between layers
saveToWorker();
//call the service layer to register our worker
service.registerWorker(worker);
//check for any errors
if (worker.getNotification().hasErrors()) {
indicateErrors();
LOGGER.info("Not registered, see errors");
} else {
LOGGER.info("Registration Succeeded");
}
}
/**
* Saves worker information to the data transfer object.
*/
private void saveToWorker() {
worker = new RegisterWorkerDto();
worker.setName(name);
worker.setOccupation(occupation);
worker.setDateOfBirth(dateOfBirth);
}
/**
* Check for any errors with form submission and show them to the user.
*/
public void indicateErrors() {
worker.getNotification().getErrors().forEach(error -> LOGGER.error(error.toString()));
}
}
@@ -0,0 +1,18 @@
package com.iluwatar;
/**
* Service used to register a worker.
* This represents the basic framework of a service layer which can be built upon.
*/
public class RegisterWorkerService {
/**
* Creates and runs a command object to do the work needed,
* in this case, register a worker in the system.
*
* @param registration worker to be registered if possible
*/
public void registerWorker(RegisterWorkerDto registration) {
var cmd = new RegisterWorker(registration);
cmd.run();
}
}
@@ -0,0 +1,21 @@
package com.iluwatar;
import lombok.AllArgsConstructor;
/**
* Stores the dto and access the notification within it.
* Acting as a layer supertype in this instance for the domain layer.
*/
@AllArgsConstructor
public class ServerCommand {
protected DataTransferObject data;
/**
* Basic getter to extract information from our data.
*
* @return the notification stored within the data
*/
public Notification getNotification() {
return data.getNotification();
}
}
@@ -0,0 +1,14 @@
package com.iluwatar;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
class AppTest {
@Test
void shouldExecuteApplicationWithoutException() {
assertDoesNotThrow(() -> App.main(new String[]{}));
}
}
@@ -0,0 +1,53 @@
package com.iluwatar;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.time.LocalDate;
import java.util.logging.Logger;
import static org.junit.jupiter.api.Assertions.*;
class RegisterWorkerFormTest {
private RegisterWorkerForm registerWorkerForm;
@Test
void submitSuccessfully() {
// Ensure the worker is null initially
registerWorkerForm = new RegisterWorkerForm("John Doe", "Engineer", LocalDate.of(1990, 1, 1));
assertNull(registerWorkerForm.worker);
// Submit the form
registerWorkerForm.submit();
// Verify that the worker is not null after submission
assertNotNull(registerWorkerForm.worker);
// Verify that the worker's properties are set correctly
assertEquals("John Doe", registerWorkerForm.worker.getName());
assertEquals("Engineer", registerWorkerForm.worker.getOccupation());
assertEquals(LocalDate.of(1990, 1, 1), registerWorkerForm.worker.getDateOfBirth());
}
@Test
void submitWithErrors() {
// Set up the worker with a notification containing errors
registerWorkerForm = new RegisterWorkerForm(null, null, null);
// Submit the form
registerWorkerForm.submit();
// Verify that the worker's properties remain unchanged
assertNull(registerWorkerForm.worker.getName());
assertNull(registerWorkerForm.worker.getOccupation());
assertNull(registerWorkerForm.worker.getDateOfBirth());
// Verify the presence of errors
assertEquals(registerWorkerForm.worker.getNotification().getErrors().size(), 4);
}
}
@@ -0,0 +1,90 @@
package com.iluwatar;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import java.time.LocalDate;
import static org.junit.jupiter.api.Assertions.*;
@Slf4j
class RegisterWorkerTest {
@Test
void runSuccessfully() {
RegisterWorkerDto validWorkerDto = createValidWorkerDto();
validWorkerDto.setupWorkerDto("name", "occupation", LocalDate.of(2000, 12, 1));
RegisterWorker registerWorker = new RegisterWorker(validWorkerDto);
// Run the registration process
registerWorker.run();
// Verify that there are no errors in the notification
assertFalse(registerWorker.getNotification().hasErrors());
}
@Test
void runWithMissingName() {
RegisterWorkerDto workerDto = createValidWorkerDto();
workerDto.setupWorkerDto(null, "occupation", LocalDate.of(2000, 12, 1));
RegisterWorker registerWorker = new RegisterWorker(workerDto);
// Run the registration process
registerWorker.run();
// Verify that the notification contains the missing name error
assertTrue(registerWorker.getNotification().hasErrors());
assertTrue(registerWorker.getNotification().getErrors().contains(RegisterWorkerDto.MISSING_NAME));
assertEquals(registerWorker.getNotification().getErrors().size(), 1);
}
@Test
void runWithMissingOccupation() {
RegisterWorkerDto workerDto = createValidWorkerDto();
workerDto.setupWorkerDto("name", null, LocalDate.of(2000, 12, 1));
RegisterWorker registerWorker = new RegisterWorker(workerDto);
// Run the registration process
registerWorker.run();
// Verify that the notification contains the missing occupation error
assertTrue(registerWorker.getNotification().hasErrors());
assertTrue(registerWorker.getNotification().getErrors().contains(RegisterWorkerDto.MISSING_OCCUPATION));
assertEquals(registerWorker.getNotification().getErrors().size(), 1);
}
@Test
void runWithMissingDOB() {
RegisterWorkerDto workerDto = createValidWorkerDto();
workerDto.setupWorkerDto("name", "occupation", null);
RegisterWorker registerWorker = new RegisterWorker(workerDto);
// Run the registration process
registerWorker.run();
// Verify that the notification contains the missing DOB error
assertTrue(registerWorker.getNotification().hasErrors());
assertTrue(registerWorker.getNotification().getErrors().contains(RegisterWorkerDto.MISSING_DOB));
assertEquals(registerWorker.getNotification().getErrors().size(), 2);
}
@Test
void runWithUnderageDOB() {
RegisterWorkerDto workerDto = createValidWorkerDto();
workerDto.setDateOfBirth(LocalDate.now().minusYears(17)); // Under 18
workerDto.setupWorkerDto("name", "occupation", LocalDate.now().minusYears(17));
RegisterWorker registerWorker = new RegisterWorker(workerDto);
// Run the registration process
registerWorker.run();
// Verify that the notification contains the underage DOB error
assertTrue(registerWorker.getNotification().hasErrors());
assertTrue(registerWorker.getNotification().getErrors().contains(RegisterWorkerDto.DOB_TOO_SOON));
assertEquals(registerWorker.getNotification().getErrors().size(), 1);
}
private RegisterWorkerDto createValidWorkerDto() {
return new RegisterWorkerDto();
}
}