forked from eclipse-vertx/vert.x
-
Notifications
You must be signed in to change notification settings - Fork 0
/
js_web_tutorial.html
415 lines (387 loc) · 25.4 KB
/
js_web_tutorial.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<link rel="stylesheet/less" href="bootstrap/less/bootstrap.less">
<script src="less-1.2.1.min.js"></script>
<link href="google-code-prettify/prettify.css" type="text/css" rel="stylesheet"/>
<script type="text/javascript" src="google-code-prettify/prettify.js"></script>
<link href="css/vertx.css" type="text/css" rel="stylesheet"/>
<link href="css/sunburst.css" type="text/css" rel="stylesheet"/>
<title>Vert.x Web Application Tutorial</title>
<script>
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-30144458-1']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
</head>
<body onload="prettyPrint()">
<div class="navbar navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<a class="btn btn-navbar" data-toggle="collapse"
data-target=".nav-collapse">
<span class="i-bar"></span>
<span class="i-bar"></span>
<span class="i-bar"></span>
</a>
<a class="brand" href="/">vert.x</a>
<div class="nav-collapse">
<ul class="nav">
<li class="active"><a href="/">Home</a></li>
<li><a href="https://github.com/purplefox/vert.x/downloads">Download</a></li>
<li><a href="install.html">Install</a></li>
<li><a href="tutorials.html">Tutorials</a></li>
<li><a href="examples.html">Examples</a></li>
<li><a href="docs.html">Documentation</a></li>
<li><a href="https://github.com/purplefox/vert.x">Source</a></li>
<li><a href="http://groups.google.com/group/vertx">Google Group</a></li>
<li><a href="community.html">Community</a></li>
<li><a href="http://vertxproject.wordpress.com/">Blog</a></li>
</ul>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="span12">
<div class="well">
<h1 style="font-size: 35px;">JavaScript Web Application Tutorial</h1>
</div>
</div>
</div>
<div class="row">
<div class="span12">
<div class="well">
<div>
<p>In this tutorial we're going to write a 'real-time' web-application using vert.x.</p>
<p>The application is a shop application called "vToons" which allows you to buy music tracks online.</p>
<p>The application architecture is very simple; it consists of a modern client-side JavaScript MVVM (MVC) application which communicates using the Vert.x event bus with a persistor module on the server. The persistor is used to store the music catalogue and also for persisting orders.</p>
<p>It also consists of a web server module on the server which serves the static resource - html, css and js files, and which bridges the server side event bus with client side JavaScript.</p>
<p><strong>The application does not require any custom server side modules to be written</strong>. You just write your client side JavaScript and it communicates with the persistor as if it was running locally.</p>
<p>The only thing that needs to be written on the server is a simple script which tells Vert.x to start the persistor and the web server. The script can also contain configuration needed for the application.</p>
<p>In this version of this tutorial we've written the script in JavaScript. If you'd prefer to write it in Ruby or Groovy, please see the Ruby or Groovy version of this tutorial.</p>
<p>If you'd rather just run the complete code, the working example is present in the <code>webapp</code> directory of the examples in the distribution. Read the README there for instructions on how to run it.</p>
<p>You can also see the code in <a href="https://github.com/purplefox/vert.x/tree/master/src/examples/javascript/webapp">github</a>.</p>
<h2 id="step-1-install-vertx">Step 1. Install vert.x</h2><br/>
<p>If you haven't yet installed vert.x, <a href="install.html">do that now</a>. </p>
<p>The rest of the tutorial will assume you have installed vert.x in directory <code>VERTX_HOME</code>.</p>
<h2 id="step-2-get-the-web-server-running">Step 2. Get the Web Server running</h2><br/>
<p>We're going to need a web server to serve the static resources. Let's get one running. With Vert.x you can write a web server in just a few lines of code, but we're not going to do that. Instead Vert.x ships with an out-of-the-box web server module, so we'll just use that.</p>
<p>We'll create a script which starts the web server module and also contains the configuration.</p>
<p>Open a text editor and copy the following into it:</p>
<pre class="prettyprint">load('vertx.js');
var webServerConf = {
port: 8080,
host: 'localhost'
};
// Start the web server, with the config we defined above
vertx.deployVerticle('web-server', webServerConf);
</pre>
<p>The call to <code>deployVerticle</code> tells Vert.x to deploy an instance of the <code>web-server</code> module. For detailed information on the <code>web-server</code> module please see the modules manual.</p>
<p>Save it as <code>app.js</code>.</p>
<p>Now, create a directory called <code>web</code> with a file <code>index.html</code> in it:</p>
<pre class="prettyprint">mkdir web
echo "<html><body>Hello World</body></html>" > web/index.html
</pre>
<p>And run the web server:</p>
<pre class="prettyprint">vertx run app.js
</pre>
<p>Point your browser at <code>http://localhost:8080</code>. You should see a page returned with 'Hello World'. <br/>
</p>
<p>That's the web server done.</p>
<h2 id="step-3-serve-the-client-side-app">Step 3. Serve the client-side app</h2><br/>
<p>Now we have a working web server, we need to serve the actual client side app.</p>
<p>For this demo, we've written it using <a href="http://knockoutjs.com/">knockout.js</a> and <a href="http://twitter.github.com/bootstrap/">Twitter bootstrap</a>, but in your apps you can use whatever client side toolset you feel most comfortable with (e.g. jQuery, backbone.js, ember.js or whatever). Any client side JavaScript will do.</p>
<p>The purpose of this tutorial is not to show you how knockout.js or Twitter bootstrap works so we won't delve into the client app in much detail.</p>
<p>Copy the client side application from the vert.x installation into our web directory as follows:</p>
<pre class="prettyprint">cp -r $VERTX_HOME/examples/javascript/webapp/web/* web
</pre>
<p>Open the file <code>web/js/client_app.js</code> in your text editor, and edit the line:</p>
<pre class="prettyprint">var eb = new vertx.EventBus('https://localhost:8080/eventbus');
</pre>
<p>So it reads:</p>
<pre class="prettyprint">var eb = new vertx.EventBus('http://localhost:8080/eventbus');
</pre>
<p>Now, refresh your browser. The client application should now be served.</p>
<p>Of course, it won't do anything useful yet, since we haven't connected it up to anything, but you should at least see the layout. It should look like this: </p>
<p><img alt="Client Application" src="tutorial_1.png"/></p>
<p>Take some time to click around the app. It's pretty self explanatory.</p>
<p>In the centre there's a set of tabs which let you flick between the shop, and your cart.</p>
<p>On the left hand bar there's a form which allows you to login. <br/>
</p>
<h3 id="step-4-get-the-persistor-up-and-running">Step 4. Get the Persistor up and running</h3><br/>
<p>Vert.x ships with an out of the box bus module (busmod) called <code>mongo-persistor</code>. A busmod is a component which communicates with other components on the vert.x event bus by exchanging JSON messages.</p>
<p>The <code>mongo-persistor</code> busmod allows you to store/update/delete/find data in a MongoDB database. (For detailed info on it, please see the modules manual).</p>
<p>We're going to use a persistor in our application for a few different things:</p>
<ul>
<li>Storing the catalogue of track data.</li>
<li>Storing usernames and passwords of users</li>
<li>Storing orders</li>
</ul>
<p>Add a line to <code>app.js</code> which starts the persistor, so the file now looks like:</p>
<pre class="prettyprint">load('vertx.js');
var webServerConf = {
port: 8080,
host: 'localhost'
};
// Start a MongoDB persistor module
vertx.deployVerticle('mongo-persistor');
// Start the web server, with the config we defined above
vertx.deployVerticle('web-server', webServerConf);
</pre>
<p>Of course you'll also need to make sure you have installed a MongoDB instance on the local machine, with default settings.</p>
<p>Now CTRL-C the web server you started earlier and run <code>app.js</code> with </p>
<pre class="prettyprint">vertx run app.js
</pre>
<p>The persistor and web server should be running and it should serve the client application as before.</p>
<h2 id="step-5-connecting-up-the-client-side-to-the-event-bus">Step 5. Connecting up the client side to the Event Bus</h2><br/>
<p>So far we have a web server running, and a server side persistor listening on the event bus, but not doing anything.</p>
<p>We need to connect up the client side so it can interact with the persistor via the event bus.</p>
<p>To that we use a SockJS bridge.</p>
<p>SockJS is a technology which allows a full-duplex WebSocket-like connection between browsers and servers, even if the browser or network doesn't support websockets.</p>
<p>You can create a SockJS server manually in the Vert.x API (see the core manual for more information on this), but the web-server module contains bridge functionality built in, so we're just going to tell it to activate the bridge. This is done in the configuration we specify to the web server.</p>
<p>Edit the web server configuration so it looks like:</p>
<pre class="prettyprint">var webServerConf = {
port: 8080,
host: 'localhost',
bridge: true,
permitted: [
{
address : 'vertx.mongopersistor',
match : {
action : 'find',
collection : 'albums'
}
}
]
};
</pre>
<p>Setting the <code>bridge</code> field to <code>true</code> tells the web server to also act like an event bus bridge as well as serving static files.</p>
<p>The other new thing here is a <code>permitted</code> field. This is an array of JSON objects which determine which event bus messages we're going to allow through from the client side. The bridge basically acts like a firewall and only allows through those messages from the client that we want to come through.</p>
<p>If we allowed the client to send any messages to the persistor, it would be able to do things like delete all data in the database, or perhaps view data it is not entitled to see.</p>
<p>(For detailed information on how the firewall works, please see the core documentation.)</p>
<p>Initially, we only want to allow through requests to the persistor to load the album data. This will be used by the client side application to display the list of available items to buy.</p>
<p>The above configuration will only allow messages from the client that are addressed to <code>vertx.mongopersistor</code> (that's the event bus address of the MongoDB persistor), and which have a field <code>action</code> with a value <code>find</code>, and a field <code>collection</code> with a value <code>albums</code>.</p>
<p>Save the file.</p>
<h2 id="step-6-inserting-the-static-data">Step 6. Inserting the Static Data</h2><br/>
<p>We're almost at the point where the client side app can see the catalogue data. But first we need to insert some static data.</p>
<p>To do this we need a script which inserts catalogue and other data needed by the application in the database. It does this by sending JSON messages on the event bus to the persistor.</p>
<p>Copy <code>static_data.js</code> into your directory as follows:</p>
<pre class="prettyprint">cp $VERTX_HOME/examples/javascript/webapp/static_data.js .
</pre>
<p>We want to insert the static data only after the persistor verticle has completed starting up so we edit <code>app.js</code> as follows:</p>
<pre class="prettyprint">vertx.deployVerticle('mongo-persistor', null, 1, function() {
load('static_data.js');
});
</pre>
<p>The function that we're specifying in the call to <code>deployVerticle</code> won't be invoked until the persistor is fully started. In that function we just load the static data script.</p>
<p>Save the edited <code>app.js</code> and restart it.</p>
<pre class="prettyprint">vertx run app.js
</pre>
<p>Refresh your browser.</p>
<p>You should now see the catalogue displayed in the client side app:</p>
<p><img alt="Client Application" src="tutorial_2.png"/><br/>
</p>
<p>Now there is some stuff to buy, you should be able to add stuff to your cart, and view the contents of your cart by clicking on the cart tab.</p>
<h2 id="step-7-requesting-data-from-the-server">Step 7. Requesting data from the server</h2><br/>
<p>As previously mentioned, this isn't a tutorial on how to write a knockout.js client-side application, but let's take a quick look at the code in the client side app that requests the catalogue data and populates the shop.</p>
<p>The client side application JavaScript is contained in the file <code>web/js/client_app.js</code>. If you open this in your text editor you will see the following line, towards the top of the script:</p>
<pre class="prettyprint">var eb = new vertx.EventBus('http://localhost:8080/eventbus');
</pre>
<p>This is using the <code>vertxbus.js</code> library to create an <code>EventBus</code> object. This object is then used to send and receive messages from the event bus.</p>
<p>If you look a little further down the script, you will find the part which loads the catalogue data from the server and renders it:</p>
<pre class="prettyprint">eb.onopen = function() {
// Get the static data
eb.send('vertx.mongopersistor', {action: 'find', collection: 'albums', matcher: {} },
function(reply) {
if (reply.status === 'ok') {
var albumArray = [];
for (var i = 0; i < reply.results.length; i++) {
albumArray[i] = new Album(reply.results[i]);
}
that.albums = ko.observableArray(albumArray);
ko.applyBindings(that);
} else {
console.error('Failed to retrieve albums: ' + reply.message);
}
});
};
</pre>
<p>}; </p>
<p>The <code>onopen</code> is called when, unsurprisingly, the event bus connection is fully setup and open.<br/>
</p>
<p>At that point we are calling the <code>send</code> function on the event bus to a send a JSON message to the address <code>vertx.mongopersistor</code>. This is the address of the MongoDB persistor busmod that we configured earlier.</p>
<p>The JSON message that we're sending specifies that we want to find and return all albums in the database. (For a full description of the operations that the MongoDBPersistor busmod expects you can consult the modules manual).</p>
<p>The final argument that we pass to to <code>send</code> is a reply handler. This is a function that gets called when the persistor has processed the operation and sent the reply back here. The first argument to the reply handler is the reply itself.</p>
<p>In this case, the reply contains a JSON message with a field <code>results</code> which contains a JSON array containing the albums.</p>
<p>Once we get the albums we give them to knockout.js to render on the view.</p>
<h2 id="step-8-handling-login">Step 8. Handling Login</h2><br/>
<p>In order to actually send an order, you need to be logged in.</p>
<p>To handle login we will start an instance of the out-of-the-box <code>auth-mgr</code> component. This is a simple busmod which handles simple user/password authentication and authorisation. Users credentials are stored in the MongoDB database. Fore more sophisticated auth, you can easily write your own auth busmod and the bridge can talk to that instead.</p>
<p>To login, the client sends a message on the event bus to the address <code>vertx.basicauthmanager.login</code> with fields <code>username</code> and <code>credentials</code>, and if successful it replies with a message containing a unique session id, in the <code>sessionID</code> field.</p>
<p>This session id should then be sent in any subsequent message from the client to the server that requires authentication (e.g. persisting an order).</p>
<p>When the bridge receives a message with a <code>sessionID</code> field in it, it will contact the auth manager to see if the session is authorised for that resource.</p>
<p>Let's add a line to start the <code>auth-mgr</code>:</p>
<p>Edit <code>app.js</code> and add the following, just after where the Mongo Persistor is deployed.</p>
<pre class="prettyprint">// Deploy an auth manager to handle the authentication
vertx.deployVerticle('auth-mgr');
</pre>
<p>We'll also need to tell the bridge to let through any login messages:</p>
<pre class="prettyprint">permitted: [
// Allow calls to login and authorise
{
address: 'vertx.basicauthmanager.login'
},
...
</pre>
<p>Save, and restart the app.</p>
<p>You can test login by attempting to log-in with username <code>tim</code> and password <code>password</code>. A message should appear on the left telling you you are logged in!.</p>
<p><img alt="Client Application" src="tutorial_3.png"/></p>
<p>Let's take a look at the client side code which does the login.</p>
<p>Open <code>web/js/client_app.js</code> and scroll down to the <code>login</code> function. This gets trigged by knockout when the login button is pressed on the page.</p>
<pre class="prettyprint">eb.send('vertx.bridge.login', {username: that.username(), password: that.password()}, function (reply) {
if (reply.status === 'ok') {
that.sessionID(reply.sessionID);
} else {
alert('invalid login');
}
});
</pre>
<p>As you can see, it sends a login JSON message to the bridge with the username and password.</p>
<p>When the reply comes back with status <code>ok</code>, it stores the session id which causes knockout to display the "Logged in as... " message.</p>
<p>It's as easy as that.</p>
<h2 id="step-9-persisting-orders">Step 9. Persisting Orders</h2><br/>
<p>Persisting an order is equally simple. We just send a message to the MongoDB persistor component saying we want to store the order.</p>
<p>We also need to tell the bridge to let through requests to persist an order. We also need to add the further constraint that only logged-in users can persist orders.</p>
<p>Edit the web server configuration so it looks like:</p>
<pre class="prettyprint">var webServerConf = {
port: 8080,
host: 'localhost',
bridge: true,
// This defines which messages from the client we will let through
// from the client
permitted: [
// Allow calls to get static album data from the persistor
{
address : 'vertx.mongopersistor',
match : {
action : 'find',
collection : 'albums'
}
},
{
address : 'vertx.mongopersistor',
requires_auth : true, // User must be logged in to send let these through
match : {
action : 'save',
collection : 'orders'
}
}
]
};
</pre>
<p>Setting the <code>requires_auth</code> field to <code>true</code> means the bridge will only let through the message if the user is logged in.</p>
<p>Ok, let's take a look at the client side code which sends the order.</p>
<p>Open up <code>web/js/client_app.js</code> again, and look for the function <code>submitOrder</code>.</p>
<pre class="prettyprint">that.submitOrder = function() {
if (!orderReady()) {
return;
}
var orderItems = ko.toJS(that.items);
var orderMsg = {
sessionID: that.sessionID(),
action: "save",
collection: "orders",
document: {
username: that.username(),
items: orderItems
}
}
eb.send('vertx.mongopersistor', orderMsg, function(reply) {
if (reply.status === 'ok') {
that.orderSubmitted(true);
// Timeout the order confirmation box after 2 seconds
// window.setTimeout(function() { that.orderSubmitted(false); }, 2000);
} else {
console.error('Failed to accept order');
}
});
};
</pre>
<p>This function converts the order into a JSON object, then calls <code>send</code> on the event bus to send it to the database where it will get persisted.</p>
<p>Notice that we add a <code>sessionID</code> field to the message with the session id that was returned when we logged in. The bridge requires this field to be set with a valid session id or the message will not make it through the bridge firewall, since we set the <code>requires_auth</code> field to true in the server side config.</p>
<p>When the reply comes back we tell knockout to display a message.</p>
<p>Everything should be in order, so restart the app again:</p>
<pre class="prettyprint">vertx run app.js
</pre>
<p>Refresh the browser.</p>
<p>Now log-in and add a few items into your cart. Click to the cart tab and click "Submit Order". The message "Your order has been accepted, an email will be on your way to you shortly" should be displayed!</p>
<p>Take a look in the console window of the application. You should see the order has been logged.</p>
<p><img alt="Client Application" src="tutorial_4.png"/></p>
<p><strong> Congratulations! You have just placed an order. </strong></p>
<h2 id="step-11-securing-the-connection">Step 11. Securing the Connection</h2><br/>
<p>So far in this tutorial, all client-server traffic has been over an unsecured socket. That's not a very good idea in a real application since we're sending login credentials and orders.</p>
<p>Configuring Vert.x to use secure sockets is very easy. (For detailed information on configuring HTTPS, please
see the manual).</p>
<p>Edit <code>app.js</code> again, and add the field <code>ssl</code> in the web server config, with the value <code>true</code>.</p>
<pre class="prettyprint">var webServerConf = {
port: 8080,
host: 'localhost',
ssl: true,
bridge: true,
...
</pre>
<p>You'll also need to provide a key store. The keystore is just a Java keystore which contains the certificate for the server. It can be manipulated using the Java <code>keytool</code> command. <br/>
</p>
<p>Copy the keystore from the distribution</p>
<pre class="prettyprint">cp $VERTX_HOME/examples/javascript/webapp/server-keystore.jks .
</pre>
<p>You'll also need to edit <code>web/js/client_app.js</code> so the line which creates the client side event bus instance now connects using <code>https</code>, not <code>http</code>.</p>
<pre class="prettyprint">var eb = new vertx.EventBus('https://localhost:8080/eventbus');
</pre>
<p>Now restart the app again.</p>
<pre class="prettyprint">vertx run app.js
</pre>
<p>And go to your browser. This time point your browser at <code>https://localhost:8080</code>. <em>Note it is <strong>https</strong> not http</em>.</p>
<p><em>You'll initially get a warning from your browser saying the server certificate is unknown. This is to be expected since we haven't told the browser to trust it. You can ignore that for now. On a real server your server cert would probably be from a trusted certificate authority.</em></p>
<p>Now login, and place an order as before.</p>
<p>Easy peasy. <strong>It just works</strong></p>
<h2 id="step-12-scaling-the-application">Step 12. Scaling the application</h2><br/>
<h3 id="scaling-the-web-server">Scaling the web server</h3><br/>
<p>Scaling up the web server part is trivial. Simply start up more instances of the webserver. You can do this by changing the line that starts the <code>web-server</code> module to something like:</p>
<pre class="prettyprint">// Start 32 instances of the web server!
vertx.deployVerticle('web-server', webServerConf, 32);
</pre>
<p>(<em>Vert.x is clever here, it notices that you are trying to start multiple servers on the same host and port, and internally it maintains a single listening server, but round robins connections between the various instances</em>.)</p>
<h3 id="more-complex-web-applications">More complex web applications</h3><br/>
<p>In this simple web application, there was no need to write any custom server side modules, but in more complex applications you might want to write your own server side services which can be used by clients (or by other server side code).</p>
<p>Doing this with Vert.x is very straightforward. Here's an example of a trivial server side service which listens on the event bus for messages and sends back the current time to the caller:</p>
<pre class="prettyprint">load('vertx.js');
vertx.eventBus.registerHandler("acme.timeService", function(message, replier) {
replier({current_time: new Date().getTime()});
});
</pre>
<p>Save this in <code>time_service.js</code>, and add a line in your <code>app.js</code> to load it on startup.</p>
<p>Then you can just call it from client side JavaScript, or other server side components:</p>
<pre class="prettyprint">eventBus.send("acme.timeService", null, function(reply) {
console.log("Time is " + reply.current_time);
});
</pre>
<h3 id="packaging-up-your-code-as-a-module">Packaging up your code as a Module</h3><br/>
<p>You can package up your entire application, or just individual Verticles as modules, so they can be easily reused by other applications, or started on the command line more easily.</p>
<p>For an explanation of how to do this, please see the modules manual. <br/>
</p>
<p><em>Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically.</em></p></div>
</div>
</div>
</div>
</div>
</body>
</html>