Get familiar with the Spring MVC and the Spring Boot Web applications.
Time: 30 minutes.
Code Snippet Manager
As developers normally we require to have some code snippets that help us to code fast and safe.
The main idea of this application is to create a site that can manage our Code Snippets. We are going to create a RESTful API for any external client.
-
Open a browser and hit the url: http://start.spring.io
-
Click the Switch to the full version link.
-
Fill out the Code Snippet Manager Project metadata with (See Figure 1.0):
Table 1. Code Snippet Manager App - metadata Property Value Group:
io.pivotal.workshop
Artifact:
code-snippet-manager
Name:
code-snippet-manager
Package Name:
io.pivotal.workshop.snippet
Dependencies:
Web, DevTools, Groovy Templates
Spring Boot:
2.0.0.M7
Figure 1.0: Spring Initializr - http://start.spring.io/TipYou can choose either Maven or Gradle project types. -
Type Web, DevTools, Groovy Templates, in the Dependencies field and press Enter.
-
Click the Generate Project button.
-
Unzip the file in any directory you want.
-
Import your project in any IDE you want.
-
Let’s start by defining the domain models. Create the following classes:
io.pivotal.workshop.snippet.domain.Language.javapackage io.pivotal.workshop.snippet.domain; public class Language { private String id; private String name; private String syntax = "text"; public Language() { this.id = java.util.UUID.randomUUID().toString().replaceAll("-", "");; } public Language(String name) { this(); this.name = name; } public Language(String name, String syntax) { this(name); this.syntax = syntax; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSyntax() { return syntax; } public void setSyntax(String syntax) { this.syntax = syntax; } }
We can have multiple snippets in different programming languages, that’s why we have this class. Take a look that also there is a syntax field; this field will be use later on for syntax highlight.
io.pivotal.workshop.snippet.domain.Code.javapackage io.pivotal.workshop.snippet.domain; public class Code { private String id; private String source; public Code() { this.id = java.util.UUID.randomUUID().toString().replaceAll("-", "");; } public Code(String source) { this(); this.source = source; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getSource() { return source; } public void setSource(String source) { this.source = source; } }
This class will be use to hold the actual snippet code.
io.pivotal.workshop.snippet.domain.Snippet.javapackage io.pivotal.workshop.snippet.domain; import java.util.Date; public class Snippet { private String id; private String title; private String keywords; private String description; private Language lang; private Code code; private Date created; private Date modified; public Snippet() { this.id = java.util.UUID.randomUUID().toString().replaceAll("-", ""); this.created = new Date(); this.modified = new Date(); } public Snippet(String title, String keywords, String description, Language lang, Code code) { this(); this.title = title; this.keywords = keywords; this.description = description; this.lang = lang; this.code = code; } public Snippet(String title, Language lang, Code code) { this(title, "", "", lang, code); } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public Language getLang() { return lang; } public void setLang(Language lang) { this.lang = lang; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getKeywords() { return keywords; } public void setKeywords(String keywords) { this.keywords = keywords; } public Code getCode() { return code; } public void setCode(Code code) { this.code = code; } public Date getCreated() { return created; } public void setCreated(Date created) { this.created = created; } public Date getModified() { return modified; } public void setModified(Date modified) { this.modified = modified; } }
The above class will be the main reponse.
-
Next let’s create a base interface that will be use as main repository. In this case we are going to hold all the data in Memory using collections:
io.pivotal.workshop.snippet.repository.SimpleRepository.javapackage io.pivotal.workshop.snippet.repository; import java.util.Collection; public interface SimpleRepository<T> { Iterable<T> findAll(); void saveAll(Collection<T> items); T saveAll(T item); T findById(String id); }
as you can see it has just the most common actions.
-
Let’s create now all the Repositories that will be implementing the SimpleRepository interface.
io.pivotal.workshop.snippet.repository.LanguageRepository.javapackage io.pivotal.workshop.snippet.repository; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.stream.StreamSupport; import org.springframework.stereotype.Repository; import io.pivotal.workshop.snippet.domain.Language; @Repository public class LanguageRepository implements SimpleRepository<Language>{ private List<Language> languages = new ArrayList<>(); public Iterable<Language> findAll(){ return languages; } public void saveAll(Collection<Language> languages){ this.languages.addAll(languages); } public Language findById(String name) { Optional<Language> language = StreamSupport .stream(this.languages.spliterator(), false) .filter(lang -> lang.getName().equals(name)) .findFirst(); if (language.isPresent()) return language.get(); return null; } public Language saveAll(Language item) { assert item.getName() != null; Language language = this.findById(item.getName()); if(language == null) { this.languages.add(item); return item; }else { language.setName(item.getName()); language.setSyntax(item.getSyntax()); return language; } } }
io.pivotal.workshop.snippet.repository.CodeRepository.javapackage io.pivotal.workshop.snippet.repository; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Optional; import org.springframework.stereotype.Repository; import io.pivotal.workshop.snippet.domain.Code; @Repository public class CodeRepository implements SimpleRepository<Code>{ private List<Code> codes = new ArrayList<>(); public List<Code> findAll() { return codes; } @Override public void saveAll(Collection<Code> items) { this.codes.addAll(items); } @Override public Code saveAll(Code item) { assert item.getSource() != null; Code code = findById(item.getId()); if(code == null){ this.codes.add(item); return item; } else { code.setSource(item.getSource()); return code; } } @Override public Code findById(String id) { Optional<Code> code = codes .stream() .filter(c -> c.getId().equals(id)) .findFirst(); if (code.isPresent()) return code.get(); return null; } }
io.pivotal.workshop.snippet.repository.SnippetRepository.javapackage io.pivotal.workshop.snippet.repository; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Optional; import org.springframework.stereotype.Repository; import io.pivotal.workshop.snippet.domain.Snippet; @Repository public class SnippetRepository implements SimpleRepository<Snippet>{ private List<Snippet> snippets = new ArrayList<>(); @Override public Snippet saveAll(Snippet snippet){ assert snippet.getTitle() != null; assert snippet.getCode() != null; assert snippet.getLang() != null; Snippet _snippet = null; if (snippet.getId() == null) { _snippet = new Snippet(snippet.getTitle(), snippet.getLang(),snippet.getCode()); } else { _snippet = this.findById(snippet.getId()); if(_snippet != null){ _snippet.setTitle(snippet.getTitle()); _snippet.setCode(snippet.getCode()); _snippet.setLang(snippet.getLang()); _snippet.setModified(new Date()); } } return _snippet; } @Override public Iterable<Snippet> findAll(){ return this.snippets; } @Override public Snippet findById(String id){ Optional<Snippet> result = snippets.stream() .filter(snippet -> snippet.getId().equals(id)) .findFirst(); if (result.isPresent()) return result.get(); return null; } @Override public void saveAll(Collection<Snippet> items) { this.snippets.addAll(items); } }
-
Next lets do the main controller that will expose the REST API:
io.pivotal.workshop.snippet.controller.SnippetController.java@RestController public class SnippetController { private SnippetRepository snippetRepository; private LanguageRepository languageRepository; public SnippetController(SnippetRepository snippetRepository,LanguageRepository languageRepository){ this.snippetRepository = snippetRepository; this.languageRepository = languageRepository; } @RequestMapping("/") public ModelAndView home(){ assert snippetRepository != null; Map<String,Object> model = new HashMap<String,Object>(); model.put("langs", languageRepository.findAll()); model.put("snippets", snippetRepository.findAll()); return new ModelAndView("views/home",model); } @RequestMapping("/snippets") public ResponseEntity<?> snippets(){ assert snippetRepository != null; return ResponseEntity.ok(snippetRepository.findAll()); } }
-
Create a configuration class to initialize your repositories:
io.pivotal.workshop.snippet.config.SnippetConfiguration.javapackage io.pivotal.workshop.snippet.config; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import org.springframework.boot.CommandLineRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import io.pivotal.workshop.snippet.domain.Code; import io.pivotal.workshop.snippet.domain.Language; import io.pivotal.workshop.snippet.domain.Snippet; import io.pivotal.workshop.snippet.repository.SnippetRepository; @Configuration public class SnippetConfiguration { @Bean public CommandLineRunner runner(SnippetRepository snippetRepo) { return args -> { @SuppressWarnings("serial") List<Snippet> snippets = new ArrayList<Snippet>() { { add(new Snippet("Hello World", new Language("HTML", "xml"),new Code(new String(Files.readAllBytes(Paths.get("code/html-code.txt")))))); add(new Snippet("Hello World", new Language("C#", "c#"),new Code(new String(Files.readAllBytes(Paths.get("code/cs-code.txt")))))); add(new Snippet("Hello World", new Language("Pascal", "py"),new Code(new String(Files.readAllBytes(Paths.get("code/pas-code.txt")))))); add(new Snippet("Hello World", new Language("Erlang", "erl"),new Code(new String(Files.readAllBytes(Paths.get("code/erl-code.txt")))))); add(new Snippet("Hello World", new Language("JavaScript", "js"),new Code(new String(Files.readAllBytes(Paths.get("code/js-code.txt")))))); add(new Snippet("Hello World", new Language("Groovy", "groovy"),new Code("println 'Hello World'"))); } }; snippetRepo.saveAll(snippets); }; } }
as you can see from the above code, we are building up our data.
If you take a close look, we are using the Files.readAllBytes helper class to read from a file, and this file is located in the code/ folder.
-
Add some code snippets in the code/ folder.
code/html-code.txt - HTML<html> <body> <h1>Hello World</h1> </body> </html>
code/cs-code.txt - C#class Hello { static void Main() { System.Console.Write("Hello World"); } }
code/pas-code.txt - Pascalprogram HelloWorld; begin writeln('Hello World'); end.
code/erl-code.txt - Erlang-module(hello). -export([hello_world/0]). hello_world() -> io:fwrite("hello, world\n").
code/js-code.txt - JavaScriptconsole.log("Hello World");
-
A lot of code, right? Well now its time to run the application. You can use your IDE or command line. Once running you application, go to your browser and hit: http://localhost:8080/snippets and you should get the a response like Figure 2.0.
Figure 2.0: Snippet Code Manager - http://localhost:8080/snippets
Challenges
Adding a UI - Home Page
You can see that the io.pivotal.workshop.snippet.controller.SnippetController.java has the home() method, and it returns the view: views/home and the model, a map that contains the languages and the snippets: Add the following missing dependencies to your pom.xml because you will need them:
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>2.2.4</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>3.3.6</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>angularjs</artifactId>
<version>1.5.7</version>
</dependency>
Add the following libraries in the src/main/resources/static/css: theme.css and offcanvas.css and in the src/main/resources/static/js folder add the syntaxhighlighter.js script.
-
Create the necessary layout and home page to have the same as Figure 3.0.
Figure 3.0: Code Snippet Manager - http://localhost:8080
Tip
|
You can take a look at the Groovy Template Engine: http://docs.groovy-lang.org/next/html/documentation/template-engines.html#_the_markuptemplateengine |
REST API
-
Complete the RESTful API:
-
provide the /snippets/{id} path endpoint to search by snippet Id.
-
provide methods to handle the: POST, PUT for adding a new snippet and updating the snippet.
-
-
Make changes to repose either JSON or XML.
-
When doing a XML response, the snippet code should be in a CDATA tag. Modify the code to show a CDATA for every source code. (tip: @JacksonXmlCData)
[HOMEWORK]
-
Add validation to the POST and PUT methods (tip: @Valid).
-
Finish up any missing UI or Controller methods.