First commit

This commit is contained in:
fecaille
2016-03-09 17:04:45 +01:00
commit 58a1bd06a1
19 changed files with 891 additions and 0 deletions

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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<Comment> comments = new ArrayList<Comment>();
@RequestMapping("/")
public String index() {
return "Greetings from Spring Boot!";
}
@RequestMapping(value="/api/comments", method=RequestMethod.GET)
public @ResponseBody List<Comment> comments() {
return comments;
}
@RequestMapping(value="/api/comments", method=RequestMethod.POST)
public @ResponseBody List<Comment> 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;
}
}

View File

@@ -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";
}
}

View File

@@ -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}

View File

@@ -0,0 +1,10 @@
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Getting Started: Serving Web Content</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p th:text="'Hello, ' + ${name} + '!'" />
</body>
</html>

View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head lang="en">
<meta charset="UTF-8"/>
<title>ReactJS</title>
<link rel="stylesheet" href="/css/react-bootstrap.css" />
</head>
<body>
<div id="content"></div>
<script src="/js/react-bootstrap.js"></script>
<script src="/js/bundle.js"></script>
<script src="/js/app.js"></script>
<script src="/js/app.render.js"></script>
</body>
</html>

123
src/main/webapp/js/app.jsx Normal file
View File

@@ -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 (
<div className="comment">
<h2 className="commentAuthor">
{this.props.author}
</h2>
<span dangerouslySetInnerHTML={this.rawMarkup()} />
</div>
);
}
});
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 (
<form className="commentForm" onSubmit={this.handleSubmit}>
<input
type="text"
placeholder="Your name"
value={this.state.author}
onChange={this.handleAuthorChange}
/>
<input
type="text"
placeholder="Say something..."
value={this.state.text}
onChange={this.handleTextChange}
/>
<input type="submit" value="Post" />
</form>
);
}
});
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 (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data}/>
<CommentForm onCommentSubmit={this.handleCommentSubmit}/>
</div>
);
}
});
var CommentList = React.createClass({
render: function() {
var commentNodes = this.props.data.map( function(comment) {
return (
<Comment author={comment.author} key={comment.id}>{comment.text}</Comment>
);
});
return (
<div className="commentList">
{commentNodes}
</div>
)
}
});

View File

@@ -0,0 +1,4 @@
ReactDOM.render(
<CommentBox url="/api/comments" pollInterval={2000}/>,
document.getElementById('content')
);

View File

@@ -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

13
src/main/wro/wro.xml Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<groups xmlns="http://www.isdc.ro/wro">
<group name="react-bootstrap">
<css>webjar:bootstrap/@bootstrap.version@/css/bootstrap.css</css>
<js>webjar:jquery/@jquery.version@/jquery.js</js>
<js>webjar:react/@react.version@/react-with-addons.js</js>
<js>webjar:react/@react.version@/react-dom.js</js>
</group>
<group name="bundle">
<js>webjar:marked/@marked-lib.version@/marked.js</js>
</group>
</groups>

View File

@@ -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")));
}
}

View File

@@ -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"));
}
}

View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=$sourceEncoding$">
$if(autoRefresh)$
<meta http-equiv="refresh" content="$autoRefreshInterval$">
$endif$
<title>Jasmine Spec Runner</title>
<script type="text/javascript">
window.onerror = function(msg,url,line) {
if (document.head) {
var jserror = document.head.getAttribute('jmp_jserror') || '';
if (jserror) {
jserror += ':!:';
}
jserror += msg;
document.head.setAttribute('jmp_jserror',jserror);
}
};
</script>
$cssDependencies$
$javascriptDependencies$
<script type="text/javascript">
window.onload = jasmine.boot;
</script>
</head>
<body>
$allScriptTags$
<script type="text/javascript">
if(window.location.href.indexOf("ManualSpecRunner.html") !== -1) {
document.body.appendChild(document.createTextNode("Warning: Opening this HTML file directly from the file system is deprecated. You should instead try running `mvn jasmine:bdd` from the command line, and then visit `http://localhost:8234` in your browser. "))
}
</script>
</body>
</html>

View File

@@ -0,0 +1,8 @@
'use strict';
$("body").append("<div id='content'></div>");
$.mockjax({
url: "/api/comments",
responseText: []
});

View File

@@ -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(<CommentBox url="/api/comments" pollInterval={200000}/>);
});
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');
});
});
});

View File

@@ -0,0 +1,8 @@
describe('Fake', function() {
describe('fake()', function() {
it("contains spec with an expectation", function() {
expect(true).toBe(true);
});
});
});