From 8aba854b113432215179d35937f3e0e2355c4e45 Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Thu, 3 Dec 2015 10:48:34 -0500 Subject: [PATCH] Add HTTP POST to the events services --- ribbon/README.md | 54 +++++-- .../examples/netflix/ribbon/events/Event.java | 37 +++++ .../netflix/ribbon/events/EventsResource.java | 62 +++---- .../frontend/src/main/resources/css/app.css | 46 +++++- ribbon/frontend/src/main/resources/js/app.js | 151 +++++++++++++++--- 5 files changed, 272 insertions(+), 78 deletions(-) create mode 100644 ribbon/events/src/main/java/org/wildfly/swarm/examples/netflix/ribbon/events/Event.java diff --git a/ribbon/README.md b/ribbon/README.md index 83d7025b3..c6f4c3f73 100644 --- a/ribbon/README.md +++ b/ribbon/README.md @@ -1,15 +1,12 @@ # Multiple services plus NetFlixOSS Ribbon -> NOTE: This example will not work yet, as it relies upon a patch -> in the upstream of WildFly not yet available in a released version. - > Please raise any issues found with this example in this repo: > https://github.com/wildfly-swarm/wildfly-swarm-examples > > Issues related to WildFly Swarm core should be raised in the main repo: > https://github.com/wildfly-swarm/wildfly-swarm/issues -The beginnings of a multi-service example. +## Services Two services exist: @@ -20,27 +17,54 @@ The `time` service simply returns the current time as a JSON map with fields for hour, minute, second, etc. The `events` service queries the `time` service, and returns a list of -currently on-going events. Currently, it just generates a list of events -that started at the top of the current hour. +currently on-going events. + +Each of these services may be accessed with a simple HTTP `GET` request. +For example, the `time` service: + + $ curl http://127.0.0.1:8081 + {"s":5,"D":3,"ms":647,"tz":"America/New_York","h":10,"Y":2015,"M":12,"m":37} + +In addition, the `event` service will accept an HTTP `POST` request with JSON data +specifying the event type. Every `GET` or `POST` request to the `event` service +generates a new event. + +## Front End + +There is a simple JAX-RS front end that uses the `ribbon-webapp` fraction to +communicate with the services. The web site displays the current Ribbon topology, +and provides a simple button-based UI to `GET` or `POST` messages to the Ribbon +services. + +## Try it Out -Build and run the time service: +First, start up the front end, so you can watch the Ribbon topology get updated +in real time. Open a terminal window. + + $ cd frontend + $ mvn wildfly-swarm:run + +Then open a browser to `http://127.0.0.1:8080`. There will be nothing to see there +yet. Leave this window open and visible while you bring up the two services. + +Open another terminal window to build and run the time service: $ cd time $ mvn -Djboss.http.port=8081 wildfly-swarm:run -Maybe run it twice: +You should see the Ribbon topology update in the browser as the time service +comes up. Now, open another terminal window, and run the `time` service again, +but this time give it a different port number. Notice how the web UI updates +itself as the service comes up. $ cd time $ mvn -Djboss.http.port=8082 wildfly-swarm:run -Then run the events service, which consumes the time service(s): +Finally, open yet another terminal window and run the events service, +which consumes the time service(s). Again, note the UI changes. $ cd events $ mvn wildfly-swarm:run -Then - -* http://localhost:8080/ - -Kill and restart one or both of the `time` services, and witness how stuff -behaves. +Now you can kill and restart one or both of the `time` services, and witness the +UI changes. You can also `GET` time and event service data, and `POST` new events. diff --git a/ribbon/events/src/main/java/org/wildfly/swarm/examples/netflix/ribbon/events/Event.java b/ribbon/events/src/main/java/org/wildfly/swarm/examples/netflix/ribbon/events/Event.java new file mode 100644 index 000000000..aaf37291f --- /dev/null +++ b/ribbon/events/src/main/java/org/wildfly/swarm/examples/netflix/ribbon/events/Event.java @@ -0,0 +1,37 @@ +package org.wildfly.swarm.examples.netflix.ribbon.events; + +import java.util.Map; + +/** + * @author Lance Ball + */ +public class Event { + private int id; + private String name; + private Map timestamp; + + + public Map getTimestamp() { + return timestamp; + } + + public void setTimestamp(Map timestamp) { + this.timestamp = timestamp; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/ribbon/events/src/main/java/org/wildfly/swarm/examples/netflix/ribbon/events/EventsResource.java b/ribbon/events/src/main/java/org/wildfly/swarm/examples/netflix/ribbon/events/EventsResource.java index 30d2df706..ccfbeb919 100644 --- a/ribbon/events/src/main/java/org/wildfly/swarm/examples/netflix/ribbon/events/EventsResource.java +++ b/ribbon/events/src/main/java/org/wildfly/swarm/examples/netflix/ribbon/events/EventsResource.java @@ -4,22 +4,18 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; -import com.netflix.ribbon.Ribbon; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufInputStream; import rx.Observable; -import javax.ws.rs.GET; -import javax.ws.rs.OPTIONS; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; +import javax.ws.rs.*; import javax.ws.rs.container.AsyncResponse; import javax.ws.rs.container.Suspended; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.io.IOException; import java.util.ArrayList; -import java.util.List; +import java.util.Date; import java.util.Map; /** @@ -29,17 +25,29 @@ public class EventsResource { private final TimeService time; + private static final ArrayList EVENTS = new ArrayList<>(); public EventsResource() { - //this.time = Ribbon.from( TimeService.class ); this.time = TimeService.INSTANCE; } @GET @Produces(MediaType.APPLICATION_JSON) public void get(@Suspended final AsyncResponse asyncResponse) { - Observable obs = this.time.currentTime().observe(); + Event event = new Event(); + event.setName("GET"); + recordEvent(event, asyncResponse); + } + + @POST + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + public void post(Event event, @Suspended final AsyncResponse asyncResponse) { + recordEvent(event, asyncResponse); + } + private void recordEvent(Event event, @Suspended AsyncResponse asyncResponse) { + Observable obs = this.time.currentTime().observe(); obs.subscribe( (result) -> { try { @@ -48,44 +56,20 @@ public void get(@Suspended final AsyncResponse asyncResponse) { JsonFactory factory = new JsonFactory(); JsonParser parser = factory.createParser(new ByteBufInputStream(result)); Map map = reader.readValue(parser, Map.class); - int hour = (int) map.get( "h" ); - int minute = (int) map.get( "m" ); - int millis = (int) map.get( "ms" ); - String tz = (String) map.get( "tz" ); - List events = new ArrayList<>(); - for ( int i = 1 ; i <= 10 ; ++i ) { - StringBuffer buffer = new StringBuffer("Event #") - .append(i) - .append(" at ") - .append(hour) - .append(":") - .append(minute) - .append(".") - .append(millis) - .append(" ") - .append(tz); - - events.add( buffer.toString() ); - } - asyncResponse.resume(events); + event.setTimestamp(map); + event.setId(EVENTS.size()); + EVENTS.add(event); + asyncResponse.resume(EVENTS); } catch (IOException e) { + System.err.println("ERROR: " + e.getLocalizedMessage()); asyncResponse.resume(e); } }, (err) -> { + System.err.println("ERROR: " + err.getLocalizedMessage()); asyncResponse.resume(err); }); - } - @OPTIONS - @Path("{path : .*}") - public Response options() { - return Response.ok("") - .header("Access-Control-Allow-Origin", "*") - .header("Access-Control-Allow-Headers", "origin, content-type, accept, authorization") - .header("Access-Control-Allow-Credentials", "true") - .header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD, undefined") - .header("Access-Control-Max-Age", "1209600") - .build(); + System.out.println("New event"); } } diff --git a/ribbon/frontend/src/main/resources/css/app.css b/ribbon/frontend/src/main/resources/css/app.css index 5aa0d299c..45b9a0293 100644 --- a/ribbon/frontend/src/main/resources/css/app.css +++ b/ribbon/frontend/src/main/resources/css/app.css @@ -4,13 +4,55 @@ body { } .service { + min-width: 500px; + display: block; + float: left; + margin: 1ex; } .service h2 { border-bottom: 1px dotted; - margin: 1ex; + margin: 1ex 0; + padding: 0px; } .service .service-address { - margin: 1ex 2ex; + margin: 0px; +} + +.btn { + -webkit-border-radius: 6; + -moz-border-radius: 6; + border-radius: 6px; + font-family: Arial; + color: #ffffff; + font-size: 16px; + background: #286e96; + padding: 8px 12px 8px 12px; + text-decoration: none; } + +.btn:hover { + background: #146ec8; + text-decoration: none; + cursor: pointer; + cursor: hand; +} + +ul.event { + border: 1px solid #ccc; + padding: 1ex; + list-style: none; +} + +ul.event.timestamp { + border: none; + padding: 0; +} + +.timestamp { + border: 1px solid #aaa; + padding: 1ex; + margin: 1ex 0; +} + diff --git a/ribbon/frontend/src/main/resources/js/app.js b/ribbon/frontend/src/main/resources/js/app.js index 4afd3436f..93e6ba684 100644 --- a/ribbon/frontend/src/main/resources/js/app.js +++ b/ribbon/frontend/src/main/resources/js/app.js @@ -34,10 +34,10 @@ var Topology = React.createClass({ ); } return ( -
+

Service Topology

{services} -
+
); } }); @@ -49,49 +49,156 @@ var Service = React.createClass({
); }); + + if (this.props.service === 'time') { + return ( +
+ +
+ ); + } else { + return ( +
+ +
+ ); + } + } +}); + +function formatTime(obj) { + return obj['h'] + ':' + obj['m'] + ':' + obj['s'] + '.' + obj['ms'] + ' on ' + + obj['Y'] + '-' + obj['M'] + '-' + obj['D'] + ' '; +} + +var TimeService = React.createClass({ + getInitialState: function() { + return {responses: []}; + }, + + updatePanel: function(response) { + console.log(response); + this.setState({ + responses: this.state.responses.concat([response]) + }); + }, + + render: function() { return ( -
-

{this.props.service}

- {addresses} - +
+

Time Service

+
+

Endpoints

+ {this.props.endpoints} +
+ + {this.state.responses.reverse().map(function(response) { + return ( + + ); + })}
); } }); -var DataButton = React.createClass({ +var TimeStamp = React.createClass({ + render: function() { + return ( +
+ {formatTime(this.props.timestamp)} +
+ ); + } +}); + +var EventService = React.createClass({ getInitialState: function() { - return {response: ''}; + return {response: []}; + }, + + updatePanel: function(response) { + console.log(response); + this.setState({ + response: response + }); }, + render: function() { + return ( +
+

Event Service

+
+

Endpoints

+ {this.props.endpoints} +
+ + + + {this.state.response.reverse().map(function(evt) { + return ( + + ); + })} + +
+ ); + } +}); + +var Event = React.createClass({ + render: function() { + return ( +
    +
  • Event ID: {this.props.event.id}
  • +
  • Event Type: {this.props.event.name}
  • +
  • Timestamp:
  • +
+ ); + } +}); + +var GetJSONButton = React.createClass({ handleClick: function(event) { - var button = this; - console.log("Calling service: " + this.props.serviceName); + console.log("GET service: " + this.props.serviceName); Ribbon.getJSON(this.props.serviceName) - .promise - .then(function(response) { - console.log("Got response ") - console.log(response); - button.setState({ - response: JSON.stringify(response) - }); - }); + .then(this.props.responseHandler); }, render: function() { return (

- Click to request {this.props.serviceName} -

-

- {this.state.response} + + Click to GET {this.props.serviceName} +

); } }); +var PostJSONButton = React.createClass({ + handleClick: function(event) { + console.log("POST service: " + this.props.serviceName); + Ribbon.postJSON(this.props.serviceName, {name: 'User POST'}) + .then(this.props.responseHandler); + }, + + render: function() { + return ( +
+

+ + Click to post a new item to {this.props.serviceName} + +

+
+ ); + } +}); + var Address = React.createClass({ render: function() { return (