From 07eb39541dadea9dded61059ed0be247caae2945 Mon Sep 17 00:00:00 2001 From: Riley Date: Tue, 9 Apr 2024 17:13:40 -0400 Subject: [PATCH 1/8] #2848 Moved project from private project directory to forked repo --- server-session/README.md | 197 ++++++++++++++++++ server-session/pom.xml | 29 +++ .../java/com/iluwatar/sessionserver/App.java | 63 ++++++ .../iluwatar/sessionserver/LoginHandler.java | 44 ++++ .../iluwatar/sessionserver/LogoutHandler.java | 45 ++++ .../LoginHandlerTest.java | 52 +++++ .../LogoutHandlerTest.java | 75 +++++++ 7 files changed, 505 insertions(+) create mode 100644 server-session/README.md create mode 100644 server-session/pom.xml create mode 100644 server-session/src/main/java/com/iluwatar/sessionserver/App.java create mode 100644 server-session/src/main/java/com/iluwatar/sessionserver/LoginHandler.java create mode 100644 server-session/src/main/java/com/iluwatar/sessionserver/LogoutHandler.java create mode 100644 server-session/src/test/java/com.iluwatar.sessionserver/LoginHandlerTest.java create mode 100644 server-session/src/test/java/com.iluwatar.sessionserver/LogoutHandlerTest.java diff --git a/server-session/README.md b/server-session/README.md new file mode 100644 index 000000000000..ae38fb948a9b --- /dev/null +++ b/server-session/README.md @@ -0,0 +1,197 @@ +--- +title: Server Session +category: Architectural +language: en +--- + +## Also known as + +Server-side session state pattern + +## Intent + +Within the context of a client-server relationship, the server is responsible for storing session data in order to maintain state in an otherwise stateless environment. + +## Explanation + +Real-world example + +> Consider a gaming website which stores user profile data such as username, password, highscore, hours played, etc. Since this website is accessed over the internet which uses the HTTP protocol, all requests sent to the server are stateless. In order for the page to display user relevent information without re-authenticating the user on every request a session must be created. Once the session is created the user can access the homescreen, statistics page, setting page, etc. and view profile specific data without needing to login in on every page request. + +In plain words + +> Session data is stored on the server, whether in a database, text file or any other form of persistent storage, rather than the client's browser. + +Wikipedia says + +> A session token is a unique identifier that is generated and sent from a server to a client to identify the current interaction session. The client usually stores and sends the token as an HTTP cookie and/or sends it as a parameter in GET or POST queries. The reason to use session tokens is that the client only has to handle the identifier—all session data is stored on the server (usually in a database, to which the client does not have direct access) linked to that identifier. + +**Programmatic Example** + +Consider a website in which a user can log in. Once logged in a session is created to maintain the specific user's data. + +First, we have the LoginHandler class, which creates a session identifier and sends it to the client as cookie. +Notice that not all data is sent to the client, the session creation time and user number are stored on the server side and thus cannot be accessed by the client. + +The user logs in by visiting localhost:8080/login + +output: + +Login successful! +Session ID: 26434a9c-e734-4a64-97ce-7802b8de46bb + +```java +public class LoginHandler implements HttpHandler { + + private Map sessions; + private Map sessionCreationTimes; + + public LoginHandler(Map sessions, Map sessionCreationTimes) { + this.sessions = sessions; + this.sessionCreationTimes = sessionCreationTimes; + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + // Generate session ID + String sessionID = UUID.randomUUID().toString(); + + // Store session data (simulated) + int newUser = sessions.size() + 1; + sessions.put(sessionID, newUser); + sessionCreationTimes.put(sessionID, Instant.now()); + + // Set session ID as cookie + exchange.getResponseHeaders().add("Set-Cookie", "sessionID=" + sessionID); + + // Send response + String response = "Login successful!\n" + + "Session ID: " + sessionID; + exchange.sendResponseHeaders(200, response.length()); + OutputStream os = exchange.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } +} +``` + +When the user logs out the session data is removed from storage using the LogoutHandler class. + +The user logs out by visiting localhost:8080/logout + +output: + +Logout successful! +Session ID: 26434a9c-e734-4a64-97ce-7802b8de46bb + +```java +public class LogoutHandler implements HttpHandler { + + private Map sessions; + private Map sessionCreationTimes; + + public LogoutHandler(Map sessions, Map sessionCreationTimes) { + this.sessions = sessions; + this.sessionCreationTimes = sessionCreationTimes; + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + // Get session ID from cookie + String sessionID = exchange.getRequestHeaders().getFirst("Cookie").replace("sessionID=", ""); + String currentSessionID = sessions.get(sessionID) == null ? null : sessionID; + + // Send response + + String response = ""; + if(currentSessionID == null) { + response += "Session has already expired!"; + } else { + response = "Logout successful!\n" + + "Session ID: " + currentSessionID; + } + + //Remove session + sessions.remove(sessionID); + sessionCreationTimes.remove(sessionID); + exchange.sendResponseHeaders(200, response.length()); + OutputStream os = exchange.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } +} +``` + +Sessions are often given a maximum time in which they will be maintained. The sessionExpirationTask() creates a thread which runs every 1 minute to check for sessions that have exceeded the maximum amount of time, in this case 1 minute and removes the session data from the server's storage. + +```java + private static void startSessionExpirationTask() { + new Thread(() -> { + while (true) { + try { + System.out.println("Session expiration checker started..."); + Thread.sleep(SESSION_EXPIRATION_TIME); // Sleep for expiration time + Instant currentTime = Instant.now(); + synchronized (sessions) { + synchronized (sessionCreationTimes) { + Iterator> iterator = sessionCreationTimes.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (entry.getValue().plusMillis(SESSION_EXPIRATION_TIME).isBefore(currentTime)) { + sessions.remove(entry.getKey()); + iterator.remove(); + } + } + } + } + System.out.println("Session expiration checker finished!"); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }).start(); +} +``` + +## Class diagram + +![alt text](./etc/adapter.urm.png "Adapter class diagram") + +## Applicability + +Use the Adapter pattern when + +* When a user logs into a website or web application and you want to keep track of their authentication status. +* In e-commerce websites when you want to maintain the contents of a user's shopping cart across different pages and visits. +* When you want to store user preferences and settings, such as language preferences, theme choices, or any other customizable options. +* When you want to keep track of user activity and behavior on a website for the sake of analytics purposes. + +## Tutorials + +* [Web Dev Simplified](https://www.youtube.com/watch?v=GihQAC1I39Q&pp=ygUMaHR0cCBzZXNzaW9u) +* [Hackersploit](https://www.youtube.com/watch?v=zHBpJA5XfDk) + +## Consequences + +Pros + +* HTTP sessions are typically not implemented using one thread per session, but by means of a database with information about the state of each session. The advantage with multiple processes or threads is relaxed complexity of the software, since each thread is an instance with its own history and encapsulated variables. +* Server-side session management is generally more secure than client-side alternatives like storing session data in cookies. +* Server-side session management can scale more easily, especially when using distributed caching systems or databases. + +Cons + +* Large overhead in terms of system resources, and that the session may be interrupted if the system is restarted. +* Can become difficult to handle in conjunction with load-balancing/high-availability systems and are not usable at all in some embedded systems with no storage. +* If the server hosting the session data goes down or experiences issues, it can disrupt the entire application's functionality, potentially leading to session data loss and user inconvenience. + +## Real-world examples + +* Express.js Session Middleware +* Spring Session in Spring Boot +* Django Session Framework +* Java Servlet Session Management + +## Credits + +* [Session(Computer Science)](https://en.wikipedia.org/wiki/Session_(computer_science) \ No newline at end of file diff --git a/server-session/pom.xml b/server-session/pom.xml new file mode 100644 index 000000000000..160c4b6198a3 --- /dev/null +++ b/server-session/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + serversession + + + + org.junit.jupiter + junit-jupiter-api + 5.10.2 + test + + + org.mockito + mockito-core + 5.11.0 + test + + + + \ No newline at end of file diff --git a/server-session/src/main/java/com/iluwatar/sessionserver/App.java b/server-session/src/main/java/com/iluwatar/sessionserver/App.java new file mode 100644 index 000000000000..dcb3faaafdc8 --- /dev/null +++ b/server-session/src/main/java/com/iluwatar/sessionserver/App.java @@ -0,0 +1,63 @@ +package com.iluwatar.sessionserver; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.time.Instant; +import java.util.*; +import java.util.logging.Logger; + +import com.sun.net.httpserver.HttpServer; + +public class App { + + // Map to store session data (simulated using a HashMap) + private static Map sessions = new HashMap<>(); + private static Map sessionCreationTimes = new HashMap<>(); + private static final long SESSION_EXPIRATION_TIME = 10000; + private static Logger logger = Logger.getLogger(App.class.getName()); + + public static void main(String[] args) throws IOException { + // Create HTTP server listening on port 8000 + HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); + + // Set up session management endpoints + server.createContext("/login", new LoginHandler(sessions, sessionCreationTimes)); + server.createContext("/logout", new LogoutHandler(sessions, sessionCreationTimes)); + + // Start the server + server.start(); + + // Start background task to check for expired sessions + sessionExpirationTask(); + + logger.info("Server started. Listening on port 8080..."); + } + + private static void sessionExpirationTask() { + new Thread(() -> { + while (true) { + try { + logger.info("Session expiration checker started..."); + Thread.sleep(SESSION_EXPIRATION_TIME); // Sleep for expiration time + Instant currentTime = Instant.now(); + synchronized (sessions) { + synchronized (sessionCreationTimes) { + Iterator> iterator = sessionCreationTimes.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (entry.getValue().plusMillis(SESSION_EXPIRATION_TIME).isBefore(currentTime)) { + sessions.remove(entry.getKey()); + iterator.remove(); + } + } + } + } + logger.info("Session expiration checker finished!"); + } catch (InterruptedException e) { + e.printStackTrace(); + Thread.currentThread().interrupt(); + } + } + }).start(); + } +} \ No newline at end of file diff --git a/server-session/src/main/java/com/iluwatar/sessionserver/LoginHandler.java b/server-session/src/main/java/com/iluwatar/sessionserver/LoginHandler.java new file mode 100644 index 000000000000..aee66e7ceb90 --- /dev/null +++ b/server-session/src/main/java/com/iluwatar/sessionserver/LoginHandler.java @@ -0,0 +1,44 @@ +package com.iluwatar.sessionserver; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; + +import java.io.IOException; +import java.io.OutputStream; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class LoginHandler implements HttpHandler { + + private Map sessions; + private Map sessionCreationTimes; + + public LoginHandler(Map sessions, Map sessionCreationTimes) { + this.sessions = sessions; + this.sessionCreationTimes = sessionCreationTimes; + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + // Generate session ID + String sessionID = UUID.randomUUID().toString(); + + // Store session data (simulated) + int newUser = sessions.size() + 1; + sessions.put(sessionID, newUser); + sessionCreationTimes.put(sessionID, Instant.now()); + + // Set session ID as cookie + exchange.getResponseHeaders().add("Set-Cookie", "sessionID=" + sessionID); + + // Send response + String response = "Login successful!\n" + + "Session ID: " + sessionID; + exchange.sendResponseHeaders(200, response.length()); + OutputStream os = exchange.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } +} diff --git a/server-session/src/main/java/com/iluwatar/sessionserver/LogoutHandler.java b/server-session/src/main/java/com/iluwatar/sessionserver/LogoutHandler.java new file mode 100644 index 000000000000..4693b0afb1a4 --- /dev/null +++ b/server-session/src/main/java/com/iluwatar/sessionserver/LogoutHandler.java @@ -0,0 +1,45 @@ +package com.iluwatar.sessionserver; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; + +import java.io.IOException; +import java.io.OutputStream; +import java.time.Instant; +import java.util.Map; + +public class LogoutHandler implements HttpHandler { + + private Map sessions; + private Map sessionCreationTimes; + + public LogoutHandler(Map sessions, Map sessionCreationTimes) { + this.sessions = sessions; + this.sessionCreationTimes = sessionCreationTimes; + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + // Get session ID from cookie + String sessionID = exchange.getRequestHeaders().getFirst("Cookie").replace("sessionID=", ""); + String currentSessionID = sessions.get(sessionID) == null ? null : sessionID; + + // Send response + + String response = ""; + if(currentSessionID == null) { + response += "Session has already expired!"; + } else { + response = "Logout successful!\n" + + "Session ID: " + currentSessionID; + } + + //Remove session + sessions.remove(sessionID); + sessionCreationTimes.remove(sessionID); + exchange.sendResponseHeaders(200, response.length()); + OutputStream os = exchange.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } +} diff --git a/server-session/src/test/java/com.iluwatar.sessionserver/LoginHandlerTest.java b/server-session/src/test/java/com.iluwatar.sessionserver/LoginHandlerTest.java new file mode 100644 index 000000000000..8c976fa08ebf --- /dev/null +++ b/server-session/src/test/java/com.iluwatar.sessionserver/LoginHandlerTest.java @@ -0,0 +1,52 @@ +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import com.iluwatar.sessionserver.LoginHandler; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.time.Instant; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class LoginHandlerTest { + + private LoginHandler loginHandler; + //private Headers headers; + private Map sessions; + private Map sessionCreationTimes; + + @Mock + private HttpExchange exchange; + + @BeforeEach + public void setUp() { + MockitoAnnotations.initMocks(this); + sessions = new HashMap<>(); + sessionCreationTimes = new HashMap<>(); + loginHandler = new LoginHandler(sessions, sessionCreationTimes); + } + + @Test + public void testHandle() throws IOException { + + //assemble + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); //Exchange object is mocked so OutputStream must be manually created + when(exchange.getResponseHeaders()).thenReturn(new Headers()); //Exchange object is mocked so Header object must be manually created + when(exchange.getResponseBody()).thenReturn(outputStream); + + //act + loginHandler.handle(exchange); + + //assert + String[] response = outputStream.toString().split("Session ID: "); + assertEquals(sessions.entrySet().toArray()[0].toString().split("=1")[0], response[1]); + } +} \ No newline at end of file diff --git a/server-session/src/test/java/com.iluwatar.sessionserver/LogoutHandlerTest.java b/server-session/src/test/java/com.iluwatar.sessionserver/LogoutHandlerTest.java new file mode 100644 index 000000000000..650cbc443629 --- /dev/null +++ b/server-session/src/test/java/com.iluwatar.sessionserver/LogoutHandlerTest.java @@ -0,0 +1,75 @@ +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import com.iluwatar.sessionserver.LogoutHandler; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.Mockito.when; + +public class LogoutHandlerTest { + + private LogoutHandler logoutHandler; + private Headers headers; + private Map sessions; + private Map sessionCreationTimes; + + @Mock + private HttpExchange exchange; + + @BeforeEach + public void setUp() { + MockitoAnnotations.initMocks(this); + sessions = new HashMap<>(); + sessionCreationTimes = new HashMap<>(); + logoutHandler = new LogoutHandler(sessions, sessionCreationTimes); + headers = new Headers(); + headers.add("Cookie", "sessionID=1234"); //Exchange object methods return Header Object but Exchange is mocked so Headers must be manually created + } + + @Test + public void testHandler_SessionNotExpired() throws IOException { + + //assemble + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + sessions.put("1234", 1); //Fake login details since LoginHandler isn't called + sessionCreationTimes.put("1234", Instant.now()); //Fake login details since LoginHandler isn't called + when(exchange.getRequestHeaders()).thenReturn(headers); + when(exchange.getResponseBody()).thenReturn(outputStream); + + //act + logoutHandler.handle(exchange); + + //assert + String[] response = outputStream.toString().split("Session ID: "); + Assertions.assertEquals("1234", response[1]); + Assertions.assertFalse(sessions.containsKey(response)); + Assertions.assertFalse(sessionCreationTimes.containsKey(response)); + } + + @Test + public void testHandler_SessionExpired() throws IOException { + + //assemble + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + when(exchange.getRequestHeaders()).thenReturn(headers); + when(exchange.getResponseBody()).thenReturn(outputStream); + + //act + logoutHandler.handle(exchange); + + //assert + String[] response = outputStream.toString().split("Session ID: "); + Assertions.assertEquals("Session has already expired!", response[0]); + Assertions.assertFalse(sessions.containsKey(response)); + Assertions.assertFalse(sessionCreationTimes.containsKey(response)); + } +} From 00a7d1312c0200d47c06970c944e69873851bd7b Mon Sep 17 00:00:00 2001 From: Riley Date: Wed, 10 Apr 2024 15:56:34 -0400 Subject: [PATCH 2/8] #2848 Requested changes implemented --- server-session/README.md | 54 +++++++++++++------ .../java/com/iluwatar/sessionserver/App.java | 30 ++++++++--- .../iluwatar/sessionserver/LoginHandler.java | 20 +++++-- .../iluwatar/sessionserver/LogoutHandler.java | 24 +++++++-- 4 files changed, 96 insertions(+), 32 deletions(-) diff --git a/server-session/README.md b/server-session/README.md index ae38fb948a9b..38ff1c298285 100644 --- a/server-session/README.md +++ b/server-session/README.md @@ -1,7 +1,11 @@ --- title: Server Session -category: Architectural +category: Behavioral language: en +tag: + - Session Management + - Session Tracking + - Cookies --- ## Also known as @@ -52,7 +56,7 @@ public class LoginHandler implements HttpHandler { } @Override - public void handle(HttpExchange exchange) throws IOException { + public void handle(HttpExchange exchange) { // Generate session ID String sessionID = UUID.randomUUID().toString(); @@ -60,6 +64,7 @@ public class LoginHandler implements HttpHandler { int newUser = sessions.size() + 1; sessions.put(sessionID, newUser); sessionCreationTimes.put(sessionID, Instant.now()); + LOGGER.info("User " + newUser + " created at time " + sessionCreationTimes.get(sessionID)); // Set session ID as cookie exchange.getResponseHeaders().add("Set-Cookie", "sessionID=" + sessionID); @@ -67,10 +72,16 @@ public class LoginHandler implements HttpHandler { // Send response String response = "Login successful!\n" + "Session ID: " + sessionID; - exchange.sendResponseHeaders(200, response.length()); - OutputStream os = exchange.getResponseBody(); - os.write(response.getBytes()); - os.close(); + try { + exchange.sendResponseHeaders(200, response.length()); + } catch (IOException e) { + LOGGER.error("An error occurred: ", e); + } + try(OutputStream os = exchange.getResponseBody()) { + os.write(response.getBytes()); + } catch(IOException e) { + LOGGER.error("An error occurred: ", e); + } } } ``` @@ -96,7 +107,7 @@ public class LogoutHandler implements HttpHandler { } @Override - public void handle(HttpExchange exchange) throws IOException { + public void handle(HttpExchange exchange) { // Get session ID from cookie String sessionID = exchange.getRequestHeaders().getFirst("Cookie").replace("sessionID=", ""); String currentSessionID = sessions.get(sessionID) == null ? null : sessionID; @@ -112,12 +123,24 @@ public class LogoutHandler implements HttpHandler { } //Remove session + if(currentSessionID != null) + LOGGER.info("User " + sessions.get(currentSessionID) + " deleted!"); + else + LOGGER.info("User already deleted!"); sessions.remove(sessionID); sessionCreationTimes.remove(sessionID); - exchange.sendResponseHeaders(200, response.length()); - OutputStream os = exchange.getResponseBody(); - os.write(response.getBytes()); - os.close(); + + try { + exchange.sendResponseHeaders(200, response.length()); + } catch(IOException e) { + LOGGER.error("An error has occurred: ", e); + } + + try(OutputStream os = exchange.getResponseBody()) { + os.write(response.getBytes()); + } catch(IOException e) { + LOGGER.error("An error has occurred: ", e); + } } } ``` @@ -125,11 +148,11 @@ public class LogoutHandler implements HttpHandler { Sessions are often given a maximum time in which they will be maintained. The sessionExpirationTask() creates a thread which runs every 1 minute to check for sessions that have exceeded the maximum amount of time, in this case 1 minute and removes the session data from the server's storage. ```java - private static void startSessionExpirationTask() { + private static void sessionExpirationTask() { new Thread(() -> { while (true) { try { - System.out.println("Session expiration checker started..."); + LOGGER.info("Session expiration checker started..."); Thread.sleep(SESSION_EXPIRATION_TIME); // Sleep for expiration time Instant currentTime = Instant.now(); synchronized (sessions) { @@ -144,9 +167,10 @@ Sessions are often given a maximum time in which they will be maintained. The se } } } - System.out.println("Session expiration checker finished!"); + LOGGER.info("Session expiration checker finished!"); } catch (InterruptedException e) { - e.printStackTrace(); + LOGGER.error("An error occurred: ", e); + Thread.currentThread().interrupt(); } } }).start(); diff --git a/server-session/src/main/java/com/iluwatar/sessionserver/App.java b/server-session/src/main/java/com/iluwatar/sessionserver/App.java index dcb3faaafdc8..ce54c5f38008 100644 --- a/server-session/src/main/java/com/iluwatar/sessionserver/App.java +++ b/server-session/src/main/java/com/iluwatar/sessionserver/App.java @@ -4,17 +4,33 @@ import java.net.InetSocketAddress; import java.time.Instant; import java.util.*; -import java.util.logging.Logger; - import com.sun.net.httpserver.HttpServer; +import lombok.extern.slf4j.Slf4j; + +/** + * The server session pattern is a behavioral design pattern concerned with assigning the responsibility + * of storing session data on the server side. Within the context of stateless protocols like HTTP all + * requests are isolated events independent of previous requests. In order to create sessions during + * user-access for a particular web application various methods can be used, such as cookies. Cookies + * are a small piece of data that can be sent between client and server on every request and response + * so that the server can "remember" the previous requests. In general cookies can either store the session + * data or the cookie can store a session identifier and be used to access appropriate data from a persistent + * storage. In the latter case the session data is stored on the server-side and appropriate data is + * identified by the cookie sent from a client's request. + * This project demonstrates the latter case. + * In the following example the ({@link App}) class starts a server and assigns ({@link LoginHandler}) + * class to handle login request. When a user logs in a session identifier is created and stored for future + * requests in a list. When a user logs out the session identifier is deleted from the list along with + * the appropriate user session data, which is handle by the ({@link LogoutHandler}) class. + */ +@Slf4j public class App { // Map to store session data (simulated using a HashMap) private static Map sessions = new HashMap<>(); private static Map sessionCreationTimes = new HashMap<>(); private static final long SESSION_EXPIRATION_TIME = 10000; - private static Logger logger = Logger.getLogger(App.class.getName()); public static void main(String[] args) throws IOException { // Create HTTP server listening on port 8000 @@ -30,14 +46,14 @@ public static void main(String[] args) throws IOException { // Start background task to check for expired sessions sessionExpirationTask(); - logger.info("Server started. Listening on port 8080..."); + LOGGER.info("Server started. Listening on port 8080..."); } private static void sessionExpirationTask() { new Thread(() -> { while (true) { try { - logger.info("Session expiration checker started..."); + LOGGER.info("Session expiration checker started..."); Thread.sleep(SESSION_EXPIRATION_TIME); // Sleep for expiration time Instant currentTime = Instant.now(); synchronized (sessions) { @@ -52,9 +68,9 @@ private static void sessionExpirationTask() { } } } - logger.info("Session expiration checker finished!"); + LOGGER.info("Session expiration checker finished!"); } catch (InterruptedException e) { - e.printStackTrace(); + LOGGER.error("An error occurred: ", e); Thread.currentThread().interrupt(); } } diff --git a/server-session/src/main/java/com/iluwatar/sessionserver/LoginHandler.java b/server-session/src/main/java/com/iluwatar/sessionserver/LoginHandler.java index aee66e7ceb90..b8f9b298b688 100644 --- a/server-session/src/main/java/com/iluwatar/sessionserver/LoginHandler.java +++ b/server-session/src/main/java/com/iluwatar/sessionserver/LoginHandler.java @@ -2,6 +2,8 @@ import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; import java.io.IOException; import java.io.OutputStream; @@ -10,6 +12,7 @@ import java.util.Map; import java.util.UUID; +@Slf4j public class LoginHandler implements HttpHandler { private Map sessions; @@ -21,7 +24,7 @@ public LoginHandler(Map sessions, Map sessionC } @Override - public void handle(HttpExchange exchange) throws IOException { + public void handle(HttpExchange exchange) { // Generate session ID String sessionID = UUID.randomUUID().toString(); @@ -29,6 +32,7 @@ public void handle(HttpExchange exchange) throws IOException { int newUser = sessions.size() + 1; sessions.put(sessionID, newUser); sessionCreationTimes.put(sessionID, Instant.now()); + LOGGER.info("User " + newUser + " created at time " + sessionCreationTimes.get(sessionID)); // Set session ID as cookie exchange.getResponseHeaders().add("Set-Cookie", "sessionID=" + sessionID); @@ -36,9 +40,15 @@ public void handle(HttpExchange exchange) throws IOException { // Send response String response = "Login successful!\n" + "Session ID: " + sessionID; - exchange.sendResponseHeaders(200, response.length()); - OutputStream os = exchange.getResponseBody(); - os.write(response.getBytes()); - os.close(); + try { + exchange.sendResponseHeaders(200, response.length()); + } catch (IOException e) { + LOGGER.error("An error occurred: ", e); + } + try(OutputStream os = exchange.getResponseBody()) { + os.write(response.getBytes()); + } catch(IOException e) { + LOGGER.error("An error occurred: ", e); + } } } diff --git a/server-session/src/main/java/com/iluwatar/sessionserver/LogoutHandler.java b/server-session/src/main/java/com/iluwatar/sessionserver/LogoutHandler.java index 4693b0afb1a4..b7a113b02b33 100644 --- a/server-session/src/main/java/com/iluwatar/sessionserver/LogoutHandler.java +++ b/server-session/src/main/java/com/iluwatar/sessionserver/LogoutHandler.java @@ -2,12 +2,14 @@ import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; +import lombok.extern.slf4j.Slf4j; import java.io.IOException; import java.io.OutputStream; import java.time.Instant; import java.util.Map; +@Slf4j public class LogoutHandler implements HttpHandler { private Map sessions; @@ -19,7 +21,7 @@ public LogoutHandler(Map sessions, Map session } @Override - public void handle(HttpExchange exchange) throws IOException { + public void handle(HttpExchange exchange) { // Get session ID from cookie String sessionID = exchange.getRequestHeaders().getFirst("Cookie").replace("sessionID=", ""); String currentSessionID = sessions.get(sessionID) == null ? null : sessionID; @@ -35,11 +37,23 @@ public void handle(HttpExchange exchange) throws IOException { } //Remove session + if(currentSessionID != null) + LOGGER.info("User " + sessions.get(currentSessionID) + " deleted!"); + else + LOGGER.info("User already deleted!"); sessions.remove(sessionID); sessionCreationTimes.remove(sessionID); - exchange.sendResponseHeaders(200, response.length()); - OutputStream os = exchange.getResponseBody(); - os.write(response.getBytes()); - os.close(); + + try { + exchange.sendResponseHeaders(200, response.length()); + } catch(IOException e) { + LOGGER.error("An error has occurred: ", e); + } + + try(OutputStream os = exchange.getResponseBody()) { + os.write(response.getBytes()); + } catch(IOException e) { + LOGGER.error("An error has occurred: ", e); + } } } From fe4a6b088a39b6fb47bff0d643a36cd1fb1578ba Mon Sep 17 00:00:00 2001 From: Riley Date: Fri, 12 Apr 2024 21:45:55 -0400 Subject: [PATCH 3/8] Added publisher-subscriber package(empty) --- publisher-subscriber/pom.xml | 58 +++++++++++++++++++ .../java/com/iluwatar/publishersubscriber | 0 2 files changed, 58 insertions(+) create mode 100644 publisher-subscriber/pom.xml create mode 100644 publisher-subscriber/src/main/java/com/iluwatar/publishersubscriber diff --git a/publisher-subscriber/pom.xml b/publisher-subscriber/pom.xml new file mode 100644 index 000000000000..a4dcbb6458e5 --- /dev/null +++ b/publisher-subscriber/pom.xml @@ -0,0 +1,58 @@ + + + + 4.0.0 + + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + publishersubscriber + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.mockito + mockito-core + test + + + org.apache.activemq + activemq-core + 5.7.0 + + + + \ No newline at end of file diff --git a/publisher-subscriber/src/main/java/com/iluwatar/publishersubscriber b/publisher-subscriber/src/main/java/com/iluwatar/publishersubscriber new file mode 100644 index 000000000000..e69de29bb2d1 From 0844618645067533302053ff6c1d5642a8d24c54 Mon Sep 17 00:00:00 2001 From: Riley Date: Wed, 17 Apr 2024 14:25:44 -0400 Subject: [PATCH 4/8] #2898 Initial commit --- publisher-subscriber/README.md | 358 ++++++++++++++++++ publisher-subscriber/pom.xml | 16 + .../java/com/iluwatar/publishersubscriber | 0 .../publishersubscriber/TBorrower.java | 138 +++++++ .../iluwatar/publishersubscriber/TLender.java | 150 ++++++++ .../src/main/resources/jndi.properties | 6 + .../publishersubscriber/TBorrowerTest.java | 27 ++ .../publishersubscriber/TLenderTest.java | 33 ++ .../src/test/resources/jndi.properties | 6 + 9 files changed, 734 insertions(+) create mode 100644 publisher-subscriber/README.md delete mode 100644 publisher-subscriber/src/main/java/com/iluwatar/publishersubscriber create mode 100644 publisher-subscriber/src/main/java/com/iluwatar/publishersubscriber/TBorrower.java create mode 100644 publisher-subscriber/src/main/java/com/iluwatar/publishersubscriber/TLender.java create mode 100644 publisher-subscriber/src/main/resources/jndi.properties create mode 100644 publisher-subscriber/src/test/java/com/iluwatar/publishersubscriber/TBorrowerTest.java create mode 100644 publisher-subscriber/src/test/java/com/iluwatar/publishersubscriber/TLenderTest.java create mode 100644 publisher-subscriber/src/test/resources/jndi.properties diff --git a/publisher-subscriber/README.md b/publisher-subscriber/README.md new file mode 100644 index 000000000000..fe7a110fca89 --- /dev/null +++ b/publisher-subscriber/README.md @@ -0,0 +1,358 @@ +--- +title: Publish-Subscribe pattern +category: Behavioral +language: en +tag: + - Distributed Systems + - Messaging + - JMS +--- + +## Also known as + +Pub-Sub Pattern + +## Intent + +To provide an asynchronous manner of handling messages between a message producer and a message consumer, without coupling the producer and consumer. + +## Explanation + +Real-world example + +> In online multipler games, players need real-time updates on player actions and game events. When updates needs to be broadcasted to multiple players the players act as subscribers to receive these updates. Depending on player level, account restrictions and so forth, a given player receives updates that another player may not receive and vice versa. + +In plain words + +> Publishers send messages that pertain to a certain topic and all subscribers to that topic will receive the message. + +Wikipedia says + +> Publish–subscribe is a messaging pattern where publishers categorize messages into classes that are received by subscribers. This is contrasted to the typical messaging pattern model where publishers send messages directly to subscribers. +Similarly, subscribers express interest in one or more classes and only receive messages that are of interest, without knowledge of which publishers, if any, there are. +Publish–subscribe is a sibling of the message queue paradigm, and is typically one part of a larger message-oriented middleware system + +**Programmatic Example** + +Consider an application in which a mortgage lender publishes interest rate updates to borrowers. When received, borrowers can decide whether they should refinance based on the updated rate. + +First, we have the Lender class which acts as a publisher. The user can enter interest rates in which they will be published to all subscribers. + +output: + +Press enter to quit application +Enter: Rate + +e.g. 6.8 + +```java +public class TLender { + + private TopicConnection tConnection; + private TopicSession tSession; + private Topic topic; + private TopicPublisher publisher; + + public TLender(String topicCFName, String topicName) { + + try { + //create context and retrieve objects from directory + Context context = new InitialContext(); + TopicConnectionFactory topicCF = (TopicConnectionFactory) context.lookup(topicCFName); + tConnection = topicCF.createTopicConnection(); + + //create connection + tSession = tConnection.createTopicSession(false, Session.AUTO_ACKNOWLEDGE); + + //Retrieve request and response queues + topic = (Topic) context.lookup(topicName); + + //Create publisher + publisher = tSession.createPublisher(topic); + + tConnection.start(); + } catch(NamingException e) { + e.printStackTrace(); + LOGGER.error("An error has occurred!", e); + System.exit(1); + } catch(JMSException e) { + e.printStackTrace(); + LOGGER.error("An error has occurred!", e); + System.exit(1); + } + } + + private void publishRate(double newRate) { + + try { + //create JMS message + BytesMessage message = tSession.createBytesMessage(); + message.writeDouble(newRate); + + //publish message + publisher.publish(message); + } catch(JMSException e) { + e.printStackTrace(); + LOGGER.error("An error has occurred!", e); + System.exit(1); + } + } + + private void exit() { + try { + tConnection.close(); + } catch(JMSException e) { + e.printStackTrace(); + LOGGER.error("An error has occurred!", e); + System.exit(1); + } + System.exit(0); + } + + public static void main(String[] args) { + + String topicCFName = null; + String topicName = null; + + if(args.length == 2) { + topicCFName = args[0]; + topicName = args[1]; + } else { + System.out.println("Invalid arguments. Should be: "); + System.out.println("java TLender [factory] [topic]"); + System.exit(1); + } + + try { + // Create and start activeMQ broker. Broker decouples publishers and subscribers. + //Additionally brokers manage threads and asynchronous sending and receiving of messages. + BrokerService broker = new BrokerService(); + broker.addConnector("tcp://localhost:61616"); + broker.start(); + + } catch(Exception e) { + e.printStackTrace(); + LOGGER.error("An error has occurred!", e); + } + + TLender tLender = new TLender(topicCFName, topicName); + + System.out.println ("TLender Application Started"); + System.out.println ("Press enter to quit application"); + System.out.println ("Enter: Rate"); + System.out.println("\ne.g. 6.8"); + + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); + //Continuously read input and send as message to subscribers + while(true) { + System.out.print("> "); + String line = reader.readLine(); + //Exit if user pressed enter or line is blank + if (line == null || line.trim().length() == 0) { + System.out.println("Exiting..."); + tLender.exit(); + } + else { //publish the entered rate + double newRate = Double.parseDouble(line); + tLender.publishRate(newRate); + } + } + } catch(IOException e) { + e.printStackTrace(); + LOGGER.error("An error has occurred!", e); + } + } + + public TopicConnection gettConnection() { + return tConnection; + } + + public TopicSession gettSession() { + return tSession; + } + + public Topic getTopic() { + return topic; + } +} +``` + +The Borrower class acts as a subscriber to a given topic, in this case mortgage interest rates. Evertime a new rate is published the subscriber receives the message. + +output: + +Initial rate is 6.0 +Waiting for new rates... +Press enter to quit application + +Running the class: + +The class must be run after the TLender class is running since TLender spins up the activeMQ broker. + +In order to see the messages being sent to multiple subscribers multiple instance of the TBorrower class need to be run. Either run multiple instances in an IDE or execute the following command in a command line from the root folder after generating the target folder: + + +mvn exec:java -Dexec.mainClass=com.iluwatar.publishersubscriber.TBorrower -Dexec.args="TopicCF RateTopic 6" + +```java +public class TBorrower implements MessageListener { + + private TopicConnection tConnection; + private TopicSession tSession; + private Topic topic; + private double currentRate; + + public TBorrower(String topicCFName, String topicName, double initialRate) { + + currentRate = initialRate; + + try { + //create context and retrieve objects from directory + Context context = new InitialContext(); + TopicConnectionFactory topicCF = (TopicConnectionFactory) context.lookup(topicCFName); + tConnection = topicCF.createTopicConnection(); + + //create connection + tSession = tConnection.createTopicSession(false, Session.AUTO_ACKNOWLEDGE); + + //Retrieve request and response queues + topic = (Topic) context.lookup(topicName); + + //Create subscriber and message listener + TopicSubscriber subscriber = tSession.createSubscriber(topic); + //Adds event listener to subscriber and uses onMessage as a callback + subscriber.setMessageListener(this); + + tConnection.start(); + System.out.println("Initial rate is " + currentRate + " \nWaiting for new rates..."); + } catch(NamingException e) { + e.printStackTrace(); + LOGGER.error("An error has occurred!", e); + System.exit(1); + } catch(JMSException e) { + e.printStackTrace(); + LOGGER.error("An error has occurred!", e); + System.exit(1); + } + } + + //This method is called asynchronously by the activeMQ broker + public void onMessage(Message message) { + + try { + BytesMessage bMessage = (BytesMessage) message; + double newRate = ((BytesMessage) message).readDouble(); + + if (currentRate - newRate >= 1) + System.out.println("New Rate is " + newRate + " - Consider refinancing"); + else + System.out.println("New Rate is " + newRate + " - Consider keeping current rate"); + } catch(JMSException e) { + e.printStackTrace(); + LOGGER.error("An error occurred!", e); + System.exit(1); + } + System.out.println("Waiting for new rates..."); + } + + private void exit() { + try { + tConnection.close(); + } catch(JMSException e) { + e.printStackTrace(); + LOGGER.error("An error has occurred!", e); + System.exit(1); + } + System.exit(0); + } + + public static void main(String[] args) { + + String topicCF = null; + String topicName = null; + int rate = 0; + if (args.length == 3) { + topicCF = args[0]; + topicName = args[1]; + rate = Integer.parseInt(args[2]); + } else { + System.out.println("Invalid arguments. Should be: "); + System.out.println("java TBorrower [factory] [topic] [rate]"); + System.exit(0); + } + + TBorrower tBorrower = new TBorrower(topicCF, topicName, rate); + + try { + // Run until enter is pressed + BufferedReader reader = new BufferedReader + (new InputStreamReader(System.in)); + System.out.println ("TBorrower application started"); + System.out.println ("Press enter to quit application"); + reader.readLine(); + tBorrower.exit(); + } catch (IOException ioe) { + ioe.printStackTrace(); + } + } + + public TopicConnection gettConnection() { + return tConnection; + } + + public TopicSession gettSession() { + return tSession; + } + + public Topic getTopic() { + return topic; + } + + public double getCurrentRate() { + return currentRate; + } +} +``` + +## Class diagram + +## Applicability + +Use the Publish-Subscribe pattern when + +* An application needs to broadcast information to a significant number of consumers. +* An application needs to communicate with one or more independently developed applications or services, which may use different platforms, programming languages, and communication protocols. +* An application needs to communicate information to multiple consumers, which may have different availability requirements or uptime schedules than the sender. + +## Tutorials + +* [Enterprise Integration Patterns](https://www.enterpriseintegrationpatterns.com/patterns/messaging/ObserverJmsExample.html) + +## Consequences + +Pros + +* Pub-Sub activity is asynchronous (a.k.a, “fire and forget”). Hence, there is little risk of performance degradation due to a process getting caught in a long-running data exchange interaction. +* Pub-sub promotes loose coupling between publishers and subscribers. Publishers and subscribers don't need to know each other's details, allowing for greater flexibility in system design and easier component replacement or updates. +* Subscribers can dynamically subscribe and unsubscribe to topics based on their interests, allowing for dynamic adaptation to changing requirements or system conditions. +* Pub-sub is well-suited for building event-driven architectures, where components react to events by subscribing to relevant topics. This enables real-time processing, event propagation, and system-wide coordination. + +Cons + +* Messaging systems typically don't guarantee strict message ordering across subscribers. While messages within a single topic are usually delivered in order, there's no guarantee of global message ordering across topics or subscribers. +* Messaging introduces some latency compared to direct point-to-point communication, as messages need to be routed through the pub-sub system to reach subscribers. While typically minimal, this latency may be a consideration for latency-sensitive applications. +* messages may be lost if subscribers are not actively consuming messages or if there are network or system failures. + +## Real-world examples + +* Market Data Feeds +* Patient Monitoring in Healthcare +* Supply Chain Visibility in Logistics +* Communication between components in a distributed computer system + +## Credits + +* [Java Message Service, 2nd Edition](Author(s): Mark Richards, Richard Monson-Haefel, David A Chappell. Publisher(s): O'Reilly Media, Inc.) +* [Red Hat](https://www.redhat.com/architect/pub-sub-pros-and-cons) +* [Microsoft](https://learn.microsoft.com/en-us/azure/architecture/patterns/publisher-subscriber) \ No newline at end of file diff --git a/publisher-subscriber/pom.xml b/publisher-subscriber/pom.xml index a4dcbb6458e5..42b58399d89b 100644 --- a/publisher-subscriber/pom.xml +++ b/publisher-subscriber/pom.xml @@ -53,6 +53,22 @@ activemq-core 5.7.0 + + org.apache.xbean + xbean-spring + 4.24 + + + log4j + log4j + 1.2.17 + + + org.apache.activemq.tooling + activemq-junit + 6.1.1 + test + \ No newline at end of file diff --git a/publisher-subscriber/src/main/java/com/iluwatar/publishersubscriber b/publisher-subscriber/src/main/java/com/iluwatar/publishersubscriber deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/publisher-subscriber/src/main/java/com/iluwatar/publishersubscriber/TBorrower.java b/publisher-subscriber/src/main/java/com/iluwatar/publishersubscriber/TBorrower.java new file mode 100644 index 000000000000..1489457e7ad8 --- /dev/null +++ b/publisher-subscriber/src/main/java/com/iluwatar/publishersubscriber/TBorrower.java @@ -0,0 +1,138 @@ +package com.iluwatar.publishersubscriber; + +import lombok.extern.slf4j.Slf4j; +import javax.jms.BytesMessage; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageListener; +import javax.jms.Session; +import javax.jms.Topic; +import javax.jms.TopicConnection; +import javax.jms.TopicConnectionFactory; +import javax.jms.TopicSession; +import javax.jms.TopicSubscriber; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +@Slf4j +public class TBorrower implements MessageListener { + + private TopicConnection tConnection; + private TopicSession tSession; + private Topic topic; + private double currentRate; + + public TBorrower(String topicCFName, String topicName, double initialRate) { + + currentRate = initialRate; + + try { + //create context and retrieve objects from directory + Context context = new InitialContext(); + TopicConnectionFactory topicCF = (TopicConnectionFactory) context.lookup(topicCFName); + tConnection = topicCF.createTopicConnection(); + + //create connection + tSession = tConnection.createTopicSession(false, Session.AUTO_ACKNOWLEDGE); + + //Retrieve request and response queues + topic = (Topic) context.lookup(topicName); + + //Create subscriber and message listener + TopicSubscriber subscriber = tSession.createSubscriber(topic); + //Adds event listener to subscriber and uses onMessage as a callback + subscriber.setMessageListener(this); + + tConnection.start(); + System.out.println("Initial rate is " + currentRate + " \nWaiting for new rates..."); + } catch(NamingException e) { + e.printStackTrace(); + LOGGER.error("An error has occurred!", e); + System.exit(1); + } catch(JMSException e) { + e.printStackTrace(); + LOGGER.error("An error has occurred!", e); + System.exit(1); + } + } + + //This method is called asynchronously by the activeMQ broker + public void onMessage(Message message) { + + try { + BytesMessage bMessage = (BytesMessage) message; + double newRate = ((BytesMessage) message).readDouble(); + + if (currentRate - newRate >= 1) + System.out.println("New Rate is " + newRate + " - Consider refinancing"); + else + System.out.println("New Rate is " + newRate + " - Consider keeping current rate"); + } catch(JMSException e) { + e.printStackTrace(); + LOGGER.error("An error occurred!", e); + System.exit(1); + } + System.out.println("Waiting for new rates..."); + } + + private void exit() { + try { + tConnection.close(); + } catch(JMSException e) { + e.printStackTrace(); + LOGGER.error("An error has occurred!", e); + System.exit(1); + } + System.exit(0); + } + + public static void main(String[] args) { + + String topicCF = null; + String topicName = null; + int rate = 0; + if (args.length == 3) { + topicCF = args[0]; + topicName = args[1]; + rate = Integer.parseInt(args[2]); + } else { + System.out.println("Invalid arguments. Should be: "); + System.out.println("java TBorrower [factory] [topic] [rate]"); + System.exit(0); + } + + TBorrower tBorrower = new TBorrower(topicCF, topicName, rate); + + try { + // Run until enter is pressed + BufferedReader reader = new BufferedReader + (new InputStreamReader(System.in)); + System.out.println ("TBorrower application started"); + System.out.println ("Press enter to quit application"); + reader.readLine(); + tBorrower.exit(); + } catch (IOException ioe) { + ioe.printStackTrace(); + } + } + + public TopicConnection gettConnection() { + return tConnection; + } + + public TopicSession gettSession() { + return tSession; + } + + public Topic getTopic() { + return topic; + } + + public double getCurrentRate() { + return currentRate; + } +} diff --git a/publisher-subscriber/src/main/java/com/iluwatar/publishersubscriber/TLender.java b/publisher-subscriber/src/main/java/com/iluwatar/publishersubscriber/TLender.java new file mode 100644 index 000000000000..3536d69fdffa --- /dev/null +++ b/publisher-subscriber/src/main/java/com/iluwatar/publishersubscriber/TLender.java @@ -0,0 +1,150 @@ +package com.iluwatar.publishersubscriber; + +import lombok.extern.slf4j.Slf4j; +import org.apache.activemq.broker.BrokerService; +import javax.jms.BytesMessage; +import javax.jms.JMSException; +import javax.jms.Session; +import javax.jms.Topic; +import javax.jms.TopicConnection; +import javax.jms.TopicConnectionFactory; +import javax.jms.TopicPublisher; +import javax.jms.TopicSession; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +@Slf4j +public class TLender { + + private TopicConnection tConnection; + private TopicSession tSession; + private Topic topic; + private TopicPublisher publisher; + + public TLender(String topicCFName, String topicName) { + + try { + //create context and retrieve objects from directory + Context context = new InitialContext(); + TopicConnectionFactory topicCF = (TopicConnectionFactory) context.lookup(topicCFName); + tConnection = topicCF.createTopicConnection(); + + //create connection + tSession = tConnection.createTopicSession(false, Session.AUTO_ACKNOWLEDGE); + + //Retrieve request and response queues + topic = (Topic) context.lookup(topicName); + + //Create publisher + publisher = tSession.createPublisher(topic); + + tConnection.start(); + } catch(NamingException e) { + e.printStackTrace(); + LOGGER.error("An error has occurred!", e); + System.exit(1); + } catch(JMSException e) { + e.printStackTrace(); + LOGGER.error("An error has occurred!", e); + System.exit(1); + } + } + + private void publishRate(double newRate) { + + try { + //create JMS message + BytesMessage message = tSession.createBytesMessage(); + message.writeDouble(newRate); + + //publish message + publisher.publish(message); + } catch(JMSException e) { + e.printStackTrace(); + LOGGER.error("An error has occurred!", e); + System.exit(1); + } + } + + private void exit() { + try { + tConnection.close(); + } catch(JMSException e) { + e.printStackTrace(); + LOGGER.error("An error has occurred!", e); + System.exit(1); + } + System.exit(0); + } + + public static void main(String[] args) { + + String topicCFName = null; + String topicName = null; + + if(args.length == 2) { + topicCFName = args[0]; + topicName = args[1]; + } else { + System.out.println("Invalid arguments. Should be: "); + System.out.println("java TLender [factory] [topic]"); + System.exit(1); + } + + try { + // Create and start activeMQ broker. Broker decouples publishers and subscribers. + //Additionally brokers manage threads and asynchronous sending and receiving of messages. + BrokerService broker = new BrokerService(); + broker.addConnector("tcp://localhost:61616"); + broker.start(); + + } catch(Exception e) { + e.printStackTrace(); + LOGGER.error("An error has occurred!", e); + } + + TLender tLender = new TLender(topicCFName, topicName); + + System.out.println ("TLender Application Started"); + System.out.println ("Press enter to quit application"); + System.out.println ("Enter: Rate"); + System.out.println("\ne.g. 6.8"); + + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); + //Continuously read input and send as message to subscribers + while(true) { + System.out.print("> "); + String line = reader.readLine(); + //Exit if user pressed enter or line is blank + if (line == null || line.trim().length() == 0) { + System.out.println("Exiting..."); + tLender.exit(); + } + else { //publish the entered rate + double newRate = Double.parseDouble(line); + tLender.publishRate(newRate); + } + } + } catch(IOException e) { + e.printStackTrace(); + LOGGER.error("An error has occurred!", e); + } + } + + public TopicConnection gettConnection() { + return tConnection; + } + + public TopicSession gettSession() { + return tSession; + } + + public Topic getTopic() { + return topic; + } +} diff --git a/publisher-subscriber/src/main/resources/jndi.properties b/publisher-subscriber/src/main/resources/jndi.properties new file mode 100644 index 000000000000..1e8303200c59 --- /dev/null +++ b/publisher-subscriber/src/main/resources/jndi.properties @@ -0,0 +1,6 @@ +java.naming.factory.initial = org.apache.activemq.jndi.ActiveMQInitialContextFactory +java.naming.provider.url = tcp://localhost:61616 +java.naming.security.principal=system +java.naming.security.credentials=manager +connectionFactoryNames = TopicCF +topic.RateTopic = jms.RateTopic \ No newline at end of file diff --git a/publisher-subscriber/src/test/java/com/iluwatar/publishersubscriber/TBorrowerTest.java b/publisher-subscriber/src/test/java/com/iluwatar/publishersubscriber/TBorrowerTest.java new file mode 100644 index 000000000000..b59e421d5b90 --- /dev/null +++ b/publisher-subscriber/src/test/java/com/iluwatar/publishersubscriber/TBorrowerTest.java @@ -0,0 +1,27 @@ +package com.iluwatar.publishersubscriber; + +import org.apache.activemq.broker.BrokerService; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class TBorrowerTest { + + private TBorrower tBorrower; + + void setUp() throws Exception { + BrokerService broker = new BrokerService(); + broker.setPersistent(false); + broker.start(); + } + + @Test + void testTBorrowerConstructor() { + //act + tBorrower = new TBorrower("TopicCF", "RateTopic", 6.0); //Arbitrary rate + + //assert + Assertions.assertNotNull(tBorrower.gettConnection()); + Assertions.assertNotNull(tBorrower.getTopic()); + Assertions.assertNotNull(tBorrower.gettSession()); + } +} diff --git a/publisher-subscriber/src/test/java/com/iluwatar/publishersubscriber/TLenderTest.java b/publisher-subscriber/src/test/java/com/iluwatar/publishersubscriber/TLenderTest.java new file mode 100644 index 000000000000..9ec82e02109f --- /dev/null +++ b/publisher-subscriber/src/test/java/com/iluwatar/publishersubscriber/TLenderTest.java @@ -0,0 +1,33 @@ +package com.iluwatar.publishersubscriber; + +import org.apache.activemq.broker.BrokerService; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import javax.jms.Connection; +import javax.jms.JMSException; +import javax.jms.Session; +import javax.jms.Topic; +import javax.jms.TopicConnectionFactory; +import javax.jms.TopicPublisher; + +public class TLenderTest { + + private TLender tLender; + + void setUp() throws Exception { + BrokerService broker = new BrokerService(); + broker.setPersistent(false); + broker.start(); + } + + @Test + void testTLenderConstructor() { + //act + tLender = new TLender("TopicCF", "RateTopic"); + + //assert + Assertions.assertNotNull(tLender.gettConnection()); + Assertions.assertNotNull(tLender.getTopic()); + Assertions.assertNotNull(tLender.gettSession()); + } +} diff --git a/publisher-subscriber/src/test/resources/jndi.properties b/publisher-subscriber/src/test/resources/jndi.properties new file mode 100644 index 000000000000..468c3a4b1a59 --- /dev/null +++ b/publisher-subscriber/src/test/resources/jndi.properties @@ -0,0 +1,6 @@ +java.naming.factory.initial = org.apache.activemq.jndi.ActiveMQInitialContextFactory +java.naming.provider.url = vm://localhost?broker.persistent=false +java.naming.security.principal=system +java.naming.security.credentials=manager +connectionFactoryNames = TopicCF +topic.RateTopic = jms.RateTopic \ No newline at end of file From f4ebf9c24d13deee3399faccab04456f61a60c41 Mon Sep 17 00:00:00 2001 From: Riley Date: Wed, 17 Apr 2024 14:32:00 -0400 Subject: [PATCH 5/8] #2898 Fixed onMessage method --- .../main/java/com/iluwatar/publishersubscriber/TBorrower.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/publisher-subscriber/src/main/java/com/iluwatar/publishersubscriber/TBorrower.java b/publisher-subscriber/src/main/java/com/iluwatar/publishersubscriber/TBorrower.java index 1489457e7ad8..1d489bdd00e0 100644 --- a/publisher-subscriber/src/main/java/com/iluwatar/publishersubscriber/TBorrower.java +++ b/publisher-subscriber/src/main/java/com/iluwatar/publishersubscriber/TBorrower.java @@ -65,7 +65,7 @@ public void onMessage(Message message) { try { BytesMessage bMessage = (BytesMessage) message; - double newRate = ((BytesMessage) message).readDouble(); + double newRate = ((BytesMessage) bMessage).readDouble(); if (currentRate - newRate >= 1) System.out.println("New Rate is " + newRate + " - Consider refinancing"); From 5e9689e407ea45aea5138f7f759629332f5e9059 Mon Sep 17 00:00:00 2001 From: Riley Date: Fri, 19 Apr 2024 19:30:46 -0400 Subject: [PATCH 6/8] #2898 Added more unit tests and refactored code --- .../README.md | 22 ++---- publish-subscribe/USAGE.md | 8 ++ .../pom.xml | 7 +- .../iluwatar/publishsubscribe/Borrower.java | 46 +++++------ .../com/iluwatar/publishsubscribe/Lender.java | 67 +++++++++------- .../src/main/resources/config.properties | 1 + .../src/main/resources/jndi.properties | 0 .../publishsubscribe/BorrowerTest.java | 78 +++++++++++++++++++ .../iluwatar/publishsubscribe/LenderTest.java | 50 ++++++++++++ .../src/test/resources/jndi.properties | 0 .../publishersubscriber/TBorrowerTest.java | 27 ------- .../publishersubscriber/TLenderTest.java | 33 -------- 12 files changed, 205 insertions(+), 134 deletions(-) rename {publisher-subscriber => publish-subscribe}/README.md (94%) create mode 100644 publish-subscribe/USAGE.md rename {publisher-subscriber => publish-subscribe}/pom.xml (92%) rename publisher-subscriber/src/main/java/com/iluwatar/publishersubscriber/TBorrower.java => publish-subscribe/src/main/java/com/iluwatar/publishsubscribe/Borrower.java (79%) rename publisher-subscriber/src/main/java/com/iluwatar/publishersubscriber/TLender.java => publish-subscribe/src/main/java/com/iluwatar/publishsubscribe/Lender.java (67%) create mode 100644 publish-subscribe/src/main/resources/config.properties rename {publisher-subscriber => publish-subscribe}/src/main/resources/jndi.properties (100%) create mode 100644 publish-subscribe/src/test/java/com/iluwatar/publishsubscribe/BorrowerTest.java create mode 100644 publish-subscribe/src/test/java/com/iluwatar/publishsubscribe/LenderTest.java rename {publisher-subscriber => publish-subscribe}/src/test/resources/jndi.properties (100%) delete mode 100644 publisher-subscriber/src/test/java/com/iluwatar/publishersubscriber/TBorrowerTest.java delete mode 100644 publisher-subscriber/src/test/java/com/iluwatar/publishersubscriber/TLenderTest.java diff --git a/publisher-subscriber/README.md b/publish-subscribe/README.md similarity index 94% rename from publisher-subscriber/README.md rename to publish-subscribe/README.md index fe7a110fca89..5e4d5c9ffb41 100644 --- a/publisher-subscriber/README.md +++ b/publish-subscribe/README.md @@ -1,5 +1,5 @@ --- -title: Publish-Subscribe pattern +title: Publish-Subscribe category: Behavioral language: en tag: @@ -135,7 +135,7 @@ public class TLender { LOGGER.error("An error has occurred!", e); } - TLender tLender = new TLender(topicCFName, topicName); + TLender lender = new TLender(topicCFName, topicName); System.out.println ("TLender Application Started"); System.out.println ("Press enter to quit application"); @@ -151,11 +151,11 @@ public class TLender { //Exit if user pressed enter or line is blank if (line == null || line.trim().length() == 0) { System.out.println("Exiting..."); - tLender.exit(); + lender.exit(); } else { //publish the entered rate double newRate = Double.parseDouble(line); - tLender.publishRate(newRate); + lender.publishRate(newRate); } } } catch(IOException e) { @@ -186,15 +186,6 @@ Initial rate is 6.0 Waiting for new rates... Press enter to quit application -Running the class: - -The class must be run after the TLender class is running since TLender spins up the activeMQ broker. - -In order to see the messages being sent to multiple subscribers multiple instance of the TBorrower class need to be run. Either run multiple instances in an IDE or execute the following command in a command line from the root folder after generating the target folder: - - -mvn exec:java -Dexec.mainClass=com.iluwatar.publishersubscriber.TBorrower -Dexec.args="TopicCF RateTopic 6" - ```java public class TBorrower implements MessageListener { @@ -282,7 +273,7 @@ public class TBorrower implements MessageListener { System.exit(0); } - TBorrower tBorrower = new TBorrower(topicCF, topicName, rate); + TBorrower borrower = new TBorrower(topicCF, topicName, rate); try { // Run until enter is pressed @@ -291,7 +282,7 @@ public class TBorrower implements MessageListener { System.out.println ("TBorrower application started"); System.out.println ("Press enter to quit application"); reader.readLine(); - tBorrower.exit(); + borrower.exit(); } catch (IOException ioe) { ioe.printStackTrace(); } @@ -316,6 +307,7 @@ public class TBorrower implements MessageListener { ``` ## Class diagram +![alt text](./etc/publishsubscribe.urm.png "Publish Subscribe class diagram") ## Applicability diff --git a/publish-subscribe/USAGE.md b/publish-subscribe/USAGE.md new file mode 100644 index 000000000000..92f82e688b25 --- /dev/null +++ b/publish-subscribe/USAGE.md @@ -0,0 +1,8 @@ +Running the Borrower class: + +The class must be run after the TLender class is running since TLender spins up the activeMQ broker. + +In order to see the messages being sent to multiple subscribers multiple instance of the TBorrower class need to be run. Either run multiple instances in an IDE or execute the following command in a command line from the root folder after generating the target folder: + + +mvn exec:java -Dexec.mainClass=com.iluwatar.publishsubscribe.Borrower -Dexec.args="TopicCF RateTopic 6" \ No newline at end of file diff --git a/publisher-subscriber/pom.xml b/publish-subscribe/pom.xml similarity index 92% rename from publisher-subscriber/pom.xml rename to publish-subscribe/pom.xml index 42b58399d89b..8b4d2806cd90 100644 --- a/publisher-subscriber/pom.xml +++ b/publish-subscribe/pom.xml @@ -35,7 +35,7 @@ java-design-patterns 1.26.0-SNAPSHOT - publishersubscriber + publishsubscribe @@ -58,11 +58,6 @@ xbean-spring 4.24 - - log4j - log4j - 1.2.17 - org.apache.activemq.tooling activemq-junit diff --git a/publisher-subscriber/src/main/java/com/iluwatar/publishersubscriber/TBorrower.java b/publish-subscribe/src/main/java/com/iluwatar/publishsubscribe/Borrower.java similarity index 79% rename from publisher-subscriber/src/main/java/com/iluwatar/publishersubscriber/TBorrower.java rename to publish-subscribe/src/main/java/com/iluwatar/publishsubscribe/Borrower.java index 1d489bdd00e0..1198fc5eeef3 100644 --- a/publisher-subscriber/src/main/java/com/iluwatar/publishersubscriber/TBorrower.java +++ b/publish-subscribe/src/main/java/com/iluwatar/publishsubscribe/Borrower.java @@ -1,4 +1,4 @@ -package com.iluwatar.publishersubscriber; +package com.iluwatar.publishsubscribe; import lombok.extern.slf4j.Slf4j; import javax.jms.BytesMessage; @@ -19,14 +19,16 @@ import java.io.InputStreamReader; @Slf4j -public class TBorrower implements MessageListener { +public class Borrower implements MessageListener { private TopicConnection tConnection; private TopicSession tSession; private Topic topic; private double currentRate; + private static final String ERROR = "An error has occured!"; + private double newRate; - public TBorrower(String topicCFName, String topicName, double initialRate) { + public Borrower(String topicCFName, String topicName, double initialRate) { currentRate = initialRate; @@ -50,44 +52,37 @@ public TBorrower(String topicCFName, String topicName, double initialRate) { tConnection.start(); System.out.println("Initial rate is " + currentRate + " \nWaiting for new rates..."); } catch(NamingException e) { - e.printStackTrace(); - LOGGER.error("An error has occurred!", e); - System.exit(1); + LOGGER.error(ERROR, e); } catch(JMSException e) { - e.printStackTrace(); - LOGGER.error("An error has occurred!", e); - System.exit(1); + LOGGER.error(ERROR, e); } } //This method is called asynchronously by the activeMQ broker public void onMessage(Message message) { - try { BytesMessage bMessage = (BytesMessage) message; - double newRate = ((BytesMessage) bMessage).readDouble(); + double newRate = bMessage.readDouble(); + setNewRate(newRate); if (currentRate - newRate >= 1) System.out.println("New Rate is " + newRate + " - Consider refinancing"); else System.out.println("New Rate is " + newRate + " - Consider keeping current rate"); } catch(JMSException e) { - e.printStackTrace(); - LOGGER.error("An error occurred!", e); - System.exit(1); + LOGGER.error(ERROR, e); } System.out.println("Waiting for new rates..."); } - private void exit() { + public boolean close() { try { tConnection.close(); + return true; } catch(JMSException e) { - e.printStackTrace(); - LOGGER.error("An error has occurred!", e); - System.exit(1); + LOGGER.error(ERROR, e); + return false; } - System.exit(0); } public static void main(String[] args) { @@ -105,7 +100,7 @@ public static void main(String[] args) { System.exit(0); } - TBorrower tBorrower = new TBorrower(topicCF, topicName, rate); + Borrower borrower = new Borrower(topicCF, topicName, rate); try { // Run until enter is pressed @@ -114,9 +109,10 @@ public static void main(String[] args) { System.out.println ("TBorrower application started"); System.out.println ("Press enter to quit application"); reader.readLine(); - tBorrower.exit(); - } catch (IOException ioe) { - ioe.printStackTrace(); + borrower.close(); + System.exit(0); + } catch (IOException e) { + LOGGER.error(ERROR, e); } } @@ -135,4 +131,8 @@ public Topic getTopic() { public double getCurrentRate() { return currentRate; } + + public double getNewRate() { return newRate; } + + private void setNewRate(double newRate) { this.newRate = newRate; } } diff --git a/publisher-subscriber/src/main/java/com/iluwatar/publishersubscriber/TLender.java b/publish-subscribe/src/main/java/com/iluwatar/publishsubscribe/Lender.java similarity index 67% rename from publisher-subscriber/src/main/java/com/iluwatar/publishersubscriber/TLender.java rename to publish-subscribe/src/main/java/com/iluwatar/publishsubscribe/Lender.java index 3536d69fdffa..dd504e57f49a 100644 --- a/publisher-subscriber/src/main/java/com/iluwatar/publishersubscriber/TLender.java +++ b/publish-subscribe/src/main/java/com/iluwatar/publishsubscribe/Lender.java @@ -1,4 +1,4 @@ -package com.iluwatar.publishersubscriber; +package com.iluwatar.publishsubscribe; import lombok.extern.slf4j.Slf4j; import org.apache.activemq.broker.BrokerService; @@ -14,18 +14,23 @@ import javax.naming.InitialContext; import javax.naming.NamingException; import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; +import java.util.Properties; @Slf4j -public class TLender { +public class Lender { private TopicConnection tConnection; private TopicSession tSession; private Topic topic; private TopicPublisher publisher; + private static final String ERROR = "An error has occured!"; - public TLender(String topicCFName, String topicName) { + public Lender(String topicCFName, String topicName) { try { //create context and retrieve objects from directory @@ -44,17 +49,13 @@ public TLender(String topicCFName, String topicName) { tConnection.start(); } catch(NamingException e) { - e.printStackTrace(); - LOGGER.error("An error has occurred!", e); - System.exit(1); + LOGGER.error(ERROR, e); } catch(JMSException e) { - e.printStackTrace(); - LOGGER.error("An error has occurred!", e); - System.exit(1); + LOGGER.error(ERROR, e); } } - private void publishRate(double newRate) { + public void publishRate(double newRate) { try { //create JMS message @@ -64,21 +65,18 @@ private void publishRate(double newRate) { //publish message publisher.publish(message); } catch(JMSException e) { - e.printStackTrace(); - LOGGER.error("An error has occurred!", e); - System.exit(1); + LOGGER.error(ERROR, e); } } - private void exit() { + public boolean close() { try { tConnection.close(); + return true; } catch(JMSException e) { - e.printStackTrace(); - LOGGER.error("An error has occurred!", e); - System.exit(1); + LOGGER.error(ERROR, e); + return false; } - System.exit(0); } public static void main(String[] args) { @@ -90,26 +88,35 @@ public static void main(String[] args) { topicCFName = args[0]; topicName = args[1]; } else { - System.out.println("Invalid arguments. Should be: "); - System.out.println("java TLender [factory] [topic]"); + LOGGER.info("Invalid arguments. Should be: "); + LOGGER.info("java TLender [factory] [topic]"); System.exit(1); } try { + //Get configuration properties + Properties props = new Properties(); + InputStream in = new FileInputStream("publish-subscribe/src/main/resources/config.properties"); + props.load(in); + in.close(); + // Create and start activeMQ broker. Broker decouples publishers and subscribers. - //Additionally brokers manage threads and asynchronous sending and receiving of messages. + // Additionally brokers manage threads and asynchronous sending and receiving of messages. BrokerService broker = new BrokerService(); - broker.addConnector("tcp://localhost:61616"); + broker.addConnector(props.getProperty("ADDRESS")); broker.start(); + } catch(FileNotFoundException e) { + LOGGER.error(ERROR, e); + } catch(IOException e) { + LOGGER.error(ERROR, e); } catch(Exception e) { - e.printStackTrace(); - LOGGER.error("An error has occurred!", e); + LOGGER.error(ERROR, e); } - TLender tLender = new TLender(topicCFName, topicName); + Lender lender = new Lender(topicCFName, topicName); - System.out.println ("TLender Application Started"); + LOGGER.info("TLender Application Started"); System.out.println ("Press enter to quit application"); System.out.println ("Enter: Rate"); System.out.println("\ne.g. 6.8"); @@ -123,16 +130,16 @@ public static void main(String[] args) { //Exit if user pressed enter or line is blank if (line == null || line.trim().length() == 0) { System.out.println("Exiting..."); - tLender.exit(); + lender.close(); + System.exit(0); } else { //publish the entered rate double newRate = Double.parseDouble(line); - tLender.publishRate(newRate); + lender.publishRate(newRate); } } } catch(IOException e) { - e.printStackTrace(); - LOGGER.error("An error has occurred!", e); + LOGGER.error(ERROR, e); } } diff --git a/publish-subscribe/src/main/resources/config.properties b/publish-subscribe/src/main/resources/config.properties new file mode 100644 index 000000000000..c90ed24de061 --- /dev/null +++ b/publish-subscribe/src/main/resources/config.properties @@ -0,0 +1 @@ +ADDRESS=tcp://localhost:61616 \ No newline at end of file diff --git a/publisher-subscriber/src/main/resources/jndi.properties b/publish-subscribe/src/main/resources/jndi.properties similarity index 100% rename from publisher-subscriber/src/main/resources/jndi.properties rename to publish-subscribe/src/main/resources/jndi.properties diff --git a/publish-subscribe/src/test/java/com/iluwatar/publishsubscribe/BorrowerTest.java b/publish-subscribe/src/test/java/com/iluwatar/publishsubscribe/BorrowerTest.java new file mode 100644 index 000000000000..ded88e244087 --- /dev/null +++ b/publish-subscribe/src/test/java/com/iluwatar/publishsubscribe/BorrowerTest.java @@ -0,0 +1,78 @@ +package com.iluwatar.publishsubscribe; + +import org.apache.activemq.Message; +import org.apache.activemq.broker.Broker; +import org.apache.activemq.broker.BrokerService; +import org.apache.activemq.command.ActiveMQTextMessage; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import javax.jms.BytesMessage; +import javax.jms.JMSException; +import javax.jms.TextMessage; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.sql.SQLOutput; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class BorrowerTest { + + private Borrower borrower; + private static BrokerService broker; + + @BeforeAll + static void setUp() throws Exception { + broker = new BrokerService(); + broker.setPersistent(false); + broker.start(); + } + + @AfterAll + static void tearDown() throws Exception { + broker.stop(); + } + + @Test + void testTBorrowerConstructor() { + //act + borrower = new Borrower("TopicCF", "RateTopic", 6.0); //Arbitrary rate + + //assert + Assertions.assertNotNull(borrower.gettConnection()); + Assertions.assertNotNull(borrower.getTopic()); + Assertions.assertNotNull(borrower.gettSession()); + } + + @Test + void testOnMessage() throws JMSException { + //assemble + borrower = new Borrower("TopicCF", "RateTopic", 6.0); + BytesMessage msg = borrower.gettSession().createBytesMessage(); + msg.writeDouble(5); + msg.reset(); + + //act + borrower.onMessage(msg); + + //assert + Assertions.assertEquals(borrower.getNewRate(), 5); + } + + @Test + void testClose() { + //assemble + borrower = new Borrower("TopicCF", "RateTopic", 6.0); + + //act + boolean isClosed = borrower.close(); + + //assert + Assertions.assertTrue(isClosed); + } +} diff --git a/publish-subscribe/src/test/java/com/iluwatar/publishsubscribe/LenderTest.java b/publish-subscribe/src/test/java/com/iluwatar/publishsubscribe/LenderTest.java new file mode 100644 index 000000000000..a103b0d9850f --- /dev/null +++ b/publish-subscribe/src/test/java/com/iluwatar/publishsubscribe/LenderTest.java @@ -0,0 +1,50 @@ +package com.iluwatar.publishsubscribe; + +import org.apache.activemq.broker.BrokerService; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class LenderTest { + + private Lender lender; + private static BrokerService broker; + + @BeforeAll + static void setUp() throws Exception { + broker = new BrokerService(); + broker.setPersistent(false); + broker.start(); + } + + @AfterAll + static void tearDown() throws Exception { + broker.stop(); + } + + @Test + void testTLenderConstructor() { + //act + lender = new Lender("TopicCF", "RateTopic"); + + //assert + Assertions.assertNotNull(lender.gettConnection()); + Assertions.assertNotNull(lender.getTopic()); + Assertions.assertNotNull(lender.gettSession()); + } + + @Test + void textExit() { + //assemble + lender = new Lender("TopicCF", "RateTopic"); + + //act + boolean isClosed = lender.close(); + + //assert + Assertions.assertTrue(isClosed); + } +} diff --git a/publisher-subscriber/src/test/resources/jndi.properties b/publish-subscribe/src/test/resources/jndi.properties similarity index 100% rename from publisher-subscriber/src/test/resources/jndi.properties rename to publish-subscribe/src/test/resources/jndi.properties diff --git a/publisher-subscriber/src/test/java/com/iluwatar/publishersubscriber/TBorrowerTest.java b/publisher-subscriber/src/test/java/com/iluwatar/publishersubscriber/TBorrowerTest.java deleted file mode 100644 index b59e421d5b90..000000000000 --- a/publisher-subscriber/src/test/java/com/iluwatar/publishersubscriber/TBorrowerTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.iluwatar.publishersubscriber; - -import org.apache.activemq.broker.BrokerService; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class TBorrowerTest { - - private TBorrower tBorrower; - - void setUp() throws Exception { - BrokerService broker = new BrokerService(); - broker.setPersistent(false); - broker.start(); - } - - @Test - void testTBorrowerConstructor() { - //act - tBorrower = new TBorrower("TopicCF", "RateTopic", 6.0); //Arbitrary rate - - //assert - Assertions.assertNotNull(tBorrower.gettConnection()); - Assertions.assertNotNull(tBorrower.getTopic()); - Assertions.assertNotNull(tBorrower.gettSession()); - } -} diff --git a/publisher-subscriber/src/test/java/com/iluwatar/publishersubscriber/TLenderTest.java b/publisher-subscriber/src/test/java/com/iluwatar/publishersubscriber/TLenderTest.java deleted file mode 100644 index 9ec82e02109f..000000000000 --- a/publisher-subscriber/src/test/java/com/iluwatar/publishersubscriber/TLenderTest.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.iluwatar.publishersubscriber; - -import org.apache.activemq.broker.BrokerService; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import javax.jms.Connection; -import javax.jms.JMSException; -import javax.jms.Session; -import javax.jms.Topic; -import javax.jms.TopicConnectionFactory; -import javax.jms.TopicPublisher; - -public class TLenderTest { - - private TLender tLender; - - void setUp() throws Exception { - BrokerService broker = new BrokerService(); - broker.setPersistent(false); - broker.start(); - } - - @Test - void testTLenderConstructor() { - //act - tLender = new TLender("TopicCF", "RateTopic"); - - //assert - Assertions.assertNotNull(tLender.gettConnection()); - Assertions.assertNotNull(tLender.getTopic()); - Assertions.assertNotNull(tLender.gettSession()); - } -} From 43d2f79cd9ce34dfa262e01fb4f52fc71f34ffa2 Mon Sep 17 00:00:00 2001 From: Riley Date: Mon, 22 Apr 2024 15:55:20 -0400 Subject: [PATCH 7/8] #2898 Reduced sout lines --- .../com/iluwatar/publishsubscribe/Borrower.java | 15 ++++++++------- .../com/iluwatar/publishsubscribe/Lender.java | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/publish-subscribe/src/main/java/com/iluwatar/publishsubscribe/Borrower.java b/publish-subscribe/src/main/java/com/iluwatar/publishsubscribe/Borrower.java index 1198fc5eeef3..a206a636af98 100644 --- a/publish-subscribe/src/main/java/com/iluwatar/publishsubscribe/Borrower.java +++ b/publish-subscribe/src/main/java/com/iluwatar/publishsubscribe/Borrower.java @@ -1,6 +1,7 @@ package com.iluwatar.publishsubscribe; import lombok.extern.slf4j.Slf4j; +import org.slf4j.event.Level; import javax.jms.BytesMessage; import javax.jms.JMSException; import javax.jms.Message; @@ -50,7 +51,7 @@ public Borrower(String topicCFName, String topicName, double initialRate) { subscriber.setMessageListener(this); tConnection.start(); - System.out.println("Initial rate is " + currentRate + " \nWaiting for new rates..."); + LOGGER.info("Initial rate is " + currentRate + " \nWaiting for new rates..."); } catch(NamingException e) { LOGGER.error(ERROR, e); } catch(JMSException e) { @@ -72,7 +73,7 @@ public void onMessage(Message message) { } catch(JMSException e) { LOGGER.error(ERROR, e); } - System.out.println("Waiting for new rates..."); + LOGGER.info("Waiting for new rates..."); } public boolean close() { @@ -95,9 +96,9 @@ public static void main(String[] args) { topicName = args[1]; rate = Integer.parseInt(args[2]); } else { - System.out.println("Invalid arguments. Should be: "); - System.out.println("java TBorrower [factory] [topic] [rate]"); - System.exit(0); + LOGGER.info("Invalid arguments. Should be: "); + LOGGER.info("java TBorrower [factory] [topic] [rate]"); + System.exit(1); } Borrower borrower = new Borrower(topicCF, topicName, rate); @@ -106,8 +107,8 @@ public static void main(String[] args) { // Run until enter is pressed BufferedReader reader = new BufferedReader (new InputStreamReader(System.in)); - System.out.println ("TBorrower application started"); - System.out.println ("Press enter to quit application"); + LOGGER.info("TBorrower application started"); + LOGGER.info("Press enter to quit application"); reader.readLine(); borrower.close(); System.exit(0); diff --git a/publish-subscribe/src/main/java/com/iluwatar/publishsubscribe/Lender.java b/publish-subscribe/src/main/java/com/iluwatar/publishsubscribe/Lender.java index dd504e57f49a..73d4200fb115 100644 --- a/publish-subscribe/src/main/java/com/iluwatar/publishsubscribe/Lender.java +++ b/publish-subscribe/src/main/java/com/iluwatar/publishsubscribe/Lender.java @@ -129,7 +129,7 @@ public static void main(String[] args) { String line = reader.readLine(); //Exit if user pressed enter or line is blank if (line == null || line.trim().length() == 0) { - System.out.println("Exiting..."); + LOGGER.info("Exiting..."); lender.close(); System.exit(0); } From b57721aa45057e686046c32c7e7390be6d402ee2 Mon Sep 17 00:00:00 2001 From: Riley Date: Tue, 23 Apr 2024 16:23:38 -0400 Subject: [PATCH 8/8] #2898 modified parent pom --- pom.xml | 1 + publish-subscribe/pom.xml | 18 +----------------- .../iluwatar/publishsubscribe/Borrower.java | 3 +-- .../publishsubscribe/BorrowerTest.java | 13 ------------- .../iluwatar/publishsubscribe/LenderTest.java | 2 -- 5 files changed, 3 insertions(+), 34 deletions(-) diff --git a/pom.xml b/pom.xml index 7dc65f454b75..9f8766d3d119 100644 --- a/pom.xml +++ b/pom.xml @@ -58,6 +58,7 @@ Java Design Patterns + publish-subscribe abstract-factory collecting-parameter monitor diff --git a/publish-subscribe/pom.xml b/publish-subscribe/pom.xml index 8b4d2806cd90..473c9362b661 100644 --- a/publish-subscribe/pom.xml +++ b/publish-subscribe/pom.xml @@ -35,7 +35,7 @@ java-design-patterns 1.26.0-SNAPSHOT - publishsubscribe + publish-subscribe @@ -43,27 +43,11 @@ junit-jupiter-api test - - org.mockito - mockito-core - test - org.apache.activemq activemq-core 5.7.0 - - org.apache.xbean - xbean-spring - 4.24 - - - org.apache.activemq.tooling - activemq-junit - 6.1.1 - test - \ No newline at end of file diff --git a/publish-subscribe/src/main/java/com/iluwatar/publishsubscribe/Borrower.java b/publish-subscribe/src/main/java/com/iluwatar/publishsubscribe/Borrower.java index a206a636af98..f94e4c31d102 100644 --- a/publish-subscribe/src/main/java/com/iluwatar/publishsubscribe/Borrower.java +++ b/publish-subscribe/src/main/java/com/iluwatar/publishsubscribe/Borrower.java @@ -1,7 +1,6 @@ package com.iluwatar.publishsubscribe; import lombok.extern.slf4j.Slf4j; -import org.slf4j.event.Level; import javax.jms.BytesMessage; import javax.jms.JMSException; import javax.jms.Message; @@ -25,7 +24,7 @@ public class Borrower implements MessageListener { private TopicConnection tConnection; private TopicSession tSession; private Topic topic; - private double currentRate; + private final double currentRate; private static final String ERROR = "An error has occured!"; private double newRate; diff --git a/publish-subscribe/src/test/java/com/iluwatar/publishsubscribe/BorrowerTest.java b/publish-subscribe/src/test/java/com/iluwatar/publishsubscribe/BorrowerTest.java index ded88e244087..cb33585a719f 100644 --- a/publish-subscribe/src/test/java/com/iluwatar/publishsubscribe/BorrowerTest.java +++ b/publish-subscribe/src/test/java/com/iluwatar/publishsubscribe/BorrowerTest.java @@ -1,25 +1,12 @@ package com.iluwatar.publishsubscribe; -import org.apache.activemq.Message; -import org.apache.activemq.broker.Broker; import org.apache.activemq.broker.BrokerService; -import org.apache.activemq.command.ActiveMQTextMessage; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import javax.jms.BytesMessage; import javax.jms.JMSException; -import javax.jms.TextMessage; -import java.io.BufferedOutputStream; -import java.io.ByteArrayOutputStream; -import java.io.OutputStream; -import java.io.PrintStream; -import java.sql.SQLOutput; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class BorrowerTest { diff --git a/publish-subscribe/src/test/java/com/iluwatar/publishsubscribe/LenderTest.java b/publish-subscribe/src/test/java/com/iluwatar/publishsubscribe/LenderTest.java index a103b0d9850f..e5420ee2603b 100644 --- a/publish-subscribe/src/test/java/com/iluwatar/publishsubscribe/LenderTest.java +++ b/publish-subscribe/src/test/java/com/iluwatar/publishsubscribe/LenderTest.java @@ -2,10 +2,8 @@ import org.apache.activemq.broker.BrokerService; import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class LenderTest {