DEV Community

Roshan Adhikari
Roshan Adhikari

Posted on • Edited on • Originally published at roshanadhikary.com.np

1

Build a Markdown-based Blog with Spring Boot - Part 4

Before continuing with part 4 of this series, make sure you have checked out the previous three parts as well: Part 1 | Part 2 | Part 3

Up to this point, we have a database ready, and we have established conventions for how and where we store our Markdown files. These Markdown files will be parsed, line by line, and rendered as HTML string before we persist it in the database. For parsing and rendering, we have already defined some utility classes -- MdFileReader and MdToHtmlRenderer.

For this session, we will write a class that implements the ApplicationListener interface such that on every firing of the ContextRefreshedEvent, our event-listener class' onApplicationEvent method is invoked. Within the onApplicationEvent method, we look for new Markdown files, and if any such new file exists, we persist it in the database.

But before that, we need to add another dependency -- jsoup.

Adding jsoup dependency

Remember how we have a synopsis attribute in the Post entity? We will use that attribute (or field, in the database) and set it to the first 150 characters of the actual post's body. However, after parsing the Markdown and rendering HTML from it, the post's body would look like, say,

<h1>Hello World!</h1><br /><p>This is the post's actual body, rendered in HTML!</p>

As you can imagine, most of the rendered text is actually HTML elements and symbols. Hence, we need to select the first 150 characters from the actual text, excluding the HTML elements and symbols.

We will need jsoup for doing exactly that. To add jsoup as one of our dependencies, make sure to add the following segment to your pom.xml file.

<!-- https://mvnrepository.com/artifact/org.jsoup/jsoup -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.13.1</version>
</dependency>
view raw POM.xml hosted with ❤ by GitHub

Like before, load the Maven changes in your POM file.

Loading Maven changes after adding jsoup dependency

Then, we can proceed to our event-listener class.

ContextEventListener class

Begin by creating the class that implements ApplicationListener interface that takes in as type parameter the ContextReferencedEvent type. With this, our class would need to override the onApplicationEvent method.

We also need some instance variables for purposes that will be apparent soon.

package np.com.roshanadhikary.mdblog.listeners;
import np.com.roshanadhikary.mdblog.repositories.AuthorRepository;
import np.com.roshanadhikary.mdblog.repositories.PostRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
@Component
public class ContextEventListener implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
private AuthorRepository authorRepository;
@Autowired
private PostRepository postRepository;
@Value("classpath:posts/*")
private Resource[] postFiles;
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
}
}

With @Value, we inject into the postFiles, an array of Resource type, any files that exist in the classpath inside posts directory.

Next, we can implement our onApplicationEvent method.

package np.com.roshanadhikary.mdblog.listeners;
import np.com.roshanadhikary.mdblog.entities.Author;
import np.com.roshanadhikary.mdblog.entities.Post;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import static np.com.roshanadhikary.mdblog.util.MdFileReader.*;
import static np.com.roshanadhikary.mdblog.util.PostUtil.*;
import static np.com.roshanadhikary.mdblog.util.AuthorUtil.*;
@Component
public class ContextEventListener implements ApplicationListener<ContextRefreshedEvent> {
// ... instance variables
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
Arrays.stream(postFiles).forEach(postFile -> {
Optional<String> postFileNameOpt = Optional.ofNullable(postFile.getFilename());
Post post = new Post();
if (postFileNameOpt.isPresent()) {
String postFileName = postFileNameOpt.get();
String title = getTitleFromFileName(postFileName);
long id = getIdFromFileName(postFileName);
List<String> mdLines = readLinesFromMdFile(postFileName);
String htmlContent = getHtmlContentFromMdLines(mdLines);
Author author = bootstrapAuthor(authorRepository);
Optional<Post> postOpt = postRepository.findById(id);
if (postOpt.isEmpty()) {
System.out.println("Post with ID: " + id + " does not exist. Creating post...");
post.setTitle(title);
post.setAuthor(author);
post.setContent(htmlContent);
post.setSynopsis(getSynopsisFromHtmlContent(htmlContent));
post.setDateTime(LocalDateTime.now());
postRepository.save(post);
System.out.println("Post with ID: " + id + " created.");
} else {
System.out.println("Post with ID: " + id + " exists.");
}
} else {
System.out.println("postFileName is null, should not be null");
}
});
}
}

