e0aea87f62
pages which give us something useful to link to when we want to point to all of the javadocs for a particular subtree of the Narya packages. git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@5411 542714f4-19e9-0310-aa3c-eee0fc999fb1
607 lines
25 KiB
HTML
607 lines
25 KiB
HTML
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
|
|
<html>
|
|
<head>
|
|
<!-- $Id: package.html 617 2001-11-13 00:12:20Z mdb $ -->
|
|
<link rel="stylesheet" href="../stylesheet.css" type="text/css"/>
|
|
</head>
|
|
|
|
<body bgcolor="white">
|
|
|
|
<h2>Presents Distributed Object System</h2>
|
|
|
|
The Presents Distributed Object System is a framework for distributing
|
|
information between multiple separate applications (over a network) and for
|
|
coordinating control flow between those applications in the form of remote
|
|
procedure call services. The normal configuration of the Presents system is
|
|
client/server; generally with many clients connecting to a single
|
|
server. All information transfer takes place through the server using the
|
|
distributed object system documented below.
|
|
|
|
<ul>
|
|
<li><a href="#distributed_objects">Distributed Objects</a>
|
|
<li><a href="#event_listeners">Event Listeners</a>
|
|
<li><a href="#distributed_collections">Distributed Collections</a>
|
|
<li><a href="#invocation_services">Invocation Services</a>
|
|
<li><a href="#ant_tasks">Ant Tasks</a>
|
|
</ul>
|
|
|
|
<p> <em>A note to the reader:</em> the Presents system is a complex
|
|
one and though a great deal of code is provided in explaining the
|
|
services it provides, it is not the intent that one should start from
|
|
only these examples and build a working system. A better approach is
|
|
to read through this documentation to come to an understanding of the
|
|
concepts and mechanisms that define the system and then take a look at
|
|
some working sample code which is provided in the <code>tests</code>
|
|
directory of this distribution.
|
|
|
|
<h3><a name="distributed_objects">Distributed Objects</a></h3>
|
|
|
|
The Presents services allow applications to access and update shared
|
|
information through a mechanism known as distributed objects.
|
|
Distributed objects are maintainedon the server and clients
|
|
"subscribe" to the objects and are provided with proxy copies which
|
|
are updated by a stream of events sent by the server when any state
|
|
changes in the objects.
|
|
|
|
<p> Clients cannot modify their proxy distributed objects directly,
|
|
instead they make use of setter methods which package up the requested
|
|
change into an event and send that event to the server for processing.
|
|
After performing access control checks, the server will apply the
|
|
event to the primary distributed object instance and then dispatch
|
|
that event to all subscribed clients. Those clients (including the
|
|
original change requesting client) then apply the event to their proxy
|
|
copy of the object and in this way all clients maintain an up to date
|
|
copy of the object's data.
|
|
|
|
<p align="center"> <img src="images/dobject.png">
|
|
|
|
<h4>Defining an object</h4>
|
|
|
|
A distributed object is defined just like a regular Java object and is
|
|
then run through a post-processor which inserts methods and constants
|
|
into the object definition which are needed by the distributed object
|
|
system. Here is a distributed object as originally defined:
|
|
|
|
<pre class="example">
|
|
public class CageObject extends DObject
|
|
{
|
|
/** The number of monkeys in the cage. */
|
|
public int monkeys;
|
|
|
|
/** The name of the owner of this cage. */
|
|
public String owner;
|
|
}</pre>
|
|
|
|
Note that all distributed fields, or attributes (fields in a
|
|
distributed object are frequently referred to as <i>attributes</i> in
|
|
this documentation and elsewhere in the system), are public fields in
|
|
our distributed object. Non-public fields will be ignored by the
|
|
system and not transmitted when a proxy object is delivered over the
|
|
network to a subscriber. Further, fields marked <code>transient</code>
|
|
will also be ignored by the system.
|
|
|
|
<p> We then run our class definition through a post-processor which
|
|
turns it into the following:
|
|
|
|
<pre class="example">
|
|
public class CageObject extends DObject
|
|
{
|
|
<b>// AUTO-GENERATED: FIELDS START
|
|
/** The field name of the <code>monkeys</code> field. */
|
|
public static final String MONKEYS = "monkeys";
|
|
|
|
/** The field name of the <code>owner</code> field. */
|
|
public static final String OWNER = "owner";
|
|
// AUTO-GENERATED: FIELDS END</b>
|
|
|
|
/** The number of monkeys in the cage. */
|
|
public int monkeys;
|
|
|
|
/** The name of the owner of this cage. */
|
|
public String owner;
|
|
|
|
<b>// AUTO-GENERATED: METHODS START
|
|
/**
|
|
* Requests that the <code>monkeys</code> field be set to the
|
|
* specified value. The local value will be updated immediately and an
|
|
* event will be propagated through the system to notify all listeners
|
|
* that the attribute did change. Proxied copies of this object (on
|
|
* clients) will apply the value change when they received the
|
|
* attribute changed notification.
|
|
*/
|
|
public void setMonkeys (int value)
|
|
{
|
|
int ovalue = this.monkeys;
|
|
requestAttributeChange(
|
|
EVEN_BASE, new Integer(value), new Integer(ovalue));
|
|
this.monkeys = value;
|
|
}
|
|
|
|
/**
|
|
* Requests that the <code>owner</code> field be set to the
|
|
* specified value. The local value will be updated immediately and an
|
|
* event will be propagated through the system to notify all listeners
|
|
* that the attribute did change. Proxied copies of this object (on
|
|
* clients) will apply the value change when they received the
|
|
* attribute changed notification.
|
|
*/
|
|
public void setOwner (String value)
|
|
{
|
|
String ovalue = this.owner;
|
|
requestAttributeChange(
|
|
ODD_BASE, value, ovalue);
|
|
this.owner = value;
|
|
}
|
|
// AUTO-GENERATED: METHODS END</b>
|
|
}</pre>
|
|
|
|
The contents of the methods are not too important, the main things to
|
|
note are that setter methods for the two attributes were generated and
|
|
constants were defined that will be used to identify which attribute
|
|
changed if we choose to inspect an event notifying us of such a
|
|
change. Note also that additional methods may be added to a
|
|
distributed object class as long as nothing is modified in the
|
|
<code>AUTO-GENERATED</code> section. As new fields are added and the
|
|
post-processing tool re-run, everything outside the auto-generated
|
|
section will be preserved.
|
|
|
|
<p> One may also notice that attribute change requests result in the
|
|
new value of the attribute being immediately written to the local copy
|
|
of the object. This is a convention that was decided upon after
|
|
repeatedly running into trouble when users of the system would set a
|
|
value in an object and immediately assume it held the new value rather
|
|
than realizing that an event would have to propagate back from the
|
|
server before the value was in fact updated. By setting the value
|
|
immediately, these problems are avoided and the opposite assumption is
|
|
almost never made. This is further justified by the fact that, in
|
|
general, attribute changes never originate on a client but instead
|
|
originate on the server after processing a request from the client
|
|
(via the below documented <a href="#invocation_services">invocation
|
|
services</a>) to do something application-specific that results in one
|
|
or more attribute changes taking place.
|
|
|
|
<p> See the section on <a href="#ant_tasks">Ant Tasks</a> for
|
|
information on how to configure and run this post-processor.
|
|
|
|
<h4>Creating an object</h4>
|
|
|
|
Generally, some entity on the server will choose to create a new
|
|
instance of a distributed object. Rather than simply instantiate the
|
|
object directly, one must create the object through the {@link
|
|
com.threerings.presents.dobj.DObjectManager}:
|
|
|
|
<pre class="example">
|
|
public class ServerEntity implements Subscriber {
|
|
public void init (DObjectManager omgr) {
|
|
omgr.createObject(CageObject.class, this);
|
|
}
|
|
|
|
// inherited from interface Subscriber
|
|
public void objectAvailable (DObject object) {
|
|
// yay! we created our object
|
|
_object = (CageObject)object;
|
|
}
|
|
|
|
// inherited from interface Subscriber
|
|
public void requestFailed (int oid, ObjectAccessException cause) {
|
|
// oh the humanity, we failed to create our object; in
|
|
// general this would only happen if we did something silly like
|
|
// passed in a DObject class that didn't extend DObject
|
|
}
|
|
|
|
protected CageObject _object;
|
|
}</pre>
|
|
|
|
You'll notice that we provide an instance of a <code>Subscriber</code>
|
|
when creating our object. This subscriber instance is in fact
|
|
subscribed to the newly created object in the same manner as is
|
|
described below for all additional subscribers to the object. It is
|
|
possible to instruct an object to automatically destroy itself when
|
|
all subscribers have unsubscribed. (See the not very terse {@link
|
|
com.threerings.presents.dobj.DObject}.setDestroyOnLastSubscriberRemoved()).
|
|
|
|
<h4>Subscribing to an object</h4>
|
|
|
|
<p> The client obtains a proxy of the object by a process called
|
|
subscription, which is accomplished via {@link
|
|
com.threerings.presents.dobj.DObjectManager}.subscribeToObject():
|
|
|
|
<pre class="example">
|
|
public class ObjectUser implements Subscriber {
|
|
public void init (Client client, int objectId) {
|
|
client.getDObjectManager().subscribeToObject(objectId, this);
|
|
}
|
|
|
|
// inherited from interface Subscriber
|
|
public void objectAvailable (DObject object) {
|
|
// yay! we got our object
|
|
_object = (CageObject)object;
|
|
}
|
|
|
|
// inherited from interface Subscriber
|
|
public void requestFailed (int oid, ObjectAccessException cause) {
|
|
// oh the humanity, we failed to subscribe
|
|
}
|
|
|
|
protected CageObject _object;
|
|
}</pre>
|
|
|
|
<p> Later a client would relinquish its subscription to the object
|
|
using a similar mechanism:
|
|
|
|
<pre class="example">
|
|
public class ObjectUser implements Subscriber {
|
|
// ...
|
|
public void shutdown (Client client) {
|
|
client.getDObjectManager().unsubscribeFromObject(
|
|
_object.getOid(), this);
|
|
_object = null;
|
|
}
|
|
// ...
|
|
}</pre>
|
|
|
|
However, this is a fine time to point out the dangers of working in an
|
|
asynchronous distributed environment. There is no guarantee that your
|
|
object subscription request will be completed before the client
|
|
decides to call shutdown() on its <code>ObjectUser</code>. Thus, in
|
|
the previous code, we could get a null pointer exception, and even
|
|
worse, we could remain subscribed to the object even though we didn't
|
|
want to be. To avoid these sorts of problems, the {@link
|
|
com.threerings.presents.util.SafeSubscriber} class is provided:
|
|
|
|
<pre class="example">
|
|
public class ObjectUser implements Subscriber {
|
|
public void init (Client client, int objectId) {
|
|
<b>_safesub = new SafeSubscriber(objectId, this);
|
|
_safesub.subcribe(client.getDObjectManager());</b>
|
|
}
|
|
|
|
// inherited from interface Subscriber
|
|
public void objectAvailable (DObject object) {
|
|
// yay! we got our object
|
|
_object = (CageObject)object;
|
|
}
|
|
|
|
// inherited from interface Subscriber
|
|
public void requestFailed (int oid, ObjectAccessException cause) {
|
|
// oh the humanity, we failed to subscribe
|
|
}
|
|
|
|
public void shutdown (Client client) {
|
|
<b>_safesub.unsubscribe(client.getDObjectManager());</b>
|
|
_object = null;
|
|
}
|
|
|
|
<b>protected SafeSubscriber _safesub;</b>
|
|
protected CageObject _object;
|
|
}</pre>
|
|
|
|
The safe subscriber will pass the object availability on to your
|
|
subscriber and when the time comes to unsubscribe, it will cope with
|
|
the case where the original subscription was not fully processed and
|
|
stick around long enough to ensure that once it is, the request to
|
|
unsubscribe is also dispatched. It will also cope with a request to
|
|
<code>unsubscribe()</code> even if the original subscription request
|
|
failed.
|
|
|
|
<h3><a name="event_listeners">Event Listeners</a></h3>
|
|
|
|
Once a client has subscribed to a distributed object, all events
|
|
pertaining to that object will be delivered to the client. Frequently,
|
|
it is useful to respond dynamically to changes in distributed object
|
|
values and this is accomplished using listeners. A client can register
|
|
any number of listeners on an object and when the object is finally
|
|
unsubscribed from and garbage collected, the listener registrations
|
|
all go away as well.
|
|
|
|
<p> The basic listener is the {@link
|
|
com.threerings.presents.dobj.AttributeChangeListener} which is
|
|
informed of all simple attribute changes (setting a primitive field to
|
|
a new value is called an attribute change). We return to our trusty
|
|
example:
|
|
|
|
<pre class="example">
|
|
public class ObjectUser
|
|
implements Subscriber, <b>AttributeChangeListener</b> {
|
|
// ...
|
|
public void init (Client client, int objectId) {
|
|
_safesub = new SafeSubscriber(_subscriber, objectId);
|
|
_safesub.subcribe(client.getDObjectManager());
|
|
}
|
|
|
|
// inherited from interface Subscriber
|
|
public void objectAvailable (DObject object) {
|
|
// yay! we got our object
|
|
_object = (CageObject)object;
|
|
<b>_object.addListener(this);</b>
|
|
}
|
|
|
|
// inherited from interface Subscriber
|
|
public void requestFailed (int oid, ObjectAccessException cause) {
|
|
// oh the humanity, we failed to subscribe
|
|
}
|
|
|
|
<b>// inherited from interface AttributeChangeListener
|
|
public void attributeChanged (AttributeChangedEvent event)
|
|
{
|
|
System.out.println("Wow! The " + event.getName() +
|
|
" field changed to " + event.getValue() + ".");
|
|
}</b>
|
|
|
|
public void shutdown (Client client) {
|
|
_safesub.unsubscribe(client.getDObjectManager());
|
|
<b>if (_object != null) {
|
|
// removing our listener not necessary as we are
|
|
// unsubscribing, but it's a good habit to develop as
|
|
// frequently listeners will come and go during the
|
|
// lifetime of an object subscription
|
|
_object.removeListener(this);
|
|
_object = null;
|
|
}</b>
|
|
}
|
|
|
|
protected SafeSubscriber _safesub;
|
|
protected CageObject _object;
|
|
}</pre>
|
|
|
|
The <code>attributeChanged()</code> method of our registered listener
|
|
will be called whenever an event is received as a result of one of the
|
|
setter methods being called on the <code>CageObject</code> by
|
|
<em>any</em> participant in the distributed system. The setter creates
|
|
an event which is sent to the server, the server dispatches the event
|
|
to all subscribers of the object and the Presents system dispatches
|
|
the event notification to all registered listeners when the event is
|
|
received on the client. Note that listeners are also used on the
|
|
server as entities on the server also frequently need to respond to
|
|
attribute changes. They are notified immediately after the server has
|
|
dispatched the event (over the network) to all subscribed clients.
|
|
|
|
<p> It is useful to note that listeners are notified of a changed
|
|
attribute <b>after</b> the change has been applied to the object. The
|
|
previous value of the attribute is available through the {@link
|
|
com.threerings.presents.dobj.AttributeChangedEvent#getOldValue}
|
|
method, though in spite of many years of experience using this system
|
|
in a variety of circumstances, we have rarely found that we cared to
|
|
know the previous value.
|
|
|
|
<h3><a name="distributed_collections">Distributed collections</a></h3>
|
|
|
|
One soon discovers that primitive object fields do not make for a very
|
|
useful information distribution mechanism and that more complex data
|
|
structures are necessary. Two collection types, sets and arrays, are
|
|
supported, and a mechanism is provided for allowing whole objects to
|
|
be passed around in toto as if they were a primitive field.
|
|
|
|
<p><b>Distributed Arrays</b><br>
|
|
|
|
Arrays of primitive types can be used in a distributed object and the
|
|
system will detect their use and provide a mechanism for updating the
|
|
entire array and an additional mechanism for updating a single element
|
|
at a time:
|
|
|
|
<pre class="example">
|
|
public class ChessObject extends DObject
|
|
{
|
|
// AUTO-GENERATED: FIELDS START
|
|
/** The field name of the <code>state</code> field. */
|
|
public static final String STATE = "state";
|
|
// AUTO-GENERATED: FIELDS END
|
|
|
|
/** Used to track our board state. */
|
|
public int[] state;
|
|
|
|
// AUTO-GENERATED: METHODS START
|
|
/**
|
|
* Requests that the <code>state</code> field be set to the
|
|
* specified value. The local value will be updated immediately and an
|
|
* event will be propagated through the system to notify all listeners
|
|
* that the attribute did change. Proxied copies of this object (on
|
|
* clients) will apply the value change when they received the
|
|
* attribute changed notification.
|
|
*/
|
|
public void setState (int[] value)
|
|
{
|
|
int[] ovalue = this.state;
|
|
requestAttributeChange(
|
|
STATE, value, ovalue);
|
|
this.state = (value == null) ? null : (int[])value.clone();
|
|
}
|
|
|
|
/**
|
|
* Requests that the <code>index</code>th element of
|
|
* <code>state</code> field be set to the specified value.
|
|
* The local value will be updated immediately and an event will be
|
|
* propagated through the system to notify all listeners that the
|
|
* attribute did change. Proxied copies of this object (on clients)
|
|
* will apply the value change when they received the attribute
|
|
* changed notification.
|
|
*/
|
|
public void setStateAt (int value, int index)
|
|
{
|
|
int ovalue = this.state[index];
|
|
requestElementUpdate(
|
|
STATE, index, new Integer(value), new Integer(ovalue));
|
|
this.state[index] = value;
|
|
}
|
|
// AUTO-GENERATED: METHODS END
|
|
}</pre>
|
|
|
|
To correspond with what is called an "element update" (the
|
|
modification of a single element in an array), there is the {@link
|
|
com.threerings.presents.dobj.ElementUpdateListener}. When an element
|
|
is updated, listeners implementing that interface will be notified.
|
|
Remember that if the whole array is changed using
|
|
<code>setState()</code>, the normal {@link
|
|
com.threerings.presents.dobj.AttributeChangeListener} is the interface
|
|
one uses to hear about it.
|
|
|
|
<p> <em>Note</em> that distributed arrays are <em>not</em>
|
|
automatically resized. If a request is made to update the element at
|
|
index 9 of an array, the array must be of at least size 10 or an array
|
|
index out of bounds exception will be thrown (as should be evident
|
|
from inspecting the code above). For more dynamic collections of
|
|
objects, see the documentation below about distributed sets.
|
|
|
|
<p> This mechanism is not actually limited to arrays of primitive
|
|
types. It also works for arrays of objects that implement the {@link
|
|
com.threerings.io.Streamable} interface which is documented next.
|
|
|
|
<p><b>Streamable and its good friend SimpleStreamableObject</b><br>
|
|
|
|
The {@link com.threerings.io.Streamable} interface is used to mark
|
|
objects that can be sent over the network by using them in distributed
|
|
object fields by using arrays of such objects as a field. This
|
|
interface functions in much the same way that {@link
|
|
java.io.Serializable} does in that it simply marks the class and an
|
|
underlying mechanism uses reflection to actually marshall and
|
|
unmarshall the object on the network. In fact, all
|
|
non-<code>transient</code> fields of a streamable object are included
|
|
during the marhsalling process. Here's an example:
|
|
|
|
<pre class="example">
|
|
public class Player implements Streamable
|
|
{
|
|
/** This player's name. */
|
|
public String name;
|
|
|
|
/** This player's rating. */
|
|
public int rating;
|
|
}
|
|
|
|
public class ChessObject extends DObject
|
|
{
|
|
/** A record for each player in the game. */
|
|
public Player[] players;
|
|
}</pre>
|
|
|
|
The generated methods are ommitted for the sake of brevity, but as you
|
|
would expect, both a <code>setPlayers(Player[] value)</code> and a
|
|
<code>setPlayersAt(Player value, int index)</code> method will be
|
|
generated and do just what you expect.
|
|
|
|
<p> It should be pointed out that streamable objects sent over the
|
|
network are sent in their entirety. No mechanism is provided for
|
|
updating just a single field in a streamable instance both because
|
|
that would increase the complexity of the system tremendously and
|
|
because it is generally not very useful. If conservation of bandwidth
|
|
is of extreme importance, special {@link
|
|
com.threerings.presents.dobj.DEvent} derived classes can be created to
|
|
transmit precisely what is desired and nothing more. Doing so is
|
|
beyond the scope of this introduction, but will hopefully be covered
|
|
in an additional tutorial.
|
|
|
|
<p> The {@link com.threerings.io.SimpleStreamableObject} class is a
|
|
convenient way to create a simple record like the <code>Player</code>
|
|
record above that implements <code>Streamable</code> and provides a
|
|
default implementation of <code>toString()</code> that uses reflection
|
|
to print out the actual values of the fields in the object (a boon
|
|
when logging and debugging).
|
|
|
|
<p><b>Distributed Sets</b><br>
|
|
|
|
In developing a distributed system, one frequently encounters
|
|
situations where one wants distributed collection of objects where
|
|
order is generally not important but the ability to fluidly add and
|
|
remove elements is. For such occasions we provide the distributed set
|
|
or {@link com.threerings.presents.dobj.DSet}.
|
|
|
|
<p> A <code>DSet</code> contains entries (called entries rather than
|
|
elements to avoid confusion with array "element updating") which must
|
|
implement the {@link com.threerings.presents.dobj.DSet.Entry}
|
|
interface. This automatically makes them {@link
|
|
com.threerings.io.Streamable} and requires that they provide a {@link
|
|
java.lang.Comparable} key which is used to distinguish them from other
|
|
entries in the set (and look them up via an efficient binary search).
|
|
|
|
<p> When using a <code>DSet</code> one is provided with three new
|
|
operations: <code>addToFoo()</code>, <code>updateFoo()</code> and
|
|
<code>removeFromFoo()</code>. Once again an example is in order:
|
|
|
|
<pre class="example">
|
|
public class Monkey implements DSet.Entry
|
|
{
|
|
/** The monkey's name. */
|
|
public String name;
|
|
|
|
/** The monkey's age. */
|
|
public int age;
|
|
|
|
// documentation inherited from interface DSet.Entry
|
|
public Comparable getKey ()
|
|
{
|
|
return name;
|
|
}
|
|
}
|
|
|
|
public class CageObject extends DObject
|
|
{
|
|
// AUTO-GENERATED: FIELDS START
|
|
/** The field name of the <code>monkeys</code> field. */
|
|
public static final String MONKEYS = "monkeys";
|
|
// AUTO-GENERATED: FIELDS END
|
|
|
|
/** A collection of monkeys. */
|
|
public DSet monkeys;
|
|
|
|
// AUTO-GENERATED: METHODS START
|
|
/**
|
|
* Requests that the specified entry be added to the
|
|
* <code>monkeys</code> set.
|
|
*/
|
|
public void addToMonkeys (DSet.Entry elem)
|
|
{
|
|
requestEntryAdd(MONKEYS, monkeys, elem);
|
|
}
|
|
|
|
/**
|
|
* Requests that the entry matching the supplied key be removed from
|
|
* the <code>monkeys</code> set.
|
|
*/
|
|
public void removeFromMonkeys (Comparable key)
|
|
{
|
|
requestEntryRemove(MONKEYS, monkeys, key);
|
|
}
|
|
|
|
/**
|
|
* Requests that the specified entry be updated in the
|
|
* <code>monkeys</code> set.
|
|
*/
|
|
public void updateMonkeys (DSet.Entry elem)
|
|
{
|
|
requestEntryUpdate(MONKEYS, monkeys, elem);
|
|
}
|
|
|
|
/**
|
|
* Requests that the <code>monkeys</code> field be set to the
|
|
* specified value.
|
|
*/
|
|
public void setMonkeys (DSet value)
|
|
{
|
|
requestAttributeChange(MONKEYS, value, this.monkeys);
|
|
this.monkeys = (value == null) ? null : (DSet)value.clone();
|
|
}
|
|
// AUTO-GENERATED: METHODS END
|
|
}</pre>
|
|
|
|
It is possible to set the entire set (which is necessary to establish
|
|
its original value even if one decides to set it to the empty set),
|
|
but more commonly one will simply add entries to the set, update those
|
|
entries and remove them using the provided methods.
|
|
|
|
<p> In conjunction with the <code>DSet</code> there exists the {@link
|
|
com.threerings.presents.dobj.SetListener} which is notified when
|
|
changes are made to a distributed set. This functions in the same was
|
|
as the previously documented listeners, so I will refrain from boring
|
|
you with yet more sample code.
|
|
|
|
<h3><a name="invocation_services">Invocation Services</a></h3>
|
|
|
|
TBD
|
|
|
|
<h3><a name="ant_tasks">Ant Tasks</a></h3>
|
|
|
|
TBD
|
|
|
|
</body>
|
|
</html>
|