1
0

SimianAssetServiceConnector.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665
  1. /*
  2. * Copyright (c) Contributors, http://opensimulator.org/
  3. * See CONTRIBUTORS.TXT for a full list of copyright holders.
  4. *
  5. * Redistribution and use in source and binary forms, with or without
  6. * modification, are permitted provided that the following conditions are met:
  7. * * Redistributions of source code must retain the above copyright
  8. * notice, this list of conditions and the following disclaimer.
  9. * * Redistributions in binary form must reproduce the above copyright
  10. * notice, this list of conditions and the following disclaimer in the
  11. * documentation and/or other materials provided with the distribution.
  12. * * Neither the name of the OpenSimulator Project nor the
  13. * names of its contributors may be used to endorse or promote products
  14. * derived from this software without specific prior written permission.
  15. *
  16. * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
  17. * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  18. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  19. * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
  20. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  21. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  22. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  23. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  24. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  25. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  26. */
  27. using System;
  28. using System.Collections.Generic;
  29. using System.Collections.Specialized;
  30. using System.IO;
  31. using System.Net;
  32. using System.Reflection;
  33. using log4net;
  34. using Mono.Addins;
  35. using Nini.Config;
  36. using OpenSim.Framework;
  37. using OpenSim.Region.Framework.Interfaces;
  38. using OpenSim.Region.Framework.Scenes;
  39. using OpenSim.Services.Interfaces;
  40. using OpenMetaverse;
  41. using OpenMetaverse.StructuredData;
  42. namespace OpenSim.Services.Connectors.SimianGrid
  43. {
  44. /// <summary>
  45. /// Connects to the SimianGrid asset service
  46. /// </summary>
  47. [Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule", Id = "SimianAssetServiceConnector")]
  48. public class SimianAssetServiceConnector : IAssetService, ISharedRegionModule
  49. {
  50. private static readonly ILog m_log =
  51. LogManager.GetLogger(
  52. MethodBase.GetCurrentMethod().DeclaringType);
  53. private static string ZeroID = UUID.Zero.ToString();
  54. private string m_serverUrl = String.Empty;
  55. private IImprovedAssetCache m_cache;
  56. private bool m_Enabled = false;
  57. #region ISharedRegionModule
  58. public Type ReplaceableInterface { get { return null; } }
  59. public void RegionLoaded(Scene scene)
  60. {
  61. if (m_cache == null)
  62. {
  63. IImprovedAssetCache cache = scene.RequestModuleInterface<IImprovedAssetCache>();
  64. if (cache is ISharedRegionModule)
  65. m_cache = cache;
  66. }
  67. }
  68. public void PostInitialise() { }
  69. public void Close() { }
  70. public SimianAssetServiceConnector() { }
  71. public string Name { get { return "SimianAssetServiceConnector"; } }
  72. public void AddRegion(Scene scene) { if (m_Enabled) { scene.RegisterModuleInterface<IAssetService>(this); } }
  73. public void RemoveRegion(Scene scene) { if (m_Enabled) { scene.UnregisterModuleInterface<IAssetService>(this); } }
  74. #endregion ISharedRegionModule
  75. public SimianAssetServiceConnector(IConfigSource source)
  76. {
  77. CommonInit(source);
  78. }
  79. public SimianAssetServiceConnector(string url)
  80. {
  81. if (!url.EndsWith("/") && !url.EndsWith("="))
  82. url = url + '/';
  83. m_serverUrl = url;
  84. }
  85. public void Initialise(IConfigSource source)
  86. {
  87. IConfig moduleConfig = source.Configs["Modules"];
  88. if (moduleConfig != null)
  89. {
  90. string name = moduleConfig.GetString("AssetServices", "");
  91. if (name == Name)
  92. CommonInit(source);
  93. }
  94. }
  95. private void CommonInit(IConfigSource source)
  96. {
  97. IConfig gridConfig = source.Configs["AssetService"];
  98. if (gridConfig != null)
  99. {
  100. string serviceUrl = gridConfig.GetString("AssetServerURI");
  101. if (!String.IsNullOrEmpty(serviceUrl))
  102. {
  103. if (!serviceUrl.EndsWith("/") && !serviceUrl.EndsWith("="))
  104. serviceUrl = serviceUrl + '/';
  105. m_serverUrl = serviceUrl;
  106. }
  107. }
  108. if (String.IsNullOrEmpty(m_serverUrl))
  109. m_log.Info("[SIMIAN ASSET CONNECTOR]: No AssetServerURI specified, disabling connector");
  110. else
  111. m_Enabled = true;
  112. }
  113. #region IAssetService
  114. public AssetBase Get(string id)
  115. {
  116. if (String.IsNullOrEmpty(m_serverUrl))
  117. {
  118. m_log.Error("[SIMIAN ASSET CONNECTOR]: No AssetServerURI configured");
  119. throw new InvalidOperationException();
  120. }
  121. // Cache fetch
  122. if (m_cache != null)
  123. {
  124. AssetBase asset = m_cache.Get(id);
  125. if (asset != null)
  126. return asset;
  127. }
  128. return SimianGetOperation(id);
  129. }
  130. public AssetBase GetCached(string id)
  131. {
  132. if (m_cache != null)
  133. return m_cache.Get(id);
  134. return null;
  135. }
  136. /// <summary>
  137. /// Get an asset's metadata
  138. /// </summary>
  139. /// <param name="id"></param>
  140. /// <returns></returns>
  141. public AssetMetadata GetMetadata(string id)
  142. {
  143. if (String.IsNullOrEmpty(m_serverUrl))
  144. {
  145. m_log.Error("[SIMIAN ASSET CONNECTOR]: No AssetServerURI configured");
  146. throw new InvalidOperationException();
  147. }
  148. // Cache fetch
  149. if (m_cache != null)
  150. {
  151. AssetBase asset = m_cache.Get(id);
  152. if (asset != null)
  153. return asset.Metadata;
  154. }
  155. // return GetRemoteMetadata(id);
  156. return SimianGetMetadataOperation(id);
  157. }
  158. public byte[] GetData(string id)
  159. {
  160. if (String.IsNullOrEmpty(m_serverUrl))
  161. {
  162. m_log.Error("[SIMIAN ASSET CONNECTOR]: No AssetServerURI configured");
  163. throw new InvalidOperationException();
  164. }
  165. AssetBase asset = Get(id);
  166. if (asset != null)
  167. return asset.Data;
  168. return null;
  169. }
  170. /// <summary>
  171. /// Get an asset asynchronously
  172. /// </summary>
  173. /// <param name="id">The asset id</param>
  174. /// <param name="sender">Represents the requester. Passed back via the handler</param>
  175. /// <param name="handler">The handler to call back once the asset has been retrieved</param>
  176. /// <returns>True if the id was parseable, false otherwise</returns>
  177. public bool Get(string id, Object sender, AssetRetrieved handler)
  178. {
  179. if (String.IsNullOrEmpty(m_serverUrl))
  180. {
  181. m_log.Error("[SIMIAN ASSET CONNECTOR]: No AssetServerURI configured");
  182. throw new InvalidOperationException();
  183. }
  184. // Cache fetch
  185. if (m_cache != null)
  186. {
  187. AssetBase asset = m_cache.Get(id);
  188. if (asset != null)
  189. {
  190. handler(id, sender, asset);
  191. return true;
  192. }
  193. }
  194. Util.FireAndForget(
  195. delegate(object o)
  196. {
  197. AssetBase asset = SimianGetOperation(id);
  198. handler(id, sender, asset);
  199. }
  200. );
  201. return true;
  202. }
  203. /// <summary>
  204. /// Creates a new asset
  205. /// </summary>
  206. /// Returns a random ID if none is passed into it
  207. /// <param name="asset"></param>
  208. /// <returns></returns>
  209. public string Store(AssetBase asset)
  210. {
  211. if (String.IsNullOrEmpty(m_serverUrl))
  212. {
  213. m_log.Error("[SIMIAN ASSET CONNECTOR]: No AssetServerURI configured");
  214. throw new InvalidOperationException();
  215. }
  216. bool storedInCache = false;
  217. // AssetID handling
  218. if (String.IsNullOrEmpty(asset.ID) || asset.ID == ZeroID)
  219. {
  220. asset.FullID = UUID.Random();
  221. asset.ID = asset.FullID.ToString();
  222. }
  223. // Cache handling
  224. if (m_cache != null)
  225. {
  226. m_cache.Cache(asset);
  227. storedInCache = true;
  228. }
  229. // Local asset handling
  230. if (asset.Local)
  231. {
  232. if (!storedInCache)
  233. {
  234. m_log.Error("Cannot store local " + asset.Metadata.ContentType + " asset without an asset cache");
  235. asset.ID = null;
  236. asset.FullID = UUID.Zero;
  237. }
  238. return asset.ID;
  239. }
  240. return SimianStoreOperation(asset);
  241. }
  242. /// <summary>
  243. /// Update an asset's content
  244. /// </summary>
  245. /// Attachments and bare scripts need this!!
  246. /// <param name="id"> </param>
  247. /// <param name="data"></param>
  248. /// <returns></returns>
  249. public bool UpdateContent(string id, byte[] data)
  250. {
  251. if (String.IsNullOrEmpty(m_serverUrl))
  252. {
  253. m_log.Error("[SIMIAN ASSET CONNECTOR]: No AssetServerURI configured");
  254. throw new InvalidOperationException();
  255. }
  256. AssetBase asset = Get(id);
  257. if (asset == null)
  258. {
  259. m_log.WarnFormat("[SIMIAN ASSET CONNECTOR]: Failed to fetch asset {0} for updating", id);
  260. return false;
  261. }
  262. asset.Data = data;
  263. string result = Store(asset);
  264. return !String.IsNullOrEmpty(result);
  265. }
  266. /// <summary>
  267. /// Delete an asset
  268. /// </summary>
  269. /// <param name="id"></param>
  270. /// <returns></returns>
  271. public bool Delete(string id)
  272. {
  273. if (String.IsNullOrEmpty(m_serverUrl))
  274. {
  275. m_log.Error("[SIMIAN ASSET CONNECTOR]: No AssetServerURI configured");
  276. throw new InvalidOperationException();
  277. }
  278. if (m_cache != null)
  279. m_cache.Expire(id);
  280. return SimianDeleteOperation(id);
  281. }
  282. #endregion IAssetService
  283. #region SimianOperations
  284. /// <summary>
  285. /// Invokes the xRemoveAsset operation on the simian server to delete an asset
  286. /// </summary>
  287. /// <param name="id"></param>
  288. /// <returns></returns>
  289. private bool SimianDeleteOperation(string id)
  290. {
  291. try
  292. {
  293. NameValueCollection requestArgs = new NameValueCollection
  294. {
  295. { "RequestMethod", "xRemoveAsset" },
  296. { "AssetID", id }
  297. };
  298. OSDMap response = SimianGrid.PostToService(m_serverUrl,requestArgs);
  299. if (! response["Success"].AsBoolean())
  300. {
  301. m_log.WarnFormat("[SIMIAN ASSET CONNECTOR]: failed to delete asset; {0}",response["Message"].AsString());
  302. return false;
  303. }
  304. return true;
  305. }
  306. catch (Exception ex)
  307. {
  308. m_log.WarnFormat("[SIMIAN ASSET CONNECTOR]: failed to delete asset {0}; {1}", id, ex.Message);
  309. }
  310. return false;
  311. }
  312. /// <summary>
  313. /// Invokes the xAddAsset operation on the simian server to create or update an asset
  314. /// </summary>
  315. /// <param name="id"></param>
  316. /// <returns></returns>
  317. private string SimianStoreOperation(AssetBase asset)
  318. {
  319. try
  320. {
  321. NameValueCollection requestArgs = new NameValueCollection
  322. {
  323. { "RequestMethod", "xAddAsset" },
  324. { "ContentType", asset.Metadata.ContentType },
  325. { "EncodedData", Convert.ToBase64String(asset.Data) },
  326. { "AssetID", asset.FullID.ToString() },
  327. { "CreatorID", asset.Metadata.CreatorID },
  328. { "Temporary", asset.Temporary ? "1" : "0" },
  329. { "Name", asset.Name }
  330. };
  331. OSDMap response = SimianGrid.PostToService(m_serverUrl,requestArgs);
  332. if (! response["Success"].AsBoolean())
  333. {
  334. m_log.WarnFormat("[SIMIAN ASSET CONNECTOR] failed to store asset; {0}",response["Message"].AsString());
  335. return null;
  336. }
  337. // asset.ID is always set before calling this function
  338. return asset.ID;
  339. }
  340. catch (Exception ex)
  341. {
  342. m_log.ErrorFormat("[SIMIAN ASSET CONNECTOR] failed to store asset; {0}",ex.Message);
  343. }
  344. return null;
  345. }
  346. /// <summary>
  347. /// Invokes the xGetAsset operation on the simian server to get data associated with an asset
  348. /// </summary>
  349. /// <param name="id"></param>
  350. /// <returns></returns>
  351. private AssetBase SimianGetOperation(string id)
  352. {
  353. try
  354. {
  355. NameValueCollection requestArgs = new NameValueCollection
  356. {
  357. { "RequestMethod", "xGetAsset" },
  358. { "ID", id }
  359. };
  360. OSDMap response = SimianGrid.PostToService(m_serverUrl,requestArgs);
  361. if (! response["Success"].AsBoolean())
  362. {
  363. m_log.WarnFormat("[SIMIAN ASSET CONNECTOR] Failed to get asset; {0}",response["Message"].AsString());
  364. return null;
  365. }
  366. AssetBase asset = new AssetBase();
  367. asset.ID = id;
  368. asset.Name = String.Empty;
  369. asset.Metadata.ContentType = response["ContentType"].AsString(); // this will also set the asset Type property
  370. asset.CreatorID = response["CreatorID"].AsString();
  371. asset.Data = System.Convert.FromBase64String(response["EncodedData"].AsString());
  372. asset.Local = false;
  373. asset.Temporary = response["Temporary"];
  374. return asset;
  375. }
  376. catch (Exception ex)
  377. {
  378. m_log.WarnFormat("[SIMIAN ASSET CONNECTOR]: failed to retrieve asset {0}; {1}", id, ex.Message);
  379. }
  380. return null;
  381. }
  382. /// <summary>
  383. /// Invokes the xGetAssetMetadata operation on the simian server to retrieve metadata for an asset
  384. /// This operation is generally used to determine if an asset exists in the database
  385. /// </summary>
  386. /// <param name="id"></param>
  387. /// <returns></returns>
  388. private AssetMetadata SimianGetMetadataOperation(string id)
  389. {
  390. try
  391. {
  392. NameValueCollection requestArgs = new NameValueCollection
  393. {
  394. { "RequestMethod", "xGetAssetMetadata" },
  395. { "ID", id }
  396. };
  397. OSDMap response = SimianGrid.PostToService(m_serverUrl,requestArgs);
  398. if (! response["Success"].AsBoolean())
  399. {
  400. // this is not really an error, this call is used to test existence
  401. // m_log.DebugFormat("[SIMIAN ASSET CONNECTOR] Failed to get asset metadata; {0}",response["Message"].AsString());
  402. return null;
  403. }
  404. AssetMetadata metadata = new AssetMetadata();
  405. metadata.ID = id;
  406. metadata.ContentType = response["ContentType"].AsString();
  407. metadata.CreatorID = response["CreatorID"].AsString();
  408. metadata.Local = false;
  409. metadata.Temporary = response["Temporary"];
  410. string lastModifiedStr = response["Last-Modified"].AsString();
  411. if (! String.IsNullOrEmpty(lastModifiedStr))
  412. {
  413. DateTime lastModified;
  414. if (DateTime.TryParse(lastModifiedStr, out lastModified))
  415. metadata.CreationDate = lastModified;
  416. }
  417. return metadata;
  418. }
  419. catch (Exception ex)
  420. {
  421. m_log.WarnFormat("[SIMIAN ASSET CONNECTOR]: Failed to get asset metadata; {0}", ex.Message);
  422. }
  423. return null;
  424. }
  425. #endregion
  426. // private AssetMetadata GetRemoteMetadata(string id)
  427. // {
  428. // Uri url;
  429. // AssetMetadata metadata = null;
  430. // // Determine if id is an absolute URL or a grid-relative UUID
  431. // if (!Uri.TryCreate(id, UriKind.Absolute, out url))
  432. // url = new Uri(m_serverUrl + id);
  433. // try
  434. // {
  435. // HttpWebRequest request = UntrustedHttpWebRequest.Create(url);
  436. // request.Method = "HEAD";
  437. // using (WebResponse response = request.GetResponse())
  438. // {
  439. // using (Stream responseStream = response.GetResponseStream())
  440. // {
  441. // // Create the metadata object
  442. // metadata = new AssetMetadata();
  443. // metadata.ContentType = response.ContentType;
  444. // metadata.ID = id;
  445. // UUID uuid;
  446. // if (UUID.TryParse(id, out uuid))
  447. // metadata.FullID = uuid;
  448. // string lastModifiedStr = response.Headers.Get("Last-Modified");
  449. // if (!String.IsNullOrEmpty(lastModifiedStr))
  450. // {
  451. // DateTime lastModified;
  452. // if (DateTime.TryParse(lastModifiedStr, out lastModified))
  453. // metadata.CreationDate = lastModified;
  454. // }
  455. // }
  456. // }
  457. // }
  458. // catch (Exception ex)
  459. // {
  460. // m_log.Warn("[SIMIAN ASSET CONNECTOR]: Asset HEAD from " + url + " failed: " + ex.Message);
  461. // }
  462. // return metadata;
  463. // }
  464. // private AssetBase GetRemote(string id)
  465. // {
  466. // AssetBase asset = null;
  467. // Uri url;
  468. // // Determine if id is an absolute URL or a grid-relative UUID
  469. // if (!Uri.TryCreate(id, UriKind.Absolute, out url))
  470. // url = new Uri(m_serverUrl + id);
  471. // try
  472. // {
  473. // HttpWebRequest request = UntrustedHttpWebRequest.Create(url);
  474. // using (WebResponse response = request.GetResponse())
  475. // {
  476. // using (Stream responseStream = response.GetResponseStream())
  477. // {
  478. // string creatorID = response.Headers.GetOne("X-Asset-Creator-Id") ?? String.Empty;
  479. // // Create the asset object
  480. // asset = new AssetBase(id, String.Empty, SLUtil.ContentTypeToSLAssetType(response.ContentType), creatorID);
  481. // UUID assetID;
  482. // if (UUID.TryParse(id, out assetID))
  483. // asset.FullID = assetID;
  484. // // Grab the asset data from the response stream
  485. // using (MemoryStream stream = new MemoryStream())
  486. // {
  487. // responseStream.CopyStream(stream, Int32.MaxValue);
  488. // asset.Data = stream.ToArray();
  489. // }
  490. // }
  491. // }
  492. // // Cache store
  493. // if (m_cache != null && asset != null)
  494. // m_cache.Cache(asset);
  495. // return asset;
  496. // }
  497. // catch (Exception ex)
  498. // {
  499. // m_log.Warn("[SIMIAN ASSET CONNECTOR]: Asset GET from " + url + " failed: " + ex.Message);
  500. // return null;
  501. // }
  502. // }
  503. // private string StoreRemote(AssetBase asset)
  504. // {
  505. // // Distinguish public and private assets
  506. // bool isPublic = true;
  507. // switch ((AssetType)asset.Type)
  508. // {
  509. // case AssetType.CallingCard:
  510. // case AssetType.Gesture:
  511. // case AssetType.LSLBytecode:
  512. // case AssetType.LSLText:
  513. // isPublic = false;
  514. // break;
  515. // }
  516. // string errorMessage = null;
  517. // // Build the remote storage request
  518. // List<MultipartForm.Element> postParameters = new List<MultipartForm.Element>()
  519. // {
  520. // new MultipartForm.Parameter("AssetID", asset.FullID.ToString()),
  521. // new MultipartForm.Parameter("CreatorID", asset.Metadata.CreatorID),
  522. // new MultipartForm.Parameter("Temporary", asset.Temporary ? "1" : "0"),
  523. // new MultipartForm.Parameter("Public", isPublic ? "1" : "0"),
  524. // new MultipartForm.File("Asset", asset.Name, asset.Metadata.ContentType, asset.Data)
  525. // };
  526. // // Make the remote storage request
  527. // try
  528. // {
  529. // // Simian does not require the asset ID to be in the URL because it's in the post data.
  530. // // By appending it to the URL also, we allow caching proxies (squid) to invalidate asset URLs
  531. // HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(m_serverUrl + asset.FullID.ToString());
  532. // using (HttpWebResponse response = MultipartForm.Post(request, postParameters))
  533. // {
  534. // using (Stream responseStream = response.GetResponseStream())
  535. // {
  536. // string responseStr = null;
  537. // try
  538. // {
  539. // responseStr = responseStream.GetStreamString();
  540. // OSD responseOSD = OSDParser.Deserialize(responseStr);
  541. // if (responseOSD.Type == OSDType.Map)
  542. // {
  543. // OSDMap responseMap = (OSDMap)responseOSD;
  544. // if (responseMap["Success"].AsBoolean())
  545. // return asset.ID;
  546. // else
  547. // errorMessage = "Upload failed: " + responseMap["Message"].AsString();
  548. // }
  549. // else
  550. // {
  551. // errorMessage = "Response format was invalid:\n" + responseStr;
  552. // }
  553. // }
  554. // catch (Exception ex)
  555. // {
  556. // if (!String.IsNullOrEmpty(responseStr))
  557. // errorMessage = "Failed to parse the response:\n" + responseStr;
  558. // else
  559. // errorMessage = "Failed to retrieve the response: " + ex.Message;
  560. // }
  561. // }
  562. // }
  563. // }
  564. // catch (WebException ex)
  565. // {
  566. // errorMessage = ex.Message;
  567. // }
  568. // m_log.WarnFormat("[SIMIAN ASSET CONNECTOR]: Failed to store asset \"{0}\" ({1}, {2}): {3}",
  569. // asset.Name, asset.ID, asset.Metadata.ContentType, errorMessage);
  570. // return null;
  571. // }
  572. }
  573. }