aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/OpenSim
diff options
context:
space:
mode:
authorSean Dague2007-12-12 22:14:43 +0000
committerSean Dague2007-12-12 22:14:43 +0000
commit7625438adeb2211ee9c3f6532819eea2a0673c5d (patch)
tree6ce5553fa4c9fb60cf1024f2db639b9a386153da /OpenSim
parentThanks to Alondria for: (diff)
downloadopensim-SC-7625438adeb2211ee9c3f6532819eea2a0673c5d.zip
opensim-SC-7625438adeb2211ee9c3f6532819eea2a0673c5d.tar.gz
opensim-SC-7625438adeb2211ee9c3f6532819eea2a0673c5d.tar.bz2
opensim-SC-7625438adeb2211ee9c3f6532819eea2a0673c5d.tar.xz
From Michael Osias (IBM)
This patch implements the llHttpRequest function via a region module, HttpScriptsRequest. There were bits and peices in LSLLong_cmd_handler, which I moved into the region module, and just check for completed requests and dispatch the http_response callback event instead. works for me as of r2674
Diffstat (limited to 'OpenSim')
-rw-r--r--OpenSim/Region/Environment/Interfaces/IHttpRequests.cs7
-rw-r--r--OpenSim/Region/Environment/Modules/ScriptsHttpRequests.cs319
-rw-r--r--OpenSim/Region/Environment/Modules/XMLRPCModule.cs2
-rw-r--r--OpenSim/Region/ScriptEngine/Common/LSL_BuiltIn_Commands_Interface.cs2
-rw-r--r--OpenSim/Region/ScriptEngine/DotNetEngine/Compiler/LSL/LSL_BaseClass.cs4
-rw-r--r--OpenSim/Region/ScriptEngine/DotNetEngine/Compiler/Server_API/LSL_BuiltIn_Commands.cs13
-rw-r--r--OpenSim/Region/ScriptEngine/DotNetEngine/LSLLongCmdHandler.cs160
7 files changed, 384 insertions, 123 deletions
diff --git a/OpenSim/Region/Environment/Interfaces/IHttpRequests.cs b/OpenSim/Region/Environment/Interfaces/IHttpRequests.cs
index 0357139..c974616 100644
--- a/OpenSim/Region/Environment/Interfaces/IHttpRequests.cs
+++ b/OpenSim/Region/Environment/Interfaces/IHttpRequests.cs
@@ -27,11 +27,16 @@
27*/ 27*/
28 28
29using libsecondlife; 29using libsecondlife;
30using OpenSim.Region.Environment.Modules;
31using System.Collections.Generic;
30 32
31namespace OpenSim.Region.Environment.Interfaces 33namespace OpenSim.Region.Environment.Interfaces
32{ 34{
33 public interface IHttpRequests 35 public interface IHttpRequests
34 { 36 {
35 LLUUID MakeHttpRequest(string url, string type, string body); 37 LLUUID MakeHttpRequest(string url, string parameters, string body);
38 LLUUID StartHttpRequest(uint localID, LLUUID itemID, string url, List<string> parameters, string body);
39 void StopHttpRequest(uint m_localID, LLUUID m_itemID);
40 HttpRequestClass GetNextCompletedRequest();
36 } 41 }
37} \ No newline at end of file 42} \ No newline at end of file
diff --git a/OpenSim/Region/Environment/Modules/ScriptsHttpRequests.cs b/OpenSim/Region/Environment/Modules/ScriptsHttpRequests.cs
index 5ac0b39..dc4ef35 100644
--- a/OpenSim/Region/Environment/Modules/ScriptsHttpRequests.cs
+++ b/OpenSim/Region/Environment/Modules/ScriptsHttpRequests.cs
@@ -26,9 +26,324 @@
26* 26*
27*/ 27*/
28 28
29using System;
30using System.IO;
31using System.Net;
32using System.Text;
33using OpenSim.Region.Environment.Interfaces;
34using OpenSim.Region.Environment.Scenes;
35using System.Collections;
36using System.Collections.Generic;
37using System.Threading;
38using libsecondlife;
39using Nini.Config;
40using Nwc.XmlRpc;
41using OpenSim.Framework.Servers;
42
43/*****************************************************
44 *
45 * ScriptsHttpRequests
46 *
47 * Implements the llHttpRequest and http_response
48 * callback.
49 *
50 * Some stuff was already in LSLLongCmdHandler, and then
51 * there was this file with a stub class in it. So,
52 * I am moving some of the objects and functions out of
53 * LSLLongCmdHandler, such as the HttpRequestClass, the
54 * start and stop methods, and setting up pending and
55 * completed queues. These are processed in the
56 * LSLLongCmdHandler polling loop. Similiar to the
57 * XMLRPCModule, since that seems to work.
58 *
59 * //TODO
60 *
61 * This probably needs some throttling mechanism but
62 * its wide open right now. This applies to both
63 * number of requests and data volume.
64 *
65 * Linden puts all kinds of header fields in the requests.
66 * Not doing any of that:
67 * User-Agent
68 * X-SecondLife-Shard
69 * X-SecondLife-Object-Name
70 * X-SecondLife-Object-Key
71 * X-SecondLife-Region
72 * X-SecondLife-Local-Position
73 * X-SecondLife-Local-Velocity
74 * X-SecondLife-Local-Rotation
75 * X-SecondLife-Owner-Name
76 * X-SecondLife-Owner-Key
77 *
78 * HTTPS support
79 *
80 * Configurable timeout?
81 * Configurable max repsonse size?
82 * Configurable
83 *
84 * **************************************************/
85
29namespace OpenSim.Region.Environment.Modules 86namespace OpenSim.Region.Environment.Modules
30{ 87{
31 internal class ScriptsHttpRequests 88 public class ScriptHTTPRequests : IRegionModule, IHttpRequests
89 {
90 private Scene m_scene;
91 private Queue<HttpRequestClass> rpcQueue = new Queue<HttpRequestClass>();
92 private object HttpListLock = new object();
93 private string m_name = "HttpScriptRequests";
94 private int httpTimeout = 300;
95
96 // <request id, HttpRequestClass>
97 private Dictionary<LLUUID, HttpRequestClass> m_pendingRequests;
98
99 public ScriptHTTPRequests()
100 {
101 }
102
103 public void Initialise(Scene scene, IConfigSource config)
104 {
105 m_scene = scene;
106
107 m_scene.RegisterModuleInterface<IHttpRequests>(this);
108
109 m_pendingRequests = new Dictionary<LLUUID, HttpRequestClass>();
110 }
111
112 public void PostInitialise()
113 {
114 }
115
116 public void Close()
117 {
118 }
119
120 public string Name
121 {
122 get { return m_name; }
123 }
124
125 public bool IsSharedModule
126 {
127 get { return true; }
128 }
129
130 public LLUUID MakeHttpRequest(string url, string parameters, string body) {
131 return LLUUID.Zero;
132 }
133
134 public LLUUID StartHttpRequest(uint localID, LLUUID itemID, string url, List<string> parameters, string body)
135 {
136 LLUUID reqID = LLUUID.Random();
137 HttpRequestClass htc = new HttpRequestClass();
138
139 // Parameters are expected in {key, value, ... , key, value}
140 if( parameters != null )
141 {
142 string[] parms = parameters.ToArray();
143 for (int i = 0; i < parms.Length / 2; i += 2)
144 {
145 switch( Int32.Parse(parms[i]) )
146 {
147 case HttpRequestClass.HTTP_METHOD:
148
149 htc.httpMethod = parms[i + 1];
150 break;
151
152 case HttpRequestClass.HTTP_MIMETYPE:
153
154 htc.httpMIMEType = parms[i + 1];
155 break;
156
157 case HttpRequestClass.HTTP_BODY_MAXLENGTH:
158
159 // TODO implement me
160 break;
161
162 case HttpRequestClass.HTTP_VERIFY_CERT:
163
164 // TODO implement me
165 break;
166 }
167 }
168 }
169
170 htc.localID = localID;
171 htc.itemID = itemID;
172 htc.url = url;
173 htc.reqID = reqID;
174 htc.httpTimeout = httpTimeout;
175 htc.outbound_body = body;
176
177 lock (HttpListLock)
178 {
179 m_pendingRequests.Add(reqID, htc);
180 }
181
182 htc.process();
183
184 return reqID;
185 }
186
187 public void StopHttpRequest(uint m_localID, LLUUID m_itemID)
188 {
189 lock (HttpListLock)
190 {
191
192 HttpRequestClass tmpReq;
193 if (m_pendingRequests.TryGetValue(m_itemID, out tmpReq))
194 {
195 tmpReq.Stop();
196 m_pendingRequests.Remove(m_itemID);
197 }
198 }
199 }
200
201 /*
202 * TODO
203 * Not sure how important ordering is is here - the next first
204 * one completed in the list is returned, based soley on its list
205 * position, not the order in which the request was started or
206 * finsihed. I thought about setting up a queue for this, but
207 * it will need some refactoring and this works 'enough' right now
208 */
209 public HttpRequestClass GetNextCompletedRequest()
210 {
211 lock (HttpListLock)
212 {
213 foreach (LLUUID luid in m_pendingRequests.Keys)
214 {
215 HttpRequestClass tmpReq;
216
217 if (m_pendingRequests.TryGetValue(luid, out tmpReq))
218 {
219 if (tmpReq.finished)
220 {
221 m_pendingRequests.Remove(luid);
222 return tmpReq;
223 }
224 }
225 }
226 }
227 return null;
228 }
229
230
231 }
232
233 //
234 // HTTP REAQUEST
235 // This class was originally in LSLLongCmdHandler
236 //
237 // TODO: setter/getter methods, maybe pass some in
238 // constructor
239 //
240
241 public class HttpRequestClass
32 { 242 {
243 // Constants for parameters
244 public const int HTTP_METHOD = 0;
245 public const int HTTP_MIMETYPE = 1;
246 public const int HTTP_BODY_MAXLENGTH = 2;
247 public const int HTTP_VERIFY_CERT = 3;
248
249 // Parameter members and default values
250 public string httpMethod = "GET";
251 public string httpMIMEType = "text/plain;charset=utf-8";
252 public int httpBodyMaxLen = 2048; // not implemented
253 public bool httpVerifyCert = true; // not implemented
254
255 // Request info
256 public uint localID;
257 public LLUUID itemID;
258 public LLUUID reqID;
259 public int httpTimeout;
260 public string url;
261 public string outbound_body;
262 public DateTime next;
263 public int status;
264 public bool finished;
265 public List<string> response_metadata;
266 public string response_body;
267 public HttpWebRequest request;
268 private Thread httpThread;
269
270 public void process()
271 {
272 httpThread = new Thread(SendRequest);
273 httpThread.Name = "HttpRequestThread";
274 httpThread.Priority = ThreadPriority.BelowNormal;
275 httpThread.IsBackground = true;
276 httpThread.Start();
277 }
278
279 /*
280 * TODO: More work on the response codes. Right now
281 * returning 200 for success or 499 for exception
282 */
283 public void SendRequest()
284 {
285
286 HttpWebResponse response = null;
287 StringBuilder sb = new StringBuilder();
288 byte[] buf = new byte[8192];
289 string tempString = null;
290 int count = 0;
291
292 try
293 {
294 request = (HttpWebRequest)
295 WebRequest.Create(url);
296 request.Method = httpMethod;
297 request.ContentType = httpMIMEType;
298
299 request.Timeout = httpTimeout;
300 // execute the request
301 response = (HttpWebResponse)
302 request.GetResponse();
303
304 Stream resStream = response.GetResponseStream();
305
306 do
307 {
308 // fill the buffer with data
309 count = resStream.Read(buf, 0, buf.Length);
310
311 // make sure we read some data
312 if (count != 0)
313 {
314 // translate from bytes to ASCII text
315 tempString = Encoding.ASCII.GetString(buf, 0, count);
316
317 // continue building the string
318 sb.Append(tempString);
319 }
320 }
321 while (count > 0); // any more data to read?
322
323 response_body = sb.ToString();
324
325 }
326 catch (Exception e)
327 {
328 status = 499;
329 response_body = e.Message;
330 finished = true;
331 return;
332 }
333
334 status = 200;
335 finished = true;
336
337 }
338
339 public void Stop()
340 {
341 try
342 {
343 httpThread.Abort();
344 }
345 catch (Exception e) { }
346 }
33 } 347 }
34} \ No newline at end of file 348
349 } \ No newline at end of file
diff --git a/OpenSim/Region/Environment/Modules/XMLRPCModule.cs b/OpenSim/Region/Environment/Modules/XMLRPCModule.cs
index 1223f5c..2618b17 100644
--- a/OpenSim/Region/Environment/Modules/XMLRPCModule.cs
+++ b/OpenSim/Region/Environment/Modules/XMLRPCModule.cs
@@ -121,7 +121,7 @@ namespace OpenSim.Region.Environment.Modules
121 121
122 public bool IsSharedModule 122 public bool IsSharedModule
123 { 123 {
124 get { return false; } 124 get { return true; }
125 } 125 }
126 126
127 /********************************************** 127 /**********************************************
diff --git a/OpenSim/Region/ScriptEngine/Common/LSL_BuiltIn_Commands_Interface.cs b/OpenSim/Region/ScriptEngine/Common/LSL_BuiltIn_Commands_Interface.cs
index 545b99c..10e71b3 100644
--- a/OpenSim/Region/ScriptEngine/Common/LSL_BuiltIn_Commands_Interface.cs
+++ b/OpenSim/Region/ScriptEngine/Common/LSL_BuiltIn_Commands_Interface.cs
@@ -614,7 +614,7 @@ namespace OpenSim.Region.ScriptEngine.Common
614 int llGetRegionFlags(); 614 int llGetRegionFlags();
615 //wiki: string llXorBase64StringsCorrect(string str1, string str2) 615 //wiki: string llXorBase64StringsCorrect(string str1, string str2)
616 string llXorBase64StringsCorrect(string str1, string str2); 616 string llXorBase64StringsCorrect(string str1, string str2);
617 void llHTTPRequest(string url, List<string> parameters, string body); 617 string llHTTPRequest(string url, List<string> parameters, string body);
618 //wiki: llResetLandBanList() 618 //wiki: llResetLandBanList()
619 void llResetLandBanList(); 619 void llResetLandBanList();
620 //wiki: llResetLandPassList() 620 //wiki: llResetLandPassList()
diff --git a/OpenSim/Region/ScriptEngine/DotNetEngine/Compiler/LSL/LSL_BaseClass.cs b/OpenSim/Region/ScriptEngine/DotNetEngine/Compiler/LSL/LSL_BaseClass.cs
index deabec3..99f8d3b 100644
--- a/OpenSim/Region/ScriptEngine/DotNetEngine/Compiler/LSL/LSL_BaseClass.cs
+++ b/OpenSim/Region/ScriptEngine/DotNetEngine/Compiler/LSL/LSL_BaseClass.cs
@@ -1759,9 +1759,9 @@ namespace OpenSim.Region.ScriptEngine.DotNetEngine.Compiler.LSL
1759 return m_LSL_Functions.llXorBase64StringsCorrect(str1, str2); 1759 return m_LSL_Functions.llXorBase64StringsCorrect(str1, str2);
1760 } 1760 }
1761 1761
1762 public void llHTTPRequest(string url, List<string> parameters, string body) 1762 public string llHTTPRequest(string url, List<string> parameters, string body)
1763 { 1763 {
1764 m_LSL_Functions.llHTTPRequest(url, parameters, body); 1764 return m_LSL_Functions.llHTTPRequest(url, parameters, body);
1765 } 1765 }
1766 1766
1767 public void llResetLandBanList() 1767 public void llResetLandBanList()
diff --git a/OpenSim/Region/ScriptEngine/DotNetEngine/Compiler/Server_API/LSL_BuiltIn_Commands.cs b/OpenSim/Region/ScriptEngine/DotNetEngine/Compiler/Server_API/LSL_BuiltIn_Commands.cs
index 94479a0..aaac294 100644
--- a/OpenSim/Region/ScriptEngine/DotNetEngine/Compiler/Server_API/LSL_BuiltIn_Commands.cs
+++ b/OpenSim/Region/ScriptEngine/DotNetEngine/Compiler/Server_API/LSL_BuiltIn_Commands.cs
@@ -2451,9 +2451,18 @@ namespace OpenSim.Region.ScriptEngine.DotNetEngine.Compiler
2451 return llStringToBase64(ret); 2451 return llStringToBase64(ret);
2452 } 2452 }
2453 2453
2454 public void llHTTPRequest(string url, List<string> parameters, string body) 2454 public string llHTTPRequest(string url, List<string> parameters, string body)
2455 { 2455 {
2456 m_ScriptEngine.m_LSLLongCmdHandler.StartHttpRequest(m_localID, m_itemID, url, parameters, body); 2456 IHttpRequests httpScriptMod =
2457 m_ScriptEngine.World.RequestModuleInterface<IHttpRequests>();
2458
2459 LLUUID reqID = httpScriptMod.
2460 StartHttpRequest(m_localID, m_itemID, url, parameters, body);
2461
2462 if( reqID != null )
2463 return reqID.ToString();
2464 else
2465 return null;
2457 } 2466 }
2458 2467
2459 public void llResetLandBanList() 2468 public void llResetLandBanList()
diff --git a/OpenSim/Region/ScriptEngine/DotNetEngine/LSLLongCmdHandler.cs b/OpenSim/Region/ScriptEngine/DotNetEngine/LSLLongCmdHandler.cs
index 5061629..ddc0c62 100644
--- a/OpenSim/Region/ScriptEngine/DotNetEngine/LSLLongCmdHandler.cs
+++ b/OpenSim/Region/ScriptEngine/DotNetEngine/LSLLongCmdHandler.cs
@@ -110,7 +110,9 @@ namespace OpenSim.Region.ScriptEngine.DotNetEngine
110 // Remove from: Timers 110 // Remove from: Timers
111 UnSetTimerEvents(localID, itemID); 111 UnSetTimerEvents(localID, itemID);
112 // Remove from: HttpRequest 112 // Remove from: HttpRequest
113 StopHttpRequest(localID, itemID); 113 IHttpRequests iHttpReq =
114 m_ScriptEngine.World.RequestModuleInterface<IHttpRequests>();
115 iHttpReq.StopHttpRequest(localID, itemID);
114 } 116 }
115 117
116 #region TIMER 118 #region TIMER
@@ -198,136 +200,67 @@ namespace OpenSim.Region.ScriptEngine.DotNetEngine
198 200
199 #region HTTP REQUEST 201 #region HTTP REQUEST
200 202
201 // 203 public void CheckHttpRequests()
202 // HTTP REAQUEST
203 //
204 private class HttpClass
205 { 204 {
206 public uint localID; 205 IHttpRequests iHttpReq =
207 public LLUUID itemID; 206 m_ScriptEngine.World.RequestModuleInterface<IHttpRequests>();
208 public string url;
209 public List<string> parameters;
210 public string body;
211 public DateTime next;
212 207
213 public string response_request_id; 208 HttpRequestClass httpInfo = null;
214 public int response_status;
215 public List<string> response_metadata;
216 public string response_body;
217 209
218 public void SendRequest() 210 if( iHttpReq != null )
219 { 211 httpInfo = iHttpReq.GetNextCompletedRequest();
220 // TODO: SEND REQUEST!!!
221 }
222
223 public void Stop()
224 {
225 // TODO: Cancel any ongoing request
226 }
227 212
228 public bool CheckResponse() 213 while ( httpInfo != null )
229 { 214 {
230 // TODO: Check if we got a response yet, return true if so -- false if not 215
231 return true; 216 Console.WriteLine("PICKED HTTP REQ:" + httpInfo.response_body + httpInfo.status);
232 217
233 // TODO: If we got a response, set the following then return true 218 // Deliver data to prim's remote_data handler
234 //response_request_id 219 //
235 //response_status 220 // TODO: Returning null for metadata, since the lsl function
236 //response_metadata 221 // only returns the byte for HTTP_BODY_TRUNCATED, which is not
237 //response_body 222 // implemented here yet anyway. Should be fixed if/when maxsize
238 } 223 // is supported
239 } 224
225 object[] resobj = new object[]
226 {
227 httpInfo.reqID.ToString(), httpInfo.status, null, httpInfo.response_body
228 };
240 229
241 private List<HttpClass> HttpRequests = new List<HttpClass>(); 230 m_ScriptEngine.m_EventQueueManager.AddToScriptQueue(
242 private object HttpListLock = new object(); 231 httpInfo.localID, httpInfo.itemID, "http_response", resobj
232 );
243 233
244 public void StartHttpRequest(uint localID, LLUUID itemID, string url, List<string> parameters, string body) 234 httpInfo.Stop();
245 { 235 httpInfo = null;
246 Console.WriteLine("StartHttpRequest");
247
248 HttpClass htc = new HttpClass();
249 htc.localID = localID;
250 htc.itemID = itemID;
251 htc.url = url;
252 htc.parameters = parameters;
253 htc.body = body;
254 lock (HttpListLock)
255 {
256 //ADD REQUEST
257 HttpRequests.Add(htc);
258 }
259 }
260 236
261 public void StopHttpRequest(uint m_localID, LLUUID m_itemID) 237 httpInfo = iHttpReq.GetNextCompletedRequest();
262 {
263 // Remove from list
264 lock (HttpListLock)
265 {
266 List<HttpClass> NewHttpList = new List<HttpClass>();
267 foreach (HttpClass ts in HttpRequests)
268 {
269 if (ts.localID != m_localID && ts.itemID != m_itemID)
270 {
271 // Keeping this one
272 NewHttpList.Add(ts);
273 }
274 else
275 {
276 // Shutting this one down
277 ts.Stop();
278 }
279 }
280 HttpRequests.Clear();
281 HttpRequests = NewHttpList;
282 } 238 }
283 } 239 }
284 240
285 public void CheckHttpRequests()
286 {
287 // Nothing to do here?
288 if (HttpRequests.Count == 0)
289 return;
290
291 lock (HttpListLock)
292 {
293 foreach (HttpClass ts in HttpRequests)
294 {
295 if (ts.CheckResponse() == true)
296 {
297 // Add it to event queue
298 //key request_id, integer status, list metadata, string body
299 object[] resobj =
300 new object[]
301 {ts.response_request_id, ts.response_status, ts.response_metadata, ts.response_body};
302 m_ScriptEngine.m_EventQueueManager.AddToScriptQueue(ts.localID, ts.itemID, "http_response",
303 resobj);
304 // Now stop it
305 StopHttpRequest(ts.localID, ts.itemID);
306 }
307 }
308 } // lock
309 }
310
311 #endregion 241 #endregion
312 242
313 public void CheckXMLRPCRequests() 243 public void CheckXMLRPCRequests()
314 { 244 {
315 IXMLRPC xmlrpc = m_ScriptEngine.World.RequestModuleInterface<IXMLRPC>(); 245 IXMLRPC xmlrpc = m_ScriptEngine.World.RequestModuleInterface<IXMLRPC>();
316 246
317 while (xmlrpc.hasRequests()) 247 if (xmlrpc != null)
318 { 248 {
319 RPCRequestInfo rInfo = xmlrpc.GetNextRequest(); 249 while (xmlrpc.hasRequests())
320 Console.WriteLine("PICKED REQUEST"); 250 {
321 251 RPCRequestInfo rInfo = xmlrpc.GetNextRequest();
322 //Deliver data to prim's remote_data handler 252 Console.WriteLine("PICKED REQUEST");
323 object[] resobj = new object[] 253
324 { 254 //Deliver data to prim's remote_data handler
325 2, rInfo.GetChannelKey().ToString(), rInfo.GetMessageID().ToString(), "", rInfo.GetIntValue(), 255 object[] resobj = new object[]
326 rInfo.GetStrVal() 256 {
327 }; 257 2, rInfo.GetChannelKey().ToString(), rInfo.GetMessageID().ToString(), "", rInfo.GetIntValue(),
328 m_ScriptEngine.m_EventQueueManager.AddToScriptQueue( 258 rInfo.GetStrVal()
329 rInfo.GetLocalID(), rInfo.GetItemID(), "remote_data", resobj 259 };
330 ); 260 m_ScriptEngine.m_EventQueueManager.AddToScriptQueue(
261 rInfo.GetLocalID(), rInfo.GetItemID(), "remote_data", resobj
262 );
263 }
331 } 264 }
332 } 265 }
333 266
@@ -338,7 +271,6 @@ namespace OpenSim.Region.ScriptEngine.DotNetEngine
338 while (comms.HasMessages()) 271 while (comms.HasMessages())
339 { 272 {
340 ListenerInfo lInfo = comms.GetNextMessage(); 273 ListenerInfo lInfo = comms.GetNextMessage();
341 Console.WriteLine("PICKED LISTENER");
342 274
343 //Deliver data to prim's listen handler 275 //Deliver data to prim's listen handler
344 object[] resobj = new object[] 276 object[] resobj = new object[]