Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Solving Small Problems (Part 1)

DZone's Guide to

Solving Small Problems (Part 1)

Learn how to take apart complex, difficult problems to make them into more manageable chunks.

· Java Zone
Free Resource

Never build auth again! The Okta Developer Platform makes it simple to implement authentication, authorization, MFA and more in Java applications. Get started with the free API.

Although I tried to make this post comprehensible by itself, you might want to read previous parts before this one.

One of the keys to being a good programmer is working in an organized way. You pick a small, easy problem, solve it and move to the next small, easy one. One after another, step by step, something greater emerges. Our brains have limited capacity and processing speed. We can't do everything at once, we can't solve multiple complicated problems at the same time. Knowing this we can conciously limit our current "context" to something simple enough to chew at once.

In this post, I'm going to show you how to put it in practice by developing a key feature of my blogging platform like this. Get ready for a ride!

Git Support

The key feature of the blogging platform is to load the posts from a separate Git repository, so that the bloggers can benefit from version control, code reviews, people's contributions and so on.

Currently, the posts are kept in the same repository as the platform and are loaded from the classpath using some ugly PathMatchingResourcePatternResolver. I want to replace the whole classpath-based solution with Git support. Ability to run the application in "local mode" by specifying local address of a repository would be a plus, but is not a must.

Creating a Plan

One easy way to prevent your brain from exploding is to create a plan - set of simple steps to achieve your goal. The plan is not set in stone, it can evolve while developing and learning new things about the problem, but it needs to remain simple. Here's my initial plan for this feature:

  1. Refactor MarkdownPost. Currently, it's coupled to the Spring's Resource class, has a nasty smart constructor and some other flaws.
  2. Create a mechanism to clone a specified Git repository to the local filesystem.
  3. Read cloned contents - make PostReader use cloned files instead of classpath resources.
  4. Add an endpoint for a Git hook to refresh the files on commit.
  5. Create automated tests for the whole thing.

Fifth point is currently very abstract and might be broken into smaller steps later. Currently, I don't have any good idea how to create or test Git-related mechanisms, so I can't tell what exactly will be done there.

Refactoring MarkdownPost

The idea to refactor MarkdownPost came from 2 sources:

  • comments under previous posts, both on my personal blog and DZone - that constructor and commonmark related logic hurt eyes
  • my thoughts about the concept of a post in the project - it's not any high-level business entity. This platform is a Read application, which is even less than CRUD. I take a file, read whatever's relevant for a post and pass it further. Therefore, a post is a holder of whatever's relevant!

This leads to the following steps:

  • rename MarkdownPost to Post
  • create relevant fields in the post - title, summary etc.
  • initialize them during creation of the post
  • free the Post from dependency on Spring's Resource
  • move parsing markdown from constructor to a factory method
  • consider moving factory method to a standalone factory
  • adjust PostReader accordingly

Yay, even simpler steps! Actually, most often you won't put so much details into your plan, but in this case I had a lot of input from readers and time to think before I found time to implement anything.

♪♪♪ sounds of refactoring ♪♪♪

Showing all intermediate steps would be even more boring than this, so I'll just show you the final effect.

MarkdownPost has turned into a simple data holder Post:

public class Post {
    private String title;
    private String summary;
    private LocalDate date;
    private String url;
    private String content;

    public Post(String title, String summary, LocalDate date, String url, String content) {
        this.title = title;
        this.summary = summary;
        this.date = date;
        this.url = url;
        this.content = content;
    }

    /* getters */
}

Markdown related logic sits in a separate MarkdownPostFactory:

public class MarkdownPostFactory {
    public static final String EXTENSION = ".md";

    public static Post create(InputStream inputStream, String filename) {
        try {
            Node parsedResource = parse(inputStream);
            Map<String, List<String>> metadata = extractMetadata(parsedResource);

            String title = getField(metadata, "title");
            String summary = getField(metadata, "summary");
            LocalDate date = toLocalDate(getField(metadata, "date"));
            String url = toUrl(filename);
            String content = toHtml(parsedResource);

            return new Post(title, summary, date, url, content);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /* nothing new */

    private static String getField(Map<String, List<String>> metadata, String field) {
        return metadata.getOrDefault(field, asList("TILT")).get(0); // TODO test TILT
    }

    /* nothing new */
}

You can notice that I added a TODO to test a case when there's missing metadata in the post. I don't want to do it right now, because this class might change really soon when integrating with Git.

PostReader was adjusted to use Post and the factory. In the process, I also renamed MissingPost to MissingPostException and made it an outer, public class.

Cloning a Git repository

I've seen somewhere that there's a Java library called JGit, developed by some smart people from big companies. I'll add it to the project and try to clone a repo using it.

♪♪♪ sounds of coding ♪♪♪

It turned out to be suprisingly easy. Four lines of a chained call and repository is cloned to our disk. When testing, I found out that we have to clear the repository directory manually, which I decided to brute force by deleting it and creating again.

@Service
public class GitCloner {

    @Value("${blog.repositoryUrl}")
    private String repositoryUrl;

    @PostConstruct
    public void cloneRepository() {
        File contentsDir = new File(".contents");
        clean(contentsDir);
        cloneTo(contentsDir);
    }

    private void clean(File dir) {
        rethrow(() -> FileUtils.deleteDirectory(dir));
        dir.mkdirs();
    }

    private void cloneTo(File contentsDir) {
        rethrow(() -> Git.cloneRepository()
                .setURI(repositoryUrl)
                .setDirectory(contentsDir)
                .call());
    }
}

To aid development, I created a simple test for the class. It verifies that the contents directory is cleaned and repository cloned correctly.

class GitClonerSpec extends Specification {
    static final CONTENTS_DIRECTORY = ".contents"
    static final CLONE_BLOCKER = Paths.get(CONTENTS_DIRECTORY + "/would-block-clone.txt");

    def gitCloner = new GitCloner()

    def setup() {
        gitCloner.repositoryUrl = "https://github.com/tidyjava/blogging-platform-hello-world.git"
    }

    def 'should clean and clone'() {
        given:
        Files.createFile(CLONE_BLOCKER)

        when:
        gitCloner.cloneRepository()

        then:
        Files.exists(Paths.get(CONTENTS_DIRECTORY + "/README.md"))
        !Files.exists(CLONE_BLOCKER)
    }
}

This test might change in the final step, when I'll decide for a testing strategy.

As you maybe saw, a rethrow construct appeared. I got pissed off by catching and rethrowing IOExceptions left and right, so I created ExceptionUtils:

public class ExceptionUtils {

    public static <T> T rethrow(ThrowingSupplier<T> supplier) {
        try {
            return supplier.get();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void rethrow(ThrowingRunnable runnable) {
        try {
            runnable.execute();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @FunctionalInterface
    public interface ThrowingSupplier<T> {
        T get() throws Exception;
    }

    @FunctionalInterface
    public interface ThrowingRunnable {
        void execute() throws Exception;
    }
}


Build and launch faster with Okta’s user management API. Register today for the free forever developer edition!

Topics:
refactoring ,java ,git ,markdown

Published at DZone with permission of Grzegorz Ziemoński, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}