DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • WebSocket vs. Server-Sent Events: Choosing the Best Real-Time Communication Protocol
  • Reactive Event Streaming Architecture With Kafka, Redis Streams, Spring Boot, and HTTP Server-Sent Events (SSE)
  • Competing Consumers With Spring Boot and Hazelcast
  • Reactive Kafka With Streaming in Spring Boot

Trending

  • The End of “Good Enough Agile”
  • How Kubernetes Cluster Sizing Affects Performance and Cost Efficiency in Cloud Deployments
  • Implementing API Design First in .NET for Efficient Development, Testing, and CI/CD
  • AI Speaks for the World... But Whose Humanity Does It Learn From?
  1. DZone
  2. Coding
  3. Frameworks
  4. Beyond Linguistics: Real-Time Domain Event Mapping with WebSocket and Spring Boot

Beyond Linguistics: Real-Time Domain Event Mapping with WebSocket and Spring Boot

Build a scalable real-time notification system using Sprint Boot and WebSocket, focusing on domain event mapping, system design, and more.

By 
Soham Sengupta user avatar
Soham Sengupta
·
May. 06, 25 · Tutorial
Likes (2)
Comment
Save
Tweet
Share
1.8K Views

Join the DZone community and get the full member experience.

Join For Free

By definition, a markup language is a system used for annotating a document in a way that is syntactically distinguishable from the text. Essentially, it provides a way to structure and format text using tags or symbols that are embedded within the content. Markup languages are used to define elements within a document, such as headings, paragraphs, lists, links, and images. Hype Text Markup Language (HTML) is the most common of them. There are many other such as XML, SGML, Markdown, MathML, BBCode, to name a few.

This article articulates the need of and presents a minimally working version to what the term “domain markup event mapping” is conferred. Lest an unfamiliar terminology introduced abruptly make the audience assume otherwise, let us illustrate the experience as a real-time commentary of an event, say a cricket match on popular online news media. ESPN, Yahoo cricket, Cricbuzz.com, Star Sports, and BBC are among the top players in this area. I remember how they used to be 15 years ago and and now, they've evolved to cater real-time updates. With advanced backend systems, communication protocols, better design approach and of course, modern browser technologies, they have always been on the top to provide their users the best intuitive updates compensating the absence of audio and video.  

Our audience must have noticed that Google and other websites have already adopted animated and visually intuitive UI components to make the user experience better.

This article focuses on the need to provide a domain specific markup event mapper for all such use cases and illustrates an approach to create a minimalistic update system using Spring boot and WebSocket.

Domain Specific Markup Event Mapper: 


An image showing Domain Specific Markup Event Mapper.


The client (such as the web browser) receives an event. For the sake of generality, neither the communication protocol nor message format (e.g., whether it is a text or binary message) is assumed. The message converter yields an event object that the client understands, such as a JSON object that the browser side script knows how to handle and render. We now must agree that not all notifications and the events they carry, belong to the same category and therefore, be rendered in the same way. 

A running commentary, for example, may be rendered like a scrolling hypertext while a boundary or an over boundary or a fall of a wicket might require special effect as they’re rendered, to stand distinct. The role of the markup look-up engine is to identify a suitable engine given the category of an event. It delegates the event to a specific rendering strategy if one for the category is registered (known) to the system (client-side UI such as a browser). If none is found, there needs to be a fallback strategy. The four components that appear black in the image above are abstractions that the domain landscape must provide as we propose in this article. The rendering techniques for a cricket match must differ from a soccer match and coverage of an electoral poll must be presented in a different way than sports.

We must now wear the developers’ hat and gear up to put the theory into practice. We aim to make the notification system minimalist with the following:

  • A media operator section that posts updates.
  • The intermediary backend that sends the notifications.
    • For simplicity, we will not use any broker or third party cloud messaging system.
    • We've chosen vanilla WebSocket as mode of communication although other approaches such as periodical long polls, server-sent-event, SockJS can be used with their respective pros and cons.
  • The viewers’ section to consume (experience) the notification.

We create a spring boot application with spring-boot-starter-websocket and spring-boot-starter-thymeleaf.  