In this method, we start by iterating over each file in the postFiles array. We check to see if a post with the same ID as the file exists in the database. If it does not, we persist the post in the database. Before saving the post, we set it's attributes.

Notice how we are using static utility methods from PostUtil and AuthorUtil classes for operations concerning posts and authors respectively.

PostUtil and AuthorUtil classes

package np.com.roshanadhikary.mdblog.util;
import org.jsoup.Jsoup;
import java.util.List;
import java.util.Optional;
import static np.com.roshanadhikary.mdblog.util.MdToHtmlRenderer.renderHtml;
public class PostUtil {
public static String getHtmlContentFromMdLines(List<String> mdLines) {
Optional<List<String>> mdLinesOpt = Optional.ofNullable(mdLines);
return mdLinesOpt.isEmpty() ? "" : renderHtml(mdLinesOpt.get());
}
public static String getSynopsisFromHtmlContent(String htmlContent) {
String content = Jsoup.parse(htmlContent).text();
return content.length() <= 150 ? content : content.substring(0, 149);
}
}
view raw PostUtil.java hosted with ❤ by GitHub

In the PostUtil class, we define methods: getHtmlContentFromMdLines and getSynopsisFromHtmlContent.

The getHtmlContentFromMdLines returns a String of HTML content rendered using the List of Markdown lines passed as argument.

The getSynopsisFromHtmlContent method returns the first 150 characters of the text content parsed from the HTML content passed as argument. If the text content is fewer than 150 characters in length, the entire String is returned. 

package np.com.roshanadhikary.mdblog.util;
import np.com.roshanadhikary.mdblog.entities.Author;
import np.com.roshanadhikary.mdblog.repositories.AuthorRepository;
import java.util.Optional;
public class AuthorUtil {
public static Author bootstrapAuthor(AuthorRepository authorRepository) {
Optional<Author> authorOpt = authorRepository.findById(1L);
if (authorOpt.isPresent()) {
return authorOpt.get();
} else {
Author roshanAuthor = new Author();
roshanAuthor.setName("Roshan Adhikari");
roshanAuthor.setEmail("nahsorad@gmail.com");
roshanAuthor.setUrl("roshanadhikari.name.np");
authorRepository.save(roshanAuthor);
return roshanAuthor;
}
}
}
view raw AuthorUtil.java hosted with ❤ by GitHub

In the AuthorUtil class, we define method: bootstrapAuthor. If no author exists in the database, it creates a new author, before persisting it in the database, and returns it. Otherwise, it returns the first author that exists in the database.

Get it running

Now, when we run our Spring Boot application, or whenever we cause the Spring Context to refresh, a ContextRefreshedEvent is fired. This looks for a new blog post in the resources/posts/ directory and persists it.

To test this, let us create a posts directory inside the resources directory.

Inside it, I will create a new file, 1_Hello_World!.md, with the following content.

# Hello World!
This is my *first* blog post. <br>
Be sure to read future parts of this blog post series, <br>
titled **Build a Markdown-based Blog with Spring Boot**.

Then, let's run our Spring Boot application. After the JVM is up and running, we can check our database to see that our new blog post has been persisted successfully, along with the author.

Code

We have now created an application that persists new blog posts to our database. Now, we will work towards displaying blog posts using the Thymeleaf template engine. But that's for the next post.

The GitHub repository has been updated with this session's code, do check it out. Or if you need to check previous sessions' code, please do so as well. 

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read full post →

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs