commit 58a1bd06a17b96ee2139afc7c49b2ad6935b413f Author: fecaille Date: Wed Mar 9 17:04:45 2016 +0100 First commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..58436cf --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.classpath +.project +.settings +src/main/resources/static +src/main/webapp/js/.module-cache +src/test/resources/static/js/compiled +src/test/resources/static/js/.module-cache +target diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..f236f08 --- /dev/null +++ b/pom.xml @@ -0,0 +1,349 @@ + + 4.0.0 + com.opengroupe.cloud.saas + catalog-ui + 1.0.0-SNAPSHOT + UI service project + Default tools to build UI BFF + jar + http://www.open-groupe.com + + + org.springframework.boot + spring-boot-starter-parent + 1.3.3.RELEASE + + + + 1.8 + 2.4.1 + 3.2.0 + + 3.3.6 + 2.2.1 + 0.14.7 + 0.3.2-1 + 0.3.2 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.boot + spring-boot-starter-test + test + + + com.jayway.jsonpath + json-path + test + + + com.jayway.jsonpath + json-path-assert + test + + + org.webjars + jasmine + ${jasmine.version} + test + + + org.webjars + jquery-mockjax + 1.5.3 + test + + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.springframework.boot + spring-boot-devtools + true + + + + org.webjars + bootstrap + ${bootstrap.version} + + + org.webjars + jquery + ${jquery.version} + + + org.webjars + react + ${react.version} + + + org.webjars + marked + ${marked.version} + + + + + + + ${project.basedir}/src/main/resources + + + + + org.springframework.boot + spring-boot-maven-plugin + + + com.fizzed + fizzed-watcher-maven-plugin + 1.0.6 + + + + src/main/webapp/js + + + src/main/wro + + + + process-resources + + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + clean + + run + + + + + + + + + + + + maven-resources-plugin + + + + copy-resources + validate + + copy-resources + + + ${basedir}/target/wro + + + src/main/wro + true + + + + + + + + uk.co.codezen + react-jsxtransformer-maven-plugin + 1.0 + + + Compile resources + process-resources + + compile + + + jsx + + ${project.basedir}/src/main/webapp/js + + + ${project.basedir}/src/main/resources/static/js + + + + + Compile test resources + process-test-resources + + compile + + + jsx + + ${project.basedir}/src/test/resources/static/js + + + ${project.basedir}/src/test/resources/static/js/compiled + + + + + + + ro.isdc.wro4j + wro4j-maven-plugin + 1.7.9 + + + process-resources + + run + + + + + ro.isdc.wro.maven.plugin.manager.factory.ConfigurableWroManagerFactory + ${project.basedir}/src/main/resources/static/css + ${project.basedir}/src/main/resources/static/js + ${project.build.directory}/wro/wro.xml + ${project.basedir}/src/main/wro/wro.properties + + + + + com.github.klieber + phantomjs-maven-plugin + 0.7 + + + + install + + + + + 2.1.1 + false + + + + com.github.searls + jasmine-maven-plugin + 2.1 + + + + test + + + + + org.openqa.selenium.phantomjs.PhantomJSDriver + + + phantomjs.binary.path + ${phantomjs.binary} + + + + /webjars/jquery.js + /webjars/react-with-addons.js + /webjars/react-dom.js + /webjars/jquery.mockjax.js + ${project.basedir}/src/test/resources/jasmine/config.js + + ${project.basedir}/src/main/resources/static/js + + **/*.js + + + **/react-bootstrap.js + **/app.render.js + + ${project.basedir}/src/test/resources/static/js + + **/*.spec.js + + ${project.basedir}/src/test/resources/jasmine/ReactJsSpecRunner.htmltemplate + + + + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + ro.isdc.wro4j + wro4j-maven-plugin + [1.7.9,) + + jshint + run + + + + + + + + + uk.co.codezen + react-jsxtransformer-maven-plugin + [1.0,) + + compile + + + + + + + + + com.github.klieber + phantomjs-maven-plugin + [0.7,) + + install + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/opengroupe/cloud/saas/Application.java b/src/main/java/com/opengroupe/cloud/saas/Application.java new file mode 100644 index 0000000..8902a7b --- /dev/null +++ b/src/main/java/com/opengroupe/cloud/saas/Application.java @@ -0,0 +1,12 @@ +package com.opengroupe.cloud.saas; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/src/main/java/com/opengroupe/cloud/saas/domain/Comment.java b/src/main/java/com/opengroupe/cloud/saas/domain/Comment.java new file mode 100644 index 0000000..fa28923 --- /dev/null +++ b/src/main/java/com/opengroupe/cloud/saas/domain/Comment.java @@ -0,0 +1,26 @@ +package com.opengroupe.cloud.saas.domain; + +import groovy.transform.ToString; + +@ToString +public class Comment { + + private final Long id; + private final String author; + private final String text; + + public Comment(Long id, String author, String text) { + this.id = id; + this.author = author; + this.text = text; + } + public Long getId() { + return id; + } + public String getAuthor() { + return author; + } + public String getText() { + return text; + } +} diff --git a/src/main/java/com/opengroupe/cloud/saas/rest/CommentController.java b/src/main/java/com/opengroupe/cloud/saas/rest/CommentController.java new file mode 100644 index 0000000..be035c0 --- /dev/null +++ b/src/main/java/com/opengroupe/cloud/saas/rest/CommentController.java @@ -0,0 +1,37 @@ +package com.opengroupe.cloud.saas.rest; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +import com.opengroupe.cloud.saas.domain.Comment; + +@RestController +public class CommentController { + + private List comments = new ArrayList(); + + @RequestMapping("/") + public String index() { + return "Greetings from Spring Boot!"; + } + + @RequestMapping(value="/api/comments", method=RequestMethod.GET) + public @ResponseBody List comments() { + return comments; + } + + @RequestMapping(value="/api/comments", method=RequestMethod.POST) + public @ResponseBody List comments( + @RequestParam(value="id", required=true) Long id, + @RequestParam(value="author", required=true) String author, + @RequestParam(value="text", required=true) String text) { + comments.add(new Comment(id, author, text)); + return comments; + } +} diff --git a/src/main/java/com/opengroupe/cloud/saas/web/ViewController.java b/src/main/java/com/opengroupe/cloud/saas/web/ViewController.java new file mode 100644 index 0000000..8a65cd3 --- /dev/null +++ b/src/main/java/com/opengroupe/cloud/saas/web/ViewController.java @@ -0,0 +1,21 @@ +package com.opengroupe.cloud.saas.web; + +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@Controller +public class ViewController { + + @RequestMapping("/greeting") + public String greeting(@RequestParam(value="name", required=false, defaultValue="World") String name, Model model) { + model.addAttribute("name", name); + return "greeting"; + } + + @RequestMapping("/index") + public String index(Model model) { + return "index"; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..1c39a80 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,16 @@ +spring: + main: + banner_mode: off + +project: + artifactId: template + name: Demo + version: X.X.X + description: Demo project for info endpoint + +info: + build: + artifact: ${project.artifactId} + name: ${project.name} + description: ${project.description} + version: ${project.version} diff --git a/src/main/resources/templates/greeting.html b/src/main/resources/templates/greeting.html new file mode 100644 index 0000000..0488fc9 --- /dev/null +++ b/src/main/resources/templates/greeting.html @@ -0,0 +1,10 @@ + + + + Getting Started: Serving Web Content + + + +

+ + diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html new file mode 100644 index 0000000..60b0e80 --- /dev/null +++ b/src/main/resources/templates/index.html @@ -0,0 +1,17 @@ + + + + + ReactJS + + + + +

+ + + + + + + \ No newline at end of file diff --git a/src/main/webapp/js/app.jsx b/src/main/webapp/js/app.jsx new file mode 100644 index 0000000..f4dc3da --- /dev/null +++ b/src/main/webapp/js/app.jsx @@ -0,0 +1,123 @@ +'use strict'; + +var Comment = React.createClass({ + rawMarkup: function() { + var rawMarkup = marked(this.props.children.toString(), {sanitize: true}); + return { __html: rawMarkup }; + }, + render: function() { + return ( +
+

+ {this.props.author} +

+ +
+ ); + } +}); + +var CommentForm = React.createClass({ + getInitialState: function() { + return {author: '', text: ''}; + }, + handleAuthorChange: function(e) { + this.setState({author: e.target.value}); + }, + handleTextChange: function(e) { + this.setState({text: e.target.value}); + }, + handleSubmit: function(e) { + e.preventDefault(); + var author = this.state.author.trim(); + var text = this.state.text.trim(); + if( !text || !author ) { + return; + } + this.props.onCommentSubmit({author: author, text: text}) + this.setState({author: '', text: ''}); + }, + render: function() { + return ( +
+ + + +
+ ); + } +}); +var CommentBox = React.createClass({ + getInitialState: function() { + return {data: []}; + }, + loadCommentsFromServer: function() { + $.ajax({ + url: this.props.url, + dataType: 'json', + cache: false, + success: function(data) { + this.setState({data: data}); + }.bind(this), + error: function(xhr, status, err) { + console.error(this.props.url, status, err.toString()); + }.bind(this) + }); + }, + handleCommentSubmit: function(comment) { + var comments = this.state.data; + comment.id = Date.now(); + var newComments = comments.concat([comment]); + this.setState({data: newComments}); + $.ajax({ + url: this.props.url, + method: 'POST', + dataType: 'json', + data: comment, + success: function(data) { + this.setState({data: data}); + }.bind(this), + error: function(xhr, status, err) { + this.setState({data: comments}); + console.error(this.props.url, status, err.toString()); + }.bind(this) + }); + }, + componentDidMount: function() { + this.loadCommentsFromServer(); + setInterval(this.loadCommentsFromServer, this.props.pollInterval); + }, + render: function() { + return ( +
+

Comments

+ + +
+ ); + } +}); +var CommentList = React.createClass({ + render: function() { + var commentNodes = this.props.data.map( function(comment) { + return ( + {comment.text} + ); + }); + return ( +
+ {commentNodes} +
+ ) + } +}); \ No newline at end of file diff --git a/src/main/webapp/js/app.render.js b/src/main/webapp/js/app.render.js new file mode 100644 index 0000000..9709ace --- /dev/null +++ b/src/main/webapp/js/app.render.js @@ -0,0 +1,4 @@ +ReactDOM.render( + , + document.getElementById('content') +); \ No newline at end of file diff --git a/src/main/wro/wro.properties b/src/main/wro/wro.properties new file mode 100644 index 0000000..e82ffed --- /dev/null +++ b/src/main/wro/wro.properties @@ -0,0 +1,8 @@ +debug=true +# Available processors : http://wro4j.readthedocs.org/en/stable/AvailableProcessors/ +preProcessors=lessCssImport +postProcessors=less4j,cssMin +# explicitly invalidates the cache each 5 seconds +cacheUpdatePeriod=5 +# check for changes each 5 seconds and invalidates the cache only when a change is detected +resourceWatcherUpdatePeriod=5 \ No newline at end of file diff --git a/src/main/wro/wro.xml b/src/main/wro/wro.xml new file mode 100644 index 0000000..1cff1a3 --- /dev/null +++ b/src/main/wro/wro.xml @@ -0,0 +1,13 @@ + + + + webjar:bootstrap/@bootstrap.version@/css/bootstrap.css + webjar:jquery/@jquery.version@/jquery.js + webjar:react/@react.version@/react-with-addons.js + webjar:react/@react.version@/react-dom.js + + + + webjar:marked/@marked-lib.version@/marked.js + + \ No newline at end of file diff --git a/src/test/java/com/opengroupe/cloud/saas/rest/CommentControllerTest.java b/src/test/java/com/opengroupe/cloud/saas/rest/CommentControllerTest.java new file mode 100644 index 0000000..f50868f --- /dev/null +++ b/src/test/java/com/opengroupe/cloud/saas/rest/CommentControllerTest.java @@ -0,0 +1,64 @@ +package com.opengroupe.cloud.saas.rest; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockServletContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = MockServletContext.class) +@WebAppConfiguration +public class CommentControllerTest { + + private MockMvc mvc; + + @Before + public void setUp() throws Exception { + mvc = MockMvcBuilders.standaloneSetup(new CommentController()).build(); + + } + + @Test + public void getHello() throws Exception { + mvc.perform(MockMvcRequestBuilders.get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().string(equalTo("Greetings from Spring Boot!"))); + } + + @Test + public void getEmptyComment() throws Exception { + mvc.perform(MockMvcRequestBuilders.get("/api/comments").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(0))); + } + + @Test + public void postComment() throws Exception { + mvc.perform(MockMvcRequestBuilders.post("/api/comments") + .accept(MediaType.APPLICATION_JSON) + .param("id", "1") + .param("author", "Lao Tzu") + .param("text", "The journey of a thousand miles begins with one step")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].id", is(1))) + .andExpect(jsonPath("$[0].author", is("Lao Tzu"))) + .andExpect(jsonPath("$[0].text", is("The journey of a thousand miles begins with one step"))); + } +} diff --git a/src/test/java/com/opengroupe/cloud/saas/web/ViewControllerTest.java b/src/test/java/com/opengroupe/cloud/saas/web/ViewControllerTest.java new file mode 100644 index 0000000..c481380 --- /dev/null +++ b/src/test/java/com/opengroupe/cloud/saas/web/ViewControllerTest.java @@ -0,0 +1,52 @@ +package com.opengroupe.cloud.saas.web; + +import static org.hamcrest.Matchers.equalTo; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; + +import javax.annotation.Resource; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import com.opengroupe.cloud.saas.Application; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = Application.class) +@WebAppConfiguration +public class ViewControllerTest { + private MockMvc mvc; + + @Resource + WebApplicationContext wac; + + @Before + public void setUp() throws Exception { + + // Process mock annotations + MockitoAnnotations.initMocks(this); + + mvc = MockMvcBuilders.webAppContextSetup(wac).build(); + } + + @Test + public void getDefaultGreetings() throws Exception { + mvc.perform(MockMvcRequestBuilders.get("/greeting")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.TEXT_HTML_VALUE + ";charset=UTF-8")) + .andExpect(model().attribute("name", equalTo("World"))) + .andExpect(view().name("greeting")); + } +} diff --git a/src/test/resources/jasmine/ReactJsSpecRunner.htmltemplate b/src/test/resources/jasmine/ReactJsSpecRunner.htmltemplate new file mode 100644 index 0000000..c6bf792 --- /dev/null +++ b/src/test/resources/jasmine/ReactJsSpecRunner.htmltemplate @@ -0,0 +1,35 @@ + + + + + $if(autoRefresh)$ + + $endif$ + Jasmine Spec Runner + + $cssDependencies$ + $javascriptDependencies$ + + + + $allScriptTags$ + + + \ No newline at end of file diff --git a/src/test/resources/jasmine/config.js b/src/test/resources/jasmine/config.js new file mode 100644 index 0000000..f1694e2 --- /dev/null +++ b/src/test/resources/jasmine/config.js @@ -0,0 +1,8 @@ +'use strict'; + +$("body").append("
"); + +$.mockjax({ + url: "/api/comments", + responseText: [] +}); \ No newline at end of file diff --git a/src/test/resources/static/js/app.spec.jsx b/src/test/resources/static/js/app.spec.jsx new file mode 100644 index 0000000..fb96374 --- /dev/null +++ b/src/test/resources/static/js/app.spec.jsx @@ -0,0 +1,80 @@ +'use strict'; + +var AjaxResponses = { + empty: { + success: { + status: 200, + responseText: '[]' + } + }, + submit: { + success: { + status: 200, + responseText: '[{id: 1, author: "Lao Tzu", text: "The journey of a thousand miles begins with one step"}]' + } + } +}; + +var TestUtils = React.addons.TestUtils; + +describe('Comments', function() { + + var instance, + container = document.createElement("div"); + + afterEach(function() { + if (instance && instance.isMounted()) { + // Only components with a parent will be unmounted + ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(instance).parentNode); + } + }); + + describe("rendered without a container reference", function() { + beforeEach(function() { + instance = TestUtils.renderIntoDocument(); + }); + + it("should render a heading with the given text", function() { + var heading = TestUtils.findRenderedDOMComponentWithTag(instance, "h1"); + expect(ReactDOM.findDOMNode(heading).textContent).toBe("Comments"); + }); + }); + + describe("with a container reference required", function() { + var inputs, + form; + + beforeEach(function() { + instance = ReactDOM.render(React.createElement(CommentBox, {"url": "/api/comments", "pollInterval": 200000}), container); + + this.eventSpy = jasmine.createSpy(); + container.addEventListener("broadcast", this.eventSpy, false); + inputs = TestUtils.scryRenderedDOMComponentsWithTag(instance, 'input'); + form = TestUtils.findRenderedDOMComponentWithTag(instance, 'form') + }); + + afterEach(function() { + container.removeEventListener("broadcast", this.eventSpy, false); + }); + + it("should send comment and retrieve a list", function() { + $.mockjax({ + url: "/api/comments", + responseText: AjaxResponses.submit.success + }); + + var name = inputs[0], + text = inputs[1]; + + TestUtils.Simulate.change(name, { target: { value: 'Lao Tzu' } }); + TestUtils.Simulate.change(text, { target: { value: 'The journey of a thousand miles begins with one step' } }); + + TestUtils.Simulate.submit(form); + var comments = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'comment'); + expect(comments.length).toBe(1); + expect(TestUtils.findRenderedDOMComponentWithClass(instance, 'commentAuthor').textContent).toBe('Lao Tzu'); + expect(TestUtils.findRenderedDOMComponentWithTag(instance, 'span').textContent.trim()).toBe('The journey of a thousand miles begins with one step'); + }); + }); + +}); \ No newline at end of file diff --git a/src/test/resources/static/js/fake.spec.js b/src/test/resources/static/js/fake.spec.js new file mode 100644 index 0000000..be989a3 --- /dev/null +++ b/src/test/resources/static/js/fake.spec.js @@ -0,0 +1,8 @@ +describe('Fake', function() { + + describe('fake()', function() { + it("contains spec with an expectation", function() { + expect(true).toBe(true); + }); + }); +}); \ No newline at end of file