XML
 
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.4.1</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.soham.demo</groupId>
	<artifactId>visually-appealing-realtime-update</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>visually-appealing-realtime-update</name>
	<description>visually-appealing-realtime-update</description>
	<url />
	<licenses>
		<license />
	</licenses>
	<developers>
		<developer />
	</developers>
	<scm>
		<connection />
		<developerConnection />
		<tag />
		<url />
	</scm>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-websocket</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>

			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>

		</dependency>
	</dependencies>

	<build>
		<finalName>${project.artifactId}</finalName>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<annotationProcessorPaths>
						<path>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</path>
					</annotationProcessorPaths>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>


We expose a WebSocket end point where clients can connect to establish a WebSocket session. 

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13


The server responds with a status code 101 (Switching Protocols):

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=


We expose an open (non-restricted ) endpoint "score" for clients to connect and configure it to allow traffic from anywhere. This is just for illustration and is not suitable on production grade.

Java
 
@Configuration
@EnableWebSocket
public class CricketScoreWebSocketConfig implements WebSocketConfigurer {

	private static final String PATH_SCORE = "score";

	@Override
	public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
		registry.addHandler(new TextNotificationHandler(), PATH_SCORE).setAllowedOrigins("*");
	}

}


To pour in minimal effort, assuming that there will be exactly one bulletin entry operator,  we'll create the class TextNotificationHandler.

Java
 
@Slf4j
public class TextNotificationHandler extends TextWebSocketHandler {
	
	
	private Set<WebSocketSession> sessions = Collections.synchronizedSet(new HashSet<>());

	@Override
	public void afterConnectionEstablished(WebSocketSession session) throws Exception {
		log.debug("afterConnectionEstablished :: session established remote host: {}",session.getRemoteAddress());
		sessions.add(session);
		log.debug("afterConnectionEstablished :: connection from: {} is added. Current Open session count : {}",session.getRemoteAddress(),sessions.size());
	}

	@Override
	protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
		for (WebSocketSession webSocketSession : sessions) {
			if (webSocketSession.isOpen()) {
				webSocketSession.sendMessage(message);
			}
		}
	}

	@Override
	public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
		log.debug("afterConnectionEstablished :: session closed remote host: {}",session.getRemoteAddress());
		sessions.remove(session);
		log.debug("afterConnectionEstablished :: connection from: {} is removed. Current Open session count : {}",session.getRemoteAddress(),sessions.size());
	}
}


Now, we create the two HTML files under src/resource/templates. 

HTML
 
<!DOCTYPE html>
<html lang="en">

<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Media Operator For XYZ Television Cricket Match IND vs ENG</title>
	<style>
		body {
			margin: 0;
			height: 100%;
			width: 100%;
		}

		canvas {
			display: block;
		}

		.container {
			display: flex;
			height: 100vh;
			width: 100%;
		}

		.left-section {

			background-color: lightblue;
			width: 80%;
			padding: 20px;
			box-sizing: border-box;

		}

		.right-section {
			width: 20%;
			background-color: lightcoral;
			padding: 20px;
			box-sizing: border-box;
		}
	</style>
</head>

