diff --git a/algorithms-modules/algorithms-miscellaneous-2/src/test/java/com/baeldung/algorithms/astar/Graph.java b/algorithms-modules/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/astar/Graph.java similarity index 100% rename from algorithms-modules/algorithms-miscellaneous-2/src/test/java/com/baeldung/algorithms/astar/Graph.java rename to algorithms-modules/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/astar/Graph.java diff --git a/algorithms-modules/algorithms-miscellaneous-2/src/test/java/com/baeldung/algorithms/astar/GraphNode.java b/algorithms-modules/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/astar/GraphNode.java similarity index 100% rename from algorithms-modules/algorithms-miscellaneous-2/src/test/java/com/baeldung/algorithms/astar/GraphNode.java rename to algorithms-modules/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/astar/GraphNode.java diff --git a/algorithms-modules/algorithms-miscellaneous-2/src/test/java/com/baeldung/algorithms/astar/RouteFinder.java b/algorithms-modules/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/astar/RouteFinder.java similarity index 100% rename from algorithms-modules/algorithms-miscellaneous-2/src/test/java/com/baeldung/algorithms/astar/RouteFinder.java rename to algorithms-modules/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/astar/RouteFinder.java diff --git a/algorithms-modules/algorithms-miscellaneous-2/src/test/java/com/baeldung/algorithms/astar/RouteNode.java b/algorithms-modules/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/astar/RouteNode.java similarity index 100% rename from algorithms-modules/algorithms-miscellaneous-2/src/test/java/com/baeldung/algorithms/astar/RouteNode.java rename to algorithms-modules/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/astar/RouteNode.java diff --git a/algorithms-modules/algorithms-miscellaneous-2/src/test/java/com/baeldung/algorithms/astar/Scorer.java b/algorithms-modules/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/astar/Scorer.java similarity index 100% rename from algorithms-modules/algorithms-miscellaneous-2/src/test/java/com/baeldung/algorithms/astar/Scorer.java rename to algorithms-modules/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/astar/Scorer.java diff --git a/algorithms-modules/algorithms-miscellaneous-2/src/test/java/com/baeldung/algorithms/astar/underground/HaversineScorer.java b/algorithms-modules/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/astar/underground/HaversineScorer.java similarity index 100% rename from algorithms-modules/algorithms-miscellaneous-2/src/test/java/com/baeldung/algorithms/astar/underground/HaversineScorer.java rename to algorithms-modules/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/astar/underground/HaversineScorer.java diff --git a/algorithms-modules/algorithms-miscellaneous-2/src/test/java/com/baeldung/algorithms/astar/underground/Station.java b/algorithms-modules/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/astar/underground/Station.java similarity index 100% rename from algorithms-modules/algorithms-miscellaneous-2/src/test/java/com/baeldung/algorithms/astar/underground/Station.java rename to algorithms-modules/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/astar/underground/Station.java diff --git a/core-java-modules/core-java-11-2/README.md b/core-java-modules/core-java-11-2/README.md index b864e1ca99..62acd33188 100644 --- a/core-java-modules/core-java-11-2/README.md +++ b/core-java-modules/core-java-11-2/README.md @@ -12,5 +12,4 @@ This module contains articles about Java 11 core features - [Java HTTPS Client Certificate Authentication](https://www.baeldung.com/java-https-client-certificate-authentication) - [Call Methods at Runtime Using Java Reflection](https://www.baeldung.com/java-method-reflection) - [Java HttpClient Basic Authentication](https://www.baeldung.com/java-httpclient-basic-auth) -- [Java HttpClient With SSL](https://www.baeldung.com/java-httpclient-ssl) -- [Adding Parameters to Java HttpClient Requests](https://www.baeldung.com/java-httpclient-request-parameters) +- [Java HttpClient With SSL](https://www.baeldung.com/java-httpclient-ssl) \ No newline at end of file diff --git a/core-java-modules/core-java-11-3/README.md b/core-java-modules/core-java-11-3/README.md new file mode 100644 index 0000000000..f46ab02bca --- /dev/null +++ b/core-java-modules/core-java-11-3/README.md @@ -0,0 +1,6 @@ +## Core Java 11 + +This module contains articles about Java 11 core features + +### Relevant articles +- [Adding Parameters to Java HttpClient Requests](https://www.baeldung.com/java-httpclient-request-parameters) diff --git a/core-java-modules/core-java-11-3/pom.xml b/core-java-modules/core-java-11-3/pom.xml new file mode 100644 index 0000000000..b4a4591b28 --- /dev/null +++ b/core-java-modules/core-java-11-3/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + core-java-11-3 + 0.1.0-SNAPSHOT + core-java-11-3 + jar + + + com.baeldung + parent-modules + 1.0.0-SNAPSHOT + ../../pom.xml + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${maven.compiler.source.version} + ${maven.compiler.target.version} + + + + + + + 11 + 11 + + + \ No newline at end of file diff --git a/core-java-modules/core-java-11-2/src/test/java/com/baeldung/httpclient/parameters/HttpClientParametersLiveTest.java b/core-java-modules/core-java-11-3/src/test/java/com/baeldung/httpclient/parameters/HttpClientParametersLiveTest.java similarity index 100% rename from core-java-modules/core-java-11-2/src/test/java/com/baeldung/httpclient/parameters/HttpClientParametersLiveTest.java rename to core-java-modules/core-java-11-3/src/test/java/com/baeldung/httpclient/parameters/HttpClientParametersLiveTest.java diff --git a/core-java-modules/core-java-17/README.md b/core-java-modules/core-java-17/README.md index d77a487932..9f39b0289f 100644 --- a/core-java-modules/core-java-17/README.md +++ b/core-java-modules/core-java-17/README.md @@ -5,4 +5,4 @@ - [Introduction to HexFormat in Java 17](https://www.baeldung.com/java-hexformat) - [New Features in Java 17](https://www.baeldung.com/java-17-new-features) - [Random Number Generators in Java 17](https://www.baeldung.com/java-17-random-number-generators) -- [Sealed Classes and Interfaces in Java 17](https://www.baeldung.com/java-sealed-classes-interfaces) +- [Sealed Classes and Interfaces in Java](https://www.baeldung.com/java-sealed-classes-interfaces) diff --git a/core-java-modules/core-java-collections-list-4/src/test/java/com/baeldung/list/replace/ReplaceUsingIndexInArrayListUnitTest.java b/core-java-modules/core-java-collections-list-4/src/test/java/com/baeldung/list/replace/ReplaceUsingIndexInArrayListUnitTest.java new file mode 100644 index 0000000000..ad6d0fee43 --- /dev/null +++ b/core-java-modules/core-java-collections-list-4/src/test/java/com/baeldung/list/replace/ReplaceUsingIndexInArrayListUnitTest.java @@ -0,0 +1,21 @@ +package com.baeldung.list.replace; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class ReplaceUsingIndexInArrayListUnitTest { + + private static final List EXPECTED = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5)); + + @Test void givenArrayList_updateUsingSet() { + List aList = new ArrayList<>(Arrays.asList(1, 2, 7, 4, 5)); + aList.set(2, 3); + assertThat(aList).isEqualTo(EXPECTED); + } +} \ No newline at end of file diff --git a/core-java-modules/core-java-collections-maps-5/src/main/java/com/baeldung/map/multikey/BaseClassUserCache.java b/core-java-modules/core-java-collections-maps-5/src/main/java/com/baeldung/map/multikey/BaseClassUserCache.java new file mode 100644 index 0000000000..1449727efc --- /dev/null +++ b/core-java-modules/core-java-collections-maps-5/src/main/java/com/baeldung/map/multikey/BaseClassUserCache.java @@ -0,0 +1,24 @@ +package com.baeldung.map.multikey; + +import java.util.HashMap; +import java.util.Map; + +public class BaseClassUserCache { + private final Map cache = new HashMap<>(); + + public User getById(String id) { + return cache.get(id); + } + + public User getById(Long id) { + return cache.get(id); + } + + public void storeById(String id, User user) { + cache.put(id, user); + } + + public void storeById(Long id, User user) { + cache.put(id, user); + } +} diff --git a/core-java-modules/core-java-collections-maps-5/src/main/java/com/baeldung/map/multikey/MultipleMapsUserCache.java b/core-java-modules/core-java-collections-maps-5/src/main/java/com/baeldung/map/multikey/MultipleMapsUserCache.java new file mode 100644 index 0000000000..6b7e56f209 --- /dev/null +++ b/core-java-modules/core-java-collections-maps-5/src/main/java/com/baeldung/map/multikey/MultipleMapsUserCache.java @@ -0,0 +1,25 @@ +package com.baeldung.map.multikey; + +import java.util.HashMap; +import java.util.Map; + +public class MultipleMapsUserCache { + private final Map stringCache = new HashMap<>(); + private final Map longCache = new HashMap<>(); + + public User getById(String id) { + return stringCache.get(id); + } + + public User getById(Long id) { + return longCache.get(id); + } + + public void storeById(String id, User user) { + stringCache.put(id, user); + } + + public void storeById(Long id, User user) { + longCache.put(id, user); + } +} diff --git a/core-java-modules/core-java-collections-maps-5/src/main/java/com/baeldung/map/multikey/User.java b/core-java-modules/core-java-collections-maps-5/src/main/java/com/baeldung/map/multikey/User.java new file mode 100644 index 0000000000..392587d305 --- /dev/null +++ b/core-java-modules/core-java-collections-maps-5/src/main/java/com/baeldung/map/multikey/User.java @@ -0,0 +1,13 @@ +package com.baeldung.map.multikey; + +public class User { + private final String name; + + public User(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/core-java-modules/core-java-collections-maps-5/src/main/java/com/baeldung/map/multikey/WrapperClassUserCache.java b/core-java-modules/core-java-collections-maps-5/src/main/java/com/baeldung/map/multikey/WrapperClassUserCache.java new file mode 100644 index 0000000000..3ce920ead1 --- /dev/null +++ b/core-java-modules/core-java-collections-maps-5/src/main/java/com/baeldung/map/multikey/WrapperClassUserCache.java @@ -0,0 +1,42 @@ +package com.baeldung.map.multikey; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class WrapperClassUserCache { + private Map cache = new HashMap<>(); + + public User getById(CacheKey key) { + return cache.get(key); + } + + public void storeById(CacheKey key, User user) { + cache.put(key, user); + } + + public static class CacheKey { + private final Object value; + + public CacheKey(String value) { + this.value = value; + } + + public CacheKey(Long value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CacheKey cacheKey = (CacheKey) o; + return value.equals(cacheKey.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + } +} diff --git a/core-java-modules/core-java-collections-maps-5/src/main/java/com/baeldung/map/multikey/WrapperInterfaceUserCache.java b/core-java-modules/core-java-collections-maps-5/src/main/java/com/baeldung/map/multikey/WrapperInterfaceUserCache.java new file mode 100644 index 0000000000..586a1bdee7 --- /dev/null +++ b/core-java-modules/core-java-collections-maps-5/src/main/java/com/baeldung/map/multikey/WrapperInterfaceUserCache.java @@ -0,0 +1,62 @@ +package com.baeldung.map.multikey; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class WrapperInterfaceUserCache { + private Map cache = new HashMap<>(); + + public User getById(CacheKey key) { + return cache.get(key); + } + + public void storeById(CacheKey key, User user) { + cache.put(key, user); + } + + public interface CacheKey { + } + + public static class StringCacheKey implements CacheKey{ + private final String value; + + public StringCacheKey(String value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + StringCacheKey that = (StringCacheKey) o; + return value.equals(that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + } + + public static class LongCacheKey implements CacheKey { + private final Long value; + + public LongCacheKey(Long value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + LongCacheKey that = (LongCacheKey) o; + return value.equals(that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + } +} diff --git a/core-java-modules/core-java-collections-maps-5/src/test/java/com/baeldung/map/multikey/BaseClassUserCacheUnitTest.java b/core-java-modules/core-java-collections-maps-5/src/test/java/com/baeldung/map/multikey/BaseClassUserCacheUnitTest.java new file mode 100644 index 0000000000..6a35e4c10a --- /dev/null +++ b/core-java-modules/core-java-collections-maps-5/src/test/java/com/baeldung/map/multikey/BaseClassUserCacheUnitTest.java @@ -0,0 +1,32 @@ +package com.baeldung.map.multikey; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class BaseClassUserCacheUnitTest { + private BaseClassUserCache cache = new BaseClassUserCache(); + + @BeforeEach + public void setup() { + cache.storeById("a", new User("User A")); + cache.storeById("b", new User("User B")); + cache.storeById(3L, new User("User 3")); + cache.storeById(4L, new User("User 4")); + } + + @Test + public void getByString() { + User user = cache.getById("b"); + assertNotNull(user); + assertEquals("User B", user.getName()); + } + + @Test + public void getByLong() { + User user = cache.getById(4L); + assertNotNull(user); + assertEquals("User 4", user.getName()); + } +} \ No newline at end of file diff --git a/core-java-modules/core-java-collections-maps-5/src/test/java/com/baeldung/map/multikey/MultipleMapsUserCacheUnitTest.java b/core-java-modules/core-java-collections-maps-5/src/test/java/com/baeldung/map/multikey/MultipleMapsUserCacheUnitTest.java new file mode 100644 index 0000000000..0b8473c0df --- /dev/null +++ b/core-java-modules/core-java-collections-maps-5/src/test/java/com/baeldung/map/multikey/MultipleMapsUserCacheUnitTest.java @@ -0,0 +1,33 @@ +package com.baeldung.map.multikey; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class MultipleMapsUserCacheUnitTest { + private MultipleMapsUserCache cache = new MultipleMapsUserCache(); + + @BeforeEach + public void setup() { + cache.storeById("a", new User("User A")); + cache.storeById("b", new User("User B")); + cache.storeById(3L, new User("User 3")); + cache.storeById(4L, new User("User 4")); + } + + @Test + public void getByString() { + User user = cache.getById("b"); + assertNotNull(user); + assertEquals("User B", user.getName()); + } + + @Test + public void getByLong() { + User user = cache.getById(4L); + assertNotNull(user); + assertEquals("User 4", user.getName()); + } +} \ No newline at end of file diff --git a/core-java-modules/core-java-collections-maps-5/src/test/java/com/baeldung/map/multikey/WrapperClassUserCacheUnitTest.java b/core-java-modules/core-java-collections-maps-5/src/test/java/com/baeldung/map/multikey/WrapperClassUserCacheUnitTest.java new file mode 100644 index 0000000000..caff7ef050 --- /dev/null +++ b/core-java-modules/core-java-collections-maps-5/src/test/java/com/baeldung/map/multikey/WrapperClassUserCacheUnitTest.java @@ -0,0 +1,33 @@ +package com.baeldung.map.multikey; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class WrapperClassUserCacheUnitTest { + private WrapperClassUserCache cache = new WrapperClassUserCache(); + + @BeforeEach + public void setup() { + cache.storeById(new WrapperClassUserCache.CacheKey("a"), new User("User A")); + cache.storeById(new WrapperClassUserCache.CacheKey("b"), new User("User B")); + cache.storeById(new WrapperClassUserCache.CacheKey(3L), new User("User 3")); + cache.storeById(new WrapperClassUserCache.CacheKey(4L), new User("User 4")); + } + + @Test + public void getByString() { + User user = cache.getById(new WrapperClassUserCache.CacheKey("b")); + assertNotNull(user); + assertEquals("User B", user.getName()); + } + + @Test + public void getByLong() { + User user = cache.getById(new WrapperClassUserCache.CacheKey(4L)); + assertNotNull(user); + assertEquals("User 4", user.getName()); + } +} \ No newline at end of file diff --git a/core-java-modules/core-java-collections-maps-5/src/test/java/com/baeldung/map/multikey/WrapperInterfaceUserCacheUnitTest.java b/core-java-modules/core-java-collections-maps-5/src/test/java/com/baeldung/map/multikey/WrapperInterfaceUserCacheUnitTest.java new file mode 100644 index 0000000000..211e6aa182 --- /dev/null +++ b/core-java-modules/core-java-collections-maps-5/src/test/java/com/baeldung/map/multikey/WrapperInterfaceUserCacheUnitTest.java @@ -0,0 +1,33 @@ +package com.baeldung.map.multikey; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class WrapperInterfaceUserCacheUnitTest { + private WrapperInterfaceUserCache cache = new WrapperInterfaceUserCache(); + + @BeforeEach + public void setup() { + cache.storeById(new WrapperInterfaceUserCache.StringCacheKey("a"), new User("User A")); + cache.storeById(new WrapperInterfaceUserCache.StringCacheKey("b"), new User("User B")); + cache.storeById(new WrapperInterfaceUserCache.LongCacheKey(3L), new User("User 3")); + cache.storeById(new WrapperInterfaceUserCache.LongCacheKey(4L), new User("User 4")); + } + + @Test + public void getByString() { + User user = cache.getById(new WrapperInterfaceUserCache.StringCacheKey("b")); + assertNotNull(user); + assertEquals("User B", user.getName()); + } + + @Test + public void getByLong() { + User user = cache.getById(new WrapperInterfaceUserCache.LongCacheKey(4L)); + assertNotNull(user); + assertEquals("User 4", user.getName()); + } +} \ No newline at end of file diff --git a/core-java-modules/core-java-lang-oop-constructors/src/main/java/com/baeldung/initializerblock/instanceblock/InstanceBlockExample.java b/core-java-modules/core-java-lang-oop-constructors/src/main/java/com/baeldung/initializerblock/instanceblock/InstanceBlockExample.java new file mode 100644 index 0000000000..c17fd8d3e6 --- /dev/null +++ b/core-java-modules/core-java-lang-oop-constructors/src/main/java/com/baeldung/initializerblock/instanceblock/InstanceBlockExample.java @@ -0,0 +1,22 @@ +package com.baeldung.initializerblock.instanceblock; + +public class InstanceBlockExample { + + { + System.out.println("Instance initializer block 1"); + } + + { + System.out.println("Instance initializer block 2"); + } + + public InstanceBlockExample() { + System.out.println("Class constructor"); + } + + public static void main(String[] args) { + InstanceBlockExample iib = new InstanceBlockExample(); + System.out.println("Main Method"); + } +} + diff --git a/core-java-modules/core-java-lang-oop-constructors/src/main/java/com/baeldung/initializerblock/staticblock/StaticBlockExample.java b/core-java-modules/core-java-lang-oop-constructors/src/main/java/com/baeldung/initializerblock/staticblock/StaticBlockExample.java new file mode 100644 index 0000000000..a409b6d7c8 --- /dev/null +++ b/core-java-modules/core-java-lang-oop-constructors/src/main/java/com/baeldung/initializerblock/staticblock/StaticBlockExample.java @@ -0,0 +1,17 @@ +package com.baeldung.initializerblock.staticblock; + +public class StaticBlockExample { + + static { + System.out.println("static block 1"); + } + + static { + System.out.println("static block 2"); + } + + public static void main(String[] args) { + System.out.println("Main Method"); + } +} + diff --git a/core-java-modules/core-java-numbers-5/pom.xml b/core-java-modules/core-java-numbers-5/pom.xml index f236d28ccb..bab1e4d622 100644 --- a/core-java-modules/core-java-numbers-5/pom.xml +++ b/core-java-modules/core-java-numbers-5/pom.xml @@ -22,4 +22,22 @@ + + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter.version} + test + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + + + com.google.guava + guava + ${guava.version} + + \ No newline at end of file diff --git a/core-java-modules/core-java-numbers-5/src/main/java/com/baeldung/intrange/IntRangeApacheCommons.java b/core-java-modules/core-java-numbers-5/src/main/java/com/baeldung/intrange/IntRangeApacheCommons.java new file mode 100644 index 0000000000..0a82f934a7 --- /dev/null +++ b/core-java-modules/core-java-numbers-5/src/main/java/com/baeldung/intrange/IntRangeApacheCommons.java @@ -0,0 +1,26 @@ +package com.baeldung.intrange; + +import org.apache.commons.lang3.Range; + +public class IntRangeApacheCommons { + + public static boolean isInClosedRange(Integer number, Integer lowerBound, Integer upperBound) { + final Range range = Range.between(lowerBound, upperBound); + return range.contains(number); + } + + public static boolean isInOpenRange(Integer number, Integer lowerBound, Integer upperBound) { + final Range range = Range.between(lowerBound + 1, upperBound - 1); + return range.contains(number); + } + + public static boolean isInOpenClosedRange(Integer number, Integer lowerBound, Integer upperBound) { + final Range range = Range.between(lowerBound + 1, upperBound); + return range.contains(number); + } + + public static boolean isInClosedOpenRange(Integer number, Integer lowerBound, Integer upperBound) { + final Range range = Range.between(lowerBound, upperBound - 1); + return range.contains(number); + } +} diff --git a/core-java-modules/core-java-numbers-5/src/main/java/com/baeldung/intrange/IntRangeGoogleGuava.java b/core-java-modules/core-java-numbers-5/src/main/java/com/baeldung/intrange/IntRangeGoogleGuava.java new file mode 100644 index 0000000000..d4cadfa050 --- /dev/null +++ b/core-java-modules/core-java-numbers-5/src/main/java/com/baeldung/intrange/IntRangeGoogleGuava.java @@ -0,0 +1,26 @@ +package com.baeldung.intrange; + +import com.google.common.collect.Range; + +public class IntRangeGoogleGuava { + + public static boolean isInClosedRange(Integer number, Integer lowerBound, Integer upperBound) { + final Range range = Range.closed(lowerBound, upperBound); + return range.contains(number); + } + + public static boolean isInOpenRange(Integer number, Integer lowerBound, Integer upperBound) { + final Range range = Range.open(lowerBound, upperBound); + return range.contains(number); + } + + public static boolean isInOpenClosedRange(Integer number, Integer lowerBound, Integer upperBound) { + final Range range = Range.openClosed(lowerBound, upperBound); + return range.contains(number); + } + + public static boolean isInClosedOpenRange(Integer number, Integer lowerBound, Integer upperBound) { + final Range range = Range.closedOpen(lowerBound, upperBound); + return range.contains(number); + } +} diff --git a/core-java-modules/core-java-numbers-5/src/main/java/com/baeldung/intrange/IntRangeOperators.java b/core-java-modules/core-java-numbers-5/src/main/java/com/baeldung/intrange/IntRangeOperators.java new file mode 100644 index 0000000000..77e32161e0 --- /dev/null +++ b/core-java-modules/core-java-numbers-5/src/main/java/com/baeldung/intrange/IntRangeOperators.java @@ -0,0 +1,20 @@ +package com.baeldung.intrange; + +public class IntRangeOperators { + + public static boolean isInClosedRange(Integer number, Integer lowerBound, Integer upperBound) { + return (lowerBound <= number && number <= upperBound); + } + + public static boolean isInOpenRange(Integer number, Integer lowerBound, Integer upperBound) { + return (lowerBound < number && number < upperBound); + } + + public static boolean isInOpenClosedRange(Integer number, Integer lowerBound, Integer upperBound) { + return (lowerBound < number && number <= upperBound); + } + + public static boolean isInClosedOpenRange(Integer number, Integer lowerBound, Integer upperBound) { + return (lowerBound <= number && number < upperBound); + } +} \ No newline at end of file diff --git a/core-java-modules/core-java-numbers-5/src/main/java/com/baeldung/intrange/IntRangeValueRange.java b/core-java-modules/core-java-numbers-5/src/main/java/com/baeldung/intrange/IntRangeValueRange.java new file mode 100644 index 0000000000..cdce555341 --- /dev/null +++ b/core-java-modules/core-java-numbers-5/src/main/java/com/baeldung/intrange/IntRangeValueRange.java @@ -0,0 +1,26 @@ +package com.baeldung.intrange; + +import java.time.temporal.ValueRange; + +public class IntRangeValueRange { + + public static boolean isInClosedRange(Integer number, Integer lowerBound, Integer upperBound) { + final ValueRange range = ValueRange.of(lowerBound, upperBound); + return range.isValidIntValue(number); + } + + public static boolean isInOpenRange(Integer number, Integer lowerBound, Integer upperBound) { + final ValueRange range = ValueRange.of(lowerBound + 1, upperBound - 1); + return range.isValidIntValue(number); + } + + public static boolean isInOpenClosedRange(Integer number, Integer lowerBound, Integer upperBound) { + final ValueRange range = ValueRange.of(lowerBound + 1, upperBound); + return range.isValidIntValue(number); + } + + public static boolean isInClosedOpenRange(Integer number, Integer lowerBound, Integer upperBound) { + final ValueRange range = ValueRange.of(lowerBound, upperBound - 1); + return range.isValidIntValue(number); + } +} diff --git a/core-java-modules/core-java-numbers-5/src/test/java/com/baeldung/intrange/IntRangeApacheCommonsUnitTest.java b/core-java-modules/core-java-numbers-5/src/test/java/com/baeldung/intrange/IntRangeApacheCommonsUnitTest.java new file mode 100644 index 0000000000..d3b15d0e42 --- /dev/null +++ b/core-java-modules/core-java-numbers-5/src/test/java/com/baeldung/intrange/IntRangeApacheCommonsUnitTest.java @@ -0,0 +1,97 @@ +package com.baeldung.intrange; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +public class IntRangeApacheCommonsUnitTest { + + @Test + void givenIntRangeApacheCommons_whenIsInClosedRange_thenSuccess() { + // when + boolean resultLowerBound = IntRangeApacheCommons.isInClosedRange(10, 10, 20); + boolean resultUpperBound = IntRangeApacheCommons.isInClosedRange(20, 10, 20); + + // then + assertTrue(resultLowerBound); + assertTrue(resultUpperBound); + } + + @Test + void givenIntRangeApacheCommons_whenIsNotInClosedRange_thenFailure() { + // when + boolean resultLowerBound = IntRangeApacheCommons.isInClosedRange(8, 10, 20); + boolean resultUpperBound = IntRangeApacheCommons.isInClosedRange(22, 10, 20); + + // then + assertFalse(resultLowerBound); + assertFalse(resultUpperBound); + } + + @Test + void givenIntRangeApacheCommons_whenIsInOpenRange_thenSuccess() { + // when + boolean resultLowerBound = IntRangeApacheCommons.isInOpenRange(11, 10, 20); + boolean resultUpperBound = IntRangeApacheCommons.isInOpenRange(19, 10, 20); + + // then + assertTrue(resultLowerBound); + assertTrue(resultUpperBound); + } + + @Test + void givenIntRangeApacheCommons_whenIsNotInOpenRange_thenFailure() { + // when + boolean resultLowerBound = IntRangeApacheCommons.isInOpenRange(10, 10, 20); + boolean resultUpperBound = IntRangeApacheCommons.isInOpenRange(20, 10, 20); + + // then + assertFalse(resultLowerBound); + assertFalse(resultUpperBound); + } + + @Test + void givenIntRangeApacheCommons_whenIsInOpenClosedRange_thenSuccess() { + // when + boolean resultLowerBound = IntRangeApacheCommons.isInOpenClosedRange(11, 10, 20); + boolean resultUpperBound = IntRangeApacheCommons.isInOpenClosedRange(20, 10, 20); + + // then + assertTrue(resultLowerBound); + assertTrue(resultUpperBound); + } + + @Test + void givenIntRangeApacheCommons_whenIsNotInOpenClosedRange_thenFailure() { + // when + boolean resultLowerBound = IntRangeApacheCommons.isInOpenClosedRange(10, 10, 20); + boolean resultUpperBound = IntRangeApacheCommons.isInOpenClosedRange(21, 10, 20); + + // then + assertFalse(resultLowerBound); + assertFalse(resultUpperBound); + } + + @Test + void givenIntRangeApacheCommons_whenIsInClosedOpenRange_thenSuccess() { + // when + boolean resultLowerBound = IntRangeApacheCommons.isInClosedOpenRange(10, 10, 20); + boolean resultUpperBound = IntRangeApacheCommons.isInClosedOpenRange(19, 10, 20); + + // then + assertTrue(resultLowerBound); + assertTrue(resultUpperBound); + } + + @Test + void givenIntRangeApacheCommons_whenIsNotInClosedOpenRange_thenFailure() { + // when + boolean resultLowerBound = IntRangeApacheCommons.isInClosedOpenRange(9, 10, 20); + boolean resultUpperBound = IntRangeApacheCommons.isInClosedOpenRange(20, 10, 20); + + // then + assertFalse(resultLowerBound); + assertFalse(resultUpperBound); + } +} diff --git a/core-java-modules/core-java-numbers-5/src/test/java/com/baeldung/intrange/IntRangeGoogleGuavaUnitTest.java b/core-java-modules/core-java-numbers-5/src/test/java/com/baeldung/intrange/IntRangeGoogleGuavaUnitTest.java new file mode 100644 index 0000000000..9abdc20d31 --- /dev/null +++ b/core-java-modules/core-java-numbers-5/src/test/java/com/baeldung/intrange/IntRangeGoogleGuavaUnitTest.java @@ -0,0 +1,87 @@ +package com.baeldung.intrange; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +public class IntRangeGoogleGuavaUnitTest { + + @Test + void givenIntRangeGoogleGuava_whenIsInOpenRange_thenSuccess() { + // when + boolean result = IntRangeGoogleGuava.isInOpenRange(14, 10, 20); + + //then + assertTrue(result); + } + + @Test + void givenIntRangeGoogleGuava_whenIsNotInOpenRange_thenFailure() { + // when + boolean resultLowerBound = IntRangeGoogleGuava.isInOpenRange(10, 10, 20); + boolean resultUpperBound = IntRangeGoogleGuava.isInOpenRange(20, 10, 20); + + // then + assertFalse(resultLowerBound); + assertFalse(resultUpperBound); + } + + @Test + void givenIntRangeGoogleGuava_whenIsInClosedRange_thenSuccess() { + // when + boolean resultLowerBound = IntRangeGoogleGuava.isInClosedRange(-10, -10, 5); + boolean resultUpperBound = IntRangeGoogleGuava.isInClosedRange(5, -10, 5); + + // then + assertTrue(resultLowerBound); + assertTrue(resultUpperBound); + } + + @Test + void givenIntRangeGoogleGuava_whenIsNotInClosedRange_thenFailure() { + // when + boolean resultLowerBound = IntRangeGoogleGuava.isInClosedRange(-11, -10, 5); + boolean resultUpperBound = IntRangeGoogleGuava.isInClosedRange(6, -10, 5); + + //then + assertFalse(resultLowerBound); + assertFalse(resultUpperBound); + } + + @Test + void givenIntRangeGoogleGuava_whenIsInOpenClosedRange_thenSuccess() { + // when + boolean result = IntRangeGoogleGuava.isInOpenClosedRange(20, 10, 20); + + // then + assertTrue(result); + } + + @Test + void givenIntRangeGoogleGuava_whenIsNotInOpenClosedRange_thenFailure() { + // when + boolean result = IntRangeGoogleGuava.isInOpenClosedRange(10, 10, 20); + + // then + assertFalse(result); + } + + @Test + void givenIntRangeGoogleGuava_whenIsInClosedOpenRange_thenSuccess() { + // when + boolean result = IntRangeGoogleGuava.isInClosedOpenRange(10, 10, 20); + + // then + assertTrue(result); + } + + @Test + void givenIntRangeGoogleGuava_whenIsNotInClosedOpenRange_thenFailure() { + // when + boolean result = IntRangeGoogleGuava.isInClosedOpenRange(20, 10, 20); + + // then + assertFalse(result); + } +} diff --git a/core-java-modules/core-java-numbers-5/src/test/java/com/baeldung/intrange/IntRangeOperatorsUnitTest.java b/core-java-modules/core-java-numbers-5/src/test/java/com/baeldung/intrange/IntRangeOperatorsUnitTest.java new file mode 100644 index 0000000000..76abeb1ee3 --- /dev/null +++ b/core-java-modules/core-java-numbers-5/src/test/java/com/baeldung/intrange/IntRangeOperatorsUnitTest.java @@ -0,0 +1,87 @@ +package com.baeldung.intrange; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +public class IntRangeOperatorsUnitTest { + + @Test + void givenIntRangeOperators_whenIsInOpenRange_thenSuccess() { + // when + boolean result = IntRangeOperators.isInOpenRange(11, 10, 20); + + //then + assertTrue(result); + } + + @Test + void givenIntRangeOperators_whenIsNotInOpenRange_thenFailure() { + // when + boolean resultLowerBound = IntRangeOperators.isInOpenRange(10, 10, 20); + boolean resultUpperBound = IntRangeOperators.isInOpenRange(20, 10, 20); + + // then + assertFalse(resultLowerBound); + assertFalse(resultUpperBound); + } + + @Test + void givenIntRangeOperators_whenIsInClosedRange_thenSuccess() { + // when + boolean resultLowerBound = IntRangeOperators.isInClosedRange(-10, -10, 5); + boolean resultUpperBound = IntRangeOperators.isInClosedRange(5, -10, 5); + + // then + assertTrue(resultUpperBound); + assertTrue(resultLowerBound); + } + + @Test + void givenIntRangeOperators_whenIsNotInClosedRange_thenFailure() { + // when + boolean resultLowerBound = IntRangeOperators.isInClosedRange(-11, -10, 5); + boolean resultUpperBound = IntRangeOperators.isInClosedRange(6, -10, 5); + + // then + assertFalse(resultLowerBound); + assertFalse(resultUpperBound); + } + + @Test + void givenIntRangeOperators_whenIsInOpenClosedRange_thenSuccess() { + // when + boolean result = IntRangeOperators.isInOpenClosedRange(20, 10, 20); + + // then + assertTrue(result); + } + + @Test + void givenIntRangeOperators_whenIsNotInOpenClosedRange_thenFailure() { + // when + boolean result = IntRangeOperators.isInOpenClosedRange(10, 10, 20); + + // then + assertFalse(result); + } + + @Test + void givenIntRangeOperators_whenIsInClosedOpenRange_thenSuccess() { + // when + boolean result = IntRangeOperators.isInClosedOpenRange(10, 10, 20); + + // then + assertTrue(result); + } + + @Test + void givenIntRangeOperators_whenIsNotInClosedOpenRange_thenFailure() { + // when + boolean result = IntRangeOperators.isInClosedOpenRange(20, 10, 20); + + // then + assertFalse(result); + } +} diff --git a/core-java-modules/core-java-numbers-5/src/test/java/com/baeldung/intrange/IntRangeValueRangeUnitTest.java b/core-java-modules/core-java-numbers-5/src/test/java/com/baeldung/intrange/IntRangeValueRangeUnitTest.java new file mode 100644 index 0000000000..dc2105c675 --- /dev/null +++ b/core-java-modules/core-java-numbers-5/src/test/java/com/baeldung/intrange/IntRangeValueRangeUnitTest.java @@ -0,0 +1,97 @@ +package com.baeldung.intrange; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +public class IntRangeValueRangeUnitTest { + + @Test + void givenIntRangeValueRange_whenIsInClosedRange_thenSuccess() { + // when + boolean resultLowerBound = IntRangeValueRange.isInClosedRange(10, 10, 20); + boolean resultUpperBound = IntRangeValueRange.isInClosedRange(20, 10, 20); + + // then + assertTrue(resultLowerBound); + assertTrue(resultUpperBound); + } + + @Test + void givenIntRangeValueRange_whenIsNotInClosedRange_thenFailure() { + // when + boolean resultLowerBound = IntRangeValueRange.isInClosedRange(9, 10, 20); + boolean resultUpperBound = IntRangeValueRange.isInClosedRange(21, 10, 20); + + // then + assertFalse(resultLowerBound); + assertFalse(resultUpperBound); + } + + @Test + void givenIntRangeValueRange_whenIsInOpenRange_thenSuccess() { + // when + boolean resultLowerBound = IntRangeValueRange.isInOpenRange(11, 10, 20); + boolean resultUpperBound = IntRangeValueRange.isInOpenRange(19, 10, 20); + + // then + assertTrue(resultLowerBound); + assertTrue(resultUpperBound); + } + + @Test + void givenIntRangeValueRange_whenIsNotInOpenRange_thenFailure() { + // when + boolean resultLowerBound = IntRangeValueRange.isInOpenRange(10, 10, 20); + boolean resultUpperBound = IntRangeValueRange.isInOpenRange(20, 10, 20); + + // then + assertFalse(resultLowerBound); + assertFalse(resultUpperBound); + } + + @Test + void givenIntRangeValueRange_whenIsInOpenClosedRange_thenSuccess() { + // when + boolean resultLowerBound = IntRangeValueRange.isInOpenClosedRange(11, 10, 20); + boolean resultUpperBound = IntRangeValueRange.isInOpenClosedRange(20, 10, 20); + + // then + assertTrue(resultLowerBound); + assertTrue(resultUpperBound); + } + + @Test + void givenIntRangeValueRange_whenIsNotInOpenClosedRange_thenFailure() { + // when + boolean resultLowerBound = IntRangeValueRange.isInOpenClosedRange(10, 10, 20); + boolean resultUpperBound = IntRangeValueRange.isInOpenClosedRange(21, 10, 20); + + // then + assertFalse(resultLowerBound); + assertFalse(resultUpperBound); + } + + @Test + void givenIntRangeValueRange_whenIsInClosedOpenRange_thenSuccess() { + // when + boolean resultLowerBound = IntRangeValueRange.isInClosedOpenRange(10, 10, 20); + boolean resultUpperBound = IntRangeValueRange.isInClosedOpenRange(19, 10, 20); + + // then + assertTrue(resultLowerBound); + assertTrue(resultUpperBound); + } + + @Test + void givenIntRangeValueRange_whenIsNotInClosedOpenRange_thenFailure() { + // when + boolean resultLowerBound = IntRangeValueRange.isInClosedOpenRange(9, 10, 20); + boolean resultUpperBound = IntRangeValueRange.isInClosedOpenRange(20, 10, 20); + + // then + assertFalse(resultLowerBound); + assertFalse(resultUpperBound); + } +} diff --git a/core-java-modules/core-java-streams-4/pom.xml b/core-java-modules/core-java-streams-4/pom.xml index beed277f78..65de1d666c 100644 --- a/core-java-modules/core-java-streams-4/pom.xml +++ b/core-java-modules/core-java-streams-4/pom.xml @@ -43,6 +43,16 @@ 3.23.1 test + + org.openjdk.jmh + jmh-core + ${jmh-core.version} + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh-generator.version} + diff --git a/core-java-modules/core-java-streams-4/src/main/java/com/baeldung/streams/filteronlyoneelement/BenchmarkRunner.java b/core-java-modules/core-java-streams-4/src/main/java/com/baeldung/streams/filteronlyoneelement/BenchmarkRunner.java new file mode 100644 index 0000000000..327bdc4df3 --- /dev/null +++ b/core-java-modules/core-java-streams-4/src/main/java/com/baeldung/streams/filteronlyoneelement/BenchmarkRunner.java @@ -0,0 +1,56 @@ +package com.baeldung.streams.filteronlyoneelement; + +import java.util.function.Predicate; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; + +public class BenchmarkRunner { + + public static void main(String[] args) throws Exception { + org.openjdk.jmh.Main.main(args); + } + + @State(Scope.Benchmark) + public static class MyState { + final Stream getIntegers() { + return IntStream.range(1, 1000000) + .boxed(); + } + + final Predicate PREDICATE = i -> i == 751879; + } + + @Benchmark + public void evaluateFindUniqueElementMatchingPredicate_WithReduction(Blackhole blackhole, MyState state) { + blackhole.consume(FilterUtils.findUniqueElementMatchingPredicate_WithReduction(state.getIntegers(), state.PREDICATE)); + } + + @Benchmark + public void evaluateFindUniqueElementMatchingPredicate_WithCollectingAndThen(Blackhole blackhole, MyState state) { + blackhole.consume(FilterUtils.findUniqueElementMatchingPredicate_WithCollectingAndThen(state.getIntegers(), state.PREDICATE)); + } + + @Benchmark + public void evaluateGetUniqueElementMatchingPredicate_WithReduction(Blackhole blackhole, MyState state) { + try { + FilterUtils.getUniqueElementMatchingPredicate_WithReduction(state.getIntegers(), state.PREDICATE); + } catch (IllegalStateException exception) { + blackhole.consume(exception); + } + } + + @Benchmark + public void evaluateGetUniqueElementMatchingPredicate_WithCollectingAndThen(Blackhole blackhole, MyState state) { + try { + FilterUtils.getUniqueElementMatchingPredicate_WithCollectingAndThen(state.getIntegers(), state.PREDICATE); + } catch (IllegalStateException exception) { + blackhole.consume(exception); + } + } + +} diff --git a/core-java-modules/core-java-streams-4/src/main/java/com/baeldung/streams/filteronlyoneelement/FilterUtils.java b/core-java-modules/core-java-streams-4/src/main/java/com/baeldung/streams/filteronlyoneelement/FilterUtils.java new file mode 100644 index 0000000000..65905a7bc4 --- /dev/null +++ b/core-java-modules/core-java-streams-4/src/main/java/com/baeldung/streams/filteronlyoneelement/FilterUtils.java @@ -0,0 +1,50 @@ +package com.baeldung.streams.filteronlyoneelement; + +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class FilterUtils { + + public static Optional findUniqueElementMatchingPredicate_WithReduction(Stream elements, Predicate predicate) { + return elements.filter(predicate) + .collect(Collectors.reducing((a, b) -> null)); + } + + public static T getUniqueElementMatchingPredicate_WithReduction(Stream elements, Predicate predicate) { + return elements.filter(predicate) + .reduce((a, b) -> { + throw new IllegalStateException("Too many elements match the predicate"); + }) + .orElseThrow(() -> new IllegalStateException("No element matches the predicate")); + } + + public static Optional findUniqueElementMatchingPredicate_WithCollectingAndThen(Stream elements, Predicate predicate) { + return elements.filter(predicate) + .collect(Collectors.collectingAndThen(Collectors.toList(), list -> Optional.ofNullable(findUniqueElement(list)))); + } + + private static T findUniqueElement(List elements) { + if (elements.size() == 1) { + return elements.get(0); + } + return null; + } + + public static T getUniqueElementMatchingPredicate_WithCollectingAndThen(Stream elements, Predicate predicate) { + return elements.filter(predicate) + .collect(Collectors.collectingAndThen(Collectors.toList(), FilterUtils::getUniqueElement)); + } + + private static T getUniqueElement(List elements) { + if (elements.size() > 1) { + throw new IllegalStateException("Too many elements match the predicate"); + } else if (elements.size() == 0) { + throw new IllegalStateException("No element matches the predicate"); + } + return elements.get(0); + } + +} diff --git a/core-java-modules/core-java-streams-4/src/test/java/com/baeldung/streams/filteronlyoneelement/FilterUtilsUnitTest.java b/core-java-modules/core-java-streams-4/src/test/java/com/baeldung/streams/filteronlyoneelement/FilterUtilsUnitTest.java new file mode 100644 index 0000000000..531dcc5abb --- /dev/null +++ b/core-java-modules/core-java-streams-4/src/test/java/com/baeldung/streams/filteronlyoneelement/FilterUtilsUnitTest.java @@ -0,0 +1,88 @@ +package com.baeldung.streams.filteronlyoneelement; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.function.Predicate; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; + +public class FilterUtilsUnitTest { + + private static final Predicate IS_STRICTLY_GREATER_THAN5 = i -> i > 5; + private static final Predicate IS_STRICTLY_GREATER_THAN4 = i -> i > 4; + private static final Predicate IS_STRICTLY_GREATER_THAN3 = i -> i > 3; + + private Stream getIntegers() { + return Stream.of(1, 2, 3, 4, 5); + } + + @Test + void givenNoElementMatchingPredicate_WhenFindUniqueElementMatchingPredicateWithReduction_ThenNoneFound() { + assertTrue(FilterUtils.findUniqueElementMatchingPredicate_WithReduction(getIntegers(), IS_STRICTLY_GREATER_THAN5) + .isEmpty()); + } + + @Test + void givenTwoElementsMatchingPredicate_WhenFindUniqueElementMatchingPredicateWithReduction_ThenEmpty() { + assertTrue(FilterUtils.findUniqueElementMatchingPredicate_WithReduction(getIntegers(), IS_STRICTLY_GREATER_THAN3) + .isEmpty()); + } + + @Test + void givenOnlyOneElementMatchingPredicate_WhenFindUniqueElementMatchingPredicateWithReduction_ThenFindsIt() { + assertEquals(5, FilterUtils.findUniqueElementMatchingPredicate_WithReduction(getIntegers(), IS_STRICTLY_GREATER_THAN4) + .get()); + } + + @Test + void givenNoElementMatchingPredicate_WhenGetUniqueElementMatchingPredicateWithReduction_ThenThrows() { + assertThrows(IllegalStateException.class, () -> FilterUtils.getUniqueElementMatchingPredicate_WithReduction(getIntegers(), IS_STRICTLY_GREATER_THAN5)); + } + + @Test + void givenTwoElementsMatchingPredicate_WhenGetUniqueElementMatchingPredicateWithReduction_ThenThrows() { + assertThrows(IllegalStateException.class, () -> FilterUtils.getUniqueElementMatchingPredicate_WithReduction(getIntegers(), IS_STRICTLY_GREATER_THAN3)); + } + + @Test + void givenOnlyOneElementMatchingPredicate_WhenFindUniqueElementMatchingPredicateWithReduction_ThenGetIt() { + assertEquals(5, FilterUtils.getUniqueElementMatchingPredicate_WithReduction(getIntegers(), IS_STRICTLY_GREATER_THAN4)); + } + + @Test + void givenNoElementMatchingPredicate_WhenFindUniqueElementMatchingPredicateWithCollectingAndThen_ThenEmpty() { + assertTrue(FilterUtils.findUniqueElementMatchingPredicate_WithCollectingAndThen(getIntegers(), IS_STRICTLY_GREATER_THAN5) + .isEmpty()); + } + + @Test + void givenTwoElementsMatchingPredicate_WhenFindUniqueElementMatchingPredicateWithCollectingAndThen_ThenEmpty() { + assertTrue(FilterUtils.findUniqueElementMatchingPredicate_WithCollectingAndThen(getIntegers(), IS_STRICTLY_GREATER_THAN3) + .isEmpty()); + } + + @Test + void givenOnlyOneElementMatchingPredicate_WhenFindUniqueElementMatchingPredicateWithCollectingAndThen_ThenFindsIt() { + assertEquals(5, FilterUtils.findUniqueElementMatchingPredicate_WithCollectingAndThen(getIntegers(), IS_STRICTLY_GREATER_THAN4) + .get()); + } + + @Test + void givenNoElementMatchingPredicate_WhenGetUniqueElementMatchingPredicateWithCollectingAndThen_ThenThrows() { + assertThrows(IllegalStateException.class, () -> FilterUtils.getUniqueElementMatchingPredicate_WithCollectingAndThen(getIntegers(), IS_STRICTLY_GREATER_THAN5)); + } + + @Test + void givenTwoElementsMatchingPredicate_WhenGetUniqueElementMatchingPredicateWithCollectingAndThen_ThenThrows() { + assertThrows(IllegalStateException.class, () -> FilterUtils.getUniqueElementMatchingPredicate_WithCollectingAndThen(getIntegers(), IS_STRICTLY_GREATER_THAN3)); + } + + @Test + void givenOnlyOneElementMatchingPredicate_WhenFindUniqueElementMatchingPredicateWithCollectingAndThen_ThenGetIt() { + assertEquals(5, FilterUtils.getUniqueElementMatchingPredicate_WithCollectingAndThen(getIntegers(), IS_STRICTLY_GREATER_THAN4)); + } + +} diff --git a/core-java-modules/core-java-string-operations-4/README.md b/core-java-modules/core-java-string-operations-4/README.md index ac52ee4ab5..07af6d23c9 100644 --- a/core-java-modules/core-java-string-operations-4/README.md +++ b/core-java-modules/core-java-string-operations-4/README.md @@ -8,3 +8,4 @@ - [Check if a String Ends with a Certain Pattern in Java](https://www.baeldung.com/java-string-ends-pattern) - [Check if a Character is a Vowel in Java](https://www.baeldung.com/java-check-character-vowel) - [How to Truncate a String in Java](https://www.baeldung.com/java-truncating-strings) +- [Remove Whitespace From a String in Java](https://www.baeldung.com/java-string-remove-whitespace) diff --git a/libraries-security/src/test/resources/logback-test.xml b/libraries-security/src/test/resources/logback-test.xml index 8d4771e308..fcbdb8b5f7 100644 --- a/libraries-security/src/test/resources/logback-test.xml +++ b/libraries-security/src/test/resources/logback-test.xml @@ -6,6 +6,10 @@ + + + + diff --git a/lightrun/api-service/pom.xml b/lightrun/api-service/pom.xml index ed9fc2152b..0541f6c5cb 100644 --- a/lightrun/api-service/pom.xml +++ b/lightrun/api-service/pom.xml @@ -10,10 +10,9 @@ Aggregator Service for LightRun Article - org.springframework.boot - spring-boot-starter-parent - 2.6.7 - + com.baelduung + lightrun + 0.0.1-SNAPSHOT @@ -43,6 +42,7 @@ 17 + 3.17.0 \ No newline at end of file diff --git a/lightrun/pom.xml b/lightrun/pom.xml index 4902688748..1fc8e5ea70 100644 --- a/lightrun/pom.xml +++ b/lightrun/pom.xml @@ -9,6 +9,13 @@ lightrun Services for LightRun Article pom + + + com.baeldung + parent-boot-2 + 0.0.1-SNAPSHOT + ../parent-boot-2 + tasks-service diff --git a/lightrun/tasks-service/pom.xml b/lightrun/tasks-service/pom.xml index 5d4bf8b6d5..19b5f0d98e 100644 --- a/lightrun/tasks-service/pom.xml +++ b/lightrun/tasks-service/pom.xml @@ -10,10 +10,9 @@ Tasks Service for LightRun Article - org.springframework.boot - spring-boot-starter-parent - 2.6.7 - + com.baelduung + lightrun + 0.0.1-SNAPSHOT @@ -64,6 +63,7 @@ 17 + 3.17.0 \ No newline at end of file diff --git a/lightrun/users-service/pom.xml b/lightrun/users-service/pom.xml index 0be696bbf0..2db0d37037 100644 --- a/lightrun/users-service/pom.xml +++ b/lightrun/users-service/pom.xml @@ -10,10 +10,9 @@ Users Service for LightRun Article - org.springframework.boot - spring-boot-starter-parent - 2.6.7 - + com.baelduung + lightrun + 0.0.1-SNAPSHOT @@ -60,6 +59,7 @@ 17 + 3.17.0 \ No newline at end of file diff --git a/mapstruct/src/main/java/com/baeldung/mapper/EmployeeMapper.java b/mapstruct/src/main/java/com/baeldung/mapper/EmployeeMapper.java index 8e00103d0e..716b05c7d9 100644 --- a/mapstruct/src/main/java/com/baeldung/mapper/EmployeeMapper.java +++ b/mapstruct/src/main/java/com/baeldung/mapper/EmployeeMapper.java @@ -6,17 +6,20 @@ import com.baeldung.entity.Division; import com.baeldung.entity.Employee; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -import org.mapstruct.Mappings; import java.util.List; @Mapper public interface EmployeeMapper { - @Mappings({ @Mapping(target = "employeeId", source = "entity.id"), @Mapping(target = "employeeName", source = "entity.name"), @Mapping(target = "employeeStartDt", source = "entity.startDt", dateFormat = "dd-MM-yyyy HH:mm:ss") }) + @Mapping(target = "employeeId", source = "entity.id") + @Mapping(target = "employeeName", source = "entity.name") + @Mapping(target = "employeeStartDt", source = "entity.startDt", dateFormat = "dd-MM-yyyy HH:mm:ss") EmployeeDTO employeeToEmployeeDTO(Employee entity); - @Mappings({ @Mapping(target = "id", source = "dto.employeeId"), @Mapping(target = "name", source = "dto.employeeName"), @Mapping(target = "startDt", source = "dto.employeeStartDt", dateFormat = "dd-MM-yyyy HH:mm:ss") }) + @Mapping(target = "id", source = "dto.employeeId") + @Mapping(target = "name", source = "dto.employeeName") + @Mapping(target = "startDt", source = "dto.employeeStartDt", dateFormat = "dd-MM-yyyy HH:mm:ss") Employee employeeDTOtoEmployee(EmployeeDTO dto); DivisionDTO divisionToDivisionDTO(Division entity); diff --git a/maven-modules/README.md b/maven-modules/README.md index 19f0473a58..29a69d37e4 100644 --- a/maven-modules/README.md +++ b/maven-modules/README.md @@ -8,3 +8,4 @@ This module contains articles about Apache Maven. Please refer to its submodules - [Apache Maven Standard Directory Layout](https://www.baeldung.com/maven-directory-structure) - [Multi-Module Project with Maven](https://www.baeldung.com/maven-multi-module) - [Maven Packaging Types](https://www.baeldung.com/maven-packaging-types) +- [Maven Snapshot Repository vs Release Repository](https://www.baeldung.com/maven-snapshot-release-repository) diff --git a/maven-modules/maven-repositories/maven-download-artifacts/pom.xml b/maven-modules/maven-repositories/maven-download-artifacts/pom.xml new file mode 100644 index 0000000000..678fc32bec --- /dev/null +++ b/maven-modules/maven-repositories/maven-download-artifacts/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + maven-repository-download + Maven Release Repository + 1.0.0-SNAPSHOT + + + com.baeldung + maven-repositories + 1.0.0-SNAPSHOT + + + + + nexus-snapshot + nexus-snapshot + http://localhost:8081/repository/maven-snapshots/ + + true + + + false + + + + nexus-release + nexus-release + http://localhost:8081/repository/maven-releases/ + + false + + + + + + + com.baeldung + maven-release-repository + 1.0.0 + + + + com.baeldung + maven-snapshot-repository + 1.0.0-SNAPSHOT + + + + \ No newline at end of file diff --git a/maven-modules/maven-repositories/maven-release-repository/pom.xml b/maven-modules/maven-repositories/maven-release-repository/pom.xml new file mode 100644 index 0000000000..bd7adcbcce --- /dev/null +++ b/maven-modules/maven-repositories/maven-release-repository/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + maven-release-repository + Maven Release Repository + 1.0.0 + + + com.baeldung + maven-repositories + 1.0.0-SNAPSHOT + + + + + nexus + nexus-release + http://localhost:8081/repository/maven-releases/ + + false + + + true + + + + + + + nexus + nexus-release + http://localhost:8081/repository/maven-releases/ + + + + \ No newline at end of file diff --git a/maven-modules/maven-repositories/maven-snapshot-repository/pom.xml b/maven-modules/maven-repositories/maven-snapshot-repository/pom.xml new file mode 100644 index 0000000000..5409e47b8e --- /dev/null +++ b/maven-modules/maven-repositories/maven-snapshot-repository/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + maven-snapshot-repository + Maven Snapshot Repository + 1.0.0-SNAPSHOT + + + com.baeldung + maven-repositories + 1.0.0-SNAPSHOT + + + + + nexus + nexus-snapshot + http://localhost:8081/repository/maven-snapshots/ + + true + + + false + + + + + + + nexus + nexus-snapshot + http://localhost:8081/repository/maven-snapshots/ + + + + \ No newline at end of file diff --git a/maven-modules/maven-repositories/pom.xml b/maven-modules/maven-repositories/pom.xml new file mode 100644 index 0000000000..ba39d00d65 --- /dev/null +++ b/maven-modules/maven-repositories/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + maven-repositories + 1.0.0-SNAPSHOT + Maven Repositories + pom + + + com.baeldung + maven-modules + 0.0.1-SNAPSHOT + + + + maven-release-repository + maven-snapshot-repository + maven-download-artifacts + + + \ No newline at end of file diff --git a/maven-modules/pom.xml b/maven-modules/pom.xml index 253f5d9fa0..412e26f041 100644 --- a/maven-modules/pom.xml +++ b/maven-modules/pom.xml @@ -41,6 +41,7 @@ maven-parent-pom-resolution maven-simple maven-classifier + maven-repositories diff --git a/parent-boot-3/README.md b/parent-boot-3/README.md new file mode 100644 index 0000000000..738d97bdfd --- /dev/null +++ b/parent-boot-3/README.md @@ -0,0 +1,3 @@ +## Parent Boot 2 + +This is a parent module for all projects using Spring Boot 3. diff --git a/parent-boot-3/pom.xml b/parent-boot-3/pom.xml new file mode 100644 index 0000000000..711096fec8 --- /dev/null +++ b/parent-boot-3/pom.xml @@ -0,0 +1,97 @@ + + + 4.0.0 + parent-boot-3 + 0.0.1-SNAPSHOT + parent-boot-3 + pom + Parent for all Spring Boot 3 modules + + + com.baeldung + parent-modules + 1.0.0-SNAPSHOT + + + + + + org.junit + junit-bom + ${junit-jupiter.version} + pom + import + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + ${start-class} + + + + + + repackage + + + + + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + + 3.0.0-M3 + 5.8.2 + 3.0.0-M7 + 1.18.22 + 17 + 3.17.0 + + + diff --git a/persistence-modules/fauna/pom.xml b/persistence-modules/fauna/pom.xml index 2f5da68ede..8c985e0b7c 100644 --- a/persistence-modules/fauna/pom.xml +++ b/persistence-modules/fauna/pom.xml @@ -9,11 +9,11 @@ fauna Blogging Service built with FaunaDB - - org.springframework.boot - spring-boot-starter-parent - 2.6.2 - + + com.baeldung + parent-boot-2 + 0.0.1-SNAPSHOT + ../../parent-boot-2 @@ -54,6 +54,7 @@ 17 + 3.17.0 \ No newline at end of file diff --git a/persistence-modules/hibernate-exceptions/README.md b/persistence-modules/hibernate-exceptions/README.md index 4acd6cd363..0225d3a753 100644 --- a/persistence-modules/hibernate-exceptions/README.md +++ b/persistence-modules/hibernate-exceptions/README.md @@ -6,4 +6,4 @@ - [Hibernate’s “Object References an Unsaved Transient Instance” Error](https://www.baeldung.com/hibernate-unsaved-transient-instance-error) - [EntityNotFoundException in Hibernate](https://www.baeldung.com/hibernate-entitynotfoundexception) - [Hibernate’s “Not-Null Property References a Null or Transient Value” Error](https://www.baeldung.com/hibernate-not-null-error) -- [Hibernate's “Detached Entity Passed to Persist” Error](https://www.baeldung.com/hibernate-detached-entity-passed-to-persist) +- [Hibernate’s “Detached Entity Passed to Persist” Error](https://www.baeldung.com/hibernate-detached-entity-passed-to-persist) diff --git a/persistence-modules/hibernate-exceptions/src/main/java/com/baeldung/hibernate/exception/detachedentity/HibernateUtil.java b/persistence-modules/hibernate-exceptions/src/main/java/com/baeldung/hibernate/exception/detachedentity/HibernateUtil.java index 0420755354..a7b1f496d9 100644 --- a/persistence-modules/hibernate-exceptions/src/main/java/com/baeldung/hibernate/exception/detachedentity/HibernateUtil.java +++ b/persistence-modules/hibernate-exceptions/src/main/java/com/baeldung/hibernate/exception/detachedentity/HibernateUtil.java @@ -24,7 +24,8 @@ public class HibernateUtil { settings.put(Environment.USER, "sa"); settings.put(Environment.PASS, ""); settings.put(Environment.DIALECT, "org.hibernate.dialect.HSQLDialect"); - settings.put(Environment.SHOW_SQL, "true"); + // enable to show Hibernate generated SQL + settings.put(Environment.SHOW_SQL, "false"); settings.put(Environment.FORMAT_SQL, "true"); settings.put(Environment.USE_SQL_COMMENTS, "true"); settings.put(Environment.HBM2DDL_AUTO, "update"); diff --git a/persistence-modules/hibernate-exceptions/src/test/resources/logback-test.xml b/persistence-modules/hibernate-exceptions/src/test/resources/logback-test.xml new file mode 100644 index 0000000000..5d5620b2c1 --- /dev/null +++ b/persistence-modules/hibernate-exceptions/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + [%d{ISO8601}]-[%thread] %-5level %logger - %msg%n + + + + + + + + + + + \ No newline at end of file diff --git a/persistence-modules/spring-data-jpa-enterprise/pom.xml b/persistence-modules/spring-data-jpa-enterprise/pom.xml index 587261dccc..16294cd3cc 100644 --- a/persistence-modules/spring-data-jpa-enterprise/pom.xml +++ b/persistence-modules/spring-data-jpa-enterprise/pom.xml @@ -87,7 +87,7 @@ 1.3.1.Final - 1.12.2 + 1.17.3 \ No newline at end of file diff --git a/persistence-modules/spring-data-jpa-enterprise/src/test/resources/application-tc-jdbc.yml b/persistence-modules/spring-data-jpa-enterprise/src/test/resources/application-tc-jdbc.yml index ad5906fa6e..d340e29bd0 100644 --- a/persistence-modules/spring-data-jpa-enterprise/src/test/resources/application-tc-jdbc.yml +++ b/persistence-modules/spring-data-jpa-enterprise/src/test/resources/application-tc-jdbc.yml @@ -1,6 +1,7 @@ spring: datasource: url: jdbc:tc:postgresql:11.1:///integration-tests-db + driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver jpa: hibernate: ddl-auto: create \ No newline at end of file diff --git a/pom.xml b/pom.xml index 6f727d0dd0..a4f6a744ea 100644 --- a/pom.xml +++ b/pom.xml @@ -1252,6 +1252,7 @@ quarkus-modules/quarkus-jandex spring-boot-modules/spring-boot-cassandre spring-boot-modules/spring-boot-camel + spring-boot-modules/spring-boot-3 testing-modules/testing-assertions persistence-modules/fauna lightrun diff --git a/quarkus-modules/quarkus-vs-springboot/README.md b/quarkus-modules/quarkus-vs-springboot/README.md index 05eaabb923..13c0b8ab5f 100644 --- a/quarkus-modules/quarkus-vs-springboot/README.md +++ b/quarkus-modules/quarkus-vs-springboot/README.md @@ -6,6 +6,9 @@ To follow this tutorial, you will need the following things: - Maven (Embedded, IDE, or local installation) - Docker (https://www.docker.com/) - Jmeter (https://jmeter.apache.org/) +- wrk (https://github.com/wg/wrk) +- hyperfoil (https://hyperfoil.io/) +- lua (https://www.lua.org/) To create this test, I used some custom features from Jmeter. You can install the Jmeter plugin manager here: https://loadium.com/blog/how-to-install-use-jmeter-plugin. After that, please install the following plugins: @@ -17,31 +20,32 @@ The test file is `load_test.jmx` in case of any change need. You can open it wit $jmeter_home/bin/jmeter -n -t load_test.jmx -l log.csv -e -o ./report ``` -Just remember to change the variable `jmeter_home` with the path to the JMeter folder. The path to the data files is relative, so either keep them in the same folder as the test or use Jmeter GUI to change it. +Just remember to change the variable `jmeter_home` with the path to the JMeter folder. The path to the data files is relative, so either keep them in the same folder as the test or use Jmeter GUI to change it. Rememeber that as mentioned in the article, we cannot consider the response times recorded by Jmeter due to the Coordinated Omission Problem. Open the VisualVM application and select your application to start monitoring before running the test, and of course, start the sample application first. ## Spring Boot To build the application, you only need to run the following command in the Spring project root: ``` -./mvnw package -f pom.xml +./mvnw clean package -f pom.xml ``` Or this one in case you want to build the native one: ``` -./mvnw -DskipTests package -Pnative -f pom.xml +./mvnw clean package -Pnative -f pom.xml ``` In this case, you will need to have the `GRAALVM_HOME` env variable defined. You only need this if you want to build the image locally. Otherwise, you can build it using docker by leveraging the Spring Boot maven plugin. It will pull a docker image of the GraalVM, and with that, it will create the native image of the app. To do that, run: ``` -./mvnw spring-boot:build-image +./mvnw clean package spring-boot:build-image -Pnative -f pom.xml ``` -You can also create a docker image with the JVM version of the app running the script `build_jvm_docker.sh` or: +You can also create a docker image with the JVM version one of the app running the script `build.sh` or: ``` -docker build -f src/main/docker/Dockerfile.jvm -t spring-project:0.1-SNAPSHOT . +./mvnw clean package spring-boot:build-image -f pom.xml + ``` -You can execute the script `start_app.sh` or `start_jvm.sh` to run the application locally. In this case, you will need the Postgres DB. You can run it in docker with the command: +You can execute the script `start_app.sh` or `start_jvm.sh` to run the application locally. In this case, you will need the Mysql DB. You can run it in docker with the command: ``` -docker run -e POSTGRES_PASSWORD=example -p 5432:5432 postgres +docker run --name mysqldb --network=host -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=baeldung -d mysql:5.7.38 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci ``` You can also run both application and DB from docker, using: ``` @@ -67,7 +71,7 @@ And to the JVM version: To start the application locally, use either the scripts `start_app.sh` and `start_jvm.sh` with the docker DB: ``` -docker run -e POSTGRES_PASSWORD=example -p 5432:5432 postgres +docker run --name mysqldb --network=host -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=baeldung -d mysql:5.7.38 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci ``` Or use the script to build the docker image of the application, running: ```bash @@ -94,6 +98,38 @@ docker-compose -f src/main/docker/quarkus.yml up Now you have all you need to reproduce the tests with your machine. +## Wrk +Another option to execute the load test is to use the wrk. This library is capable of generation a pretty high load only using a single core. To install it you only have to checkout the project compile it (using make) and define the `wrk_home` envvar. To run the test use: + +``` +./run_test_wrk.sh +``` +You will need to have installed lua in your machine. + +### Tips +If you want to run the applications in your machine you can use the following command to restrict the CPUs available to the app: + +``` +cpulimit -l 300 -p ## 300 means at most 3 cores. +``` + +This will make sure the load is on the application and not in the DB. +## Hyperfoil + +To the hyperfoil test to get a report regarding the performance of the application, its throughput and response time. You can run the `docker_run.sh` from the hyperfoil folder, or the following: + +``` +docker run -it -v volume:/benchmarks:Z -v tmp/reports:/tmp/reports:Z --network=host quay.io/hyperfoil/hyperfoil cli +``` +And then: +``` +start-local && upload /benchmarks/benchmark.hf.yaml && run benchmark +``` +Optionally, we can extract a html report from it, by running: +``` +report --destination=/tmp/reports +``` + ### Relevant Articles: - [Spring Boot vs Quarkus](https://www.baeldung.com/spring-boot-vs-quarkus) diff --git a/quarkus-modules/quarkus-vs-springboot/hyperfoil/docker_run.sh b/quarkus-modules/quarkus-vs-springboot/hyperfoil/docker_run.sh new file mode 100644 index 0000000000..ee0ee35a29 --- /dev/null +++ b/quarkus-modules/quarkus-vs-springboot/hyperfoil/docker_run.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +docker run -it -v $SCRIPTPATH/volume:/benchmarks:Z -v $SCRIPTPATH/tmp/reports:/tmp/reports:Z --network=host quay.io/hyperfoil/hyperfoil cli + +#start-local && upload /benchmarks/benchmark.hf.yaml && run benchmark + +# step 1 run: start-local +# step 2 run (Run this every time the file is modified): upload /benchmarks/benchmark.hf.yaml +# step 3 run: run benchmark +# step 4 run: stats +# step 5 run: report --destination=/tmp/reports diff --git a/quarkus-modules/quarkus-vs-springboot/hyperfoil/volume/benchmark.hf.yaml b/quarkus-modules/quarkus-vs-springboot/hyperfoil/volume/benchmark.hf.yaml new file mode 100644 index 0000000000..598c61249e --- /dev/null +++ b/quarkus-modules/quarkus-vs-springboot/hyperfoil/volume/benchmark.hf.yaml @@ -0,0 +1,86 @@ +name: benchmark +http: + host: http://localhost:8080 + sharedConnections: 100 +phases: + - main: + constantRate: + startAfter: rampup + usersPerSec: 3300 + maxSessions: 6000 + duration: 5m + forks: + - post_zipcode: &post_zipcode + scenario: + - fetchIndex: + - randomCsvRow: + file: /benchmarks/zip_code_database.csv + removeQuotes: true + columns: + 0: zip + 1: type + 3: city + 6: state + 7: county + 8: timezone + - httpRequest: + sla: + - blockedRatio: 500 + POST: /zipcode + headers: + Content-Type: application/json;charset=UTF-8 + Accept: application/json + body: | + { + "zip" : "${zip}", + "type" : "${type}", + "city" : "${city}", + "state" : "${state}", + "county" : "${county}", + "timezone" : "${timezone}" + } + - get_zipcode: &get_zipcode + scenario: + - fetchIndex: + - randomCsvRow: + file: /benchmarks/zip_code_database.csv + removeQuotes: true + columns: + 0: zipcode + - httpRequest: + sla: + - blockedRatio: 500 + headers: + accept: application/json + GET: /zipcode/${zipcode} + - get_zipcode_by_city: &get_zipcode_by_city + scenario: + - fetchDetails: + - randomCsvRow: + file: /benchmarks/cities.csv + removeQuotes: true + columns: + 0: city + - httpRequest: + sla: + - blockedRatio: 500 + headers: + accept: application/json + GET: /zipcode/by_city?city=${city} + - spike: + constantRate: + startAfter: main + usersPerSec: 4400 + duration: 2m + forks: + - get_zipcode_by_city: *get_zipcode_by_city + - get_zipcode: *get_zipcode + + - rampup: + increasingRate: + initialUsersPerSec: 3 + targetUsersPerSec: 2500 + duration: 1m + forks: + - post_zipcode: *post_zipcode + - get_zipcode: *get_zipcode diff --git a/quarkus-modules/quarkus-vs-springboot/cities.csv b/quarkus-modules/quarkus-vs-springboot/hyperfoil/volume/cities.csv similarity index 100% rename from quarkus-modules/quarkus-vs-springboot/cities.csv rename to quarkus-modules/quarkus-vs-springboot/hyperfoil/volume/cities.csv diff --git a/quarkus-modules/quarkus-vs-springboot/jmeter/cities.csv b/quarkus-modules/quarkus-vs-springboot/jmeter/cities.csv new file mode 100644 index 0000000000..3b7016f3b5 --- /dev/null +++ b/quarkus-modules/quarkus-vs-springboot/jmeter/cities.csv @@ -0,0 +1,136 @@ +Holtsville +Adjuntas +Aguada +Aguadilla +Maricao +Anasco +Angeles +Arecibo +Bajadero +Barceloneta +Boqueron +Cabo Rojo +Penuelas +Camuy +Castaner +Rosario +Sabana Grande +Ciales +Utuado +Dorado +Ensenada +Florida +Garrochales +Guanica +Guayanilla +Hatillo +Hormigueros +Isabela +Jayuya +Lajas +Lares +Las Marias +Manati +Moca +Rincon +Quebradillas +Mayaguez +San German +San Sebastian +Morovis +Sabana Hoyos +San Antonio +Vega Alta +Vega Baja +Yauco +Aguas Buenas +Aguirre +Aibonito +Maunabo +Arroyo +Mercedita +Ponce +Naguabo +Naranjito +Orocovis +Palmer +Patillas +Caguas +Canovanas +Ceiba +Cayey +Fajardo +Cidra +Puerto Real +Punta Santiago +Roosevelt Roads +Rio Blanco +Rio Grande +Salinas +San Lorenzo +Santa Isabel +Vieques +Villalba +Yabucoa +Coamo +Las Piedras +Loiza +Luquillo +Culebra +Juncos +Gurabo +Coto Laurel +Comerio +Corozal +Guayama +La Plata +Humacao +Barranquitas +Juana Diaz +St Thomas +Christiansted +St John +Frederiksted +Kingshill +San Juan +Fort Buchanan +Toa Baja +Sabana Seca +Toa Alta +Bayamon +Catano +Guaynabo +Trujillo Alto +Saint Just +Carolina +Agawam +Amherst +Barre +Belchertown +Blandford +Bondsville +Brimfield +Chester +Chesterfield +Chicopee +Cummington +Easthampton +East Longmeadow +East Otis +Feeding Hills +Gilbertville +Goshen +Granby +Granville +Hadley +Hampden +Hardwick +Hatfield +Haydenville +Holyoke +Huntington +Leeds +Leverett +Ludlow +Monson +North Amherst \ No newline at end of file diff --git a/quarkus-modules/quarkus-vs-springboot/load_test.jmx b/quarkus-modules/quarkus-vs-springboot/jmeter/load_test.jmx similarity index 100% rename from quarkus-modules/quarkus-vs-springboot/load_test.jmx rename to quarkus-modules/quarkus-vs-springboot/jmeter/load_test.jmx diff --git a/quarkus-modules/quarkus-vs-springboot/run_test.sh b/quarkus-modules/quarkus-vs-springboot/jmeter/run_test_jmeter.sh similarity index 100% rename from quarkus-modules/quarkus-vs-springboot/run_test.sh rename to quarkus-modules/quarkus-vs-springboot/jmeter/run_test_jmeter.sh diff --git a/quarkus-modules/quarkus-vs-springboot/quarkus-project/build.sh b/quarkus-modules/quarkus-vs-springboot/quarkus-project/build.sh index 85761adab0..22b6d5c9d4 100644 --- a/quarkus-modules/quarkus-vs-springboot/quarkus-project/build.sh +++ b/quarkus-modules/quarkus-vs-springboot/quarkus-project/build.sh @@ -2,12 +2,14 @@ SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" -./mvnw quarkus:add-extension -Dextensions=container-image-docker +mvn quarkus:add-extension -Dextensions=container-image-docker if [ "$1" = "native" ]; then - ./mvnw package -Pnative -Dquarkus.native.container-build=true -f $SCRIPTPATH/pom.xml && + mvn clean package -Pnative -Dquarkus.native.container-build=true -f $SCRIPTPATH/pom.xml && docker build -f $SCRIPTPATH/src/main/docker/Dockerfile.native -t quarkus-project:0.1-SNAPSHOT $SCRIPTPATH/. +elif [ "$1" = "local-native" ]; then + mvn clean package -DskipTests -Pnative -f $SCRIPTPATH/pom.xml else - ./mvnw package -Dquarkus.container-build=true -f $SCRIPTPATH/pom.xml && + mvn clean package -Dquarkus.container-build=true -f $SCRIPTPATH/pom.xml && docker build -f $SCRIPTPATH/src/main/docker/Dockerfile.jvm -t quarkus-project:0.1-SNAPSHOT $SCRIPTPATH/. fi \ No newline at end of file diff --git a/quarkus-modules/quarkus-vs-springboot/quarkus-project/pom.xml b/quarkus-modules/quarkus-vs-springboot/quarkus-project/pom.xml index eeeb9d3256..8f28fde4a6 100644 --- a/quarkus-modules/quarkus-vs-springboot/quarkus-project/pom.xml +++ b/quarkus-modules/quarkus-vs-springboot/quarkus-project/pom.xml @@ -1,150 +1,143 @@ - - 4.0.0 - quarkus-project - 0.1-SNAPSHOT - - - com.baeldung - quarkus-vs-springboot - 1.0-SNAPSHOT - - - - - - ${quarkus.platform.group-id} - ${quarkus.platform.artifact-id} - ${quarkus.platform.version} - pom - import - - - - + 4.0.0 + + com.baeldung + quarkus-vs-springboot + 1.0-SNAPSHOT + + quarkus-project + 0.1-SNAPSHOT + + 3.10.1 + true + 11 + 11 + UTF-8 + UTF-8 + quarkus-bom + io.quarkus.platform + 2.9.2.Final + 3.0.0-M6 + + - - io.quarkus - quarkus-hibernate-reactive-panache - - - io.quarkus - quarkus-resteasy-reactive - - - io.quarkus - quarkus-resteasy-reactive-jackson - - - io.quarkus - quarkus-reactive-pg-client - - - io.quarkus - quarkus-arc - - - io.quarkus - quarkus-container-image-docker - - - io.quarkus - quarkus-junit5 - test - - - io.rest-assured - rest-assured - test - + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + - - + + + + io.quarkus + quarkus-hibernate-reactive-panache + + + io.quarkus + quarkus-resteasy-reactive + + + io.quarkus + quarkus-resteasy-reactive-jackson + + + io.quarkus + quarkus-reactive-mysql-client + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-container-image-docker + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + + + + ${quarkus.platform.group-id} + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + build + generate-code + generate-code-tests + + + + + + maven-compiler-plugin + ${compiler-plugin.version} + + ${maven.compiler.parameters} + + + + maven-surefire-plugin + ${surefire-plugin.version} + + false + + org.jboss.logmanager.LogManager + + + + + + + + native + + + native + + + - - ${quarkus.platform.group-id} - quarkus-maven-plugin - ${quarkus.platform.version} - true - - - - build - generate-code - generate-code-tests - - - - - - maven-compiler-plugin - ${compiler-plugin.version} + + maven-failsafe-plugin + ${surefire-plugin.version} + + + + integration-test + verify + - ${maven.compiler.parameters} + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + - - - maven-surefire-plugin - ${surefire-plugin.version} - - false - - org.jboss.logmanager.LogManager - - - + + + - - - - native - - - native - - - - - - maven-failsafe-plugin - ${surefire-plugin.version} - - - - integration-test - verify - - - - ${project.build.directory}/${project.build.finalName}-runner - org.jboss.logmanager.LogManager - - - - - - - - - -H:+AllowVMInspection - native - - - - - - 3.8.1 - true - 11 - 11 - UTF-8 - UTF-8 - quarkus-bom - io.quarkus.platform - 2.2.2.Final - 3.0.0-M4 - - - \ No newline at end of file + + + -H:+AllowVMInspection + native + + + + diff --git a/quarkus-modules/quarkus-vs-springboot/quarkus-project/src/main/docker/Dockerfile.jvm b/quarkus-modules/quarkus-vs-springboot/quarkus-project/src/main/docker/Dockerfile.jvm index e5d6d4d851..63ba538a4a 100644 --- a/quarkus-modules/quarkus-vs-springboot/quarkus-project/src/main/docker/Dockerfile.jvm +++ b/quarkus-modules/quarkus-vs-springboot/quarkus-project/src/main/docker/Dockerfile.jvm @@ -41,7 +41,8 @@ RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \ && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/conf/security/java.security # Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size. -ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.port=5000 -Dcom.sun.management.jmxremote.rmi.port=5001 -Dcom.sun.management.jmxremote.host=0.0.0.0 -Djava.rmi.server.hostname=0.0.0.0" + # We make four distinct layers so if there are application changes the library layers can be re-used COPY --chown=1001 target/quarkus-app/lib/ /deployments/lib/ COPY --chown=1001 target/quarkus-app/*.jar /deployments/ diff --git a/quarkus-modules/quarkus-vs-springboot/quarkus-project/src/main/docker/quarkus.yml b/quarkus-modules/quarkus-vs-springboot/quarkus-project/src/main/docker/quarkus.yml index 00bdcf9292..60e35b6cca 100644 --- a/quarkus-modules/quarkus-vs-springboot/quarkus-project/src/main/docker/quarkus.yml +++ b/quarkus-modules/quarkus-vs-springboot/quarkus-project/src/main/docker/quarkus.yml @@ -1,23 +1,25 @@ version: '3.1' - services: db: - image: postgres + image: mysql:5.7.38 ports: - - '5432:5432' + - '3306:3306' environment: - POSTGRES_PASSWORD: example + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: baeldung + command: [ 'mysqld', '--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci' ] + healthcheck: + test: mysqladmin ping -h 127.0.0.1 -u $$MYSQL_USER --password=$$MYSQL_PASSWORD app: image: quarkus-project:0.1-SNAPSHOT - ports: - - '8080:8080' + network_mode: "host" environment: - DB_URL: postgresql://db:5432/postgres - links: - - "db" + DB_URL: mysql://localhost:3306/baeldung?useSSL=true&requireSSL=true + HOST_HOSTNAME: ${EXTERNAL_IP} depends_on: - - "db" -networks: - default: - driver: bridge - + db: + condition: service_healthy + deploy: + resources: + limits: + cpus: '3.00' diff --git a/quarkus-modules/quarkus-vs-springboot/quarkus-project/src/main/java/com/baeldung/quarkus_project/ZipCodeRepo.java b/quarkus-modules/quarkus-vs-springboot/quarkus-project/src/main/java/com/baeldung/quarkus_project/ZipCodeRepo.java index 74f46c33ea..f6736a6e9e 100644 --- a/quarkus-modules/quarkus-vs-springboot/quarkus-project/src/main/java/com/baeldung/quarkus_project/ZipCodeRepo.java +++ b/quarkus-modules/quarkus-vs-springboot/quarkus-project/src/main/java/com/baeldung/quarkus_project/ZipCodeRepo.java @@ -1,6 +1,7 @@ package com.baeldung.quarkus_project; import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase; +import io.quarkus.hibernate.reactive.panache.common.runtime.ReactiveTransactional; import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; @@ -13,7 +14,8 @@ public class ZipCodeRepo implements PanacheRepositoryBase { return find("city = ?1", city).stream(); } + @ReactiveTransactional public Uni save(ZipCode zipCode) { - return zipCode.persistAndFlush(); + return zipCode.persist(); } } diff --git a/quarkus-modules/quarkus-vs-springboot/quarkus-project/src/main/java/com/baeldung/quarkus_project/ZipCodeResource.java b/quarkus-modules/quarkus-vs-springboot/quarkus-project/src/main/java/com/baeldung/quarkus_project/ZipCodeResource.java index b4d41fd855..cb9b0226f3 100644 --- a/quarkus-modules/quarkus-vs-springboot/quarkus-project/src/main/java/com/baeldung/quarkus_project/ZipCodeResource.java +++ b/quarkus-modules/quarkus-vs-springboot/quarkus-project/src/main/java/com/baeldung/quarkus_project/ZipCodeResource.java @@ -2,9 +2,8 @@ package com.baeldung.quarkus_project; import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; -import org.jboss.logging.Logger; -import javax.transaction.Transactional; +import javax.persistence.PersistenceException; import javax.ws.rs.*; import javax.ws.rs.core.MediaType; @@ -22,7 +21,7 @@ public class ZipCodeResource { @GET @Path("/{zipcode}") public Uni findById(@PathParam("zipcode") String zipcode) { - return zipRepo.findById(zipcode); + return getById(zipcode); } @GET @@ -32,12 +31,17 @@ public class ZipCodeResource { } @POST - @Transactional public Uni create(ZipCode zipCode) { - return zipRepo.findById(zipCode.getZip()) + return getById(zipCode.getZip()) .onItem() .ifNull() - .switchTo(createZipCode(zipCode)); + .switchTo(createZipCode(zipCode)) + .onFailure(PersistenceException.class) + .recoverWithUni(() -> getById(zipCode.getZip())); + } + + private Uni getById(String zipCode) { + return zipRepo.findById(zipCode); } private Uni createZipCode(ZipCode zipCode) { diff --git a/quarkus-modules/quarkus-vs-springboot/quarkus-project/src/main/resources/application.properties b/quarkus-modules/quarkus-vs-springboot/quarkus-project/src/main/resources/application.properties index 918a129500..7c1bee8da5 100644 --- a/quarkus-modules/quarkus-vs-springboot/quarkus-project/src/main/resources/application.properties +++ b/quarkus-modules/quarkus-vs-springboot/quarkus-project/src/main/resources/application.properties @@ -1,9 +1,12 @@ -quarkus.datasource.db-kind=postgresql -quarkus.datasource.username=postgres -quarkus.datasource.password=example +quarkus.datasource.db-kind=mysql +quarkus.datasource.username=root +quarkus.datasource.password=root -quarkus.datasource.reactive.url=${DB_URL:postgresql://localhost:5432/postgres} -quarkus.datasource.reactive.max-size=20 +quarkus.datasource.reactive.url=${DB_URL:mysql://localhost:3306/baeldung?useSSL=true&requireSSL=true} +quarkus.datasource.reactive.max-size=95 +quarkus.datasource.reactive.mysql.ssl-mode=required #quarkus.hibernate-orm.log.sql=true quarkus.hibernate-orm.database.generation=drop-and-create +quarkus.native.enable-vm-inspection=true +quarkus.datasource.reactive.trust-all=true \ No newline at end of file diff --git a/quarkus-modules/quarkus-vs-springboot/spring-project/build.sh b/quarkus-modules/quarkus-vs-springboot/spring-project/build.sh new file mode 100755 index 0000000000..d8e131d244 --- /dev/null +++ b/quarkus-modules/quarkus-vs-springboot/spring-project/build.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ "$1" = "native" ]; then + mvn clean package -DskipTests spring-boot:build-image -Pnative -f $SCRIPTPATH/pom.xml +elif [ "$1" = "local-native" ]; then + mvn clean package -DskipTests -Plocal-native -f $SCRIPTPATH/pom.xml +else + mvn clean package -DskipTests spring-boot:build-image -f $SCRIPTPATH/pom.xml +fi \ No newline at end of file diff --git a/quarkus-modules/quarkus-vs-springboot/spring-project/build_jvm_docker.sh b/quarkus-modules/quarkus-vs-springboot/spring-project/build_jvm_docker.sh deleted file mode 100644 index c7ee730ec7..0000000000 --- a/quarkus-modules/quarkus-vs-springboot/spring-project/build_jvm_docker.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" - -docker build -f $SCRIPTPATH/src/main/docker/Dockerfile.jvm -t spring-project:0.1-SNAPSHOT $SCRIPTPATH/. - diff --git a/quarkus-modules/quarkus-vs-springboot/spring-project/pom.xml b/quarkus-modules/quarkus-vs-springboot/spring-project/pom.xml index 7f0fa4c8c6..408c223e9f 100644 --- a/quarkus-modules/quarkus-vs-springboot/spring-project/pom.xml +++ b/quarkus-modules/quarkus-vs-springboot/spring-project/pom.xml @@ -10,8 +10,8 @@ org.springframework.boot spring-boot-starter-parent - 2.6.0 - + 2.6.9 + @@ -29,14 +29,9 @@ ${spring-native.version} - io.r2dbc - r2dbc-postgresql - runtime - - - org.postgresql - postgresql - runtime + com.github.jasync-sql + jasync-r2dbc-mysql + 2.0.8 org.springframework.boot @@ -48,131 +43,210 @@ reactor-test test + + + org.testcontainers + testcontainers + test + + + + org.testcontainers + r2dbc + test + + + + org.testcontainers + mysql + test + + + + mysql + mysql-connector-java + test + + + + org.testcontainers + junit-jupiter + test + + + + + + org.testcontainers + testcontainers-bom + 1.17.2 + pom + import + + + + org.springframework.boot spring-boot-maven-plugin - ${repackage.classifier} + exec + + true + paketobuildpacks/builder:tiny - true + false + true - org.springframework.experimental - spring-aot-maven-plugin - ${spring-native.version} - - - test-generate - - test-generate - - - - generate - - generate - - - + maven-surefire-plugin + ${surefire-plugin.version} + + + **/*IT + + - spring-releases - Spring Releases + spring-release + Spring release https://repo.spring.io/release - - false - - - - - spring-milestones - Spring Milestones - https://repo.spring.io/libs-milestone-local - - false - - spring-releases - Spring Releases + spring-release + Spring release https://repo.spring.io/release - - false - - - - - spring-milestones - Spring Milestones - https://repo.spring.io/libs-milestone-local - - false - native - - exec - 0.9.3 - - org.graalvm.buildtools - junit-platform-native - ${native-buildtools.version} + org.junit.platform + junit-platform-launcher + test + + org.springframework.boot + spring-boot-maven-plugin + + + true + + + paketobuildpacks/builder:tiny + + true + true + + + + + + org.springframework.experimental + spring-aot-maven-plugin + + + test-generate + + test-generate + + + + generate + + generate + + + + + + + + + local-native + + exec + 0.9.11 + + + + org.junit.platform + junit-platform-launcher + test + + + + + + org.springframework.experimental + spring-aot-maven-plugin + + + test-generate + + test-generate + + + + generate + + generate + + + + org.graalvm.buildtools native-maven-plugin ${native-buildtools.version} + true -H:+AllowVMInspection - - test-native - test - - test - - build-native - package build + package + + + test-native + + test + + test org.apache.maven.plugins maven-surefire-plugin + 3.0.0-M6 -DspringAot=true -agentlib:native-image-agent=access-filter-file=src/test/resources/access-filter.json,config-merge-dir=target/classes/META-INF/native-image @@ -185,9 +259,8 @@ 11 - - 0.11.0-RC1 - 2.17.1 + 0.12.1 + 3.0.0-M6 - \ No newline at end of file + diff --git a/quarkus-modules/quarkus-vs-springboot/spring-project/src/main/docker/Dockerfile.jvm b/quarkus-modules/quarkus-vs-springboot/spring-project/src/main/docker/Dockerfile.jvm deleted file mode 100644 index ca3f3cca76..0000000000 --- a/quarkus-modules/quarkus-vs-springboot/spring-project/src/main/docker/Dockerfile.jvm +++ /dev/null @@ -1,12 +0,0 @@ -FROM openjdk:11 - -ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' - -COPY --chown=1001 target/spring-project-0.1-SNAPSHOT-exec.jar /spring-app/ - -WORKDIR /spring-app - -EXPOSE 8080 -USER 1001 - -ENTRYPOINT ["java", "-jar", "spring-project-0.1-SNAPSHOT-exec.jar" ] \ No newline at end of file diff --git a/quarkus-modules/quarkus-vs-springboot/spring-project/src/main/docker/spring.yml b/quarkus-modules/quarkus-vs-springboot/spring-project/src/main/docker/spring.yml index 2214e0a898..347b5dfe2f 100644 --- a/quarkus-modules/quarkus-vs-springboot/spring-project/src/main/docker/spring.yml +++ b/quarkus-modules/quarkus-vs-springboot/spring-project/src/main/docker/spring.yml @@ -2,21 +2,25 @@ version: '3.1' services: db: - image: postgres + image: mysql:5.7.38 ports: - - '5432:5432' + - '3306:3306' environment: - POSTGRES_PASSWORD: example + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: baeldung + command: [ 'mysqld', '--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci' ] + healthcheck: + test: mysqladmin ping -h 127.0.0.1 -u $$MYSQL_USER --password=$$MYSQL_PASSWORD app: - image: spring-project:0.1-SNAPSHOT - ports: - - '8080:8080' + image: docker.io/library/spring-project:0.1-SNAPSHOT + network_mode: "host" environment: - DB_URL: r2dbc:postgresql://db:5432/postgres - links: - - "db" + DB_URL: r2dbc:mysql://localhost:3306/baeldung?useSSL=true&requireSSL=true + HOST_HOSTNAME: ${EXTERNAL_IP} depends_on: - - "db" -networks: - default: - driver: bridge \ No newline at end of file + db: + condition: service_healthy + deploy: + resources: + limits: + cpus: '3.00' diff --git a/quarkus-modules/quarkus-vs-springboot/spring-project/src/main/java/com/baeldung/spring_project/Startup.java b/quarkus-modules/quarkus-vs-springboot/spring-project/src/main/java/com/baeldung/spring_project/Startup.java index 48cf7e8ed1..e8544da8db 100644 --- a/quarkus-modules/quarkus-vs-springboot/spring-project/src/main/java/com/baeldung/spring_project/Startup.java +++ b/quarkus-modules/quarkus-vs-springboot/spring-project/src/main/java/com/baeldung/spring_project/Startup.java @@ -1,21 +1,22 @@ package com.baeldung.spring_project; -import com.baeldung.spring_project.domain.ZIPRepo; import io.r2dbc.spi.ConnectionFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.core.io.ByteArrayResource; +import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; import org.springframework.r2dbc.connection.R2dbcTransactionManager; import org.springframework.r2dbc.connection.init.ConnectionFactoryInitializer; import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator; import org.springframework.transaction.ReactiveTransactionManager; @SpringBootApplication +@EnableR2dbcRepositories public class Startup { public static void main(String[] args) { - SpringApplication.run(Startup.class, args).getBean(ZIPRepo.class).findById(""); + SpringApplication.run(Startup.class, args); } @Bean @@ -34,4 +35,5 @@ public class Startup { @Bean ReactiveTransactionManager transactionManager(ConnectionFactory connectionFactory) { return new R2dbcTransactionManager(connectionFactory); } + } diff --git a/quarkus-modules/quarkus-vs-springboot/spring-project/src/main/java/com/baeldung/spring_project/ZipCodeApi.java b/quarkus-modules/quarkus-vs-springboot/spring-project/src/main/java/com/baeldung/spring_project/ZipCodeApi.java index 263ce67e21..8d1f07b7b9 100644 --- a/quarkus-modules/quarkus-vs-springboot/spring-project/src/main/java/com/baeldung/spring_project/ZipCodeApi.java +++ b/quarkus-modules/quarkus-vs-springboot/spring-project/src/main/java/com/baeldung/spring_project/ZipCodeApi.java @@ -2,11 +2,14 @@ package com.baeldung.spring_project; import com.baeldung.spring_project.domain.ZIPRepo; import com.baeldung.spring_project.domain.ZipCode; -import org.springframework.transaction.annotation.Transactional; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.r2dbc.UncategorizedR2dbcException; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.function.Function; import java.util.function.Supplier; @RestController @@ -21,7 +24,7 @@ public class ZipCodeApi { @GetMapping("/{zipcode}") public Mono findById(@PathVariable String zipcode) { - return zipRepo.findById(zipcode); + return getById(zipcode); } @GetMapping("/by_city") @@ -29,10 +32,23 @@ public class ZipCodeApi { return zipRepo.findByCity(city); } - @Transactional @PostMapping public Mono create(@RequestBody ZipCode zipCode) { - return zipRepo.findById(zipCode.getZip()).switchIfEmpty(Mono.defer(createZipCode(zipCode))); + return getById(zipCode.getZip()) + .switchIfEmpty(Mono.defer(createZipCode(zipCode))) + .onErrorResume(this::isKeyDuplicated, this.recoverWith(zipCode)); + } + + private Mono getById(String zipCode) { + return zipRepo.findById(zipCode); + } + + private boolean isKeyDuplicated(Throwable ex) { + return ex instanceof DataIntegrityViolationException || ex instanceof UncategorizedR2dbcException; + } + + private Function> recoverWith(ZipCode zipCode) { + return throwable -> zipRepo.findById(zipCode.getZip()); } private Supplier> createZipCode(ZipCode zipCode) { diff --git a/quarkus-modules/quarkus-vs-springboot/spring-project/src/main/resources/application.properties b/quarkus-modules/quarkus-vs-springboot/spring-project/src/main/resources/application.properties index 1d49b67fda..e303baf6f6 100644 --- a/quarkus-modules/quarkus-vs-springboot/spring-project/src/main/resources/application.properties +++ b/quarkus-modules/quarkus-vs-springboot/spring-project/src/main/resources/application.properties @@ -1,5 +1,7 @@ -spring.r2dbc.url=${DB_URL:r2dbc:postgresql://localhost:5432/postgres} -spring.r2dbc.username=postgres -spring.r2dbc.password=example -spring.r2dbc.pool.enabled=true -spring.r2dbc.pool.maxSize=20 \ No newline at end of file +spring.r2dbc.url=${DB_URL:r2dbc:mysql://localhost:3306/baeldung?useSSL=true&requireSSL=true} +spring.r2dbc.properties.sslMode=required +spring.r2dbc.username=root +spring.r2dbc.password=root +spring.r2dbc.pool.enabled=true +spring.r2dbc.pool.maxSize=95 + diff --git a/quarkus-modules/quarkus-vs-springboot/spring-project/src/test/java/com/baeldung/spring_project/StartupIT.java b/quarkus-modules/quarkus-vs-springboot/spring-project/src/test/java/com/baeldung/spring_project/StartupIT.java index 7487e5aa7f..7715fdc1d2 100644 --- a/quarkus-modules/quarkus-vs-springboot/spring-project/src/test/java/com/baeldung/spring_project/StartupIT.java +++ b/quarkus-modules/quarkus-vs-springboot/spring-project/src/test/java/com/baeldung/spring_project/StartupIT.java @@ -1,9 +1,20 @@ package com.baeldung.spring_project; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.boot.test.context.SpringBootTest; +import org.testcontainers.junit.jupiter.Testcontainers; -@SpringBootTest +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; + +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { "spring.r2dbc.url=r2dbc:tc:mysql:///baeldung?TC_IMAGE_TAG=5.7.34"} +) +@TestInstance(value = PER_CLASS) +@Testcontainers +@Disabled class StartupIT { @Test diff --git a/quarkus-modules/quarkus-vs-springboot/wrk/cities.csv b/quarkus-modules/quarkus-vs-springboot/wrk/cities.csv new file mode 100644 index 0000000000..3b7016f3b5 --- /dev/null +++ b/quarkus-modules/quarkus-vs-springboot/wrk/cities.csv @@ -0,0 +1,136 @@ +Holtsville +Adjuntas +Aguada +Aguadilla +Maricao +Anasco +Angeles +Arecibo +Bajadero +Barceloneta +Boqueron +Cabo Rojo +Penuelas +Camuy +Castaner +Rosario +Sabana Grande +Ciales +Utuado +Dorado +Ensenada +Florida +Garrochales +Guanica +Guayanilla +Hatillo +Hormigueros +Isabela +Jayuya +Lajas +Lares +Las Marias +Manati +Moca +Rincon +Quebradillas +Mayaguez +San German +San Sebastian +Morovis +Sabana Hoyos +San Antonio +Vega Alta +Vega Baja +Yauco +Aguas Buenas +Aguirre +Aibonito +Maunabo +Arroyo +Mercedita +Ponce +Naguabo +Naranjito +Orocovis +Palmer +Patillas +Caguas +Canovanas +Ceiba +Cayey +Fajardo +Cidra +Puerto Real +Punta Santiago +Roosevelt Roads +Rio Blanco +Rio Grande +Salinas +San Lorenzo +Santa Isabel +Vieques +Villalba +Yabucoa +Coamo +Las Piedras +Loiza +Luquillo +Culebra +Juncos +Gurabo +Coto Laurel +Comerio +Corozal +Guayama +La Plata +Humacao +Barranquitas +Juana Diaz +St Thomas +Christiansted +St John +Frederiksted +Kingshill +San Juan +Fort Buchanan +Toa Baja +Sabana Seca +Toa Alta +Bayamon +Catano +Guaynabo +Trujillo Alto +Saint Just +Carolina +Agawam +Amherst +Barre +Belchertown +Blandford +Bondsville +Brimfield +Chester +Chesterfield +Chicopee +Cummington +Easthampton +East Longmeadow +East Otis +Feeding Hills +Gilbertville +Goshen +Granby +Granville +Hadley +Hampden +Hardwick +Hatfield +Haydenville +Holyoke +Huntington +Leeds +Leverett +Ludlow +Monson +North Amherst \ No newline at end of file diff --git a/quarkus-modules/quarkus-vs-springboot/wrk/generator.lua b/quarkus-modules/quarkus-vs-springboot/wrk/generator.lua new file mode 100644 index 0000000000..a1973072d9 --- /dev/null +++ b/quarkus-modules/quarkus-vs-springboot/wrk/generator.lua @@ -0,0 +1,65 @@ +local require = require +local json = require "json" + +math.randomseed(os.time()) + +-- read csv lines +function ParseCSVLine(line,sep) + local res = {} + local pos = 1 + sep = sep or ',' + while true do + local c = string.sub(line,pos,pos) + if (c == "") then break end + if (c == '"') then + local txt = "" + repeat + local startp,endp = string.find(line,'^%b""',pos) + txt = txt..string.sub(line,startp+1,endp-1) + pos = endp + 1 + c = string.sub(line,pos,pos) + if (c == '"') then txt = txt..'"' end + until (c ~= '"') + table.insert(res,txt) + assert(c == sep or c == "") + pos = pos + 1 + else + local startp,endp = string.find(line,sep,pos) + if (startp) then + table.insert(res,string.sub(line,pos,startp-1)) + pos = endp + 1 + else + table.insert(res,string.sub(line,pos)) + break + end + end + end + return res +end + +loadFile = function() + local filename = "zip_code_database.csv" + + local data = {} + local count = 0 + local sep = "," + + for line in io.lines(filename) do + local values = ParseCSVLine(line,sep) + data[count + 1] = { zip=values[1], type=values[2], city=values[4], state=values[7], county=values[8], timezone=values[9] } + count = count + 1 + end + + return data +end + +generator = function() + local data = loadFile() + return coroutine.create(function() + for k,v in pairs(data) do + coroutine.yield(json.stringify(v)) + end + end) +end + +return generator() \ No newline at end of file diff --git a/quarkus-modules/quarkus-vs-springboot/wrk/get_by_city.lua b/quarkus-modules/quarkus-vs-springboot/wrk/get_by_city.lua new file mode 100644 index 0000000000..b101e3bbda --- /dev/null +++ b/quarkus-modules/quarkus-vs-springboot/wrk/get_by_city.lua @@ -0,0 +1,79 @@ +local require = require +local json = require "json" + +math.randomseed(os.clock()*100000000000) + +function ParseCSVLine(line,sep) + local res = {} + local pos = 1 + sep = sep or ',' + while true do + local c = string.sub(line,pos,pos) + if (c == "") then break end + if (c == '"') then + local txt = "" + repeat + local startp,endp = string.find(line,'^%b""',pos) + txt = txt..string.sub(line,startp+1,endp-1) + pos = endp + 1 + c = string.sub(line,pos,pos) + if (c == '"') then txt = txt..'"' end + until (c ~= '"') + table.insert(res,txt) + assert(c == sep or c == "") + pos = pos + 1 + else + local startp,endp = string.find(line,sep,pos) + if (startp) then + table.insert(res,string.sub(line,pos,startp-1)) + pos = endp + 1 + else + table.insert(res,string.sub(line,pos)) + break + end + end + end + return res +end + +loadFile = function() + local filename = "cities.csv" + + local data = {} + local count = 0 + local sep = "," + + for line in io.lines(filename) do + local values = ParseCSVLine(line,sep) + data[count + 1] = values[1] + count = count + 1 + end + + return data +end + +local data = loadFile() + +local urlencode = function (str) + str = string.gsub (str, "([^0-9a-zA-Z !'()*._~-])", -- locale independent + function (c) return string.format ("%%%02X", string.byte(c)) end) + str = string.gsub (str, " ", "+") + return str +end + +request = function() + url_path = "/zipcode/by_city?city=" .. urlencode(data[math.random(1, 136)]) + + local headers = { ["Content-Type"] = "application/json;charset=UTF-8" } + + return wrk.format("GET", url_path, headers, nil) +end + +done = function(summary, latency, requests) + io.write("--------------GET CITY ZIPCODES----------------\n") + for _, p in pairs({ 50, 90, 99, 99.999 }) do + n = latency:percentile(p) + io.write(string.format("%g%%,%d\n", p, n)) + end + io.write("-----------------------------------------------\n\n") +end \ No newline at end of file diff --git a/quarkus-modules/quarkus-vs-springboot/wrk/get_zipcode.lua b/quarkus-modules/quarkus-vs-springboot/wrk/get_zipcode.lua new file mode 100644 index 0000000000..f2abf607c2 --- /dev/null +++ b/quarkus-modules/quarkus-vs-springboot/wrk/get_zipcode.lua @@ -0,0 +1,77 @@ +local require = require +local json = require "json" + +math.randomseed(os.clock()*100000000000) + +function ParseCSVLine(line,sep) + local res = {} + local pos = 1 + sep = sep or ',' + while true do + local c = string.sub(line,pos,pos) + if (c == "") then break end + if (c == '"') then + local txt = "" + repeat + local startp,endp = string.find(line,'^%b""',pos) + txt = txt..string.sub(line,startp+1,endp-1) + pos = endp + 1 + c = string.sub(line,pos,pos) + if (c == '"') then txt = txt..'"' end + + until (c ~= '"') + table.insert(res,txt) + assert(c == sep or c == "") + pos = pos + 1 + else + local startp,endp = string.find(line,sep,pos) + if (startp) then + table.insert(res,string.sub(line,pos,startp-1)) + pos = endp + 1 + else + table.insert(res,string.sub(line,pos)) + break + end + end + end + return res +end + +loadFile = function() + local filename = "zip_code_database.csv" + + local data = {} + local count = 0 + local sep = "," + + for line in io.lines(filename) do + local values = ParseCSVLine(line,sep) + data[count + 1] = values[1] + count = count + 1 + end + + return data +end + +local data = loadFile() + +request = function() + + local value = data[math.random(1, 12079)] + + url_path = "/zipcode/" .. value + + local headers = { ["Content-Type"] = "application/json;charset=UTF-8" } + + return wrk.format("GET", url_path, headers, nil) +end + + +done = function(summary, latency, requests) + io.write("--------------GET ZIPCODE----------------\n") + for _, p in pairs({ 50, 90, 99, 99.999 }) do + n = latency:percentile(p) + io.write(string.format("%g%%,%d\n", p, n)) + end + io.write("-----------------------------------------\n\n") +end diff --git a/quarkus-modules/quarkus-vs-springboot/wrk/json.lua b/quarkus-modules/quarkus-vs-springboot/wrk/json.lua new file mode 100644 index 0000000000..6f023a9294 --- /dev/null +++ b/quarkus-modules/quarkus-vs-springboot/wrk/json.lua @@ -0,0 +1,133 @@ +local json = {} + +local function kind_of(obj) + if type(obj) ~= 'table' then return type(obj) end + local i = 1 + for _ in pairs(obj) do + if obj[i] ~= nil then i = i + 1 else return 'table' end + end + if i == 1 then return 'table' else return 'array' end +end + +local function escape_str(s) + local in_char = {'\\', '"', '/', '\b', '\f', '\n', '\r', '\t'} + local out_char = {'\\', '"', '/', 'b', 'f', 'n', 'r', 't'} + for i, c in ipairs(in_char) do + s = s:gsub(c, '\\' .. out_char[i]) + end + return s +end + +local function skip_delim(str, pos, delim, err_if_missing) + pos = pos + #str:match('^%s*', pos) + if str:sub(pos, pos) ~= delim then + if err_if_missing then + error('Expected ' .. delim .. ' near position ' .. pos) + end + return pos, false + end + return pos + 1, true +end + +local function parse_str_val(str, pos, val) + val = val or '' + local early_end_error = 'End of input found while parsing string.' + if pos > #str then error(early_end_error) end + local c = str:sub(pos, pos) + if c == '"' then return val, pos + 1 end + if c ~= '\\' then return parse_str_val(str, pos + 1, val .. c) end + local esc_map = {b = '\b', f = '\f', n = '\n', r = '\r', t = '\t'} + local nextc = str:sub(pos + 1, pos + 1) + if not nextc then error(early_end_error) end + return parse_str_val(str, pos + 2, val .. (esc_map[nextc] or nextc)) +end + +local function parse_num_val(str, pos) + local num_str = str:match('^-?%d+%.?%d*[eE]?[+-]?%d*', pos) + local val = tonumber(num_str) + if not val then error('Error parsing number at position ' .. pos .. '.') end + return val, pos + #num_str +end + +function json.stringify(obj, as_key) + local s = {} + local kind = kind_of(obj) + if kind == 'array' then + if as_key then error('Can\'t encode array as key.') end + s[#s + 1] = '[' + for i, val in ipairs(obj) do + if i > 1 then s[#s + 1] = ', ' end + s[#s + 1] = json.stringify(val) + end + s[#s + 1] = ']' + elseif kind == 'table' then + if as_key then error('Can\'t encode table as key.') end + s[#s + 1] = '{' + for k, v in pairs(obj) do + if #s > 1 then s[#s + 1] = ', ' end + s[#s + 1] = json.stringify(k, true) + s[#s + 1] = ':' + s[#s + 1] = json.stringify(v) + end + s[#s + 1] = '}' + elseif kind == 'string' then + return '"' .. escape_str(obj) .. '"' + elseif kind == 'number' then + if as_key then return '"' .. tostring(obj) .. '"' end + return tostring(obj) + elseif kind == 'boolean' then + return tostring(obj) + elseif kind == 'nil' then + return 'null' + else + error('Unjsonifiable type: ' .. kind .. '.') + end + return table.concat(s) +end + +json.null = {} + +function json.parse(str, pos, end_delim) + pos = pos or 1 + if pos > #str then error('Reached unexpected end of input.') end + local pos = pos + #str:match('^%s*', pos) + local first = str:sub(pos, pos) + if first == '{' then + local obj, key, delim_found = {}, true, true + pos = pos + 1 + while true do + key, pos = json.parse(str, pos, '}') + if key == nil then return obj, pos end + if not delim_found then error('Comma missing between object items.') end + pos = skip_delim(str, pos, ':', true) + obj[key], pos = json.parse(str, pos) + pos, delim_found = skip_delim(str, pos, ',') + end + elseif first == '[' then + local arr, val, delim_found = {}, true, true + pos = pos + 1 + while true do + val, pos = json.parse(str, pos, ']') + if val == nil then return arr, pos end + if not delim_found then error('Comma missing between array items.') end + arr[#arr + 1] = val + pos, delim_found = skip_delim(str, pos, ',') + end + elseif first == '"' then + return parse_str_val(str, pos + 1) + elseif first == '-' or first:match('%d') then + return parse_num_val(str, pos) + elseif first == end_delim then + return nil, pos + 1 + else + local literals = {['true'] = true, ['false'] = false, ['null'] = json.null} + for lit_str, lit_val in pairs(literals) do + local lit_end = pos + #lit_str - 1 + if str:sub(pos, lit_end) == lit_str then return lit_val, lit_end + 1 end + end + local pos_info_str = 'position ' .. pos .. ': ' .. str:sub(pos, pos + 10) + error('Invalid json syntax starting at ' .. pos_info_str) + end +end + +return json diff --git a/quarkus-modules/quarkus-vs-springboot/wrk/post_zipcode.lua b/quarkus-modules/quarkus-vs-springboot/wrk/post_zipcode.lua new file mode 100644 index 0000000000..b8e60da015 --- /dev/null +++ b/quarkus-modules/quarkus-vs-springboot/wrk/post_zipcode.lua @@ -0,0 +1,73 @@ +local require = require +local json = require "json" + +math.randomseed(os.clock()*100000000000) + +function ParseCSVLine(line,sep) + local res = {} + local pos = 1 + sep = sep or ',' + while true do + local c = string.sub(line,pos,pos) + if (c == "") then break end + if (c == '"') then + local txt = "" + repeat + local startp,endp = string.find(line,'^%b""',pos) + txt = txt..string.sub(line,startp+1,endp-1) + pos = endp + 1 + c = string.sub(line,pos,pos) + if (c == '"') then txt = txt..'"' end + + until (c ~= '"') + table.insert(res,txt) + assert(c == sep or c == "") + pos = pos + 1 + else + local startp,endp = string.find(line,sep,pos) + if (startp) then + table.insert(res,string.sub(line,pos,startp-1)) + pos = endp + 1 + else + table.insert(res,string.sub(line,pos)) + break + end + end + end + return res +end + +loadFile = function() + local filename = "zip_code_database.csv" + + local data = {} + local count = 0 + local sep = "," + + for line in io.lines(filename) do + local values = ParseCSVLine(line,sep) + data[count + 1] = { zip=values[1], type=values[2], city=values[4], state=values[7], county=values[8], timezone=values[9] } + count = count + 1 + end + + return data +end + +local data = loadFile() + +request = function() + local url_path = "/zipcode" + local val = data[math.random(1, 12079)] + + local headers = { ["Content-Type"] = "application/json;charset=UTF-8" } + return wrk.format("POST", url_path, headers, json.stringify(val)) +end + +done = function(summary, latency, requests) + io.write("--------------POST ZIPCODE----------------\n") + for _, p in pairs({ 50, 75, 90, 99, 99.999 }) do + n = latency:percentile(p) + io.write(string.format("%g%%,%d\n", p, n)) + end + io.write("------------------------------------------\n\n") +end \ No newline at end of file diff --git a/quarkus-modules/quarkus-vs-springboot/wrk/run_test_wrk.sh b/quarkus-modules/quarkus-vs-springboot/wrk/run_test_wrk.sh new file mode 100755 index 0000000000..0d565d0688 --- /dev/null +++ b/quarkus-modules/quarkus-vs-springboot/wrk/run_test_wrk.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +$wrk_home/wrk -t1 -c5 -d1m -s ./post_zipcode.lua --timeout 2m -H 'Host: localhost' http://localhost:8080 & sleep 60 + +$wrk_home/wrk -t1 -c20 -d5m -s ./post_zipcode.lua --timeout 2m -H 'Host: localhost' http://localhost:8080 & sleep 60 + +$wrk_home/wrk -t1 -c20 -d5m -s ./get_by_city.lua --timeout 2m -H 'Host: localhost' http://localhost:8080 \ & +$wrk_home/wrk -t1 -c20 -d5m -s ./get_zipcode.lua --timeout 2m -H 'Host: localhost' http://localhost:8080 \ & sleep 120 + +$wrk_home/wrk -t2 -c10 -d3m -s ./get_by_city.lua --timeout 2m -H 'Host: localhost' http://localhost:8080 \ & +$wrk_home/wrk -t2 -c10 -d3m -s ./get_zipcode.lua --timeout 2m -H 'Host: localhost' http://localhost:8080 \ & + +wait + diff --git a/quarkus-modules/quarkus/README.md b/quarkus-modules/quarkus/README.md index 94b71dd954..9e45cb1c81 100644 --- a/quarkus-modules/quarkus/README.md +++ b/quarkus-modules/quarkus/README.md @@ -1,4 +1,4 @@ ## Relevant Articles: -- [Guide to QuarkusIO](https://www.baeldung.com/quarkus-io) +- [Guide to Quarkus](https://www.baeldung.com/quarkus-io) - [Testing Quarkus Applications](https://www.baeldung.com/java-quarkus-testing) diff --git a/spring-5-reactive-modules/spring-5-reactive-3/README.md b/spring-5-reactive-modules/spring-5-reactive-3/README.md index da44bf98fc..044b3db5f4 100644 --- a/spring-5-reactive-modules/spring-5-reactive-3/README.md +++ b/spring-5-reactive-modules/spring-5-reactive-3/README.md @@ -3,4 +3,5 @@ This module contains articles about reactive Spring 5. - [Logging a Reactive Sequence](https://www.baeldung.com/spring-reactive-sequence-logging) +- [Reading Flux Into a Single InputStream Using Spring Reactive WebClient](https://www.baeldung.com/spring-reactive-read-flux-into-inputstream) - More articles: [[<-- prev]](../spring-5-reactive-2) diff --git a/spring-5-reactive-modules/spring-5-reactive-3/src/main/java/com/baeldung/databuffer/DataBufferToInputStream.java b/spring-5-reactive-modules/spring-5-reactive-3/src/main/java/com/baeldung/databuffer/DataBufferToInputStream.java new file mode 100644 index 0000000000..82f0658f51 --- /dev/null +++ b/spring-5-reactive-modules/spring-5-reactive-3/src/main/java/com/baeldung/databuffer/DataBufferToInputStream.java @@ -0,0 +1,94 @@ +package com.baeldung.databuffer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.web.reactive.function.BodyExtractors; +import org.springframework.web.reactive.function.client.WebClient; + +import reactor.core.publisher.Flux; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; + +public class DataBufferToInputStream { + private static final Logger logger = LoggerFactory.getLogger(DataBufferToInputStream.class); + private static final String REQUEST_ENDPOINT = "https://gorest.co.in/public/v2/users"; + + private static WebClient getWebClient() { + WebClient.Builder webClientBuilder = WebClient.builder(); + return webClientBuilder.build(); + } + + public static InputStream getResponseAsInputStream(WebClient client, String url) throws IOException, InterruptedException { + + PipedOutputStream pipedOutputStream = new PipedOutputStream(); + PipedInputStream pipedInputStream = new PipedInputStream(1024 * 10); + pipedInputStream.connect(pipedOutputStream); + + Flux body = client.get() + .uri(url) + .exchangeToFlux(clientResponse -> { + return clientResponse.body(BodyExtractors.toDataBuffers()); + }) + .doOnError(error -> { + logger.error("error occurred while reading body", error); + }) + .doFinally(s -> { + try { + pipedOutputStream.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }) + .doOnCancel(() -> { + logger.error("Get request is cancelled"); + }); + + DataBufferUtils.write(body, pipedOutputStream) + .log("Writing to output buffer") + .subscribe(); + return pipedInputStream; + } + + private static String readContentFromPipedInputStream(PipedInputStream stream) throws IOException { + StringBuffer contentStringBuffer = new StringBuffer(); + try { + Thread pipeReader = new Thread(() -> { + try { + contentStringBuffer.append(readContent(stream)); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + pipeReader.start(); + pipeReader.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + stream.close(); + } + + return String.valueOf(contentStringBuffer); + } + + private static String readContent(InputStream stream) throws IOException { + StringBuffer contentStringBuffer = new StringBuffer(); + byte[] tmp = new byte[stream.available()]; + int byteCount = stream.read(tmp, 0, tmp.length); + logger.info(String.format("read %d bytes from the stream\n", byteCount)); + contentStringBuffer.append(new String(tmp)); + return String.valueOf(contentStringBuffer); + } + + public static void main(String[] args) throws IOException, InterruptedException { + WebClient webClient = getWebClient(); + InputStream inputStream = getResponseAsInputStream(webClient, REQUEST_ENDPOINT); + Thread.sleep(3000); + String content = readContentFromPipedInputStream((PipedInputStream) inputStream); + logger.info("response content: \n{}", content.replace("}", "}\n")); + } +} diff --git a/spring-5-reactive-modules/spring-5-reactive-3/src/test/java/databuffer/DataBufferToInputStreamUnitTest.java b/spring-5-reactive-modules/spring-5-reactive-3/src/test/java/databuffer/DataBufferToInputStreamUnitTest.java new file mode 100644 index 0000000000..b885919bbb --- /dev/null +++ b/spring-5-reactive-modules/spring-5-reactive-3/src/test/java/databuffer/DataBufferToInputStreamUnitTest.java @@ -0,0 +1,77 @@ +package databuffer; + +import com.baeldung.databuffer.DataBufferToInputStream; + +import io.restassured.internal.util.IOUtils; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.http.HttpStatus; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFunction; +import org.springframework.web.reactive.function.client.WebClient; + +import reactor.core.publisher.Mono; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +class DataBufferToInputStreamUnitTest { + private String getResponseStub() throws IOException { + InputStream inputStream = null; + BufferedReader reader = null; + String content = null; + try { + inputStream = this.getClass() + .getClassLoader() + .getResourceAsStream("user-response.json"); + if (inputStream != null) { + reader = new BufferedReader(new InputStreamReader(inputStream)); + content = reader.lines() + .collect(Collectors.joining(System.lineSeparator())); + } + } catch (Exception ex) { + throw new RuntimeException("exception caught while getting response stub"); + } finally { + reader.close(); + inputStream.close(); + } + return content; + } + + private InputStream getResponseStubAsInputStream() { + return this.getClass() + .getClassLoader() + .getResourceAsStream("user-response.json"); + } + + private WebClient getMockWebClient() throws IOException { + String content = getResponseStub(); + ClientResponse clientResponse = ClientResponse.create(HttpStatus.OK) + .header("Content-Type", "application/json") + .body(content) + .build(); + + ExchangeFunction exchangeFunction = clientRequest -> Mono.just(clientResponse); + + WebClient.Builder webClientBuilder = WebClient.builder() + .exchangeFunction(exchangeFunction); + WebClient webClient = webClientBuilder.build(); + return webClient; + } + + @Test + public void testResponseAsInputStream() throws IOException, InterruptedException { + String mockUrl = Mockito.anyString(); + WebClient mockWebClient = getMockWebClient(); + InputStream inputStream = DataBufferToInputStream.getResponseAsInputStream(mockWebClient, mockUrl); + byte[] expectedBytes = IOUtils.toByteArray(getResponseStubAsInputStream()); + byte[] actualBytes = IOUtils.toByteArray(inputStream); + assertArrayEquals(expectedBytes, actualBytes); + } +} \ No newline at end of file diff --git a/spring-5-reactive-modules/spring-5-reactive-3/src/test/resources/user-response.json b/spring-5-reactive-modules/spring-5-reactive-3/src/test/resources/user-response.json new file mode 100644 index 0000000000..33c03dc492 --- /dev/null +++ b/spring-5-reactive-modules/spring-5-reactive-3/src/test/resources/user-response.json @@ -0,0 +1,72 @@ +[ + { + "id": 2683, + "name": "Maheswar Kocchar", + "email": "maheswar_kocchar@kihn.info", + "gender": "male", + "status": "active" + }, + { + "id": 2680, + "name": "Lakshminath Khan", + "email": "lakshminath_khan@barrows-cormier.biz", + "gender": "female", + "status": "inactive" + }, + { + "id": 2679, + "name": "Tarun Arora", + "email": "tarun_arora@rolfson.net", + "gender": "female", + "status": "inactive" + }, + { + "id": 2678, + "name": "Agnivesh Dubashi", + "email": "dubashi_agnivesh@senger.name", + "gender": "male", + "status": "inactive" + }, + { + "id": 2677, + "name": "Dhanu Gowda", + "email": "gowda_dhanu@hayes.org", + "gender": "male", + "status": "active" + }, + { + "id": 2675, + "name": "Harinakshi Pilla Jr.", + "email": "pilla_jr_harinakshi@rutherford-monahan.com", + "gender": "female", + "status": "inactive" + }, + { + "id": 2673, + "name": "Kalpana Prajapat", + "email": "prajapat_kalpana@wilkinson-schaefer.net", + "gender": "female", + "status": "active" + }, + { + "id": 2672, + "name": "Chakradhar Jha", + "email": "jha_chakradhar@baumbach.info", + "gender": "male", + "status": "active" + }, + { + "id": 2670, + "name": "Divaakar Deshpande Jr.", + "email": "deshpande_jr_divaakar@mertz.info", + "gender": "female", + "status": "inactive" + }, + { + "id": 2669, + "name": "Prasanna Mehra", + "email": "prasanna_mehra@ruecker-larkin.name", + "gender": "female", + "status": "active" + } +] \ No newline at end of file diff --git a/spring-5-reactive-modules/spring-5-reactive/src/main/java/com/baeldung/reactive/urlmatch/ExploreSpring5URLPatternUsingRouterFunctions.java b/spring-5-reactive-modules/spring-5-reactive/src/main/java/com/baeldung/reactive/urlmatch/ExploreSpring5URLPatternUsingRouterFunctions.java index 34abada2f1..b7bb53600e 100644 --- a/spring-5-reactive-modules/spring-5-reactive/src/main/java/com/baeldung/reactive/urlmatch/ExploreSpring5URLPatternUsingRouterFunctions.java +++ b/spring-5-reactive-modules/spring-5-reactive/src/main/java/com/baeldung/reactive/urlmatch/ExploreSpring5URLPatternUsingRouterFunctions.java @@ -24,11 +24,12 @@ public class ExploreSpring5URLPatternUsingRouterFunctions { private RouterFunction routingFunction() { - return route(GET("/p?ths"), serverRequest -> ok().body(fromValue("/p?ths"))).andRoute(GET("/test/{*id}"), serverRequest -> ok().body(fromValue(serverRequest.pathVariable("id")))) - .andRoute(GET("/*card"), serverRequest -> ok().body(fromValue("/*card path was accessed"))) + return route(GET("/t?st"), serverRequest -> ok().body(fromValue("Path /t?st is accessed"))).andRoute(GET("/test/{*id}"), serverRequest -> ok().body(fromValue(serverRequest.pathVariable("id")))) + .andRoute(GET("/baeldung/*Id"), serverRequest -> ok().body(fromValue("/baeldung/*Id path was accessed"))) .andRoute(GET("/{var1}_{var2}"), serverRequest -> ok().body(fromValue(serverRequest.pathVariable("var1") + " , " + serverRequest.pathVariable("var2")))) .andRoute(GET("/{baeldung:[a-z]+}"), serverRequest -> ok().body(fromValue("/{baeldung:[a-z]+} was accessed and baeldung=" + serverRequest.pathVariable("baeldung")))) - .and(RouterFunctions.resources("/files/{*filepaths}", new ClassPathResource("files/"))); + .and(RouterFunctions.resources("/files/{*filepaths}", new ClassPathResource("files/"))) + .and(RouterFunctions.resources("/resources/**", new ClassPathResource("resources/"))); } WebServer start() throws Exception { diff --git a/spring-5-reactive-modules/spring-5-reactive/src/main/resources/resources/test/test.txt b/spring-5-reactive-modules/spring-5-reactive/src/main/resources/resources/test/test.txt new file mode 100644 index 0000000000..30d74d2584 --- /dev/null +++ b/spring-5-reactive-modules/spring-5-reactive/src/main/resources/resources/test/test.txt @@ -0,0 +1 @@ +test \ No newline at end of file diff --git a/spring-5-reactive-modules/spring-5-reactive/src/test/java/com/baeldung/reactive/urlmatch/ExploreSpring5URLPatternUsingRouterFunctionsIntegrationTest.java b/spring-5-reactive-modules/spring-5-reactive/src/test/java/com/baeldung/reactive/urlmatch/ExploreSpring5URLPatternUsingRouterFunctionsIntegrationTest.java index 91721d2cef..a77a67c6ba 100644 --- a/spring-5-reactive-modules/spring-5-reactive/src/test/java/com/baeldung/reactive/urlmatch/ExploreSpring5URLPatternUsingRouterFunctionsIntegrationTest.java +++ b/spring-5-reactive-modules/spring-5-reactive/src/test/java/com/baeldung/reactive/urlmatch/ExploreSpring5URLPatternUsingRouterFunctionsIntegrationTest.java @@ -27,12 +27,12 @@ public class ExploreSpring5URLPatternUsingRouterFunctionsIntegrationTest { @Test public void givenRouter_whenGetPathWithSingleCharWildcard_thenGotPathPattern() throws Exception { client.get() - .uri("/paths") + .uri("/test") .exchange() .expectStatus() .isOk() .expectBody(String.class) - .isEqualTo("/p?ths"); + .isEqualTo("Path /t?st is accessed"); } @Test @@ -50,12 +50,12 @@ public class ExploreSpring5URLPatternUsingRouterFunctionsIntegrationTest { public void givenRouter_whenGetMultipleCharWildcard_thenGotPathPattern() throws Exception { client.get() - .uri("/wildcard") + .uri("/baeldung/tutorialId") .exchange() .expectStatus() .isOk() .expectBody(String.class) - .isEqualTo("/*card path was accessed"); + .isEqualTo("/baeldung/*Id path was accessed"); } @Test @@ -107,4 +107,14 @@ public class ExploreSpring5URLPatternUsingRouterFunctionsIntegrationTest { .isEqualTo("hello"); } + @Test + public void givenRouter_whenAccess_thenGot() throws Exception { + client.get() + .uri("/resources/test/test.txt") + .exchange() + .expectStatus() + .isOk() + .expectBody(String.class) + .isEqualTo("test"); + } } diff --git a/spring-5-reactive-modules/spring-5-reactive/src/test/java/com/baeldung/reactive/urlmatch/PathPatternsUsingHandlerMethodIntegrationTest.java b/spring-5-reactive-modules/spring-5-reactive/src/test/java/com/baeldung/reactive/urlmatch/PathPatternsUsingHandlerMethodIntegrationTest.java index d4c1cfe4c8..0b4607b54a 100644 --- a/spring-5-reactive-modules/spring-5-reactive/src/test/java/com/baeldung/reactive/urlmatch/PathPatternsUsingHandlerMethodIntegrationTest.java +++ b/spring-5-reactive-modules/spring-5-reactive/src/test/java/com/baeldung/reactive/urlmatch/PathPatternsUsingHandlerMethodIntegrationTest.java @@ -27,13 +27,21 @@ public class PathPatternsUsingHandlerMethodIntegrationTest { @Test public void givenHandlerMethod_whenMultipleURIVariablePattern_then200() { - client.get() - .uri("/spring5/ab/cd") + client.get() + .uri("/spring5/baeldung/tutorial") .exchange() .expectStatus() .is2xxSuccessful() .expectBody() - .equals("/ab/cd"); + .equals("/baeldung/tutorial"); + + client.get() + .uri("/spring5/baeldung") + .exchange() + .expectStatus() + .is2xxSuccessful() + .expectBody() + .equals("/baeldung"); } @Test diff --git a/spring-boot-modules/spring-boot-3/pom.xml b/spring-boot-modules/spring-boot-3/pom.xml new file mode 100644 index 0000000000..556168e205 --- /dev/null +++ b/spring-boot-modules/spring-boot-3/pom.xml @@ -0,0 +1,128 @@ + + + 4.0.0 + + com.baeldung + parent-boot-3 + 0.0.1-SNAPSHOT + ../../parent-boot-3 + + spring-boot-3-sample + 0.0.1-SNAPSHOT + spring-boot-3-sample + Demo project for Spring Boot + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-hateoas + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.h2database + h2 + + runtime + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.projectlombok + lombok + true + + + org.mapstruct + mapstruct + ${mapstruct.version} + true + + + org.springframework.boot + spring-boot-starter-test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + org.projectlombok + lombok + ${lombok.version} + + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + 1.5.2.Final + com.baeldung.sample.TodoApplication + + + diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/TodoApplication.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/TodoApplication.java new file mode 100644 index 0000000000..72c9c0e482 --- /dev/null +++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/TodoApplication.java @@ -0,0 +1,11 @@ +package com.baeldung.sample; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +@SpringBootApplication +public class TodoApplication { + public static void main(String[] args) { + SpringApplication.run(TodoApplication.class, args); + } + +} diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/CorsConfiguration.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/CorsConfiguration.java new file mode 100644 index 0000000000..e02e3d5442 --- /dev/null +++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/CorsConfiguration.java @@ -0,0 +1,42 @@ +package com.baeldung.sample.boundary; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import static java.util.Arrays.stream; +import static org.springframework.http.HttpHeaders.ACCEPT; +import static org.springframework.http.HttpHeaders.ACCEPT_LANGUAGE; +import static org.springframework.http.HttpHeaders.CONTENT_LANGUAGE; +import static org.springframework.http.HttpHeaders.CONTENT_TYPE; +import static org.springframework.http.HttpHeaders.IF_MATCH; +import static org.springframework.http.HttpHeaders.IF_NONE_MATCH; +import static org.springframework.http.HttpHeaders.LINK; +import static org.springframework.http.HttpHeaders.LOCATION; +import static org.springframework.http.HttpHeaders.ORIGIN; + +@Configuration +public class CorsConfiguration { + + @Bean + public WebMvcConfigurer corsConfigurer(final CorsConfigurationData allowed) { + return new WebMvcConfigurer() { + + @Override + public void addCorsMappings(final CorsRegistry registry) { + registry.addMapping("/**") + .exposedHeaders(LOCATION, LINK) + // allow all HTTP request methods + .allowedMethods(stream(RequestMethod.values()).map(Enum::name).toArray(String[]::new)) // + // allow the commonly used headers + .allowedHeaders(ORIGIN, CONTENT_TYPE, CONTENT_LANGUAGE, ACCEPT, ACCEPT_LANGUAGE, IF_MATCH, IF_NONE_MATCH) // + // this is stage specific + .allowedOrigins(allowed.getOrigins()) + .allowCredentials(allowed.isCredentials()); + } + }; + } + +} diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/CorsConfigurationData.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/CorsConfigurationData.java new file mode 100644 index 0000000000..1c4ef55e6c --- /dev/null +++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/CorsConfigurationData.java @@ -0,0 +1,25 @@ +package com.baeldung.sample.boundary; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * The properties from application.yml. You can specify them by the following snippet: + * + *
+ * server:
+ *   endpoints:
+ *     api:
+ *       v1: /api/v1
+ * 
+ */ +@Configuration +@ConfigurationProperties(prefix = "cors.allow") +@Data +public class CorsConfigurationData { + + private String[] origins = { "*" }; + private boolean credentials = false; + +} diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/GlobalExceptionHandler.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/GlobalExceptionHandler.java new file mode 100644 index 0000000000..eb5435656e --- /dev/null +++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/GlobalExceptionHandler.java @@ -0,0 +1,22 @@ +package com.baeldung.sample.boundary; + +import com.baeldung.sample.control.NotFoundException; +import jakarta.validation.ValidationException; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(NotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + protected void handleNotFoundException() {} + + @ExceptionHandler({ValidationException.class, MethodArgumentNotValidException.class}) + @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) + protected void handleValidationException() {} + +} diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/TodoDtoMapper.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/TodoDtoMapper.java new file mode 100644 index 0000000000..a870eade7b --- /dev/null +++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/TodoDtoMapper.java @@ -0,0 +1,36 @@ +package com.baeldung.sample.boundary; + +import com.baeldung.sample.control.Todo; +import org.mapstruct.Mapper; + +import java.util.Locale; + +/** + * Dieser Mapper kopiert die Informationen zwischen den Schichten. + */ +@Mapper(componentModel = "spring") +interface TodoDtoMapper { + + TodoResponseDto map(Todo todo); + + default String _mapStatus(Todo.Status status) { + return switch (status) { + case NEW -> "new"; + case PROGRESS -> "progress"; + case COMPLETED -> "completed"; + case ARCHIVED -> "archived"; + }; + } + + Todo map(TodoRequestDto todo, Long id); + + default Todo.Status _mapStatus(String status) { + return null == status ? Todo.Status.NEW : switch (status) { + case "progress" -> Todo.Status.PROGRESS; + case "completed" -> Todo.Status.COMPLETED; + case "archived" -> Todo.Status.ARCHIVED; + default -> Todo.Status.NEW; + }; + } + +} diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/TodoRequestDto.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/TodoRequestDto.java new file mode 100644 index 0000000000..fb7e646772 --- /dev/null +++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/TodoRequestDto.java @@ -0,0 +1,17 @@ +package com.baeldung.sample.boundary; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +import java.time.LocalDate; + +@Data +public class TodoRequestDto { + @NotBlank + private String title; + private String description; + private LocalDate dueDate; + @Pattern(regexp = "new|progress|completed|archived") + private String status; +} diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/TodoResponseDto.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/TodoResponseDto.java new file mode 100644 index 0000000000..140db7b296 --- /dev/null +++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/TodoResponseDto.java @@ -0,0 +1,15 @@ +package com.baeldung.sample.boundary; + +import lombok.Data; + +import java.time.LocalDate; + +@Data +public class TodoResponseDto { + private Long id; + private String title; + private String description; + private LocalDate dueDate; + private String status; + +} diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/TodosController.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/TodosController.java new file mode 100644 index 0000000000..7efa7dfee3 --- /dev/null +++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/TodosController.java @@ -0,0 +1,83 @@ +package com.baeldung.sample.boundary; + +import com.baeldung.sample.control.NotFoundException; +import com.baeldung.sample.control.TodosService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +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.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import java.net.URI; +import java.util.Collection; +import java.util.stream.Collectors; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; +import static org.springframework.http.HttpStatus.NO_CONTENT; + +@RestController +@RequestMapping("/api/v1/todos") +@RequiredArgsConstructor +public class TodosController { + + private static final String DEFAULT_MEDIA_TYPE = MediaType.APPLICATION_JSON_VALUE; + + private final TodosService service; + // Mapping zwischen den Schichten + private final TodoDtoMapper mapper; + + @GetMapping(produces = DEFAULT_MEDIA_TYPE) + public Collection findAll() { + return service.findAll().stream() + .map(mapper::map) + .collect(Collectors.toList()); + } + + @GetMapping(value = "/{id}", produces = DEFAULT_MEDIA_TYPE) + public TodoResponseDto findById( + @PathVariable("id") final Long id + ) { + // Action + return service.findById(id) // + .map(mapper::map) // map to dto + .orElseThrow(NotFoundException::new); + } + + @PostMapping(consumes = DEFAULT_MEDIA_TYPE) + public ResponseEntity create(final @Valid @RequestBody TodoRequestDto item) { + // Action + final var todo = mapper.map(item, null); + final var newTodo = service.create(todo); + final var result = mapper.map(newTodo); + // Response + final URI locationHeader = linkTo(methodOn(TodosController.class).findById(result.getId())).toUri(); // HATEOAS + return ResponseEntity.created(locationHeader).body(result); + } + + @PutMapping(value = "{id}", consumes = DEFAULT_MEDIA_TYPE) + @ResponseStatus(NO_CONTENT) + public void update( + @PathVariable("id") final Long id, + @Valid @RequestBody final TodoRequestDto item + ) { + service.update(mapper.map(item, id)); + } + + @DeleteMapping("/{id}") + @ResponseStatus(NO_CONTENT) + public void delete( + @PathVariable("id") final Long id + ) { + service.delete(id); + } + +} diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/DataInitializationConfigurationData.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/DataInitializationConfigurationData.java new file mode 100644 index 0000000000..5871325714 --- /dev/null +++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/DataInitializationConfigurationData.java @@ -0,0 +1,14 @@ +package com.baeldung.sample.control; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "application.data") +@Data +public class DataInitializationConfigurationData { + + private boolean initializeOnStartup = true; + +} diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/NotFoundException.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/NotFoundException.java new file mode 100644 index 0000000000..b06b54f547 --- /dev/null +++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/NotFoundException.java @@ -0,0 +1,5 @@ +package com.baeldung.sample.control; + +public class NotFoundException extends RuntimeException { + +} diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/Todo.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/Todo.java new file mode 100644 index 0000000000..a972a9a21a --- /dev/null +++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/Todo.java @@ -0,0 +1,24 @@ +package com.baeldung.sample.control; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +import java.time.LocalDate; + +public record Todo( + Long id, + @NotNull @Size(min = 1) String title, + String description, + LocalDate dueDate, + @NotNull Status status +) { + + public enum Status { + NEW, PROGRESS, COMPLETED, ARCHIVED + } + + public Todo(Long id, String title) { + this(id, title, null, null, Status.NEW); + } + +} diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/TodoEntityMapper.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/TodoEntityMapper.java new file mode 100644 index 0000000000..37a74f7f85 --- /dev/null +++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/TodoEntityMapper.java @@ -0,0 +1,16 @@ +package com.baeldung.sample.control; + +import com.baeldung.sample.entity.TodoEntity; +import org.mapstruct.Mapper; + +/** + * Dieser Mapper kopiert die Informationen zwischen den Schichten. + */ +@Mapper(componentModel = "spring") +interface TodoEntityMapper { + + TodoEntity map(Todo todo); + + Todo map(TodoEntity todo); + +} diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/TodosInitializer.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/TodosInitializer.java new file mode 100644 index 0000000000..0c7b76d165 --- /dev/null +++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/TodosInitializer.java @@ -0,0 +1,28 @@ +package com.baeldung.sample.control; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class TodosInitializer { + + private final TodosService service; + /* + * we cannot use @Profile("default") because + * we are not able inject the bean during test + * depending from the profile activation + */ + private final DataInitializationConfigurationData config; + + @EventListener(ContextRefreshedEvent.class) + public void initializeTodos() { + if (this.config.isInitializeOnStartup() && this.service.count() < 1) { + this.service.create(new Todo(null, "Deploy and run the application.")); + this.service.create(new Todo(null, "Enter some TODO items!")); + } + } + +} diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/TodosService.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/TodosService.java new file mode 100644 index 0000000000..e10ef6440f --- /dev/null +++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/TodosService.java @@ -0,0 +1,103 @@ +package com.baeldung.sample.control; + +import com.baeldung.sample.entity.TodosRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.Optional; + +import static java.util.stream.Collectors.toList; + +/** + * Ein Service ist ein Singleton auf Control Layer, der in der Boundary von mehreren (REST) Controllern gemeinsam genutzt werden kann. + * Dieser hat keinen Bezug mehr zu HTTP. + */ +@Service +@RequiredArgsConstructor +public class TodosService { + + private final TodoEntityMapper mapper; + private final TodosRepository repo; + + /** + * Gibt die Anzahl an Datensätzen zurück. + * @return die Anzahl an Datensätzen + */ + long count() { + return repo.count(); + } + + /** + * Gibt alle Todos zurück. + * + * @return eine unveränderliche Collection + */ + public Collection findAll() { + return repo.findAll().stream() + .map(mapper::map) + .collect(toList()); + } + + /** + * Durchsucht die Todos nach einer ID. + * + * @param id die ID + * @return das Suchergebnis + */ + public Optional findById(long id) { + return repo.findById(id) + .map(mapper::map); + } + + /** + * Fügt ein Item in den Datenbestand hinzu. Dabei wird eine ID generiert. + * + * @param item das anzulegende Item (ohne ID) + * @return das gespeicherte Item (mit ID) + * @throws IllegalArgumentException wenn das Item null oder die ID bereits belegt ist + */ + public Todo create(Todo item) { + if (null == item || null != item.id()) { + throw new IllegalArgumentException("item must exist without any id"); + } + return mapper.map(repo.save(mapper.map(item))); + } + + /** + * Aktualisiert ein Item im Datenbestand. + * + * @param item das zu ändernde Item mit ID + * @throws IllegalArgumentException + * wenn das Item oder dessen ID nicht belegt ist + * @throws NotFoundException + * wenn das Element mit der ID nicht gefunden wird + */ + public void update(Todo item) { + if (null == item || null == item.id()) { + throw new IllegalArgumentException("item must exist with an id"); + } + // remove separat, um nicht neue Einträge hinzuzufügen (put allein würde auch ersetzen) + if (repo.existsById(item.id())) { + repo.save(mapper.map(item)); + } else { + throw new NotFoundException(); + } + } + + /** + * Entfernt ein Item aus dem Datenbestand. + * + * @param id die ID des zu löschenden Items + * @throws NotFoundException + * wenn das Element mit der ID nicht gefunden wird + */ + public void delete(long id) { + if (repo.existsById(id)) { + repo.deleteById(id); + } else { + throw new NotFoundException(); + } + } + +} diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/entity/TodoEntity.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/entity/TodoEntity.java new file mode 100644 index 0000000000..e551876584 --- /dev/null +++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/entity/TodoEntity.java @@ -0,0 +1,53 @@ +package com.baeldung.sample.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.time.LocalDate; + +/* + * Auch hier wird eine separate Klasse erstellt. + * Und auch hier geht es wieder um Unabhängigkeit der beiden Layer (Control und Persistence). + * So kann z.B. die Auflösung von Fremdschlüsseln (assignee, priority, topic) in der Persistence Layer erfolgen (JPA unterstützt das), oder aber auch erst in der Control Layer. + */ +@Entity(name = "todo") +@Table(name = "todos") +// we do not use @Data because hashCode() and equals() might influence JPA's behaviour +@NoArgsConstructor +@Getter +@Setter +@ToString +public class TodoEntity { + + public enum StatusEntity { + NEW, PROGRESS, COMPLETED, ARCHIVED + } + + @GeneratedValue(strategy = GenerationType.AUTO) + @Id + private Long id; + @NotNull + @Size(min = 1) + private String title; + private String description; + private LocalDate dueDate; + @Enumerated + @NotNull + private StatusEntity status = StatusEntity.NEW; + + public TodoEntity(Long id, String title) { + this.id = id; + this.title = title; + } + +} diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/entity/TodosRepository.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/entity/TodosRepository.java new file mode 100644 index 0000000000..9f305a61c2 --- /dev/null +++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/entity/TodosRepository.java @@ -0,0 +1,9 @@ +package com.baeldung.sample.entity; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface TodosRepository extends JpaRepository { + +} diff --git a/spring-boot-modules/spring-boot-3/src/main/resources/application.yml b/spring-boot-modules/spring-boot-3/src/main/resources/application.yml new file mode 100644 index 0000000000..9a966a5bbd --- /dev/null +++ b/spring-boot-modules/spring-boot-3/src/main/resources/application.yml @@ -0,0 +1,21 @@ +spring: + mvc: + throw-exception-if-no-handler-found: true + jackson: + deserialization: + FAIL_ON_UNKNOWN_PROPERTIES: true + property-naming-strategy: SNAKE_CASE + jpa: + open-in-view: false + generate-ddl: true + show-sql: true + hibernate: + ddl-auto: update + properties: + hibernate: + dialect: org.hibernate.dialect.H2Dialect +# Custom Properties +cors: + allow: + origins: ${CORS_ALLOWED_ORIGINS:*} + credentials: ${CORS_ALLOW_CREDENTIALS:false} diff --git a/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/boundary/TodosBoundaryLayer.java b/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/boundary/TodosBoundaryLayer.java new file mode 100644 index 0000000000..39797bf2fb --- /dev/null +++ b/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/boundary/TodosBoundaryLayer.java @@ -0,0 +1,13 @@ +package com.baeldung.sample.boundary; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.ComponentScan; + +/** + * This class bundles all classes in the boundary layer. + * This includes also the generated mapper classes. + */ +@TestConfiguration +@ComponentScan(basePackageClasses = TodosBoundaryLayer.class) +public class TodosBoundaryLayer { +} diff --git a/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/boundary/TodosControllerApiIntegrationTest.java b/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/boundary/TodosControllerApiIntegrationTest.java new file mode 100644 index 0000000000..680b6c85bb --- /dev/null +++ b/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/boundary/TodosControllerApiIntegrationTest.java @@ -0,0 +1,218 @@ +package com.baeldung.sample.boundary; + +import com.baeldung.sample.control.NotFoundException; +import com.baeldung.sample.control.Todo; +import com.baeldung.sample.control.TodosService; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.List; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Integrationstests der Todos REST API auf HTTP Layer. Die Anwendung wird als Black Box behandelt. + * Somit müssen alle Tests, die einen existierenden Datensatz benötigen, diesen im Test anlegen. + * Spring Boot startet den sog. "ApplicationContext" automatisch und bietet Möglichkeiten, die Anwendungsteile vom Test aus aufzurufen. + */ +@WebMvcTest +@ContextConfiguration(classes = TodosBoundaryLayer.class) +class TodosControllerApiIntegrationTest { + + private static final String BASEURL = "/api/v1/todos"; // URL to Resource + private static final String DEFAULT_MEDIA_TYPE = MediaType.APPLICATION_JSON_VALUE; + + @MockBean + TodosService service; + @Autowired + MockMvc mvc; // testing by sending HTTP requests and verifying HTTP responses + @Autowired + ObjectMapper mapper; // used to render or parse JSON + + /* + * Testfall: + * - GET auf alle Todos -> 200 OK mit JSON + */ + @DisplayName("GET auf alle Daten (200 OK)") + @Test + void testFindAllTodos() throws Exception { + when(service.findAll()).thenReturn(List.of()); + mvc + .perform(get(BASEURL).accept(DEFAULT_MEDIA_TYPE)) + .andExpect(status().isOk()) + .andExpect(content().contentType(DEFAULT_MEDIA_TYPE)); + } + + /* + * Testfall: + * - einzelnes Todos auslesen -> kein Fehler + */ + @Test + void testFindById() throws Exception { + when(service.findById(1L)) + .thenReturn(Optional.of(new Todo(1L, "test"))); + mvc + .perform(get(BASEURL + "/1").accept(DEFAULT_MEDIA_TYPE)) + .andExpect(status().isOk()) + .andExpect(content().contentType(DEFAULT_MEDIA_TYPE)) + .andExpect(jsonPath("$.title").value("test")); + } + + /* + * Testfall: + * - einzelnes Todos auslesen -> 404 + */ + @Test + void testFindByIdNotExisting() throws Exception { + when(service.findById(1L)) + .thenReturn(Optional.empty()); + mvc + .perform(get(BASEURL + "/1").accept(DEFAULT_MEDIA_TYPE)) + .andExpect(status().isNotFound()); + } + + /* + * Testfall: + * - Anlegen eines Todos per POST -> 201 mit Location Header + */ + @DisplayName("POST liefert 201 mit Location-Header") + @Test + void testCreateTodo() throws Exception { + // etwas umständlich, die ID zu besetzen, wenn auf dem Service create() aufgerufen wird + when(service.create(any())).thenReturn(new Todo(5L, "test-todo")); + final var json = "{\"title\":\"test-todo\"}"; + mvc + .perform(post(BASEURL).contentType(DEFAULT_MEDIA_TYPE).content(json)) + .andExpect(status().isCreated()) + .andExpect(header().exists(HttpHeaders.LOCATION)) + .andExpect(jsonPath("$.id").value(5L)); + } + + /* + * Testfall: + * - Anlegen eines Todos per POST ohne Titel -> 422 + */ + @DisplayName("POST erzeugt kein Todo, wenn kein Titel angegeben ist") + @Test + void testCreateTodoWithoutTitle() throws Exception { + final var json = "{}"; + mvc + .perform(post(BASEURL).contentType(DEFAULT_MEDIA_TYPE).content(json)) + .andExpect(status().isUnprocessableEntity()); + verifyNoInteractions(service); + } + + /* + * Testfall: + * - Anlegen eines Todos per POST mit ID -> 400 + */ + @DisplayName("POST erzeugt kein Todo, wenn die ID mitgegeben wird (undefinierte Property)") + @Test + void testCreateTodoWithID() throws Exception { + final var json = "{\"id\":1, \"title\":\"test\"}"; + mvc + .perform(post(BASEURL).contentType(DEFAULT_MEDIA_TYPE).content(json)) + .andExpect(status().isBadRequest()); + verifyNoInteractions(service); + } + + /* + * Testfall: + * - Anlegen eines Todos per POST mit leerem Titel -> 400 + */ + @DisplayName("POST erzeugt kein Todo, wenn Titel weniger als 1 Zeichen hat") + @Test + void testCreateTodoWithEmptyTitle() throws Exception { + final var newTodo = new TodoRequestDto(); + newTodo.setTitle(""); + final String json = mapper.writeValueAsString(newTodo); + this.mvc // + .perform(post(BASEURL).contentType(DEFAULT_MEDIA_TYPE).content(json)) // + .andExpect(status().isUnprocessableEntity()); + } + + /* + * Testfall: + * - Ändern -> 204 + */ + @Test + void testUpdateTodo() throws Exception { + final var json = "{\"title\":\"test-todo\"}"; + mvc + .perform(put(BASEURL + "/5").contentType(DEFAULT_MEDIA_TYPE).content(json)) + .andExpect(status().isNoContent()); + } + + /* + * Testfall: + * - Ändern -> 404 + */ + @Test + void testUpdateTodoNotExisting() throws Exception { + doThrow(NotFoundException.class).when(service).update(any()); + final var json = "{\"title\":\"test-todo\"}"; + mvc + .perform(put(BASEURL + "/5").contentType(DEFAULT_MEDIA_TYPE).content(json)) + .andExpect(status().isNotFound()); + } + + /* + * Testfall: + * - Ändern des Todos mit leerem Titel + */ + @Test + @DisplayName("PUT mit leerem Titel") + void testUpdateWithEmptyTitle() throws Exception { + // Act + final var json = "{}"; + mvc + .perform(put(BASEURL + "/5").contentType(DEFAULT_MEDIA_TYPE).content(json)) + .andExpect(status().isUnprocessableEntity()); + verifyNoInteractions(service); + } + + /* + * Testfall: + * - Löschen -> 204 + */ + @Test + void testDeleteTodo() throws Exception { + mvc + .perform(delete(BASEURL + "/5").contentType(DEFAULT_MEDIA_TYPE)) + .andExpect(status().isNoContent()); + } + + /* + * Testfall: + * - Löschen -> 404 + */ + @Test + void testDeleteTodoNotExisting() throws Exception { + doThrow(NotFoundException.class).when(service).delete(anyLong()); + mvc + .perform(delete(BASEURL + "/5").contentType(DEFAULT_MEDIA_TYPE)) + .andExpect(status().isNotFound()); + } + +} diff --git a/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/control/TodosControlLayer.java b/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/control/TodosControlLayer.java new file mode 100644 index 0000000000..ac07355eb9 --- /dev/null +++ b/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/control/TodosControlLayer.java @@ -0,0 +1,13 @@ +package com.baeldung.sample.control; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.ComponentScan; + +/** + * This class bundles all classes in the control layer. + * This includes also the generated mapper classes. + */ +@TestConfiguration +@ComponentScan(basePackageClasses = TodosControlLayer.class) +public class TodosControlLayer { +} diff --git a/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/control/TodosInitializerActivationIntegrationTest.java b/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/control/TodosInitializerActivationIntegrationTest.java new file mode 100644 index 0000000000..ff28756e8e --- /dev/null +++ b/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/control/TodosInitializerActivationIntegrationTest.java @@ -0,0 +1,39 @@ +package com.baeldung.sample.control; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@SpringBootTest +class TodosInitializerActivationIntegrationTest { + + @MockBean + TodosService service; + + @BeforeEach + void serviceHasEmptyData() { + when(service.count()).thenReturn(0L); + } + + @AfterEach + void serviceHasNoFurtherInteractions() { + verifyNoMoreInteractions(service); + } + + @Test + @DisplayName("data initialization is invoked on default profile") + void testIsInvoked() { + verify(service).count(); + verify(service, atLeastOnce()).create(any()); + } + +} diff --git a/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/control/TodosServiceDatabaseIntegrationTest.java b/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/control/TodosServiceDatabaseIntegrationTest.java new file mode 100644 index 0000000000..4344e4ec3f --- /dev/null +++ b/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/control/TodosServiceDatabaseIntegrationTest.java @@ -0,0 +1,63 @@ +package com.baeldung.sample.control; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SpringBootTest +@AutoConfigureTestDatabase +@Transactional +@TestPropertySource( + properties = { + "application.data.initialize-on-startup=false" + } +) +class TodosServiceDatabaseIntegrationTest { + + @Autowired + TodosService service; + + @BeforeEach + void assertEmpty() { + assertThat(service.count()) + .isZero(); + } + + @Test + void testCreate() { + service.create(new Todo(null, "test")); + assertThat(service.count()) + .isEqualTo(1); + assertThat(service.findAll()) + .hasSize(1) + .element(0).extracting(Todo::title).isEqualTo("test"); + } + + @Test + void testFindById() { + final var todo = service.create(new Todo(null, "test")); + final var result = service.findById(todo.id()); + assertThat(result) + .isNotEmpty() + .get().usingRecursiveComparison().isEqualTo(todo); + } + + @Test + void testDelete() { + final var todo = service.create(new Todo(null, "test")); + final var id = todo.id(); + service.delete(id); + final var result = service.findById(id); + assertThat(result).isEmpty(); + assertThatThrownBy(() -> service.delete(id)) + .isInstanceOf(NotFoundException.class); + } + +} diff --git a/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/control/TodosServiceIntegrationTest.java b/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/control/TodosServiceIntegrationTest.java new file mode 100644 index 0000000000..49bf34ac80 --- /dev/null +++ b/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/control/TodosServiceIntegrationTest.java @@ -0,0 +1,126 @@ +package com.baeldung.sample.control; + +import com.baeldung.sample.entity.TodoEntity; +import com.baeldung.sample.entity.TodosRepository; +import org.junit.jupiter.api.Test; +import org.mockito.Answers; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.refEq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +/** + * Integrationstests des TodosServices. Die Anwendung wird als Black Box behandelt. + * Somit müssen alle Tests, die einen existierenden Datensatz benötigen, diesen im Test anlegen. + */ +@SpringBootTest(classes = TodosControlLayer.class) +class TodosServiceIntegrationTest { + + // dieses Objekt wird als Mock instruiert + // Answers.RETURNS_MOCKS ist notwendig, da Methoden in Service-Init-Methode bereits aufgerufen werden + @MockBean(answer = Answers.RETURNS_MOCKS) + TodosRepository repo; + + @Autowired + TodosService service; + + /* + * Testfall: + * - alle Todos auslesen -> kein Fehler + */ + @Test + void testFindAllTodos() { + when(repo.findAll()).thenReturn(List.of(new TodoEntity(1L, "test"))); + final var result = service.findAll(); + assertThat(result).hasSize(1) // + .element(0).extracting(Todo::id, Todo::title).containsExactly(1L, "test"); + } + + /* + * Testfall: + * - Anlegen eines Todos -> ID besetzt + * - Auslesen -> gefunden mit entsprechenden Werten + */ + @Test + @SuppressWarnings("ConstantConditions") + void testCreateTodo() { + final var newTodo = new Todo(null, "test-todo"); + when(repo.save(any())).thenReturn(new TodoEntity(5L, "test-todo")); + // create + final var result = this.service.create(newTodo); + // find out id + assertThat(result).extracting(Todo::id).isEqualTo(5L); + verify(repo).save(refEq(new TodoEntity(null, "test-todo"))); + } + + /* + * Testfall: + * - Ändern eines bestehenden Todos + * - Aufruf der Repo-Methode und Rückgabewert prüfen + */ + @Test + @SuppressWarnings("ConstantConditions") + void testUpdateExisting() { + final var todo = new Todo(5L, "test-todo"); + when(repo.existsById(todo.id())).thenReturn(true); + // Test + this.service.update(todo); + // Assert + verify(repo).save(refEq(new TodoEntity(5L, "test-todo"))); + } + + /* + * Testfall: + * - Ändern eines nicht existenten Todos + * - Aufruf der Repo-Methode und Rückgabewert prüfen + */ + @Test + void testUpdateNotExisting() { + final var todo = new Todo(5L, "test-todo"); + when(repo.existsById(todo.id())).thenReturn(false); + // Test+Assert + assertThatThrownBy(() -> this.service.update(todo)) + .isInstanceOf(NotFoundException.class); + verify(repo).existsById(todo.id()); + verifyNoMoreInteractions(repo); + } + + /* + * Testfall: + * - Löschen eines existenten Todos + * - Aufruf der Repo-Methode und Rückgabewert prüfen + */ + @Test + void testDeleteExisting() { + when(repo.existsById(5L)).thenReturn(true); + // Test + this.service.delete(5L); + // Assert + verify(repo).deleteById(5L); + } + + /* + * Testfall: + * - Löschen eines nicht existenten Todos + * - Aufruf der Repo-Methode und Rückgabewert prüfen + */ + @Test + void testDeleteNotExisting() { + when(repo.existsById(5L)).thenReturn(false); + // Test+Assert + assertThatThrownBy(() -> this.service.delete(5L)) + .isInstanceOf(NotFoundException.class); + verify(repo).existsById(5L); + verifyNoMoreInteractions(repo); + } + +} diff --git a/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/shared/EnableBeanValidation.java b/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/shared/EnableBeanValidation.java new file mode 100644 index 0000000000..5780ac46fe --- /dev/null +++ b/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/shared/EnableBeanValidation.java @@ -0,0 +1,15 @@ +package com.baeldung.sample.shared; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; + +@TestConfiguration +public class EnableBeanValidation { + + @Bean + public MethodValidationPostProcessor validator() { + return new MethodValidationPostProcessor(); + } + +} diff --git a/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/jandexlib/JandexScannerService.java b/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/jandexlib/JandexScannerService.java index fc10db223e..7ed41af65d 100644 --- a/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/jandexlib/JandexScannerService.java +++ b/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/jandexlib/JandexScannerService.java @@ -27,7 +27,9 @@ public class JandexScannerService implements SampleAnnotationScanner { try { final IndexReader reader = new IndexReader(appFile.getInputStream()); Index jandexFile = reader.read(); - final List appAnnotationList = jandexFile.getAnnotations(DotName.createSimple("com.baeldung.annotation.scanner.SampleAnnotation")); + final List appAnnotationList = jandexFile + .getAnnotations(DotName + .createSimple("com.baeldung.annotation.scanner.SampleAnnotation")); List annotatedMethods = new ArrayList<>(); for (AnnotationInstance annotationInstance : appAnnotationList) { if (annotationInstance.target() @@ -48,7 +50,9 @@ public class JandexScannerService implements SampleAnnotationScanner { try { final IndexReader reader = new IndexReader(appFile.getInputStream()); Index jandexFile = reader.read(); - final List appAnnotationList = jandexFile.getAnnotations(DotName.createSimple("com.baeldung.annotation.scanner.SampleAnnotation")); + final List appAnnotationList = jandexFile + .getAnnotations(DotName + .createSimple("com.baeldung.annotation.scanner.SampleAnnotation")); List annotatedClasses = new ArrayList<>(); for (AnnotationInstance annotationInstance : appAnnotationList) { if (annotationInstance.target() diff --git a/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/javareflectionlib/JavaReflectionsScannerService.java b/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/javareflectionlib/JavaReflectionsScannerService.java index 2833bb1326..76a2293965 100644 --- a/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/javareflectionlib/JavaReflectionsScannerService.java +++ b/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/javareflectionlib/JavaReflectionsScannerService.java @@ -1,5 +1,6 @@ package com.baeldung.annotation.scanner.javareflectionlib; +import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; @@ -37,9 +38,14 @@ public class JavaReflectionsScannerService implements SampleAnnotationScanner { try { Class clazz = ClassLoader.getSystemClassLoader() .loadClass("com.baeldung.annotation.scanner.SampleAnnotatedClass"); - SampleAnnotation classAnnotation = clazz.getAnnotation(SampleAnnotation.class); List annotatedClasses = new ArrayList<>(); - annotatedClasses.add(classAnnotation.name()); + Annotation[] classAnnotations = clazz.getAnnotations(); + for (Annotation annotation : classAnnotations) { + if (annotation.annotationType() + .equals(SampleAnnotation.class)) { + annotatedClasses.add(((SampleAnnotation) annotation).name()); + } + } return Collections.unmodifiableList(annotatedClasses); } catch (ClassNotFoundException e) { throw new UnexpectedScanException(e); diff --git a/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/reflectionslib/ReflectionsScannerService.java b/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/reflectionslib/ReflectionsScannerService.java index 82a10e21aa..9cee4f19e2 100644 --- a/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/reflectionslib/ReflectionsScannerService.java +++ b/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/reflectionslib/ReflectionsScannerService.java @@ -16,7 +16,8 @@ public class ReflectionsScannerService implements SampleAnnotationScanner { @Override public List scanAnnotatedMethods() { Reflections reflections = new Reflections("com.baeldung.annotation.scanner"); - Set methods = reflections.getMethodsAnnotatedWith(SampleAnnotation.class); + Set methods = reflections + .getMethodsAnnotatedWith(SampleAnnotation.class); return methods.stream() .map(method -> method.getAnnotation(SampleAnnotation.class) .name()) @@ -26,7 +27,8 @@ public class ReflectionsScannerService implements SampleAnnotationScanner { @Override public List scanAnnotatedClasses() { Reflections reflections = new Reflections("com.baeldung.annotation.scanner"); - Set> types = reflections.getTypesAnnotatedWith(SampleAnnotation.class); + Set> types = reflections + .getTypesAnnotatedWith(SampleAnnotation.class); return types.stream() .map(clazz -> clazz.getAnnotation(SampleAnnotation.class) .name()) diff --git a/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/springcontextlib/SpringBeanAnnotationScannerService.java b/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/springcontextlib/SpringContextAnnotationScannerService.java similarity index 80% rename from spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/springcontextlib/SpringBeanAnnotationScannerService.java rename to spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/springcontextlib/SpringContextAnnotationScannerService.java index cd31ae686e..8d52067ccc 100644 --- a/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/springcontextlib/SpringBeanAnnotationScannerService.java +++ b/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/springcontextlib/SpringContextAnnotationScannerService.java @@ -17,7 +17,7 @@ import com.baeldung.annotation.scanner.SampleAnnotationScanner; import com.baeldung.annotation.scanner.ScanNotSupportedException; @Service -public class SpringBeanAnnotationScannerService implements SampleAnnotationScanner { +public class SpringContextAnnotationScannerService implements SampleAnnotationScanner { @Override public List scanAnnotatedMethods() { throw new ScanNotSupportedException(); @@ -25,13 +25,16 @@ public class SpringBeanAnnotationScannerService implements SampleAnnotationScann @Override public List scanAnnotatedClasses() { - ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + ClassPathScanningCandidateComponentProvider provider = + new ClassPathScanningCandidateComponentProvider(false); provider.addIncludeFilter(new AnnotationTypeFilter(SampleAnnotation.class)); - Set beanDefs = provider.findCandidateComponents("com.baeldung.annotation.scanner"); + Set beanDefs = provider + .findCandidateComponents("com.baeldung.annotation.scanner"); List annotatedBeans = new ArrayList<>(); for (BeanDefinition bd : beanDefs) { if (bd instanceof AnnotatedBeanDefinition) { - Map annotAttributeMap = ((AnnotatedBeanDefinition) bd).getMetadata() + Map annotAttributeMap = ((AnnotatedBeanDefinition) bd) + .getMetadata() .getAnnotationAttributes(SampleAnnotation.class.getCanonicalName()); annotatedBeans.add(annotAttributeMap.get("name") .toString()); diff --git a/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/springcorelib/SpringCoreAnnotationScannerService.java b/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/springcorelib/SpringCoreAnnotationScannerService.java index f3421fe46d..14542a9104 100644 --- a/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/springcorelib/SpringCoreAnnotationScannerService.java +++ b/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/springcorelib/SpringCoreAnnotationScannerService.java @@ -8,21 +8,24 @@ import org.springframework.core.annotation.AnnotationUtils; import org.springframework.stereotype.Service; import org.springframework.util.ClassUtils; -import com.baeldung.annotation.scanner.SampleAnnotationScanner; import com.baeldung.annotation.scanner.SampleAnnotatedClass; import com.baeldung.annotation.scanner.SampleAnnotation; +import com.baeldung.annotation.scanner.SampleAnnotationScanner; import com.baeldung.annotation.scanner.ScanNotSupportedException; @Service public class SpringCoreAnnotationScannerService implements SampleAnnotationScanner { @Override public List scanAnnotatedMethods() { - final Class userClass = ClassUtils.getUserClass(SampleAnnotatedClass.class); - return Arrays.stream(userClass.getMethods()) - .filter(method -> AnnotationUtils.getAnnotation(method, SampleAnnotation.class) != null) + Class userClass = ClassUtils.getUserClass(SampleAnnotatedClass.class); + List annotatedMethods = Arrays.stream(userClass.getMethods()) + .filter(method -> AnnotationUtils + .getAnnotation(method, SampleAnnotation.class) != null) .map(method -> method.getAnnotation(SampleAnnotation.class) .name()) .collect(Collectors.toList()); + + return annotatedMethods; } @Override diff --git a/spring-boot-modules/spring-boot-libraries-2/src/test/java/com/baeldung/annotation/scanner/SampleAnnotationScannerUnitTest.java b/spring-boot-modules/spring-boot-libraries-2/src/test/java/com/baeldung/annotation/scanner/SampleAnnotationScannerUnitTest.java index 80eca2b4c5..7c3d01b5e8 100644 --- a/spring-boot-modules/spring-boot-libraries-2/src/test/java/com/baeldung/annotation/scanner/SampleAnnotationScannerUnitTest.java +++ b/spring-boot-modules/spring-boot-libraries-2/src/test/java/com/baeldung/annotation/scanner/SampleAnnotationScannerUnitTest.java @@ -28,7 +28,8 @@ public class SampleAnnotationScannerUnitTest { assertNotNull(annotatedClasses); assertEquals(4, annotatedClasses.size()); - annotatedClasses.forEach(annotValue -> assertEquals("SampleAnnotatedClass", annotValue)); + annotatedClasses.forEach(annotValue -> assertEquals("SampleAnnotatedClass", + annotValue)); } @Test @@ -41,7 +42,7 @@ public class SampleAnnotationScannerUnitTest { assertNotNull(annotatedMethods); assertEquals(3, annotatedMethods.size()); - annotatedMethods.forEach(annotValue -> assertEquals("annotatedMethod", annotValue)); + annotatedMethods.forEach(annotValue -> assertEquals("annotatedMethod", + annotValue)); } - } diff --git a/spring-boot-modules/spring-boot-mvc-3/pom.xml b/spring-boot-modules/spring-boot-mvc-3/pom.xml index 43a492786e..6b0477cfc8 100644 --- a/spring-boot-modules/spring-boot-mvc-3/pom.xml +++ b/spring-boot-modules/spring-boot-mvc-3/pom.xml @@ -40,6 +40,11 @@ commons-io ${commons-io.version} + + org.springframework.boot + spring-boot-starter-test + test + \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-mvc-3/src/main/java/com/baeldung/produceimage/controller/DataProducerController.java b/spring-boot-modules/spring-boot-mvc-3/src/main/java/com/baeldung/produceimage/controller/DataProducerController.java index ab233a8b60..218be68a45 100644 --- a/spring-boot-modules/spring-boot-mvc-3/src/main/java/com/baeldung/produceimage/controller/DataProducerController.java +++ b/spring-boot-modules/spring-boot-mvc-3/src/main/java/com/baeldung/produceimage/controller/DataProducerController.java @@ -1,9 +1,12 @@ package com.baeldung.produceimage.controller; import org.apache.commons.io.IOUtils; +import org.springframework.core.io.InputStreamResource; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import java.io.IOException; @@ -29,6 +32,18 @@ public class DataProducerController { return IOUtils.toByteArray(in); } + @GetMapping("/get-image-dynamic-type") + @ResponseBody + public ResponseEntity getImageDynamicType(@RequestParam("jpg") boolean jpg) { + final MediaType contentType = jpg ? MediaType.IMAGE_JPEG : MediaType.IMAGE_PNG; + final InputStream in = jpg ? + getClass().getResourceAsStream("/com/baeldung/produceimage/image.jpg") : + getClass().getResourceAsStream("/com/baeldung/produceimage/image.png"); + return ResponseEntity.ok() + .contentType(contentType) + .body(new InputStreamResource(in)); + } + @GetMapping(value = "/get-file", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) public @ResponseBody byte[] getFile() throws IOException { final InputStream in = getClass().getResourceAsStream("/com/baeldung/produceimage/data.txt"); diff --git a/spring-boot-modules/spring-boot-mvc-3/src/main/resources/com/baeldung/produceimage/data.txt b/spring-boot-modules/spring-boot-mvc-3/src/main/resources/com/baeldung/produceimage/data.txt new file mode 100644 index 0000000000..5a8297fc5e --- /dev/null +++ b/spring-boot-modules/spring-boot-mvc-3/src/main/resources/com/baeldung/produceimage/data.txt @@ -0,0 +1 @@ +Hello Baeldung! \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-mvc-3/src/main/resources/com/baeldung/produceimage/image.jpg b/spring-boot-modules/spring-boot-mvc-3/src/main/resources/com/baeldung/produceimage/image.jpg new file mode 100644 index 0000000000..db3abce0d6 Binary files /dev/null and b/spring-boot-modules/spring-boot-mvc-3/src/main/resources/com/baeldung/produceimage/image.jpg differ diff --git a/spring-boot-modules/spring-boot-mvc-3/src/main/resources/com/baeldung/produceimage/image.png b/spring-boot-modules/spring-boot-mvc-3/src/main/resources/com/baeldung/produceimage/image.png new file mode 100644 index 0000000000..16d218e334 Binary files /dev/null and b/spring-boot-modules/spring-boot-mvc-3/src/main/resources/com/baeldung/produceimage/image.png differ diff --git a/spring-boot-modules/spring-boot-mvc-3/src/test/java/com/baeldung/etag/EtagIntegrationTest.java b/spring-boot-modules/spring-boot-mvc-3/src/test/java/com/baeldung/etag/EtagIntegrationTest.java index 97de6d06f1..d7b50cb7fb 100644 --- a/spring-boot-modules/spring-boot-mvc-3/src/test/java/com/baeldung/etag/EtagIntegrationTest.java +++ b/spring-boot-modules/spring-boot-mvc-3/src/test/java/com/baeldung/etag/EtagIntegrationTest.java @@ -9,7 +9,6 @@ import org.assertj.core.util.Preconditions; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.web.server.LocalServerPort; @@ -26,7 +25,6 @@ import static org.junit.Assert.assertTrue; @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @ComponentScan(basePackageClasses = WebConfig.class) -@EnableAutoConfiguration public class EtagIntegrationTest { @LocalServerPort diff --git a/spring-boot-modules/spring-boot-mvc-3/src/test/java/com/baeldung/produceimage/DataProducerControllerIntegrationTest.java b/spring-boot-modules/spring-boot-mvc-3/src/test/java/com/baeldung/produceimage/DataProducerControllerIntegrationTest.java new file mode 100644 index 0000000000..eb459d88cc --- /dev/null +++ b/spring-boot-modules/spring-boot-mvc-3/src/test/java/com/baeldung/produceimage/DataProducerControllerIntegrationTest.java @@ -0,0 +1,37 @@ +package com.baeldung.produceimage; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest(classes = ImageApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +public class DataProducerControllerIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Test + void givenJpgTrue_whenGetImageDynamicType_ThenContentTypeIsJpg() throws Exception { + mockMvc.perform(get("/get-image-dynamic-type?jpg=true")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.IMAGE_JPEG)) + .andExpect(header().stringValues(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_JPEG_VALUE)); + } + + @Test + void givenJpgFalse_whenGetImageDynamicType_ThenContentTypeIsFalse() throws Exception { + mockMvc.perform(get("/get-image-dynamic-type?jpg=false")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.IMAGE_PNG)) + .andExpect(header().stringValues(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE)); + } + +} diff --git a/spring-boot-modules/spring-boot-mvc-3/src/test/java/com/baeldung/springvalidation/UserAccountUnitTest.java b/spring-boot-modules/spring-boot-mvc-3/src/test/java/com/baeldung/springvalidation/UserAccountIntegrationTest.java similarity index 84% rename from spring-boot-modules/spring-boot-mvc-3/src/test/java/com/baeldung/springvalidation/UserAccountUnitTest.java rename to spring-boot-modules/spring-boot-mvc-3/src/test/java/com/baeldung/springvalidation/UserAccountIntegrationTest.java index 507321bf8e..7c23153951 100644 --- a/spring-boot-modules/spring-boot-mvc-3/src/test/java/com/baeldung/springvalidation/UserAccountUnitTest.java +++ b/spring-boot-modules/spring-boot-mvc-3/src/test/java/com/baeldung/springvalidation/UserAccountIntegrationTest.java @@ -1,9 +1,7 @@ package com.baeldung.springvalidation; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -15,7 +13,7 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureMockMvc -public class UserAccountUnitTest { +public class UserAccountIntegrationTest { @Autowired private MockMvc mockMvc; @@ -46,10 +44,8 @@ public class UserAccountUnitTest { public void givenSaveBasicInfoStep1_whenIncorrectInput_thenError() throws Exception { this.mockMvc.perform(MockMvcRequestBuilders.post("/saveBasicInfoStep1") .accept(MediaType.TEXT_HTML)) - // .param("name", "test123") - // .param("password", "pass")) .andExpect(model().errorCount(2)) - // .andExpect(view().name("error")) + .andExpect(view().name("error")) .andExpect(status().isOk()) .andDo(print()); } diff --git a/spring-boot-modules/spring-boot-springdoc/README.md b/spring-boot-modules/spring-boot-springdoc/README.md index 733e31e698..3d0fd19ab1 100644 --- a/spring-boot-modules/spring-boot-springdoc/README.md +++ b/spring-boot-modules/spring-boot-springdoc/README.md @@ -6,3 +6,4 @@ - [Swagger @Api Description Is Deprecated](https://www.baeldung.com/java-swagger-api-description-deprecated) - [Set List of Objects in Swagger API Response](https://www.baeldung.com/java-swagger-set-list-response) - [Configure JWT Authentication for OpenAPI](https://www.baeldung.com/openapi-jwt-authentication) +- [Apply Default Global SecurityScheme in springdoc-openapi](https://www.baeldung.com/spring-openapi-global-securityscheme) diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/DefaultGlobalSecuritySchemeApplication.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/DefaultGlobalSecuritySchemeApplication.java new file mode 100644 index 0000000000..1ce81a1e83 --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/DefaultGlobalSecuritySchemeApplication.java @@ -0,0 +1,32 @@ +package com.baeldung.defaultglobalsecurityscheme; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityScheme; + +@SpringBootApplication +@OpenAPIDefinition(info = @Info(title = "Apply Default Global SecurityScheme in springdoc-openapi", version = "1.0.0"), security = { @SecurityRequirement(name = "api_key") }) +@SecurityScheme(type = SecuritySchemeType.APIKEY, name = "api_key", in = SecuritySchemeIn.HEADER) +public class DefaultGlobalSecuritySchemeApplication { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + return http.authorizeHttpRequests(authorizeRequests -> authorizeRequests.antMatchers("/api/auth/**", "/swagger-ui-custom.html", "/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**", "/webjars/**", "/swagger-ui/index.html", "/api-docs/**") + .permitAll() + .anyRequest() + .authenticated()) + .build(); + } + + public static void main(String[] args) { + SpringApplication.run(DefaultGlobalSecuritySchemeApplication.class, args); + } +} \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/controller/DefaultGlobalSecuritySchemeOpenApiController.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/controller/DefaultGlobalSecuritySchemeOpenApiController.java new file mode 100644 index 0000000000..4ad7a2a2c3 --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/controller/DefaultGlobalSecuritySchemeOpenApiController.java @@ -0,0 +1,61 @@ +package com.baeldung.defaultglobalsecurityscheme.controller; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +import javax.validation.Valid; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import com.baeldung.defaultglobalsecurityscheme.dto.LoginDto; +import com.baeldung.defaultglobalsecurityscheme.dto.ApplicationExceptionDto; +import com.baeldung.defaultglobalsecurityscheme.dto.PingResponseDto; +import com.baeldung.defaultglobalsecurityscheme.dto.TokenDto; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; + +@RestController +@RequestMapping("/") +public class DefaultGlobalSecuritySchemeOpenApiController { + @RequestMapping(method = RequestMethod.POST, value = "/login", produces = { "application/json" }, consumes = { "application/json" }) + @Operation(operationId = "login", responses = { + @ApiResponse(responseCode = "200", description = "api_key to be used in the secured-ping entry point", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = TokenDto.class)) }), + @ApiResponse(responseCode = "401", description = "Unauthorized request", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = ApplicationExceptionDto.class)) }) }) + @SecurityRequirements() + public ResponseEntity login(@Parameter(name = "LoginDto", description = "Login") @Valid @RequestBody(required = true) LoginDto loginDto) { + TokenDto token = new TokenDto(); + token.setRaw("Generated Token"); + return ResponseEntity.ok(token); + } + + @Operation(operationId = "ping", responses = { + @ApiResponse(responseCode = "200", description = "Ping that needs an api_key attribute in the header", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = PingResponseDto.class), examples = { @ExampleObject(value = "{ pong: '2022-06-17T18:30:33.465+02:00' }") }) }), + @ApiResponse(responseCode = "401", description = "Unauthorized request", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = ApplicationExceptionDto.class)) }), + @ApiResponse(responseCode = "403", description = "Forbidden request", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = ApplicationExceptionDto.class)) }) }) + @RequestMapping(method = RequestMethod.GET, value = "/ping", produces = { "application/json" }) + public ResponseEntity ping(@RequestHeader(name = "api_key", required = false) String api_key) { + int year = 2000; + int month = 1; + int dayOfMonth = 1; + int hour = 0; + int minute = 0; + int second = 0; + int nanoSeccond = 0; + ZoneOffset offset = ZoneOffset.UTC; + PingResponseDto response = new PingResponseDto(); + response.setPong(OffsetDateTime.of(year, month, dayOfMonth, hour, minute, second, nanoSeccond, offset)); + return ResponseEntity.ok(response); + } +} \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/ApplicationExceptionDto.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/ApplicationExceptionDto.java new file mode 100644 index 0000000000..5fb63793c4 --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/ApplicationExceptionDto.java @@ -0,0 +1,26 @@ +package com.baeldung.defaultglobalsecurityscheme.dto; + +public class ApplicationExceptionDto { + private long errorCode; + private String description; + + public ApplicationExceptionDto() { + super(); + } + + public long getErrorCode() { + return errorCode; + } + + public void setErrorCode(long errorCode) { + this.errorCode = errorCode; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/LoginDto.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/LoginDto.java new file mode 100644 index 0000000000..cf88cc4d98 --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/LoginDto.java @@ -0,0 +1,103 @@ +package com.baeldung.defaultglobalsecurityscheme.dto; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * LoginDto + */ + +@JsonTypeName("Login") +public class LoginDto { + + @JsonProperty("user") + private String user; + + @JsonProperty("pass") + private String pass; + + public LoginDto user(String user) { + this.user = user; + return this; + } + + /** + * Get user + * @return user + */ + + @Schema(name = "user", required = true) + public String getUser() { + return user; + } + + public void setUser(String user) { + this.user = user; + } + + public LoginDto pass(String pass) { + this.pass = pass; + return this; + } + + /** + * Get pass + * @return pass + */ + + @Schema(name = "pass", required = true) + public String getPass() { + return pass; + } + + public void setPass(String pass) { + this.pass = pass; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + LoginDto login = (LoginDto) o; + return Objects.equals(this.user, login.user) && Objects.equals(this.pass, login.pass); + } + + @Override + public int hashCode() { + return Objects.hash(user, pass); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class LoginDto {\n"); + sb.append(" user: ") + .append(toIndentedString(user)) + .append("\n"); + sb.append(" pass: ") + .append(toIndentedString(pass)) + .append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString() + .replace("\n", "\n "); + } +} diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/PingResponseDto.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/PingResponseDto.java new file mode 100644 index 0000000000..0d367785d8 --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/PingResponseDto.java @@ -0,0 +1,84 @@ +package com.baeldung.defaultglobalsecurityscheme.dto; + +import java.time.OffsetDateTime; +import java.util.Objects; + +import javax.validation.Valid; + +import org.springframework.format.annotation.DateTimeFormat; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * SecuredPingResponseDto + */ + +@JsonTypeName("PingResponse") +public class PingResponseDto { + + @JsonProperty("pong") + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + private OffsetDateTime pong; + + public PingResponseDto pong(OffsetDateTime pong) { + this.pong = pong; + return this; + } + + /** + * Get pong + * @return pong + */ + @Valid + @Schema(name = "pong", required = false) + public OffsetDateTime getPong() { + return pong; + } + + public void setPong(OffsetDateTime pong) { + this.pong = pong; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PingResponseDto securedPingResponse = (PingResponseDto) o; + return Objects.equals(this.pong, securedPingResponse.pong); + } + + @Override + public int hashCode() { + return Objects.hash(pong); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class PingResponseDto {\n"); + sb.append(" pong: ") + .append(toIndentedString(pong)) + .append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString() + .replace("\n", "\n "); + } +} diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/TokenDto.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/TokenDto.java new file mode 100644 index 0000000000..d8f7daa09c --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/TokenDto.java @@ -0,0 +1,57 @@ +package com.baeldung.defaultglobalsecurityscheme.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * LoginDto + */ + +@JsonTypeName("Token") +public class TokenDto { + + @JsonProperty("raw") + private String raw; + + @Schema(name = "raw", example = "app token") + public String getRaw() { + return raw; + } + + public void setRaw(String raw) { + this.raw = raw; + } + + @Override + public String toString() { + return "TokenDto [raw=" + raw + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((raw == null) ? 0 : raw.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + TokenDto other = (TokenDto) obj; + if (raw == null) { + if (other.raw != null) + return false; + } else if (!raw.equals(other.raw)) + return false; + return true; + } + +} diff --git a/spring-boot-modules/spring-boot-testing-2/src/test/java/com/baeldung/keycloaktestcontainers/IntegrationTest.java b/spring-boot-modules/spring-boot-testing-2/src/test/java/com/baeldung/keycloaktestcontainers/KeycloakTestContainers.java similarity index 95% rename from spring-boot-modules/spring-boot-testing-2/src/test/java/com/baeldung/keycloaktestcontainers/IntegrationTest.java rename to spring-boot-modules/spring-boot-testing-2/src/test/java/com/baeldung/keycloaktestcontainers/KeycloakTestContainers.java index 902c27b16e..44e24c98d1 100644 --- a/spring-boot-modules/spring-boot-testing-2/src/test/java/com/baeldung/keycloaktestcontainers/IntegrationTest.java +++ b/spring-boot-modules/spring-boot-testing-2/src/test/java/com/baeldung/keycloaktestcontainers/KeycloakTestContainers.java @@ -26,11 +26,11 @@ import org.springframework.web.reactive.function.client.WebClient; import dasniko.testcontainers.keycloak.KeycloakContainer; import io.restassured.RestAssured; -@ContextConfiguration(initializers = { IntegrationTest.Initializer.class }) +@ContextConfiguration(initializers = { KeycloakTestContainers.Initializer.class }) @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -public abstract class IntegrationTest { +public abstract class KeycloakTestContainers { - private static final Logger LOGGER = LoggerFactory.getLogger(IntegrationTest.class.getName()); + private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakTestContainers.class.getName()); @LocalServerPort private int port; diff --git a/spring-boot-modules/spring-boot-testing-2/src/test/java/com/baeldung/keycloaktestcontainers/UserControllerIntegrationTest.java b/spring-boot-modules/spring-boot-testing-2/src/test/java/com/baeldung/keycloaktestcontainers/UserControllerManualTest.java similarity index 77% rename from spring-boot-modules/spring-boot-testing-2/src/test/java/com/baeldung/keycloaktestcontainers/UserControllerIntegrationTest.java rename to spring-boot-modules/spring-boot-testing-2/src/test/java/com/baeldung/keycloaktestcontainers/UserControllerManualTest.java index 4015612860..817c8ae130 100644 --- a/spring-boot-modules/spring-boot-testing-2/src/test/java/com/baeldung/keycloaktestcontainers/UserControllerIntegrationTest.java +++ b/spring-boot-modules/spring-boot-testing-2/src/test/java/com/baeldung/keycloaktestcontainers/UserControllerManualTest.java @@ -5,7 +5,11 @@ import static org.hamcrest.Matchers.equalTo; import org.junit.jupiter.api.Test; -class UserControllerIntegrationTest extends IntegrationTest { +/** + * Requires Docker running on the machine to run without errors + * Therefore, skipped from pipeline + */ +class UserControllerManualTest extends KeycloakTestContainers { @Test void givenAuthenticatedUser_whenGetMe_shouldReturnMyInfo() { diff --git a/spring-cloud-modules/spring-cloud-archaius/pom.xml b/spring-cloud-modules/spring-cloud-archaius/pom.xml index 1208daaba6..66b7bb9b19 100644 --- a/spring-cloud-modules/spring-cloud-archaius/pom.xml +++ b/spring-cloud-modules/spring-cloud-archaius/pom.xml @@ -16,11 +16,11 @@ - basic-config additional-sources-simple + basic-config + dynamodb-config extra-configs jdbc-config - dynamodb-config zookeeper-config diff --git a/spring-cloud-modules/spring-cloud-eureka/pom.xml b/spring-cloud-modules/spring-cloud-eureka/pom.xml index 2a9c4f5fde..23523f2c2f 100644 --- a/spring-cloud-modules/spring-cloud-eureka/pom.xml +++ b/spring-cloud-modules/spring-cloud-eureka/pom.xml @@ -16,10 +16,11 @@ - spring-cloud-eureka-server spring-cloud-eureka-client + spring-cloud-eureka-client-profiles spring-cloud-eureka-feign-client spring-cloud-eureka-feign-client-integration-test + spring-cloud-eureka-server diff --git a/spring-cloud-modules/spring-cloud-hystrix/pom.xml b/spring-cloud-modules/spring-cloud-hystrix/pom.xml index e1f30c25dc..41548ba36a 100644 --- a/spring-cloud-modules/spring-cloud-hystrix/pom.xml +++ b/spring-cloud-modules/spring-cloud-hystrix/pom.xml @@ -15,9 +15,9 @@ + feign-rest-consumer rest-producer rest-consumer - feign-rest-consumer \ No newline at end of file diff --git a/spring-cloud-modules/spring-cloud-netflix-sidecar/echo-demo/pom.xml b/spring-cloud-modules/spring-cloud-netflix-sidecar/echo-demo/pom.xml index 60b6eab020..d8b1654eaa 100644 --- a/spring-cloud-modules/spring-cloud-netflix-sidecar/echo-demo/pom.xml +++ b/spring-cloud-modules/spring-cloud-netflix-sidecar/echo-demo/pom.xml @@ -9,7 +9,6 @@ com.baeldung.cloud spring-cloud-netflix-sidecar 0.0.1-SNAPSHOT - ../pom.xml diff --git a/spring-cloud-modules/spring-cloud-netflix-sidecar/pom.xml b/spring-cloud-modules/spring-cloud-netflix-sidecar/pom.xml index 0d95714bf9..3a72496279 100644 --- a/spring-cloud-modules/spring-cloud-netflix-sidecar/pom.xml +++ b/spring-cloud-modules/spring-cloud-netflix-sidecar/pom.xml @@ -11,10 +11,9 @@ Netflix Sidecar project for Spring Boot - com.baeldung - parent-boot-2 - 0.0.1-SNAPSHOT - ../../parent-boot-2 + com.baeldung.spring.cloud + spring-cloud-modules + 1.0.0-SNAPSHOT diff --git a/spring-cloud-modules/spring-cloud-netflix-sidecar/sidecar-demo/pom.xml b/spring-cloud-modules/spring-cloud-netflix-sidecar/sidecar-demo/pom.xml index 3c8d498456..200897652f 100644 --- a/spring-cloud-modules/spring-cloud-netflix-sidecar/sidecar-demo/pom.xml +++ b/spring-cloud-modules/spring-cloud-netflix-sidecar/sidecar-demo/pom.xml @@ -11,7 +11,6 @@ com.baeldung.cloud spring-cloud-netflix-sidecar 0.0.1-SNAPSHOT - ../pom.xml diff --git a/spring-cloud-modules/spring-cloud-openfeign/pom.xml b/spring-cloud-modules/spring-cloud-openfeign/pom.xml index 480663eb1c..5657577cb0 100644 --- a/spring-cloud-modules/spring-cloud-openfeign/pom.xml +++ b/spring-cloud-modules/spring-cloud-openfeign/pom.xml @@ -9,10 +9,9 @@ OpenFeign project for Spring Boot - com.baeldung - parent-boot-2 - 0.0.1-SNAPSHOT - ../../parent-boot-2 + com.baeldung.spring.cloud + spring-cloud-modules + 1.0.0-SNAPSHOT diff --git a/spring-cloud-modules/spring-cloud-zuul/pom.xml b/spring-cloud-modules/spring-cloud-zuul/pom.xml index e99cbdf355..b04a21da1c 100644 --- a/spring-cloud-modules/spring-cloud-zuul/pom.xml +++ b/spring-cloud-modules/spring-cloud-zuul/pom.xml @@ -17,9 +17,9 @@ spring-zuul-foos-resource - spring-zuul-ui - spring-zuul-rate-limiting spring-zuul-post-filter + spring-zuul-rate-limiting + spring-zuul-ui diff --git a/spring-jms/README.md b/spring-jms/README.md index fdeb64953a..666e32fa4b 100644 --- a/spring-jms/README.md +++ b/spring-jms/README.md @@ -4,3 +4,4 @@ This module contains articles about Spring with JMS ### Relevant Articles: - [Getting Started with Spring JMS](https://www.baeldung.com/spring-jms) +- [Testing Spring JMS](https://www.baeldung.com/spring-jms-testing) diff --git a/spring-jms/pom.xml b/spring-jms/pom.xml index 09bf854221..ab202402f3 100644 --- a/spring-jms/pom.xml +++ b/spring-jms/pom.xml @@ -41,6 +41,25 @@ ${spring-boot-test.version} test + + + org.mockito + mockito-core + 4.6.1 + test + + + org.apache.activemq.tooling + activemq-junit + 5.16.5 + test + + + org.testcontainers + testcontainers + 1.17.3 + test + diff --git a/spring-jms/src/main/java/com/baeldung/spring/jms/testing/JmsApplication.java b/spring-jms/src/main/java/com/baeldung/spring/jms/testing/JmsApplication.java new file mode 100644 index 0000000000..0a89d422b4 --- /dev/null +++ b/spring-jms/src/main/java/com/baeldung/spring/jms/testing/JmsApplication.java @@ -0,0 +1,13 @@ +package com.baeldung.spring.jms.testing; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; + +@ComponentScan +public class JmsApplication { + + public static void main(String[] args) { + ApplicationContext context = new AnnotationConfigApplicationContext(JmsApplication.class); + } +} diff --git a/spring-jms/src/main/java/com/baeldung/spring/jms/testing/JmsConfig.java b/spring-jms/src/main/java/com/baeldung/spring/jms/testing/JmsConfig.java new file mode 100644 index 0000000000..92abb32861 --- /dev/null +++ b/spring-jms/src/main/java/com/baeldung/spring/jms/testing/JmsConfig.java @@ -0,0 +1,34 @@ +package com.baeldung.spring.jms.testing; + +import javax.jms.ConnectionFactory; + +import org.apache.activemq.ActiveMQConnectionFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jms.annotation.EnableJms; +import org.springframework.jms.config.DefaultJmsListenerContainerFactory; +import org.springframework.jms.config.JmsListenerContainerFactory; +import org.springframework.jms.core.JmsTemplate; + +@Configuration +@EnableJms +public class JmsConfig { + + @Bean + public JmsListenerContainerFactory jmsListenerContainerFactory() { + DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory(); + factory.setConnectionFactory(connectionFactory()); + return factory; + } + + @Bean + public ConnectionFactory connectionFactory() { + return new ActiveMQConnectionFactory("tcp://localhost:61616"); + } + + @Bean + public JmsTemplate jmsTemplate() { + return new JmsTemplate(connectionFactory()); + } + +} diff --git a/spring-jms/src/main/java/com/baeldung/spring/jms/testing/MessageListener.java b/spring-jms/src/main/java/com/baeldung/spring/jms/testing/MessageListener.java new file mode 100644 index 0000000000..f66a99c876 --- /dev/null +++ b/spring-jms/src/main/java/com/baeldung/spring/jms/testing/MessageListener.java @@ -0,0 +1,20 @@ +package com.baeldung.spring.jms.testing; + +import javax.jms.JMSException; +import javax.jms.TextMessage; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.jms.annotation.JmsListener; +import org.springframework.stereotype.Component; + +@Component +public class MessageListener { + + private static final Logger logger = LoggerFactory.getLogger(MessageListener.class); + + @JmsListener(destination = "queue-1") + public void sampleJmsListenerMethod(TextMessage message) throws JMSException { + logger.info("JMS listener received text message: {}", message.getText()); + } +} diff --git a/spring-jms/src/main/java/com/baeldung/spring/jms/testing/MessageSender.java b/spring-jms/src/main/java/com/baeldung/spring/jms/testing/MessageSender.java new file mode 100644 index 0000000000..6cb199b0e9 --- /dev/null +++ b/spring-jms/src/main/java/com/baeldung/spring/jms/testing/MessageSender.java @@ -0,0 +1,21 @@ +package com.baeldung.spring.jms.testing; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jms.core.JmsTemplate; +import org.springframework.stereotype.Component; + +@Component +public class MessageSender { + + @Autowired + private JmsTemplate jmsTemplate; + + private static final Logger logger = LoggerFactory.getLogger(MessageSender.class); + + public void sendTextMessage(String destination, String message) { + logger.info("Sending message to {} destination with text {}", destination, message); + jmsTemplate.send(destination, s -> s.createTextMessage(message)); + } +} diff --git a/spring-jms/src/test/java/com/baeldung/spring/jms/testing/EmbeddedActiveMqIntegrationTest.java b/spring-jms/src/test/java/com/baeldung/spring/jms/testing/EmbeddedActiveMqIntegrationTest.java new file mode 100644 index 0000000000..6644ee50ac --- /dev/null +++ b/spring-jms/src/test/java/com/baeldung/spring/jms/testing/EmbeddedActiveMqIntegrationTest.java @@ -0,0 +1,91 @@ +package com.baeldung.spring.jms.testing; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import javax.jms.ConnectionFactory; +import javax.jms.JMSException; +import javax.jms.TextMessage; + +import org.apache.activemq.ActiveMQConnectionFactory; +import org.apache.activemq.junit.EmbeddedActiveMQBroker; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jms.annotation.EnableJms; +import org.springframework.jms.config.DefaultJmsListenerContainerFactory; +import org.springframework.jms.config.JmsListenerContainerFactory; +import org.springframework.jms.core.JmsTemplate; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +import com.baeldung.spring.jms.testing.EmbeddedActiveMqIntegrationTest.TestConfiguration; + +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = { TestConfiguration.class }) +public class EmbeddedActiveMqIntegrationTest { + + @ClassRule + public static EmbeddedActiveMQBroker embeddedBroker = new EmbeddedActiveMQBroker(); + + @SpyBean + private MessageListener messageListener; + + @SpyBean + private MessageSender messageSender; + + @Test + public void whenListening_thenReceivingCorrectMessage() throws JMSException { + String queueName = "queue-1"; + String messageText = "Test message"; + + embeddedBroker.pushMessage(queueName, messageText); + assertEquals(1, embeddedBroker.getMessageCount(queueName)); + + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(TextMessage.class); + + Mockito.verify(messageListener, Mockito.timeout(100)) + .sampleJmsListenerMethod(messageCaptor.capture()); + + TextMessage receivedMessage = messageCaptor.getValue(); + assertEquals(messageText, receivedMessage.getText()); + } + + @Test + public void whenSendingMessage_thenCorrectQueueAndMessageText() throws JMSException { + String queueName = "queue-2"; + String messageText = "Test message"; + + messageSender.sendTextMessage(queueName, messageText); + + assertEquals(1, embeddedBroker.getMessageCount(queueName)); + TextMessage sentMessage = embeddedBroker.peekTextMessage(queueName); + assertEquals(messageText, sentMessage.getText()); + } + + @Configuration + @EnableJms + static class TestConfiguration { + @Bean + public JmsListenerContainerFactory jmsListenerContainerFactory() { + DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory(); + factory.setConnectionFactory(connectionFactory()); + return factory; + } + + @Bean + public ConnectionFactory connectionFactory() { + return new ActiveMQConnectionFactory(embeddedBroker.getVmURL()); + } + + @Bean + public JmsTemplate jmsTemplate() { + return new JmsTemplate(connectionFactory()); + } + } + +} diff --git a/spring-jms/src/test/java/com/baeldung/spring/jms/testing/TestContainersActiveMqIntegrationTest.java b/spring-jms/src/test/java/com/baeldung/spring/jms/testing/TestContainersActiveMqIntegrationTest.java new file mode 100644 index 0000000000..d117b90423 --- /dev/null +++ b/spring-jms/src/test/java/com/baeldung/spring/jms/testing/TestContainersActiveMqIntegrationTest.java @@ -0,0 +1,101 @@ +package com.baeldung.spring.jms.testing; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import javax.jms.ConnectionFactory; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.TextMessage; + +import org.apache.activemq.ActiveMQConnectionFactory; +import org.assertj.core.api.Assertions; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jms.annotation.EnableJms; +import org.springframework.jms.config.DefaultJmsListenerContainerFactory; +import org.springframework.jms.config.JmsListenerContainerFactory; +import org.springframework.jms.core.JmsTemplate; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.utility.DockerImageName; + +import com.baeldung.spring.jms.testing.TestContainersActiveMqIntegrationTest.TestConfiguration; + +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = { TestConfiguration.class, MessageSender.class }) +public class TestContainersActiveMqIntegrationTest { + + @ClassRule + public static GenericContainer activeMqContainer = new GenericContainer<>(DockerImageName.parse("rmohr/activemq:5.14.3")).withExposedPorts(61616); + + @SpyBean + private MessageListener messageListener; + + @Autowired + private MessageSender messageSender; + + @Autowired + private JmsTemplate jmsTemplate; + + @Test + public void whenListening_thenReceivingCorrectMessage() throws JMSException { + String queueName = "queue-1"; + String messageText = "Test message"; + + jmsTemplate.send(queueName, s -> s.createTextMessage(messageText)); + + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(TextMessage.class); + + Mockito.verify(messageListener, Mockito.timeout(100)) + .sampleJmsListenerMethod(messageCaptor.capture()); + + TextMessage receivedMessage = messageCaptor.getValue(); + assertEquals(messageText, receivedMessage.getText()); + } + + @Test + public void whenSendingMessage_thenCorrectQueueAndMessageText() throws JMSException { + String queueName = "queue-2"; + String messageText = "Test message"; + + messageSender.sendTextMessage(queueName, messageText); + + Message sentMessage = jmsTemplate.receive(queueName); + Assertions.assertThat(sentMessage) + .isInstanceOf(TextMessage.class); + + assertEquals(messageText, ((TextMessage) sentMessage).getText()); + } + + @Configuration + @EnableJms + static class TestConfiguration { + @Bean + public JmsListenerContainerFactory jmsListenerContainerFactory() { + DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory(); + factory.setConnectionFactory(connectionFactory()); + return factory; + } + + @Bean + public ConnectionFactory connectionFactory() { + String brokerUrlFormat = "tcp://%s:%d"; + String brokerUrl = String.format(brokerUrlFormat, activeMqContainer.getHost(), activeMqContainer.getFirstMappedPort()); + return new ActiveMQConnectionFactory(brokerUrl); + } + + @Bean + public JmsTemplate jmsTemplate() { + return new JmsTemplate(connectionFactory()); + } + } + +} diff --git a/spring-security-modules/pom.xml b/spring-security-modules/pom.xml index 2bd6d23058..83412d2252 100644 --- a/spring-security-modules/pom.xml +++ b/spring-security-modules/pom.xml @@ -32,6 +32,7 @@ spring-security-web-boot-1 spring-security-web-boot-2 spring-security-web-boot-3 + spring-security-web-boot-4 spring-security-web-digest-auth spring-security-web-login spring-security-web-login-2 diff --git a/spring-security-modules/spring-security-web-boot-4/README.md b/spring-security-modules/spring-security-web-boot-4/README.md new file mode 100644 index 0000000000..0856315682 --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-4/README.md @@ -0,0 +1,10 @@ +## Spring Boot Security MVC + +This module contains articles about Spring Security with Spring MVC in Boot applications + +### The Course +The "REST With Spring" Classes: http://github.learnspringsecurity.com + +### Relevant Articles: + +- More articles: [[<-- prev]](/spring-security-modules/spring-security-web-boot-3) diff --git a/spring-security-modules/spring-security-web-boot-4/pom.xml b/spring-security-modules/spring-security-web-boot-4/pom.xml new file mode 100644 index 0000000000..8dd56e1de2 --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-4/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + spring-security-web-boot-4 + 0.0.1-SNAPSHOT + spring-security-web-boot-4 + jar + Spring Security MVC Boot - 4 + + + com.baeldung + parent-boot-2 + 0.0.1-SNAPSHOT + ../../parent-boot-2 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + \ No newline at end of file diff --git a/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/securityfilterchain/SecurityFilterChainApplication.java b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/securityfilterchain/SecurityFilterChainApplication.java new file mode 100644 index 0000000000..86f98b651b --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/securityfilterchain/SecurityFilterChainApplication.java @@ -0,0 +1,14 @@ +package com.baeldung.securityfilterchain; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +@SpringBootApplication +@EnableWebMvc +public class SecurityFilterChainApplication { + + public static void main(String[] args) { + SpringApplication.run(SecurityFilterChainApplication.class, args); + } +} diff --git a/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/securityfilterchain/configuration/SecurityConfig.java b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/securityfilterchain/configuration/SecurityConfig.java new file mode 100644 index 0000000000..4d3bec2ad2 --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/securityfilterchain/configuration/SecurityConfig.java @@ -0,0 +1,50 @@ +package com.baeldung.securityfilterchain.configuration; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; + +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true) +public class SecurityConfig { + + @Value("${spring.security.debug:false}") + boolean securityDebug; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.csrf() + .disable() + .authorizeRequests() + .antMatchers(HttpMethod.DELETE) + .hasRole("ADMIN") + .antMatchers("/admin/**") + .hasAnyRole("ADMIN") + .antMatchers("/user/**") + .hasAnyRole("USER", "ADMIN") + .antMatchers("/login/**") + .anonymous() + .anyRequest() + .authenticated() + .and() + .httpBasic() + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS); + + return http.build(); + } + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return (web) -> web.debug(securityDebug) + .ignoring() + .antMatchers("/css/**", "/js/**", "/img/**", "/lib/**", "/favicon.ico"); + } +} diff --git a/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/securityfilterchain/configuration/UserDetailServiceConfig.java b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/securityfilterchain/configuration/UserDetailServiceConfig.java new file mode 100644 index 0000000000..6a614e888b --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/securityfilterchain/configuration/UserDetailServiceConfig.java @@ -0,0 +1,31 @@ +package com.baeldung.securityfilterchain.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; + +@Configuration +public class UserDetailServiceConfig { + + @Bean + public UserDetailsService userDetailsService(BCryptPasswordEncoder bCryptPasswordEncoder) { + InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); + manager.createUser(User.withUsername("user") + .password(bCryptPasswordEncoder.encode("userPass")) + .roles("USER") + .build()); + manager.createUser(User.withUsername("admin") + .password(bCryptPasswordEncoder.encode("adminPass")) + .roles("ADMIN", "USER") + .build()); + return manager; + } + + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/securityfilterchain/controller/ResourceController.java b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/securityfilterchain/controller/ResourceController.java new file mode 100644 index 0000000000..e01d4ae9b3 --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/securityfilterchain/controller/ResourceController.java @@ -0,0 +1,34 @@ +package com.baeldung.securityfilterchain.controller; + +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class ResourceController { + @GetMapping("/login") + public String loginEndpoint() { + return "Login!"; + } + + @GetMapping("/admin") + public String adminEndpoint() { + return "Admin!"; + } + + @GetMapping("/user") + public String userEndpoint() { + return "User!"; + } + + @GetMapping("/all") + public String allRolesEndpoint() { + return "All Roles!"; + } + + @DeleteMapping("/delete") + public String deleteEndpoint(@RequestBody String s) { + return "I am deleting " + s; + } +} diff --git a/spring-security-modules/spring-security-web-boot-4/src/test/java/com/baeldung/securityfilterchain/SecurityFilterChainIntegrationTest.java b/spring-security-modules/spring-security-web-boot-4/src/test/java/com/baeldung/securityfilterchain/SecurityFilterChainIntegrationTest.java new file mode 100644 index 0000000000..e94b1b2f12 --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-4/src/test/java/com/baeldung/securityfilterchain/SecurityFilterChainIntegrationTest.java @@ -0,0 +1,94 @@ +package com.baeldung.securityfilterchain; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.test.context.support.WithAnonymousUser; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +@SpringBootTest(classes = SecurityFilterChainApplication.class) +public class SecurityFilterChainIntegrationTest { + @Autowired + private WebApplicationContext context; + + private MockMvc mvc; + + @BeforeEach + public void setup() { + mvc = MockMvcBuilders.webAppContextSetup(context) + .apply(springSecurity()) + .build(); + } + + @Test + @WithUserDetails(value = "admin") + public void whenAdminAccessUserEndpoint_thenOk() throws Exception { + mvc.perform(get("/user")) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails(value = "admin") + public void whenAdminAccessAdminSecuredEndpoint_thenIsOk() throws Exception { + mvc.perform(get("/admin")) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails(value = "admin") + public void whenAdminAccessDeleteSecuredEndpoint_thenIsOk() throws Exception { + mvc.perform(delete("/delete").content("{}")) + .andExpect(status().isOk()); + } + + @Test + @WithAnonymousUser + public void whenAnonymousAccessLogin_thenOk() throws Exception { + mvc.perform(get("/login")) + .andExpect(status().isOk()); + } + + @Test + @WithAnonymousUser + public void whenAnonymousAccessRestrictedEndpoint_thenIsUnauthorized() throws Exception { + mvc.perform(get("/all")) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails() + public void whenUserAccessUserSecuredEndpoint_thenOk() throws Exception { + mvc.perform(get("/user")) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails() + public void whenUserAccessRestrictedEndpoint_thenOk() throws Exception { + mvc.perform(get("/all")) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails() + public void whenUserAccessAdminSecuredEndpoint_thenIsForbidden() throws Exception { + mvc.perform(get("/admin")) + .andExpect(status().isForbidden()); + } + + @Test + @WithUserDetails() + public void whenUserAccessDeleteSecuredEndpoint_thenIsForbidden() throws Exception { + mvc.perform(delete("/delete")) + .andExpect(status().isForbidden()); + } +} diff --git a/spring-security-modules/spring-security-web-boot-4/src/test/resources/application.properties b/spring-security-modules/spring-security-web-boot-4/src/test/resources/application.properties new file mode 100644 index 0000000000..090ff54e92 --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-4/src/test/resources/application.properties @@ -0,0 +1 @@ +spring.security.debug=true \ No newline at end of file diff --git a/spring-security-modules/spring-security-web-boot-4/src/test/resources/logback-test.xml b/spring-security-modules/spring-security-web-boot-4/src/test/resources/logback-test.xml new file mode 100644 index 0000000000..c5a4b0ab1c --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-4/src/test/resources/logback-test.xml @@ -0,0 +1,12 @@ + + + + + [%d{ISO8601}]-[%thread] %-5level %logger - %msg%n + + + + + + + \ No newline at end of file diff --git a/spring-web-modules/spring-resttemplate-2/pom.xml b/spring-web-modules/spring-resttemplate-2/pom.xml index b87b245da9..0228b1dc4f 100644 --- a/spring-web-modules/spring-resttemplate-2/pom.xml +++ b/spring-web-modules/spring-resttemplate-2/pom.xml @@ -12,7 +12,7 @@ com.baeldung parent-boot-2 0.0.1-SNAPSHOT - ../../parent-boot-2/pom.xml + ../../parent-boot-2