diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..63e9001 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 8159baf..01001da 100644 --- a/pom.xml +++ b/pom.xml @@ -3,17 +3,49 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.2 + + + it.aboutbits spring-boot-toolbox BUILD-SNAPSHOT Utility library for Spring Boot projects. + jar 21 + + org.springframework.boot + spring-boot-starter-validation + + + + + org.projectlombok + lombok + true + + + + + + commons-validator + commons-validator + 1.9.0 + + + + org.springframework.boot + spring-boot-starter-test + diff --git a/src/main/java/it/aboutbits/springboot/toolbox/type/EmailAddress.java b/src/main/java/it/aboutbits/springboot/toolbox/type/EmailAddress.java new file mode 100644 index 0000000..4c2869b --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/type/EmailAddress.java @@ -0,0 +1,37 @@ +package it.aboutbits.springboot.toolbox.type; + +import it.aboutbits.springboot.toolbox.validation.util.EmailAddressValidator; +import lombok.NonNull; + +/** + * A record representing an email address, validated and stored in lowercase. + *

+ * The EmailAddress record ensures that the provided email address string + * is in a valid format and automatically converts it to lowercase for + * consistent internal representation. It implements the Comparable + * interface to allow comparison between different email addresses. + *

+ * + * @param value the email address string + * @throws IllegalArgumentException if the provided email address is not in a valid format + */ +public record EmailAddress(String value) implements Comparable { + public EmailAddress(String value) { + if (value == null || EmailAddressValidator.isNotValid(value)) { + throw new IllegalArgumentException("Value is not a valid email address: " + value); + } + + this.value = value.toLowerCase(); + } + + @NonNull + @Override + public String toString() { + return value; + } + + @Override + public int compareTo(@NonNull EmailAddress o) { + return value().compareTo(o.value()); + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/type/Iban.java b/src/main/java/it/aboutbits/springboot/toolbox/type/Iban.java new file mode 100644 index 0000000..0098da5 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/type/Iban.java @@ -0,0 +1,48 @@ +package it.aboutbits.springboot.toolbox.type; + +import it.aboutbits.springboot.toolbox.validation.util.IbanValidator; +import lombok.NonNull; + +import java.util.Optional; + +/** + * The {@code Iban} class represents an International Bank Account Number (IBAN) and provides functionality for validation + * and comparison of IBANs. It ensures the valid format of the IBAN according to predefined rules. + * + * @param value the IBAN value, which must be a valid and non-null string + */ +public record Iban(String value) implements Comparable { + public Iban(String value) { + if (value == null || IbanValidator.isNotValid(value.toUpperCase())) { + throw new IllegalArgumentException("Value is not a valid IBAN: " + value); + } + + this.value = value.toUpperCase(); + } + + @NonNull + @Override + public String toString() { + return value; + } + + /** + * Retrieves the ABI (Bank Branch Code) if the IBAN belongs to an Italian bank. + * + * @return An {@code Optional} containing the ABI value if the IBAN is Italian, or an empty {@code Optional} otherwise. + */ + @NonNull + public Optional getAbiIfItalian() { + var iban = value(); + + if (!iban.startsWith("IT")) { + return Optional.empty(); + } + return Optional.of(iban.substring(5, 10)); + } + + @Override + public int compareTo(@NonNull Iban o) { + return value().compareTo(o.value()); + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/type/ScaledBigDecimal.java b/src/main/java/it/aboutbits/springboot/toolbox/type/ScaledBigDecimal.java new file mode 100644 index 0000000..3ffc5ec --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/type/ScaledBigDecimal.java @@ -0,0 +1,231 @@ +package it.aboutbits.springboot.toolbox.type; + +import lombok.NonNull; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.MathContext; +import java.math.RoundingMode; + +/** + * A record that represents a scaled BigDecimal with a fixed MathContext. + * This class provides various constructors for creating instances with different numerical types. + * It also provides a set of arithmetic operations that return new instances with the appropriate scale and rounding. + */ +public record ScaledBigDecimal( + @NonNull BigDecimal value +) implements Comparable { + private static final MathContext MATH_CONTEXT = new MathContext(15, RoundingMode.HALF_UP); + + public static final ScaledBigDecimal ZERO = new ScaledBigDecimal(0); + public static final ScaledBigDecimal ONE = new ScaledBigDecimal(1); + public static final ScaledBigDecimal TWO = new ScaledBigDecimal(2); + public static final ScaledBigDecimal TEN = new ScaledBigDecimal(10); + + public ScaledBigDecimal(@NonNull BigDecimal value) { + this.value = value.setScale(MATH_CONTEXT.getPrecision(), MATH_CONTEXT.getRoundingMode()); + } + + public ScaledBigDecimal(int value) { + this(new BigDecimal(value, MATH_CONTEXT)); + } + + public ScaledBigDecimal(@NonNull Integer value) { + this(new BigDecimal(value, MATH_CONTEXT)); + } + + public ScaledBigDecimal(@NonNull BigInteger value) { + this(new BigDecimal(value, MATH_CONTEXT)); + } + + public ScaledBigDecimal(long value) { + this(new BigDecimal(value, MATH_CONTEXT)); + } + + public ScaledBigDecimal(@NonNull Long value) { + this(new BigDecimal(value, MATH_CONTEXT)); + } + + public ScaledBigDecimal(float value) { + this(new BigDecimal(String.valueOf(value), MATH_CONTEXT)); + } + + public ScaledBigDecimal(@NonNull Float value) { + this(new BigDecimal(String.valueOf(value), MATH_CONTEXT)); + } + + public ScaledBigDecimal(double value) { + this(new BigDecimal(String.valueOf(value), MATH_CONTEXT)); + } + + public ScaledBigDecimal(@NonNull Double value) { + this(new BigDecimal(String.valueOf(value), MATH_CONTEXT)); + } + + public ScaledBigDecimal(@NonNull String value) { + this(new BigDecimal(value, MATH_CONTEXT)); + } + + public static ScaledBigDecimal valueOf(int value) { + return new ScaledBigDecimal(value); + } + + public static ScaledBigDecimal valueOf(@NonNull Integer value) { + return new ScaledBigDecimal(value); + } + + public static ScaledBigDecimal valueOf(@NonNull BigInteger value) { + return new ScaledBigDecimal(value); + } + + public static ScaledBigDecimal valueOf(long value) { + return new ScaledBigDecimal(value); + } + + public static ScaledBigDecimal valueOf(@NonNull Long value) { + return new ScaledBigDecimal(value); + } + + public static ScaledBigDecimal valueOf(float value) { + return new ScaledBigDecimal(value); + } + + public static ScaledBigDecimal valueOf(@NonNull Float value) { + return new ScaledBigDecimal(value); + } + + public static ScaledBigDecimal valueOf(double value) { + return new ScaledBigDecimal(value); + } + + public static ScaledBigDecimal valueOf(@NonNull Double value) { + return new ScaledBigDecimal(value); + } + + public static ScaledBigDecimal valueOf(@NonNull String value) { + return new ScaledBigDecimal(value); + } + + @NonNull + public ScaledBigDecimal add(@NonNull ScaledBigDecimal other) { + return new ScaledBigDecimal(this.value().add(other.value())); + } + + @NonNull + public ScaledBigDecimal subtract(@NonNull ScaledBigDecimal other) { + return new ScaledBigDecimal(this.value().subtract(other.value())); + } + + @NonNull + public ScaledBigDecimal multiply(@NonNull ScaledBigDecimal other) { + return new ScaledBigDecimal(this.value().multiply(other.value())); + } + + @NonNull + public ScaledBigDecimal divide(@NonNull ScaledBigDecimal other) { + return new ScaledBigDecimal(this.value().divide(other.value(), MATH_CONTEXT)); + } + + @NonNull + public ScaledBigDecimal add(@NonNull BigDecimal other) { + return new ScaledBigDecimal(this.value().add(other)); + } + + @NonNull + public ScaledBigDecimal subtract(@NonNull BigDecimal other) { + return new ScaledBigDecimal(this.value().subtract(other)); + } + + @NonNull + public ScaledBigDecimal multiply(@NonNull BigDecimal other) { + return new ScaledBigDecimal(this.value().multiply(other)); + } + + @NonNull + public ScaledBigDecimal divide(@NonNull BigDecimal other) { + return new ScaledBigDecimal(this.value().divide(other, MATH_CONTEXT)); + } + + @NonNull + public ScaledBigDecimal remainder(@NonNull ScaledBigDecimal divisor) { + return new ScaledBigDecimal(this.value().remainder(divisor.value(), MATH_CONTEXT)); + } + + @NonNull + public ScaledBigDecimal remainder(@NonNull BigDecimal divisor) { + return new ScaledBigDecimal(this.value().remainder(divisor, MATH_CONTEXT)); + } + + @NonNull + public ScaledBigDecimal sqrt() { + return new ScaledBigDecimal(this.value().sqrt(MATH_CONTEXT)); + } + + @NonNull + public ScaledBigDecimal pow(int n) { + return new ScaledBigDecimal(this.value().pow(n, MATH_CONTEXT)); + } + + @NonNull + public ScaledBigDecimal abs() { + return new ScaledBigDecimal(this.value().abs(MATH_CONTEXT)); + } + + @NonNull + public ScaledBigDecimal negate() { + return new ScaledBigDecimal(this.value().negate(MATH_CONTEXT)); + } + + @NonNull + public ScaledBigDecimal min(@NonNull ScaledBigDecimal val) { + return new ScaledBigDecimal(this.value().min(val.value())); + } + + @NonNull + public ScaledBigDecimal min(@NonNull BigDecimal val) { + return new ScaledBigDecimal(this.value().min(val)); + } + + @NonNull + public ScaledBigDecimal max(@NonNull ScaledBigDecimal val) { + return new ScaledBigDecimal(this.value().max(val.value())); + } + + @NonNull + public ScaledBigDecimal max(@NonNull BigDecimal val) { + return new ScaledBigDecimal(this.value().max(val)); + } + + @NonNull + public BigDecimal toBigDecimal(int scale) { + return this.value().setScale(scale, RoundingMode.HALF_UP); + } + + @NonNull + public ScaledBigDecimal roundToScale(int scale) { + return new ScaledBigDecimal(this.value().setScale(scale, RoundingMode.HALF_UP)); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o instanceof ScaledBigDecimal other) { + return this.value().compareTo(other.value()) == 0; + } + + return false; + } + + @Override + public int hashCode() { + return value().hashCode(); + } + + @Override + public int compareTo(@NonNull ScaledBigDecimal o) { + return this.value().compareTo(o.value()); + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/validation/util/EmailAddressValidator.java b/src/main/java/it/aboutbits/springboot/toolbox/validation/util/EmailAddressValidator.java new file mode 100644 index 0000000..62ed128 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/validation/util/EmailAddressValidator.java @@ -0,0 +1,27 @@ +package it.aboutbits.springboot.toolbox.validation.util; + +import org.apache.commons.validator.routines.EmailValidator; + +public final class EmailAddressValidator { + private static final EmailValidator EMAIL_VALIDATOR = EmailValidator.getInstance(); + + private EmailAddressValidator() { + } + + /** + * Validates if the provided email address is in a proper format. + * + * @param email the email address to validate + * @return true if the email address is valid, false otherwise + */ + public static boolean isValid(String email) { + if (email == null) { + return false; + } + return EMAIL_VALIDATOR.isValid(email); + } + + public static boolean isNotValid(String email) { + return !isValid(email); + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/validation/util/IbanValidator.java b/src/main/java/it/aboutbits/springboot/toolbox/validation/util/IbanValidator.java new file mode 100644 index 0000000..b3de1a1 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/validation/util/IbanValidator.java @@ -0,0 +1,27 @@ +package it.aboutbits.springboot.toolbox.validation.util; + +import org.apache.commons.validator.routines.IBANValidator; + +public final class IbanValidator { + private static final IBANValidator IBAN_VALIDATOR = new IBANValidator(); + + private IbanValidator() { + } + + /** + * Validates whether the given IBAN (International Bank Account Number) is valid according to predefined rules. + * + * @param iban the IBAN to be validated + * @return true if the IBAN is valid, false otherwise + */ + public static boolean isValid(String iban) { + if (iban == null) { + return false; + } + return IBAN_VALIDATOR.isValid(iban); + } + + public static boolean isNotValid(String iban) { + return !isValid(iban); + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/type/EmailAddressTest.java b/src/test/java/it/aboutbits/springboot/toolbox/type/EmailAddressTest.java new file mode 100644 index 0000000..bbed40e --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/type/EmailAddressTest.java @@ -0,0 +1,82 @@ +package it.aboutbits.springboot.toolbox.type; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class EmailAddressTest { + @Nested + class Constructor { + @ParameterizedTest + @ValueSource(strings = {"egon@aboutbits.it", "hans.mueller@aboutbits.it", "peterP_pansky@aboutbits.it"}) + void validValues_shouldSucceed(String value) { + assertThatCode( + () -> new EmailAddress(value) + ).doesNotThrowAnyException(); + } + + @ParameterizedTest + @ValueSource(strings = {"", " ", " ", "\t", "\r", "\n", "sepp", "x@ y", "@aboutbits.it", "hans mueller@aboutbits.it", "peter@pansky@aboutbits.it"}) + void invalidValues_shouldFail(String value) { + assertThatIllegalArgumentException().isThrownBy( + () -> new EmailAddress(value) + ); + } + + @Test + void null_shouldFail() { + assertThatIllegalArgumentException().isThrownBy( + () -> new EmailAddress(null) + ); + } + } + + @Nested + class ToStringAndValue { + @Test + void shouldAlwaysReturnTheSameValue() { + var emailAddress = new EmailAddress("someValue@aboutbits.it"); + + assertThat(emailAddress.value()).isEqualTo(emailAddress.toString()); + } + + @Test + void shouldReturnLowercaseValue() { + var emailAddress = new EmailAddress("someValue@aboutbits.it"); + + assertThat(emailAddress.value()).isEqualTo("somevalue@aboutbits.it"); + } + } + + @Nested + class CompareTo { + @Test + void shouldCompareToReturnNegativeWhenInputValueIsGreater() { + var emailAddress1 = new EmailAddress("abc@aboutbits.it"); + var emailAddress2 = new EmailAddress("xyz@aboutbits.it"); + + assertThat(emailAddress1.compareTo(emailAddress2)).isNegative(); + } + + @Test + void shouldCompareToReturnPositiveWhenInputValueIsLesser() { + var emailAddress1 = new EmailAddress("xyz@aboutbits.it"); + var emailAddress2 = new EmailAddress("abc@aboutbits.it"); + + assertThat(emailAddress1.compareTo(emailAddress2)).isPositive(); + } + + @Test + void shouldCompareToReturnZeroWhenInputValuesAreEqual() { + var emailAddress1 = new EmailAddress("abc@aboutbits.it"); + var emailAddress2 = new EmailAddress("abc@aboutbits.it"); + + assertThat(emailAddress1).isEqualByComparingTo(emailAddress2); + } + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/type/IbanTest.java b/src/test/java/it/aboutbits/springboot/toolbox/type/IbanTest.java new file mode 100644 index 0000000..3b9fe55 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/type/IbanTest.java @@ -0,0 +1,82 @@ +package it.aboutbits.springboot.toolbox.type; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class IbanTest { + @Nested + class Constructor { + @ParameterizedTest + @ValueSource(strings = {"NL08INGB3330533676", "HR3425000097537651489", "IT37G0300203280237914848919", "AT705400073311799347", "DE60500105175378199617"}) + void validValues_shouldSucceed(String value) { + assertThatCode( + () -> new Iban(value) + ).doesNotThrowAnyException(); + } + + @ParameterizedTest + @ValueSource(strings = {"IT60X0542811101000000123450", "some-wrong-stuff", "", " ", " ", "\t", "\r", "\n"}) + void invalidValues_shouldFail(String value) { + assertThatIllegalArgumentException().isThrownBy( + () -> new Iban(value) + ); + } + + @Test + void null_shouldFail() { + assertThatIllegalArgumentException().isThrownBy( + () -> new Iban(null) + ); + } + } + + @Nested + class ToStringAndValue { + @Test + void shouldAlwaysReturnTheSameValue() { + var emailAddress = new Iban("HR3425000097537651489"); + + assertThat(emailAddress.value()).isEqualTo(emailAddress.toString()); + } + + @Test + void shouldReturnUppercaseValue() { + var emailAddress = new Iban("hr3425000097537651489"); + + assertThat(emailAddress.value()).isEqualTo("HR3425000097537651489"); + } + } + + @Nested + class CompareTo { + @Test + void shouldCompareToReturnNegativeWhenInputValueIsGreater() { + var iban1 = new Iban("HR3425000097537651489"); + var iban2 = new Iban("NL08INGB3330533676"); + + assertThat(iban1.compareTo(iban2)).isNegative(); + } + + @Test + void shouldCompareToReturnPositiveWhenInputValueIsLesser() { + var iban1 = new Iban("NL08INGB3330533676"); + var iban2 = new Iban("HR3425000097537651489"); + + assertThat(iban1.compareTo(iban2)).isPositive(); + } + + @Test + void shouldCompareToReturnZeroWhenInputValuesAreEqual() { + var iban1 = new Iban("HR3425000097537651489"); + var iban2 = new Iban("HR3425000097537651489"); + + assertThat(iban1).isEqualByComparingTo(iban2); + } + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/validation/util/EmailAddressValidatorTest.java b/src/test/java/it/aboutbits/springboot/toolbox/validation/util/EmailAddressValidatorTest.java new file mode 100644 index 0000000..3e3a189 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/validation/util/EmailAddressValidatorTest.java @@ -0,0 +1,62 @@ +package it.aboutbits.springboot.toolbox.validation.util; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; + +class EmailAddressValidatorTest { + @Nested + class IsValid { + @ParameterizedTest + @ValueSource(strings = {"egon@aboutbits.it", "hans.mueller@aboutbits.it", "peterP_pansky@aboutbits.it"}) + void validValues_shouldSucceed(String value) { + assertThat( + EmailAddressValidator.isValid(value) + ).isTrue(); + } + + @ParameterizedTest + @ValueSource(strings = {"", " ", " ", "\t", "\r", "\n", "sepp", "x@ y", "@aboutbits.it", "hans mueller@aboutbits.it", "peter@pansky@aboutbits.it"}) + void invalidValues_shouldFail(String value) { + assertThat( + EmailAddressValidator.isValid(value) + ).isFalse(); + } + + @Test + void null_shouldFail() { + assertThat( + EmailAddressValidator.isValid(null) + ).isFalse(); + } + } + + @Nested + class IsNotValid { + @ParameterizedTest + @ValueSource(strings = {"egon@aboutbits.it", "hans.mueller@aboutbits.it", "peterP_pansky@aboutbits.it"}) + void validValues_shouldSucceed(String value) { + assertThat( + EmailAddressValidator.isNotValid(value) + ).isFalse(); + } + + @ParameterizedTest + @ValueSource(strings = {"", " ", " ", "\t", "\r", "\n", "sepp", "x@ y", "@aboutbits.it", "hans mueller@aboutbits.it", "peter@pansky@aboutbits.it"}) + void invalidValues_shouldFail(String value) { + assertThat( + EmailAddressValidator.isNotValid(value) + ).isTrue(); + } + + @Test + void null_shouldFail() { + assertThat( + EmailAddressValidator.isNotValid(null) + ).isTrue(); + } + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/validation/util/IbanValidatorTest.java b/src/test/java/it/aboutbits/springboot/toolbox/validation/util/IbanValidatorTest.java new file mode 100644 index 0000000..d70ea11 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/validation/util/IbanValidatorTest.java @@ -0,0 +1,62 @@ +package it.aboutbits.springboot.toolbox.validation.util; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; + +class IbanValidatorTest { + @Nested + class IsValid { + @ParameterizedTest + @ValueSource(strings = {"NL08INGB3330533676", "HR3425000097537651489", "IT37G0300203280237914848919", "AT705400073311799347", "DE60500105175378199617"}) + void validValues_shouldSucceed(String value) { + assertThat( + IbanValidator.isValid(value) + ).isTrue(); + } + + @ParameterizedTest + @ValueSource(strings = {"IT60X0542811101000000123450", "some-wrong-stuff", "", " ", " ", "\t", "\r", "\n"}) + void invalidValues_shouldFail(String value) { + assertThat( + IbanValidator.isValid(value) + ).isFalse(); + } + + @Test + void null_shouldFail() { + assertThat( + IbanValidator.isValid(null) + ).isFalse(); + } + } + + @Nested + class IsNotValid { + @ParameterizedTest + @ValueSource(strings = {"NL08INGB3330533676", "HR3425000097537651489", "IT37G0300203280237914848919", "AT705400073311799347", "DE60500105175378199617"}) + void validValues_shouldSucceed(String value) { + assertThat( + IbanValidator.isNotValid(value) + ).isFalse(); + } + + @ParameterizedTest + @ValueSource(strings = {"IT60X0542811101000000123450", "some-wrong-stuff", "", " ", " ", "\t", "\r", "\n"}) + void invalidValues_shouldFail(String value) { + assertThat( + IbanValidator.isNotValid(value) + ).isTrue(); + } + + @Test + void null_shouldFail() { + assertThat( + IbanValidator.isNotValid(null) + ).isTrue(); + } + } +}