<body>
	<div class="container">
		<div class="left-section">
			<canvas id="cricketField"></canvas>
		</div>
		<div class="right-section">
			<textarea rows="10" cols="50" id="tb_Comment" placeholder="Message here"></textarea>
			<button onclick="sendTextMsg()">Send</button>
			<fieldset>
				<legend>Quick Panel</legend>
				<button onclick="sendToastMsg('boundary')">Boundary</button>
				<button onclick="sendToastMsg('over-boundary')">Over-boundary</button>
				<button onclick="sendToastMsg('out')">OUT!!</button>
				<button onclick="sendToastMsg('100')">100* NOT OUT</button>
			</fieldset>

		</div>
	</div>
	<script>
		let socket;
		let dto;
		window.onload = function () {
			socket = new WebSocket("ws://"+window.location.host+"/score");
			socket.onmessage = function (event) {
				console.log(event);
			};

		};


		const canvas = document.getElementById('cricketField');
		const ctx = canvas.getContext('2d');

		canvas.width = window.innerWidth;
		canvas.height = window.innerHeight;

		function drawCricketField() {
			ctx.fillStyle = 'green';
			ctx.fillRect(0, 0, canvas.width, canvas.height);

			const centerX = canvas.width / 2;
			const centerY = canvas.height / 2;
			const fieldWidth = 600;
			const fieldHeight = 400;

			// Draw the oval cricket field
			ctx.fillStyle = 'lightgreen';
			ctx.beginPath();
			ctx.ellipse(centerX, centerY, fieldWidth / 2, fieldHeight / 2, 0, 0, Math.PI * 2);
			ctx.fill();

			// Draw pitch and creases
			ctx.fillStyle = 'white';
			ctx.fillRect(centerX - 3, centerY - 150, 6, 300); // Pitch
			ctx.fillRect(centerX - 150, centerY - 3, 300, 6); // Crease

			// Draw stumps
			ctx.fillRect(centerX - 6, centerY - 160, 4, 20);
			ctx.fillRect(centerX - 2, centerY - 160, 4, 20);
			ctx.fillRect(centerX + 2, centerY - 160, 4, 20);

			ctx.fillRect(centerX - 6, centerY + 140, 4, 20);
			ctx.fillRect(centerX - 2, centerY + 140, 4, 20);
			ctx.fillRect(centerX + 2, centerY + 140, 4, 20);
		}

		let drawing = false;
		let startX = 0;
		let startY = 0;
		let currentX = 0;
		let currentY = 0;

		canvas.addEventListener('mousedown', (e) => {
			const rect = canvas.getBoundingClientRect();
			const mouseX = e.clientX - rect.left;
			const mouseY = e.clientY - rect.top;
			// console.log(mouseX+" " +mouseY+ctx.isPointInPath(mouseX, mouseY));
			//  if (ctx.isPointInPath(mouseX, mouseY)) {
			drawing = true;
			startX = mouseX;
			startY = mouseY;
			currentX = mouseX;
			currentY = mouseY;
			// }
		});

		canvas.addEventListener('mousemove', (e) => {
			if (drawing) {
				const rect = canvas.getBoundingClientRect();
				const mouseX = e.clientX - rect.left;
				const mouseY = e.clientY - rect.top;
				currentX = mouseX;
				currentY = mouseY;
				clearCanvas();
				drawLine(startX, startY, currentX, currentY);
			}
		});

		canvas.addEventListener('mouseup', () => {
			drawing = false;
		});

		canvas.addEventListener('mouseout', () => {
			drawing = false;
		});

		function clearCanvas() {
			ctx.clearRect(0, 0, canvas.width, canvas.height);
			drawCricketField();
		}

		function drawLine(startX, startY, endX, endY) {
			ctx.beginPath();
			ctx.moveTo(startX, startY);
			ctx.lineTo(endX, endY);
			ctx.strokeStyle = 'red';
			ctx.lineWidth = 5;  // Increase the line width for better visibility
			ctx.stroke();
			ctx.closePath();
			dto = {};


			dto.startX = startX;
			dto.startY = startY;
			dto.endX = endX;
			dto.endY = endY;
			sendMessage("VISUAL", dto);
		}

		function sendMessage(strType, dto) {

			dto.id = Date.now();
			dto.type = strType;
			socket.send(JSON.stringify(dto));
		}
		function sendTextMsg() {
			dto = {};
			dto.message = document.getElementById("tb_Comment").value;

			sendMessage("TEXT", dto);
		}
		function sendToastMsg(msg) {
			dto = {};
			dto.message = msg;

			sendMessage("TOAST", dto);
		}
		drawCricketField();
	</script>
</body>

</html>




The full source code is available here .  You can also run the application from Docker hub by this command using the target port that you prefer.

Dockerfile
 
docker run -p 9876:9876 sohamsg/dockerhub:websocket-cricket-match-commetary 


However, the proposition the author made at the very beginning can now be revisited to understand its usage and need. Currently, the code is written to cater to selected specific use cases considering a cricket match. However, all these codes are created by individual teams/developers, though they were targeting the same thing of course, in different ways and USPs. To help visualize the components, let us take this enum, which is used in the mark up classifier below:

Java
 
public enum EvtType {

	VISUAL,TEXT,TOAST , // Keep adding your event types for another domain
}
class MarkUpClassifierService{
   
  public Optional<EvtType> classifyMessagge(AbstractMessage message){
       return classifierStgragey.apply(message);
   }
  
   /**
     Define your strategy to extract the category of the message.
     return empty Optional unless a category is found
   */
  private Function<AbstractMessage,Optional<EvtType>> classifierStgragey;
   

}


The mark up look-up engine looks for a mark up strategy and the mark up implementation simply renders them, fetching the strategies from the server to browser/client only once. A CDN can be used, too! 

The perinodal structure looks like this:

Java
 
interface IMarkup{
  public void markup(AbstractMessage message, OutputStream outStream);
}

@Service
@SLF4j
@RequiredArgsConstructor
// We use it as regsitered bean in Spring but this is not specific to any framework   

class MarkupLookupService{
  private final MarkupRegistry regitry; // We use it as regsitered bean in Spring but this is not specific to any framework   
  public Optional<IMarkup> lookupMarkup(EvtType evtType){
     if(regitry.supports(evtType)){
       return registry.get(evtType);//write your look up logic
     }else{
        return Optional.<IMarkup>empty();
     }
  }

}
HTML
 
<!DOCTYPE html>
<html lang="en">

<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Cricket Match IND vs ENG </title>
	<style>
		body {
			margin: 0;
			overflow: hidden;
		}

		canvas {
			display: block;
		}

		.container {
			display: flex;
			height: 100vh;
			width: 100%;
		}

		.left-section {

			background-color: lightblue;
			width: 80%;
			padding: 20px;
			box-sizing: border-box;

		}

		.right-section {
			width: 20%;
			background-color: white;
			padding: 20px;
			box-sizing: border-box;
		}

		#toast {
			visibility: hidden;
			min-width: 250px;
			margin-left: -125px;
			background-color: #333;
			color: #fff;
			text-align: center;
			border-radius: 5px;
			padding: 16px;
			position: fixed;
			z-index: 1;
			left: 50%;
			bottom: 30px;
			font-size: 37px;
		}

		#toast.show {
			visibility: visible;
			animation: fadein 0.5s, fadeout 0.5s 2.5s;
		}

		@keyframes fadein {
			from {
				top: -50px;
				opacity: 0;
			}

			to {
				top: 30px;
				opacity: 1;
			}
		}

		@keyframes fadeout {
			from {
				top: 30px;
				opacity: 1;
			}

			to {
				top: -50px;
				opacity: 0;
			}
		}
	</style>
</head>

