From 7866275b10ac98708fac1131c1a490fae88b1cf8 Mon Sep 17 00:00:00 2001 From: mperor <mpietryga93@gmail.com> Date: Tue, 1 Apr 2025 14:24:28 +0200 Subject: [PATCH] Implement validator using Visitor pattern and move Customer to a dedicated package --- .../behavioral/visitor/CorporateCustomer.java | 4 --- .../pattern/behavioral/visitor/Letter.java | 4 --- .../behavioral/visitor/PremiumCustomer.java | 4 --- .../behavioral/visitor/RegularCustomer.java | 4 --- .../visitor/customer/CorporateCustomer.java | 4 +++ .../visitor/{ => customer}/Customer.java | 2 +- .../{ => customer}/CustomerVisitor.java | 2 +- .../visitor/{ => customer}/DiscountLevel.java | 2 +- .../{ => customer}/DiscountProvider.java | 2 +- .../InvitationLetterGenerator.java | 2 +- .../behavioral/visitor/customer/Letter.java | 4 +++ .../visitor/customer/PremiumCustomer.java | 4 +++ .../visitor/customer/RegularCustomer.java | 4 +++ ...AggregatingValidationExceptionHandler.java | 22 ++++++++++++++ .../behavioral/visitor/validator/Email.java | 29 +++++++++++++++++++ .../visitor/validator/Password.java | 25 ++++++++++++++++ .../ThrowingValidationExceptionHandler.java | 9 ++++++ .../validator/ValidationException.java | 4 +++ .../validator/ValidationExceptionHandler.java | 6 ++++ .../visitor/validator/Validator.java | 6 ++++ .../{ => customer}/CustomerVisitorTest.java | 2 +- .../validator/ValidatorVisitorTest.java | 26 +++++++++++++++++ 22 files changed, 149 insertions(+), 22 deletions(-) delete mode 100644 DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/CorporateCustomer.java delete mode 100644 DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/Letter.java delete mode 100644 DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/PremiumCustomer.java delete mode 100644 DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/RegularCustomer.java create mode 100644 DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/CorporateCustomer.java rename DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/{ => customer}/Customer.java (73%) rename DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/{ => customer}/CustomerVisitor.java (51%) rename DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/{ => customer}/DiscountLevel.java (71%) rename DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/{ => customer}/DiscountProvider.java (86%) rename DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/{ => customer}/InvitationLetterGenerator.java (88%) create mode 100644 DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/Letter.java create mode 100644 DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/PremiumCustomer.java create mode 100644 DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/RegularCustomer.java create mode 100644 DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/AggregatingValidationExceptionHandler.java create mode 100644 DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/Email.java create mode 100644 DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/Password.java create mode 100644 DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/ThrowingValidationExceptionHandler.java create mode 100644 DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/ValidationException.java create mode 100644 DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/ValidationExceptionHandler.java create mode 100644 DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/Validator.java rename DesignPatterns/src/test/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/{ => customer}/CustomerVisitorTest.java (93%) create mode 100644 DesignPatterns/src/test/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/ValidatorVisitorTest.java diff --git a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/CorporateCustomer.java b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/CorporateCustomer.java deleted file mode 100644 index 19fe58e..0000000 --- a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/CorporateCustomer.java +++ /dev/null @@ -1,4 +0,0 @@ -package pl.mperor.lab.java.design.pattern.behavioral.visitor; - -record CorporateCustomer() implements Customer { -} diff --git a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/Letter.java b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/Letter.java deleted file mode 100644 index f334633..0000000 --- a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/Letter.java +++ /dev/null @@ -1,4 +0,0 @@ -package pl.mperor.lab.java.design.pattern.behavioral.visitor; - -record Letter(String content) { -} diff --git a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/PremiumCustomer.java b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/PremiumCustomer.java deleted file mode 100644 index 082024d..0000000 --- a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/PremiumCustomer.java +++ /dev/null @@ -1,4 +0,0 @@ -package pl.mperor.lab.java.design.pattern.behavioral.visitor; - -record PremiumCustomer() implements Customer { -} diff --git a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/RegularCustomer.java b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/RegularCustomer.java deleted file mode 100644 index c1e00c7..0000000 --- a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/RegularCustomer.java +++ /dev/null @@ -1,4 +0,0 @@ -package pl.mperor.lab.java.design.pattern.behavioral.visitor; - -record RegularCustomer() implements Customer { -} diff --git a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/CorporateCustomer.java b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/CorporateCustomer.java new file mode 100644 index 0000000..a96c2c9 --- /dev/null +++ b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/CorporateCustomer.java @@ -0,0 +1,4 @@ +package pl.mperor.lab.java.design.pattern.behavioral.visitor.customer; + +record CorporateCustomer() implements Customer { +} diff --git a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/Customer.java b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/Customer.java similarity index 73% rename from DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/Customer.java rename to DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/Customer.java index 9af89f3..0074729 100644 --- a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/Customer.java +++ b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/Customer.java @@ -1,4 +1,4 @@ -package pl.mperor.lab.java.design.pattern.behavioral.visitor; +package pl.mperor.lab.java.design.pattern.behavioral.visitor.customer; public sealed interface Customer permits CorporateCustomer, RegularCustomer, PremiumCustomer { diff --git a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/CustomerVisitor.java b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/CustomerVisitor.java similarity index 51% rename from DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/CustomerVisitor.java rename to DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/CustomerVisitor.java index 7e2844f..f9c866d 100644 --- a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/CustomerVisitor.java +++ b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/CustomerVisitor.java @@ -1,4 +1,4 @@ -package pl.mperor.lab.java.design.pattern.behavioral.visitor; +package pl.mperor.lab.java.design.pattern.behavioral.visitor.customer; public interface CustomerVisitor<T> { diff --git a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/DiscountLevel.java b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/DiscountLevel.java similarity index 71% rename from DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/DiscountLevel.java rename to DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/DiscountLevel.java index 27bdd1d..b0bc5f6 100644 --- a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/DiscountLevel.java +++ b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/DiscountLevel.java @@ -1,4 +1,4 @@ -package pl.mperor.lab.java.design.pattern.behavioral.visitor; +package pl.mperor.lab.java.design.pattern.behavioral.visitor.customer; enum DiscountLevel { BRONZE(10), diff --git a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/DiscountProvider.java b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/DiscountProvider.java similarity index 86% rename from DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/DiscountProvider.java rename to DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/DiscountProvider.java index b3d8546..41cc9f3 100644 --- a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/DiscountProvider.java +++ b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/DiscountProvider.java @@ -1,4 +1,4 @@ -package pl.mperor.lab.java.design.pattern.behavioral.visitor; +package pl.mperor.lab.java.design.pattern.behavioral.visitor.customer; public class DiscountProvider implements CustomerVisitor<DiscountLevel> { diff --git a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/InvitationLetterGenerator.java b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/InvitationLetterGenerator.java similarity index 88% rename from DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/InvitationLetterGenerator.java rename to DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/InvitationLetterGenerator.java index 1e15749..cb7091d 100644 --- a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/InvitationLetterGenerator.java +++ b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/InvitationLetterGenerator.java @@ -1,4 +1,4 @@ -package pl.mperor.lab.java.design.pattern.behavioral.visitor; +package pl.mperor.lab.java.design.pattern.behavioral.visitor.customer; public class InvitationLetterGenerator implements CustomerVisitor<Letter> { diff --git a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/Letter.java b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/Letter.java new file mode 100644 index 0000000..7261aba --- /dev/null +++ b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/Letter.java @@ -0,0 +1,4 @@ +package pl.mperor.lab.java.design.pattern.behavioral.visitor.customer; + +record Letter(String content) { +} diff --git a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/PremiumCustomer.java b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/PremiumCustomer.java new file mode 100644 index 0000000..af644f9 --- /dev/null +++ b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/PremiumCustomer.java @@ -0,0 +1,4 @@ +package pl.mperor.lab.java.design.pattern.behavioral.visitor.customer; + +record PremiumCustomer() implements Customer { +} diff --git a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/RegularCustomer.java b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/RegularCustomer.java new file mode 100644 index 0000000..794abf0 --- /dev/null +++ b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/RegularCustomer.java @@ -0,0 +1,4 @@ +package pl.mperor.lab.java.design.pattern.behavioral.visitor.customer; + +record RegularCustomer() implements Customer { +} diff --git a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/AggregatingValidationExceptionHandler.java b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/AggregatingValidationExceptionHandler.java new file mode 100644 index 0000000..fb200ab --- /dev/null +++ b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/AggregatingValidationExceptionHandler.java @@ -0,0 +1,22 @@ +package pl.mperor.lab.java.design.pattern.behavioral.visitor.validator; + +import java.util.ArrayList; +import java.util.List; + +class AggregatingValidationExceptionHandler implements ValidationExceptionHandler { + + private List<ValidationException> errors = new ArrayList<>(); + + @Override + public void handle(ValidationException exception) { + errors.add(exception); + } + + boolean hasErrors() { + return !errors.isEmpty(); + } + + public List<ValidationException> getErrors() { + return List.copyOf(errors); + } +} diff --git a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/Email.java b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/Email.java new file mode 100644 index 0000000..a1cdffc --- /dev/null +++ b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/Email.java @@ -0,0 +1,29 @@ +package pl.mperor.lab.java.design.pattern.behavioral.visitor.validator; + +import java.util.regex.Pattern; + +record Email(String address) { + + public Email { + test(address, new ThrowingValidationExceptionHandler()); + } + + public static void test(String address, ValidationExceptionHandler handler) { + new EmailValidator().validate(address, handler); + } + + static class EmailValidator implements Validator<String> { + + public static final Pattern FORMAT_PATTERN = Pattern.compile("^[a-zA-Z0-9+_.-]+@[a-zA-Z0-9.-]+$"); + + @Override + public void validate(String address, ValidationExceptionHandler validationExceptionHandler) { + if (FORMAT_PATTERN.asMatchPredicate().negate().test(address)) { + validationExceptionHandler.handle(new InvalidFormatException()); + } + } + } + + static class InvalidFormatException extends ValidationException { + } +} diff --git a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/Password.java b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/Password.java new file mode 100644 index 0000000..e55688d --- /dev/null +++ b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/Password.java @@ -0,0 +1,25 @@ +package pl.mperor.lab.java.design.pattern.behavioral.visitor.validator; + +record Password(String value) { + + public Password { + Password.test(value, new ThrowingValidationExceptionHandler()); + } + + public static void test(String password, ValidationExceptionHandler validationExceptionHandler) { + new PasswordValidator().validate(password, validationExceptionHandler); + } + + static class PasswordValidator implements Validator<String> { + + @Override + public void validate(String password, ValidationExceptionHandler validationExceptionHandler) { + if (password.length() < 8 || password.length() > 16) { + validationExceptionHandler.handle(new InvalidLengthException()); + } + } + } + + static class InvalidLengthException extends ValidationException { + } +} diff --git a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/ThrowingValidationExceptionHandler.java b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/ThrowingValidationExceptionHandler.java new file mode 100644 index 0000000..0500ece --- /dev/null +++ b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/ThrowingValidationExceptionHandler.java @@ -0,0 +1,9 @@ +package pl.mperor.lab.java.design.pattern.behavioral.visitor.validator; + +class ThrowingValidationExceptionHandler implements ValidationExceptionHandler{ + + @Override + public void handle(ValidationException exception) { + throw exception; + } +} diff --git a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/ValidationException.java b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/ValidationException.java new file mode 100644 index 0000000..d00a46d --- /dev/null +++ b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/ValidationException.java @@ -0,0 +1,4 @@ +package pl.mperor.lab.java.design.pattern.behavioral.visitor.validator; + +abstract class ValidationException extends RuntimeException { +} diff --git a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/ValidationExceptionHandler.java b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/ValidationExceptionHandler.java new file mode 100644 index 0000000..32c6557 --- /dev/null +++ b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/ValidationExceptionHandler.java @@ -0,0 +1,6 @@ +package pl.mperor.lab.java.design.pattern.behavioral.visitor.validator; + +interface ValidationExceptionHandler { + + void handle(ValidationException exception); +} diff --git a/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/Validator.java b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/Validator.java new file mode 100644 index 0000000..88b6390 --- /dev/null +++ b/DesignPatterns/src/main/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/Validator.java @@ -0,0 +1,6 @@ +package pl.mperor.lab.java.design.pattern.behavioral.visitor.validator; + +interface Validator<T> { + + void validate(T toValid, ValidationExceptionHandler validationExceptionHandler); +} diff --git a/DesignPatterns/src/test/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/CustomerVisitorTest.java b/DesignPatterns/src/test/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/CustomerVisitorTest.java similarity index 93% rename from DesignPatterns/src/test/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/CustomerVisitorTest.java rename to DesignPatterns/src/test/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/CustomerVisitorTest.java index 782fb4e..fd181f4 100644 --- a/DesignPatterns/src/test/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/CustomerVisitorTest.java +++ b/DesignPatterns/src/test/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/customer/CustomerVisitorTest.java @@ -1,4 +1,4 @@ -package pl.mperor.lab.java.design.pattern.behavioral.visitor; +package pl.mperor.lab.java.design.pattern.behavioral.visitor.customer; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; diff --git a/DesignPatterns/src/test/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/ValidatorVisitorTest.java b/DesignPatterns/src/test/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/ValidatorVisitorTest.java new file mode 100644 index 0000000..1a151b0 --- /dev/null +++ b/DesignPatterns/src/test/java/pl/mperor/lab/java/design/pattern/behavioral/visitor/validator/ValidatorVisitorTest.java @@ -0,0 +1,26 @@ +package pl.mperor.lab.java.design.pattern.behavioral.visitor.validator; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class ValidatorVisitorTest { + + @Test + public void testValidatePasswordAndEmailWithExceptionAggregation() { + var aggregationHandler = new AggregatingValidationExceptionHandler(); + Password.test("xyz", aggregationHandler); + Email.test("user1$gmail.com", aggregationHandler); + Assertions.assertTrue(aggregationHandler.hasErrors()); + Assertions.assertEquals(aggregationHandler.getErrors().size(), 2); + } + + @Test + public void testValidatePasswordAndEmailWithExceptionThrowing() { + Assertions.assertThrows(Password.InvalidLengthException.class, () -> new Password("xyz")); + Assertions.assertThrows(Email.InvalidFormatException.class, () -> new Email("user1$gmail.com")); + + var throwingHandler = new ThrowingValidationExceptionHandler(); + Assertions.assertThrows(Password.InvalidLengthException.class, () -> Password.test("xyz", throwingHandler)); + Assertions.assertThrows(Email.InvalidFormatException.class, () -> Email.test("user1$gmail.com", throwingHandler)); + } +} \ No newline at end of file