2007-05-04 06:53:32 +00:00
using System ;
using System.Collections ;
using System.Collections.Generic ;
using System.Text ;
using System.Reflection ;
using OpenGrid.Framework.Data ;
using OpenSim.Framework.Utilities ;
using OpenSim.Framework.Console ;
using OpenSim.Framework.Sims ;
using libsecondlife ;
using Nwc.XmlRpc ;
using System.Xml ;
namespace OpenGridServices.GridServer
{
class GridManager
{
Dictionary < string , IGridData > _plugins = new Dictionary < string , IGridData > ( ) ;
2007-05-28 22:20:25 +00:00
Dictionary < string , ILogData > _logplugins = new Dictionary < string , ILogData > ( ) ;
2007-05-13 14:59:24 +00:00
public OpenSim . Framework . Interfaces . GridConfig config ;
2007-05-04 06:53:32 +00:00
2007-05-04 07:29:33 +00:00
/// <summary>
/// Adds a new grid server plugin - grid servers will be requested in the order they were loaded.
/// </summary>
/// <param name="FileName">The filename to the grid server plugin DLL</param>
2007-05-04 06:53:32 +00:00
public void AddPlugin ( string FileName )
{
2007-05-12 15:44:10 +00:00
OpenSim . Framework . Console . MainConsole . Instance . WriteLine ( OpenSim . Framework . Console . LogPriority . LOW , "Storage: Attempting to load " + FileName ) ;
2007-05-04 06:53:32 +00:00
Assembly pluginAssembly = Assembly . LoadFrom ( FileName ) ;
2007-05-07 16:32:30 +00:00
2007-05-12 15:44:10 +00:00
OpenSim . Framework . Console . MainConsole . Instance . WriteLine ( OpenSim . Framework . Console . LogPriority . LOW , "Storage: Found " + pluginAssembly . GetTypes ( ) . Length + " interfaces." ) ;
2007-05-04 06:53:32 +00:00
foreach ( Type pluginType in pluginAssembly . GetTypes ( ) )
{
2007-05-07 16:32:30 +00:00
if ( ! pluginType . IsAbstract )
{
2007-05-28 22:20:25 +00:00
// Regions go here
2007-05-07 16:32:30 +00:00
Type typeInterface = pluginType . GetInterface ( "IGridData" , true ) ;
if ( typeInterface ! = null )
{
IGridData plug = ( IGridData ) Activator . CreateInstance ( pluginAssembly . GetType ( pluginType . ToString ( ) ) ) ;
plug . Initialise ( ) ;
this . _plugins . Add ( plug . getName ( ) , plug ) ;
2007-05-12 15:44:10 +00:00
OpenSim . Framework . Console . MainConsole . Instance . WriteLine ( OpenSim . Framework . Console . LogPriority . LOW , "Storage: Added IGridData Interface" ) ;
2007-05-07 16:32:30 +00:00
}
typeInterface = null ;
2007-05-28 22:20:25 +00:00
// Logs go here
typeInterface = pluginType . GetInterface ( "ILogData" , true ) ;
if ( typeInterface ! = null )
{
ILogData plug = ( ILogData ) Activator . CreateInstance ( pluginAssembly . GetType ( pluginType . ToString ( ) ) ) ;
plug . Initialise ( ) ;
this . _logplugins . Add ( plug . getName ( ) , plug ) ;
OpenSim . Framework . Console . MainConsole . Instance . WriteLine ( OpenSim . Framework . Console . LogPriority . LOW , "Storage: Added ILogData Interface" ) ;
}
typeInterface = null ;
2007-05-07 16:32:30 +00:00
}
2007-05-04 06:53:32 +00:00
}
pluginAssembly = null ;
}
2007-05-28 22:20:25 +00:00
/// <summary>
/// Logs a piece of information to the database
/// </summary>
/// <param name="target">What you were operating on (in grid server, this will likely be the region UUIDs)</param>
/// <param name="method">Which method is being called?</param>
/// <param name="args">What arguments are being passed?</param>
/// <param name="priority">How high priority is this? 1 = Max, 6 = Verbose</param>
/// <param name="message">The message to log</param>
private void logToDB ( string target , string method , string args , int priority , string message )
{
foreach ( KeyValuePair < string , ILogData > kvp in _logplugins )
{
try
{
kvp . Value . saveLog ( "Gridserver" , target , method , args , priority , message ) ;
}
catch ( Exception e )
{
OpenSim . Framework . Console . MainConsole . Instance . WriteLine ( OpenSim . Framework . Console . LogPriority . NORMAL , "Storage: unable to write log via " + kvp . Key ) ;
}
}
}
2007-05-04 07:29:33 +00:00
/// <summary>
/// Returns a region by argument
/// </summary>
/// <param name="uuid">A UUID key of the region to return</param>
/// <returns>A SimProfileData for the region</returns>
2007-05-04 06:53:32 +00:00
public SimProfileData getRegion ( libsecondlife . LLUUID uuid )
{
foreach ( KeyValuePair < string , IGridData > kvp in _plugins ) {
try
{
return kvp . Value . GetProfileByLLUUID ( uuid ) ;
}
catch ( Exception e )
{
2007-05-12 15:44:10 +00:00
OpenSim . Framework . Console . MainConsole . Instance . WriteLine ( OpenSim . Framework . Console . LogPriority . NORMAL , "Storage: Unable to find region " + uuid . ToStringHyphenated ( ) + " via " + kvp . Key ) ;
2007-05-04 06:53:32 +00:00
}
}
return null ;
}
2007-05-04 07:29:33 +00:00
/// <summary>
/// Returns a region by argument
/// </summary>
/// <param name="uuid">A regionHandle of the region to return</param>
/// <returns>A SimProfileData for the region</returns>
2007-05-04 06:53:32 +00:00
public SimProfileData getRegion ( ulong handle )
{
foreach ( KeyValuePair < string , IGridData > kvp in _plugins )
{
try
{
return kvp . Value . GetProfileByHandle ( handle ) ;
}
catch ( Exception e )
{
2007-05-12 15:44:10 +00:00
OpenSim . Framework . Console . MainConsole . Instance . WriteLine ( OpenSim . Framework . Console . LogPriority . NORMAL , "Storage: Unable to find region " + handle . ToString ( ) + " via " + kvp . Key ) ;
2007-05-04 06:53:32 +00:00
}
}
return null ;
}
2007-05-14 16:52:52 +00:00
public Dictionary < ulong , SimProfileData > getRegions ( uint xmin , uint ymin , uint xmax , uint ymax )
{
Dictionary < ulong , SimProfileData > regions = new Dictionary < ulong , SimProfileData > ( ) ;
SimProfileData [ ] neighbours ;
foreach ( KeyValuePair < string , IGridData > kvp in _plugins )
{
try
{
neighbours = kvp . Value . GetProfilesInRange ( xmin , ymin , xmax , ymax ) ;
foreach ( SimProfileData neighbour in neighbours )
{
regions [ neighbour . regionHandle ] = neighbour ;
}
}
catch ( Exception e )
{
OpenSim . Framework . Console . MainConsole . Instance . WriteLine ( OpenSim . Framework . Console . LogPriority . NORMAL , "Storage: Unable to query regionblock via " + kvp . Key ) ;
}
}
return regions ;
}
2007-05-04 06:53:32 +00:00
/// <summary>
/// Returns a XML String containing a list of the neighbouring regions
/// </summary>
/// <param name="reqhandle">The regionhandle for the center sim</param>
/// <returns>An XML string containing neighbour entities</returns>
public string GetXMLNeighbours ( ulong reqhandle )
{
string response = "" ;
SimProfileData central_region = getRegion ( reqhandle ) ;
SimProfileData neighbour ;
for ( int x = - 1 ; x < 2 ; x + + ) for ( int y = - 1 ; y < 2 ; y + + )
{
if ( getRegion ( Util . UIntsToLong ( ( uint ) ( ( central_region . regionLocX + x ) * 256 ) , ( uint ) ( central_region . regionLocY + y ) * 256 ) ) ! = null )
{
neighbour = getRegion ( Util . UIntsToLong ( ( uint ) ( ( central_region . regionLocX + x ) * 256 ) , ( uint ) ( central_region . regionLocY + y ) * 256 ) ) ;
response + = "<neighbour>" ;
response + = "<sim_ip>" + neighbour . serverIP + "</sim_ip>" ;
response + = "<sim_port>" + neighbour . serverPort . ToString ( ) + "</sim_port>" ;
response + = "<locx>" + neighbour . regionLocX . ToString ( ) + "</locx>" ;
response + = "<locy>" + neighbour . regionLocY . ToString ( ) + "</locy>" ;
response + = "<regionhandle>" + neighbour . regionHandle . ToString ( ) + "</regionhandle>" ;
response + = "</neighbour>" ;
}
}
return response ;
}
2007-05-04 07:29:33 +00:00
/// <summary>
/// Performed when a region connects to the grid server initially.
/// </summary>
/// <param name="request">The XMLRPC Request</param>
/// <returns>Startup parameters</returns>
2007-05-04 06:53:32 +00:00
public XmlRpcResponse XmlRpcLoginToSimulatorMethod ( XmlRpcRequest request )
{
XmlRpcResponse response = new XmlRpcResponse ( ) ;
Hashtable responseData = new Hashtable ( ) ;
response . Value = responseData ;
SimProfileData TheSim = null ;
Hashtable requestData = ( Hashtable ) request . Params [ 0 ] ;
if ( requestData . ContainsKey ( "UUID" ) )
{
TheSim = getRegion ( new LLUUID ( ( string ) requestData [ "UUID" ] ) ) ;
2007-05-28 22:20:25 +00:00
logToDB ( ( new LLUUID ( ( string ) requestData [ "UUID" ] ) ) . ToStringHyphenated ( ) , "XmlRpcLoginToSimulatorMethod" , "" , 5 , "Region attempting login with UUID." ) ;
2007-05-04 06:53:32 +00:00
}
else if ( requestData . ContainsKey ( "region_handle" ) )
{
TheSim = getRegion ( ( ulong ) Convert . ToUInt64 ( requestData [ "region_handle" ] ) ) ;
2007-05-28 22:20:25 +00:00
logToDB ( ( string ) requestData [ "region_handle" ] , "XmlRpcLoginToSimulatorMethod" , "" , 5 , "Region attempting login with regionHandle." ) ;
}
else
{
responseData [ "error" ] = "No UUID or region_handle passed to grid server - unable to connect you" ;
return response ;
2007-05-04 06:53:32 +00:00
}
if ( TheSim = = null )
{
responseData [ "error" ] = "sim not found" ;
2007-05-28 22:20:25 +00:00
return response ;
2007-05-04 06:53:32 +00:00
}
else
{
ArrayList SimNeighboursData = new ArrayList ( ) ;
SimProfileData neighbour ;
Hashtable NeighbourBlock ;
2007-05-14 16:52:52 +00:00
bool fastMode = false ; // Only compatible with MySQL right now
2007-05-04 06:53:32 +00:00
2007-05-14 16:52:52 +00:00
if ( fastMode )
{
2007-05-14 16:53:55 +00:00
Dictionary < ulong , SimProfileData > neighbours = getRegions ( TheSim . regionLocX - 1 , TheSim . regionLocY - 1 , TheSim . regionLocX + 1 , TheSim . regionLocY + 1 ) ;
2007-05-14 16:52:52 +00:00
foreach ( KeyValuePair < ulong , SimProfileData > aSim in neighbours )
{
NeighbourBlock = new Hashtable ( ) ;
NeighbourBlock [ "sim_ip" ] = aSim . Value . serverIP . ToString ( ) ;
NeighbourBlock [ "sim_port" ] = aSim . Value . serverPort . ToString ( ) ;
NeighbourBlock [ "region_locx" ] = aSim . Value . regionLocX . ToString ( ) ;
NeighbourBlock [ "region_locy" ] = aSim . Value . regionLocY . ToString ( ) ;
NeighbourBlock [ "UUID" ] = aSim . Value . UUID . ToString ( ) ;
if ( aSim . Value . UUID ! = TheSim . UUID )
SimNeighboursData . Add ( NeighbourBlock ) ;
2007-05-04 06:53:32 +00:00
}
2007-05-14 16:52:52 +00:00
}
else
{
for ( int x = - 1 ; x < 2 ; x + + ) for ( int y = - 1 ; y < 2 ; y + + )
{
if ( getRegion ( Helpers . UIntsToLong ( ( uint ) ( ( TheSim . regionLocX + x ) * 256 ) , ( uint ) ( TheSim . regionLocY + y ) * 256 ) ) ! = null )
{
neighbour = getRegion ( Helpers . UIntsToLong ( ( uint ) ( ( TheSim . regionLocX + x ) * 256 ) , ( uint ) ( TheSim . regionLocY + y ) * 256 ) ) ;
NeighbourBlock = new Hashtable ( ) ;
NeighbourBlock [ "sim_ip" ] = neighbour . serverIP ;
NeighbourBlock [ "sim_port" ] = neighbour . serverPort . ToString ( ) ;
NeighbourBlock [ "region_locx" ] = neighbour . regionLocX . ToString ( ) ;
NeighbourBlock [ "region_locy" ] = neighbour . regionLocY . ToString ( ) ;
NeighbourBlock [ "UUID" ] = neighbour . UUID . ToString ( ) ;
if ( neighbour . UUID ! = TheSim . UUID ) SimNeighboursData . Add ( NeighbourBlock ) ;
}
}
}
2007-05-04 06:53:32 +00:00
responseData [ "UUID" ] = TheSim . UUID . ToString ( ) ;
responseData [ "region_locx" ] = TheSim . regionLocX . ToString ( ) ;
responseData [ "region_locy" ] = TheSim . regionLocY . ToString ( ) ;
responseData [ "regionname" ] = TheSim . regionName ;
responseData [ "estate_id" ] = "1" ;
responseData [ "neighbours" ] = SimNeighboursData ;
responseData [ "sim_ip" ] = TheSim . serverIP ;
responseData [ "sim_port" ] = TheSim . serverPort . ToString ( ) ;
responseData [ "asset_url" ] = TheSim . regionAssetURI ;
responseData [ "asset_sendkey" ] = TheSim . regionAssetSendKey ;
responseData [ "asset_recvkey" ] = TheSim . regionAssetRecvKey ;
responseData [ "user_url" ] = TheSim . regionUserURI ;
responseData [ "user_sendkey" ] = TheSim . regionUserSendKey ;
responseData [ "user_recvkey" ] = TheSim . regionUserRecvKey ;
responseData [ "authkey" ] = TheSim . regionSecret ;
2007-05-04 07:09:42 +00:00
// New! If set, use as URL to local sim storage (ie http://remotehost/region.yap)
2007-05-04 07:23:41 +00:00
responseData [ "data_uri" ] = TheSim . regionDataURI ;
2007-05-04 06:53:32 +00:00
}
return response ;
}
2007-05-20 02:31:20 +00:00
public XmlRpcResponse XmlRpcMapBlockMethod ( XmlRpcRequest request )
{
int xmin = 980 , ymin = 980 , xmax = 1020 , ymax = 1020 ;
Hashtable requestData = ( Hashtable ) request . Params [ 0 ] ;
if ( requestData . ContainsKey ( "xmin" ) )
{
xmin = ( Int32 ) requestData [ "xmin" ] ;
}
if ( requestData . ContainsKey ( "ymin" ) )
{
ymin = ( Int32 ) requestData [ "ymin" ] ;
}
if ( requestData . ContainsKey ( "xmax" ) )
{
xmax = ( Int32 ) requestData [ "xmax" ] ;
}
if ( requestData . ContainsKey ( "ymax" ) )
{
ymax = ( Int32 ) requestData [ "ymax" ] ;
}
XmlRpcResponse response = new XmlRpcResponse ( ) ;
Hashtable responseData = new Hashtable ( ) ;
response . Value = responseData ;
IList simProfileList = new ArrayList ( ) ;
SimProfileData simProfile ;
for ( int x = xmin ; x < xmax ; x + + )
{
for ( int y = ymin ; y < ymax ; y + + )
{
simProfile = getRegion ( Helpers . UIntsToLong ( ( uint ) ( x * 256 ) , ( uint ) ( y * 256 ) ) ) ;
if ( simProfile ! = null )
{
Hashtable simProfileBlock = new Hashtable ( ) ;
simProfileBlock [ "x" ] = x ;
simProfileBlock [ "y" ] = y ;
simProfileBlock [ "name" ] = simProfile . regionName ;
simProfileBlock [ "access" ] = 0 ;
simProfileBlock [ "region-flags" ] = 0 ;
simProfileBlock [ "water-height" ] = 20 ;
simProfileBlock [ "agents" ] = 1 ;
simProfileBlock [ "map-image-id" ] = simProfile . regionMapTextureID . ToString ( ) ;
simProfileList . Add ( simProfileBlock ) ;
}
}
}
responseData [ "sim-profiles" ] = simProfileList ;
return response ;
}
2007-05-04 07:29:33 +00:00
/// <summary>
/// Performs a REST Get Operation
/// </summary>
/// <param name="request"></param>
/// <param name="path"></param>
/// <param name="param"></param>
/// <returns></returns>
2007-05-04 06:53:32 +00:00
public string RestGetRegionMethod ( string request , string path , string param )
{
return RestGetSimMethod ( "" , "/sims/" , param ) ;
}
2007-05-04 07:29:33 +00:00
/// <summary>
/// Performs a REST Set Operation
/// </summary>
/// <param name="request"></param>
/// <param name="path"></param>
/// <param name="param"></param>
/// <returns></returns>
2007-05-04 06:53:32 +00:00
public string RestSetRegionMethod ( string request , string path , string param )
{
return RestSetSimMethod ( "" , "/sims/" , param ) ;
}
2007-05-04 07:29:33 +00:00
/// <summary>
/// Returns information about a sim via a REST Request
/// </summary>
/// <param name="request"></param>
/// <param name="path"></param>
/// <param name="param"></param>
/// <returns>Information about the sim in XML</returns>
2007-05-04 06:53:32 +00:00
public string RestGetSimMethod ( string request , string path , string param )
{
string respstring = String . Empty ;
SimProfileData TheSim ;
LLUUID UUID = new LLUUID ( param ) ;
TheSim = getRegion ( UUID ) ;
if ( ! ( TheSim = = null ) )
{
respstring = "<Root>" ;
respstring + = "<authkey>" + TheSim . regionSendKey + "</authkey>" ;
respstring + = "<sim>" ;
respstring + = "<uuid>" + TheSim . UUID . ToString ( ) + "</uuid>" ;
respstring + = "<regionname>" + TheSim . regionName + "</regionname>" ;
respstring + = "<sim_ip>" + TheSim . serverIP + "</sim_ip>" ;
respstring + = "<sim_port>" + TheSim . serverPort . ToString ( ) + "</sim_port>" ;
respstring + = "<region_locx>" + TheSim . regionLocX . ToString ( ) + "</region_locx>" ;
respstring + = "<region_locy>" + TheSim . regionLocY . ToString ( ) + "</region_locy>" ;
respstring + = "<estate_id>1</estate_id>" ;
respstring + = "</sim>" ;
respstring + = "</Root>" ;
}
return respstring ;
}
2007-05-04 07:29:33 +00:00
/// <summary>
/// Creates or updates a sim via a REST Method Request
/// BROKEN with SQL Update
/// </summary>
/// <param name="request"></param>
/// <param name="path"></param>
/// <param name="param"></param>
/// <returns>"OK" or an error</returns>
2007-05-04 06:53:32 +00:00
public string RestSetSimMethod ( string request , string path , string param )
{
2007-05-28 22:20:25 +00:00
Console . WriteLine ( "Processing region update" ) ;
2007-05-04 06:53:32 +00:00
SimProfileData TheSim ;
TheSim = getRegion ( new LLUUID ( param ) ) ;
if ( ( TheSim ) = = null )
{
TheSim = new SimProfileData ( ) ;
LLUUID UUID = new LLUUID ( param ) ;
TheSim . UUID = UUID ;
2007-05-13 14:59:24 +00:00
TheSim . regionRecvKey = config . SimRecvKey ;
2007-05-04 06:53:32 +00:00
}
XmlDocument doc = new XmlDocument ( ) ;
doc . LoadXml ( request ) ;
XmlNode rootnode = doc . FirstChild ;
XmlNode authkeynode = rootnode . ChildNodes [ 0 ] ;
if ( authkeynode . Name ! = "authkey" )
{
return "ERROR! bad XML - expected authkey tag" ;
}
XmlNode simnode = rootnode . ChildNodes [ 1 ] ;
if ( simnode . Name ! = "sim" )
{
return "ERROR! bad XML - expected sim tag" ;
}
if ( authkeynode . InnerText ! = TheSim . regionRecvKey )
{
return "ERROR! invalid key" ;
}
2007-05-13 14:59:24 +00:00
//TheSim.regionSendKey = Cfg;
TheSim . regionRecvKey = config . SimRecvKey ;
TheSim . regionSendKey = config . SimSendKey ;
TheSim . regionSecret = config . SimRecvKey ;
TheSim . regionDataURI = "" ;
TheSim . regionAssetURI = config . DefaultAssetServer ;
TheSim . regionAssetRecvKey = config . AssetRecvKey ;
TheSim . regionAssetSendKey = config . AssetSendKey ;
TheSim . regionUserURI = config . DefaultUserServer ;
TheSim . regionUserSendKey = config . UserSendKey ;
TheSim . regionUserRecvKey = config . UserRecvKey ;
2007-05-04 06:53:32 +00:00
for ( int i = 0 ; i < simnode . ChildNodes . Count ; i + + )
{
switch ( simnode . ChildNodes [ i ] . Name )
{
case "regionname" :
TheSim . regionName = simnode . ChildNodes [ i ] . InnerText ;
break ;
case "sim_ip" :
TheSim . serverIP = simnode . ChildNodes [ i ] . InnerText ;
break ;
case "sim_port" :
TheSim . serverPort = Convert . ToUInt32 ( simnode . ChildNodes [ i ] . InnerText ) ;
break ;
case "region_locx" :
TheSim . regionLocX = Convert . ToUInt32 ( ( string ) simnode . ChildNodes [ i ] . InnerText ) ;
TheSim . regionHandle = Helpers . UIntsToLong ( ( TheSim . regionLocX * 256 ) , ( TheSim . regionLocY * 256 ) ) ;
break ;
case "region_locy" :
TheSim . regionLocY = Convert . ToUInt32 ( ( string ) simnode . ChildNodes [ i ] . InnerText ) ;
TheSim . regionHandle = Helpers . UIntsToLong ( ( TheSim . regionLocX * 256 ) , ( TheSim . regionLocY * 256 ) ) ;
break ;
}
}
2007-05-13 14:59:24 +00:00
TheSim . serverURI = "http://" + TheSim . serverIP + ":" + TheSim . serverPort + "/" ;
2007-05-18 23:46:14 +00:00
bool requirePublic = false ;
if ( requirePublic & & ( TheSim . serverIP . StartsWith ( "172.16" ) | | TheSim . serverIP . StartsWith ( "192.168" ) | | TheSim . serverIP . StartsWith ( "10." ) | | TheSim . serverIP . StartsWith ( "0." ) | | TheSim . serverIP . StartsWith ( "255." ) ) )
2007-05-16 21:10:45 +00:00
{
return "ERROR! Servers must register with public addresses." ;
}
2007-05-04 06:53:32 +00:00
try
{
2007-05-28 22:20:25 +00:00
OpenSim . Framework . Console . MainConsole . Instance . WriteLine ( OpenSim . Framework . Console . LogPriority . LOW , "Updating / adding via " + _plugins . Count + " storage provider(s) registered." ) ;
2007-05-05 23:04:47 +00:00
foreach ( KeyValuePair < string , IGridData > kvp in _plugins )
{
try
{
kvp . Value . AddProfile ( TheSim ) ;
2007-05-12 15:44:10 +00:00
OpenSim . Framework . Console . MainConsole . Instance . WriteLine ( OpenSim . Framework . Console . LogPriority . LOW , "New sim added to grid (" + TheSim . regionName + ")" ) ;
2007-05-28 22:20:25 +00:00
logToDB ( TheSim . UUID . ToStringHyphenated ( ) , "RestSetSimMethod" , "" , 5 , "Region successfully updated and connected to grid." ) ;
2007-05-05 23:04:47 +00:00
}
catch ( Exception e )
{
2007-05-12 15:44:10 +00:00
OpenSim . Framework . Console . MainConsole . Instance . WriteLine ( OpenSim . Framework . Console . LogPriority . LOW , "getRegionPlugin Handle " + kvp . Key + " unable to add new sim: " + e . ToString ( ) ) ;
2007-05-05 23:04:47 +00:00
}
}
2007-05-04 06:53:32 +00:00
return "OK" ;
}
catch ( Exception e )
{
2007-05-16 21:10:45 +00:00
return "ERROR! Could not save to database! (" + e . ToString ( ) + ")" ;
2007-05-04 06:53:32 +00:00
}
}
}
}