<body>
	<div class="container">
		<div class="left-section">
			<canvas id="cricketField"></canvas>
			<div id="toast"></div>
		</div>
		<div class="right-section">
			<textarea rows="10" cols="50" id="tb_Comment" placeholder="Message here"
				style="color:gold;background-color:black;read-only:true" readonly></textarea>
			<button onclick="sendTextMsg()">Send</button>
		</div>
	</div>
	<script>
		let socket;
		let dto;
		window.onload = function () {
			socket = new WebSocket("ws://"+window.location.host+"/score");
			socket.onmessage = function (event) {

				var d = event.data;
				var data = JSON.parse(d);
				console.log(data + "  " + data.startX);
				clearCanvas();
				if ("VISUAL" === data.type) {
					drawLine(data.startX, data.startY, data.endX, data.endY);
				} else if("TEXT"===data.type){

					document.getElementById("tb_Comment").value += data.message + "\n";
				}else if("TOAST"===data.type){
					showToast(data.message);
				}else{
					console.error("Unsupported message type "+d);
				}
			};

		};
		const canvas = document.getElementById('cricketField');
		const ctx = canvas.getContext('2d');

		canvas.width = window.innerWidth;
		canvas.height = window.innerHeight;

		function drawCricketField() {
			ctx.fillStyle = 'green';
			ctx.fillRect(0, 0, canvas.width, canvas.height);

			const centerX = canvas.width / 2;
			const centerY = canvas.height / 2;
			const fieldWidth = 600;
			const fieldHeight = 400;

			// Draw the oval cricket field
			ctx.fillStyle = 'lightgreen';
			ctx.beginPath();
			ctx.ellipse(centerX, centerY, fieldWidth / 2, fieldHeight / 2, 0, 0, Math.PI * 2);
			ctx.fill();

			// Draw pitch and creases
			ctx.fillStyle = 'white';
			ctx.fillRect(centerX - 3, centerY - 150, 6, 300); // Pitch
			ctx.fillRect(centerX - 150, centerY - 3, 300, 6); // Crease

			// Draw stumps
			ctx.fillRect(centerX - 6, centerY - 160, 4, 20);
			ctx.fillRect(centerX - 2, centerY - 160, 4, 20);
			ctx.fillRect(centerX + 2, centerY - 160, 4, 20);

			ctx.fillRect(centerX - 6, centerY + 140, 4, 20);
			ctx.fillRect(centerX - 2, centerY + 140, 4, 20);
			ctx.fillRect(centerX + 2, centerY + 140, 4, 20);
		}

		let drawing = false;
		let startX = 0;
		let startY = 0;
		let currentX = 0;
		let currentY = 0;

		canvas.addEventListener('mousedown', (e) => {
			const rect = canvas.getBoundingClientRect();
			const mouseX = e.clientX - rect.left;
			const mouseY = e.clientY - rect.top;
			// console.log(mouseX+" " +mouseY+ctx.isPointInPath(mouseX, mouseY));
			//  if (ctx.isPointInPath(mouseX, mouseY)) {
			drawing = true;
			startX = mouseX;
			startY = mouseY;
			currentX = mouseX;
			currentY = mouseY;
			// }
		});

		canvas.addEventListener('mousemove', (e) => {
			if (drawing) {
				const rect = canvas.getBoundingClientRect();
				const mouseX = e.clientX - rect.left;
				const mouseY = e.clientY - rect.top;
				currentX = mouseX;
				currentY = mouseY;
				clearCanvas();
				drawLine(startX, startY, currentX, currentY);
			}
		});

		canvas.addEventListener('mouseup', () => {
			drawing = false;
		});

		canvas.addEventListener('mouseout', () => {
			drawing = false;
		});

		function clearCanvas() {
			ctx.clearRect(0, 0, canvas.width, canvas.height);
			drawCricketField();
		}

		function drawLine(startX, startY, endX, endY) {

			ctx.beginPath();
			ctx.moveTo(startX, startY);
			ctx.lineTo(endX, endY);
			ctx.strokeStyle = 'red';
			ctx.lineWidth = 5;  // Increase the line width for better visibility
			ctx.stroke();
			ctx.closePath();
		}

		function showToast(message) {
			var toast = document.getElementById("toast");
			toast.className = "show";
			toast.textContent = mapToVisual(message);
			setTimeout(function () {
				toast.className = toast.className.replace("show", "");
			}, 3000);
		}
		
		function mapToVisual(msg){
			switch(msg){
				case "100": return "100*";
				case "out": return "OUT!!";
				case "boundary": return "Boundary";
				case "over-boundary": return "Over-boundary";
			}
			return "";
		}
		drawCricketField();
	</script>
</body>

</html>


The logic to translation of marking up different types of event differently to the client can be done in multiple ways and we list down only a few:

  • Write the strategy in client side code as a library (e.g., a JavaScript library). 
    • The downside is that updating the logic is prone to errors, as with any scripting.
    • Caching and CDN—Ensuring the updates reflect and is not cached except beyond current session. 
  • Writing the strategy in the server side and sending transpiled Script back. 
    • The client side code and the  backend is no more loosely coupled then.

We will cover each approach in detail some other time. 

WebSocket Event Spring Boot

Opinions expressed by DZone contributors are their own.

Related

  • WebSocket vs. Server-Sent Events: Choosing the Best Real-Time Communication Protocol
  • Reactive Event Streaming Architecture With Kafka, Redis Streams, Spring Boot, and HTTP Server-Sent Events (SSE)
  • Competing Consumers With Spring Boot and Hazelcast
  • Reactive Kafka With Streaming in Spring Boot

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!