RequestBody.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using System.IO;
  5. using COSXML.Common;
  6. using COSXML.Log;
  7. using COSXML.Utils;
  8. namespace COSXML.Network
  9. {
  10. /// <summary>
  11. /// request body for http request
  12. /// </summary>
  13. public abstract class RequestBody
  14. {
  15. protected static string TAG = typeof(RequestBody).Name;
  16. // 64kb
  17. public const int SEGMENT_SIZE = 64 * 1024;
  18. protected long contentLength;
  19. protected string contentType;
  20. protected Callback.OnProgressCallback progressCallback;
  21. /// <summary>
  22. /// body length
  23. /// </summary>
  24. public virtual long ContentLength
  25. {
  26. get
  27. {
  28. return contentLength;
  29. }
  30. set { contentLength = value; }
  31. }
  32. /// <summary>
  33. /// body mime type
  34. /// </summary>
  35. public virtual string ContentType
  36. {
  37. get
  38. {
  39. return contentType;
  40. }
  41. set { contentType = value; }
  42. }
  43. /// <summary>
  44. /// calculation content md5
  45. /// </summary>
  46. /// <returns></returns>
  47. public virtual string GetMD5()
  48. {
  49. throw new NotImplementedException();
  50. }
  51. /// <summary>
  52. /// Synchronization method: write data to outputStream
  53. /// </summary>
  54. /// <param name="outputStream"> output stream for writing data</param>
  55. public abstract void OnWrite(Stream outputStream);
  56. /// <summary>
  57. /// Asynchronous method: handle request body
  58. /// </summary>
  59. /// <param name="outputStream"></param>
  60. /// <param name="EndRequestBody"></param>
  61. public abstract void StartHandleRequestBody(Stream outputStream, EndRequestBody endRequestBody);
  62. public Callback.OnProgressCallback ProgressCallback
  63. {
  64. get
  65. {
  66. return progressCallback;
  67. }
  68. set { progressCallback = value; }
  69. }
  70. /// <summary>
  71. /// notify progress is complete!
  72. /// </summary>
  73. internal void OnNotifyGetResponse()
  74. {
  75. if (progressCallback != null && contentLength >= 0)
  76. {
  77. progressCallback(contentLength, contentLength);
  78. }
  79. }
  80. /// <summary>
  81. /// calculation progress
  82. /// </summary>
  83. /// <param name="complete"></param>
  84. /// <param name="total"></param>
  85. protected void UpdateProgress(long complete, long total)
  86. {
  87. if (total == 0)
  88. {
  89. progressCallback(0, 0);
  90. }
  91. else if (complete < total)
  92. {
  93. progressCallback(complete, total);
  94. }
  95. else
  96. {
  97. progressCallback(total - 1, total);
  98. }
  99. }
  100. }
  101. public class RequestBodyState
  102. {
  103. public byte[] buffer;
  104. public long complete;
  105. public Stream outputStream;
  106. public EndRequestBody endRequestBody;
  107. }
  108. public delegate void EndRequestBody(Exception exception);
  109. public class ByteRequestBody : RequestBody
  110. {
  111. private readonly byte[] data;
  112. //private RequestBodyState requestBodyState;
  113. public ByteRequestBody(byte[] data)
  114. {
  115. this.data = data;
  116. contentLength = data.Length;
  117. }
  118. public override void OnWrite(Stream outputStream)
  119. {
  120. StartHandleRequestBody(outputStream);
  121. }
  122. public override string GetMD5()
  123. {
  124. return DigestUtils.GetMd5ToBase64(data);
  125. }
  126. public override void StartHandleRequestBody(Stream outputStream, EndRequestBody endRequestBody = null)
  127. {
  128. try
  129. {
  130. int completed = 0;
  131. while (completed + SEGMENT_SIZE < contentLength)
  132. {
  133. outputStream.Write(data, completed, SEGMENT_SIZE);
  134. outputStream.Flush();
  135. completed += SEGMENT_SIZE;
  136. if (progressCallback != null)
  137. {
  138. UpdateProgress(completed, contentLength);
  139. }
  140. }
  141. if (completed < contentLength)
  142. {
  143. //包括本身
  144. //包括本身
  145. outputStream.Write(data, completed, (int)(contentLength - completed));
  146. outputStream.Flush();
  147. if (progressCallback != null)
  148. {
  149. UpdateProgress(contentLength, contentLength);
  150. }
  151. }
  152. if (endRequestBody != null)
  153. {
  154. endRequestBody(null);
  155. }
  156. }
  157. catch (Exception ex)
  158. {
  159. if (endRequestBody != null)
  160. {
  161. endRequestBody(ex);
  162. }
  163. else
  164. {
  165. throw;
  166. }
  167. }
  168. finally
  169. {
  170. if (outputStream != null)
  171. {
  172. outputStream.Flush();
  173. outputStream.Close();
  174. outputStream.Dispose();
  175. //QLog.D("XIAO", "stream close");
  176. outputStream = null;
  177. }
  178. }
  179. }
  180. }
  181. public class FileRequestBody : RequestBody
  182. {
  183. private readonly string srcPath;
  184. private readonly long fileOffset;
  185. //private RequestBodyState requestBodyState;
  186. private FileStream fileStream;
  187. public FileRequestBody(string srcPath, long fileOffset, long sendContentSize)
  188. {
  189. this.srcPath = srcPath;
  190. this.fileOffset = fileOffset;
  191. contentLength = sendContentSize;
  192. }
  193. public override void OnWrite(Stream outputStream)
  194. {
  195. StartHandleRequestBody(outputStream);
  196. }
  197. public override string GetMD5()
  198. {
  199. try
  200. {
  201. fileStream = new FileStream(srcPath, FileMode.Open, FileAccess.Read);
  202. fileStream.Seek(fileOffset, SeekOrigin.Begin);
  203. return DigestUtils.GetMd5ToBase64(fileStream, contentLength);
  204. }
  205. catch (Exception ex)
  206. {
  207. QLog.Error(TAG, ex.Message, ex);
  208. throw;
  209. }
  210. finally
  211. {
  212. if (fileStream != null)
  213. {
  214. fileStream.Close();
  215. }
  216. }
  217. }
  218. public override void StartHandleRequestBody(Stream outputStream, EndRequestBody endRequestBody = null)
  219. {
  220. FileStream fileStream = null;
  221. try
  222. {
  223. byte[] buffer = new byte[SEGMENT_SIZE];
  224. int bytesRead = 0;
  225. long completed = bytesRead;
  226. fileStream = new FileStream(srcPath, FileMode.Open, FileAccess.Read);
  227. //seek to designated position
  228. //seek to designated position
  229. fileStream.Seek(fileOffset, SeekOrigin.Begin);
  230. long remain = contentLength - completed;
  231. if (remain > 0)
  232. {
  233. while ((bytesRead = fileStream.Read(buffer, 0, (int)(buffer.Length > remain ? remain : buffer.Length))) != 0)
  234. {
  235. outputStream.Write(buffer, 0, bytesRead);
  236. outputStream.Flush();
  237. completed += bytesRead;
  238. if (progressCallback != null)
  239. {
  240. UpdateProgress(completed, contentLength);
  241. }
  242. remain = contentLength - completed;
  243. if (remain == 0)
  244. {
  245. break;
  246. }
  247. }
  248. }
  249. else
  250. {
  251. if (progressCallback != null)
  252. {
  253. UpdateProgress(completed, contentLength);
  254. }
  255. }
  256. buffer = null;
  257. if (endRequestBody != null)
  258. {
  259. endRequestBody(null);
  260. }
  261. }
  262. catch (Exception ex)
  263. {
  264. if (endRequestBody != null)
  265. {
  266. endRequestBody(ex);
  267. }
  268. else
  269. {
  270. throw;
  271. }
  272. }
  273. finally
  274. {
  275. if (fileStream != null)
  276. {
  277. fileStream.Close();
  278. fileStream.Dispose();
  279. //QLog.D("XIAO", "stream close");
  280. fileStream = null;
  281. }
  282. if (outputStream != null)
  283. {
  284. outputStream.Flush();
  285. outputStream.Close();
  286. outputStream.Dispose();
  287. //QLog.D("XIAO", "stream close");
  288. outputStream = null;
  289. }
  290. }
  291. }
  292. }
  293. public class StreamRequestBody : RequestBody
  294. {
  295. private readonly long fileOffset;
  296. //private RequestBodyState requestBodyState;
  297. private Stream stream;
  298. public StreamRequestBody(Stream stream, long fileOffset, long sendContentSize)
  299. {
  300. this.stream = stream;
  301. this.fileOffset = fileOffset;
  302. contentLength = sendContentSize;
  303. }
  304. public override void OnWrite(Stream outputStream)
  305. {
  306. StartHandleRequestBody(outputStream);
  307. }
  308. public override string GetMD5()
  309. {
  310. try
  311. {
  312. stream.Seek(fileOffset, SeekOrigin.Begin);
  313. return DigestUtils.GetMd5ToBase64(stream, contentLength);
  314. }
  315. catch (Exception ex)
  316. {
  317. QLog.Error(TAG, ex.Message, ex);
  318. throw;
  319. }
  320. }
  321. public override void StartHandleRequestBody(Stream outputStream, EndRequestBody endRequestBody = null)
  322. {
  323. try
  324. {
  325. byte[] buffer = new byte[SEGMENT_SIZE];
  326. int bytesRead = 0;
  327. long completed = bytesRead;
  328. //seek to designated position
  329. //seek to designated position
  330. stream.Seek(fileOffset, SeekOrigin.Begin);
  331. long remain = contentLength - completed;
  332. if (remain > 0)
  333. {
  334. while ((bytesRead = stream.Read(buffer, 0, (int)(buffer.Length > remain ? remain : buffer.Length))) != 0)
  335. {
  336. outputStream.Write(buffer, 0, bytesRead);
  337. outputStream.Flush();
  338. completed += bytesRead;
  339. if (progressCallback != null)
  340. {
  341. UpdateProgress(completed, contentLength);
  342. }
  343. remain = contentLength - completed;
  344. if (remain == 0)
  345. {
  346. break;
  347. }
  348. }
  349. }
  350. else
  351. {
  352. if (progressCallback != null)
  353. {
  354. UpdateProgress(completed, contentLength);
  355. }
  356. }
  357. buffer = null;
  358. if (endRequestBody != null)
  359. {
  360. endRequestBody(null);
  361. }
  362. }
  363. catch (Exception ex)
  364. {
  365. if (endRequestBody != null)
  366. {
  367. endRequestBody(ex);
  368. }
  369. else
  370. {
  371. throw;
  372. }
  373. }
  374. finally
  375. {
  376. if (outputStream != null)
  377. {
  378. outputStream.Flush();
  379. outputStream.Close();
  380. outputStream.Dispose();
  381. //QLog.D("XIAO", "stream close");
  382. outputStream = null;
  383. }
  384. }
  385. }
  386. }
  387. public class FileStreamRequestBody : RequestBody
  388. {
  389. private readonly long fileOffset;
  390. //private RequestBodyState requestBodyState;
  391. private FileStream fileStream;
  392. public FileStreamRequestBody(FileStream fileStream, long fileOffset, long sendContentSize)
  393. {
  394. this.fileStream = fileStream;
  395. this.fileOffset = fileOffset;
  396. contentLength = sendContentSize;
  397. }
  398. public override void OnWrite(Stream outputStream)
  399. {
  400. StartHandleRequestBody(outputStream);
  401. }
  402. public override string GetMD5()
  403. {
  404. try
  405. {
  406. fileStream.Seek(fileOffset, SeekOrigin.Begin);
  407. return DigestUtils.GetMd5ToBase64(fileStream, contentLength);
  408. }
  409. catch (Exception ex)
  410. {
  411. QLog.Error(TAG, ex.Message, ex);
  412. throw;
  413. }
  414. }
  415. public override void StartHandleRequestBody(Stream outputStream, EndRequestBody endRequestBody = null)
  416. {
  417. try
  418. {
  419. byte[] buffer = new byte[SEGMENT_SIZE];
  420. int bytesRead = 0;
  421. long completed = bytesRead;
  422. //seek to designated position
  423. //seek to designated position
  424. fileStream.Seek(fileOffset, SeekOrigin.Begin);
  425. long remain = contentLength - completed;
  426. if (remain > 0)
  427. {
  428. while ((bytesRead = fileStream.Read(buffer, 0, (int)(buffer.Length > remain ? remain : buffer.Length))) != 0)
  429. {
  430. outputStream.Write(buffer, 0, bytesRead);
  431. outputStream.Flush();
  432. completed += bytesRead;
  433. if (progressCallback != null)
  434. {
  435. UpdateProgress(completed, contentLength);
  436. }
  437. remain = contentLength - completed;
  438. if (remain == 0)
  439. {
  440. break;
  441. }
  442. }
  443. }
  444. else
  445. {
  446. if (progressCallback != null)
  447. {
  448. UpdateProgress(completed, contentLength);
  449. }
  450. }
  451. buffer = null;
  452. if (endRequestBody != null)
  453. {
  454. endRequestBody(null);
  455. }
  456. }
  457. catch (Exception ex)
  458. {
  459. if (endRequestBody != null)
  460. {
  461. endRequestBody(ex);
  462. }
  463. else
  464. {
  465. throw;
  466. }
  467. }
  468. finally
  469. {
  470. if (outputStream != null)
  471. {
  472. outputStream.Flush();
  473. outputStream.Close();
  474. outputStream.Dispose();
  475. //QLog.D("XIAO", "stream close");
  476. outputStream = null;
  477. }
  478. }
  479. }
  480. }
  481. public class MultipartRequestBody : RequestBody
  482. {
  483. private readonly string DASHDASH = "--";
  484. public static string BOUNDARY = "314159265358979323------------";
  485. private readonly string CRLF = "\r\n";
  486. private readonly string CONTENT_DISPOSITION = "Content-Disposition: form-data; ";
  487. private Dictionary<string, string> parameters;
  488. private string name;
  489. private string fileName;
  490. private byte[] data;
  491. private string srcPath;
  492. private long fileOffset;
  493. private Stream fileStream;
  494. private long realContentLength;
  495. private RequestBodyState requestBodyState;
  496. public MultipartRequestBody()
  497. {
  498. contentType = "multipart/form-data; boundary=" + BOUNDARY;
  499. parameters = new Dictionary<string, string>();
  500. contentLength = -1L;
  501. }
  502. public void AddParamters(Dictionary<string, string> parameters)
  503. {
  504. if (parameters != null)
  505. {
  506. foreach (KeyValuePair<string, string> pair in parameters)
  507. {
  508. this.parameters.Add(pair.Key, pair.Value);
  509. }
  510. }
  511. }
  512. public void AddParameter(string key, string value)
  513. {
  514. if (key != null)
  515. {
  516. parameters.Add(key, value);
  517. }
  518. }
  519. public void AddData(byte[] data, string name, string fileName)
  520. {
  521. this.data = data;
  522. this.name = name;
  523. this.fileName = fileName;
  524. this.realContentLength = data.Length;
  525. }
  526. public void AddData(string srcPath, long fileOffset, long sendContentSize, string name, string fileName)
  527. {
  528. this.srcPath = srcPath;
  529. this.fileOffset = fileOffset;
  530. this.name = name;
  531. this.fileName = fileName;
  532. realContentLength = sendContentSize;
  533. }
  534. //计算长度
  535. public override long ContentLength
  536. {
  537. get
  538. {
  539. ComputerContentLength();
  540. return base.ContentLength;
  541. }
  542. }
  543. private void ComputerContentLength()
  544. {
  545. if (contentLength != -1)
  546. {
  547. return;
  548. }
  549. contentLength = 0;
  550. if (parameters != null && parameters.Count > 0)
  551. {
  552. StringBuilder parametersBuilder = new StringBuilder();
  553. foreach (KeyValuePair<string, string> pair in parameters)
  554. {
  555. parametersBuilder.Append(DASHDASH).Append(BOUNDARY).Append(CRLF);
  556. parametersBuilder.Append(CONTENT_DISPOSITION).Append("name=\"").Append(pair.Key).Append("\"").Append(CRLF);
  557. parametersBuilder.Append(CRLF);
  558. parametersBuilder.Append(pair.Value).Append(CRLF);
  559. }
  560. string content = parametersBuilder.ToString();
  561. byte[] data = Encoding.UTF8.GetBytes(content);
  562. contentLength += data.Length;
  563. }
  564. if (name != null)
  565. {
  566. StringBuilder parametersBuilder = new StringBuilder();
  567. parametersBuilder.Append(DASHDASH).Append(BOUNDARY).Append(CRLF);
  568. parametersBuilder.Append(CONTENT_DISPOSITION).Append("name=\"").Append(name).Append("\"");
  569. if (!String.IsNullOrEmpty(fileName))
  570. {
  571. parametersBuilder.Append("; filename=").Append("\"").Append(fileName).Append("\"");
  572. }
  573. parametersBuilder.Append(CRLF);
  574. parametersBuilder.Append("Content-Type: ").Append("application/octet-stream").Append(CRLF);
  575. parametersBuilder.Append(CRLF);
  576. string content = parametersBuilder.ToString();
  577. byte[] data = Encoding.UTF8.GetBytes(content);
  578. contentLength += data.Length;
  579. }
  580. contentLength += realContentLength;
  581. string endLine = CRLF + DASHDASH + BOUNDARY + DASHDASH + CRLF;
  582. byte[] endData = Encoding.UTF8.GetBytes(endLine);
  583. contentLength += endData.Length;
  584. }
  585. private void WriteParameters(Stream outputStream)
  586. {
  587. if (parameters != null && parameters.Count > 0)
  588. {
  589. StringBuilder parametersBuilder = new StringBuilder();
  590. foreach (KeyValuePair<string, string> pair in parameters)
  591. {
  592. parametersBuilder.Append(DASHDASH).Append(BOUNDARY).Append(CRLF);
  593. parametersBuilder.Append(CONTENT_DISPOSITION).Append("name=\"").Append(pair.Key).Append("\"").Append(CRLF);
  594. parametersBuilder.Append(CRLF);
  595. parametersBuilder.Append(pair.Value).Append(CRLF);
  596. }
  597. string content = parametersBuilder.ToString();
  598. byte[] data = Encoding.UTF8.GetBytes(content);
  599. outputStream.Write(data, 0, data.Length);
  600. }
  601. }
  602. private void WriteFileParameters(Stream outputStream)
  603. {
  604. StringBuilder parametersBuilder = new StringBuilder();
  605. parametersBuilder.Append(DASHDASH).Append(BOUNDARY).Append(CRLF);
  606. parametersBuilder.Append(CONTENT_DISPOSITION).Append("name=\"").Append(name).Append("\"");
  607. if (!String.IsNullOrEmpty(fileName))
  608. {
  609. parametersBuilder.Append("; filename=").Append("\"").Append(fileName).Append("\"");
  610. }
  611. parametersBuilder.Append(CRLF);
  612. parametersBuilder.Append("Content-Type: ").Append("application/octet-stream").Append(CRLF);
  613. parametersBuilder.Append(CRLF);
  614. string content = parametersBuilder.ToString();
  615. byte[] data = Encoding.UTF8.GetBytes(content);
  616. outputStream.Write(data, 0, data.Length);
  617. }
  618. private void WriteEndLine(Stream outputStream)
  619. {
  620. string endLine = CRLF + DASHDASH + BOUNDARY + DASHDASH + CRLF;
  621. byte[] data = Encoding.UTF8.GetBytes(endLine);
  622. outputStream.Write(data, 0, data.Length);
  623. }
  624. public override void OnWrite(Stream outputStream)
  625. {
  626. StartHandleRequestBody(outputStream);
  627. }
  628. public override void StartHandleRequestBody(Stream outputStream, EndRequestBody endRequestBody = null)
  629. {
  630. //write paramters
  631. WriteParameters(outputStream);
  632. //写入content-disposition: form-data; name = "file"; filename = "xxx"\r\n
  633. WriteFileParameters(outputStream);
  634. outputStream.Flush();
  635. //wrtie content: file or bintary
  636. try
  637. {
  638. if (data != null)
  639. {
  640. int completed = 0;
  641. while (completed + SEGMENT_SIZE < realContentLength)
  642. {
  643. outputStream.Write(data, completed, SEGMENT_SIZE);
  644. outputStream.Flush();
  645. completed += SEGMENT_SIZE;
  646. if (progressCallback != null)
  647. {
  648. UpdateProgress(completed, realContentLength);
  649. }
  650. }
  651. if (completed < realContentLength)
  652. {
  653. //包括本身
  654. //包括本身
  655. outputStream.Write(data, completed, (int)(realContentLength - completed));
  656. if (progressCallback != null)
  657. {
  658. UpdateProgress(realContentLength, realContentLength);
  659. }
  660. }
  661. WriteEndLine(outputStream);
  662. outputStream.Flush();
  663. }
  664. else if (srcPath != null)
  665. {
  666. // 64kb
  667. // 64kb
  668. byte[] buffer = new byte[SEGMENT_SIZE];
  669. int bytesRead = 0;
  670. long completed = bytesRead;
  671. fileStream = new FileStream(srcPath, FileMode.Open, FileAccess.Read);
  672. fileStream.Seek(fileOffset, SeekOrigin.Begin);
  673. long remain = realContentLength - completed;
  674. if (remain > 0)
  675. {
  676. while ((bytesRead = fileStream.Read(buffer, 0, (int)(buffer.Length > remain ? remain : buffer.Length))) != 0)
  677. {
  678. outputStream.Write(buffer, 0, bytesRead);
  679. outputStream.Flush();
  680. completed += bytesRead;
  681. if (progressCallback != null)
  682. {
  683. UpdateProgress(completed, realContentLength);
  684. }
  685. remain = realContentLength - completed;
  686. if (remain == 0)
  687. {
  688. break;
  689. }
  690. }
  691. }
  692. else
  693. {
  694. if (progressCallback != null)
  695. {
  696. completed += bytesRead;
  697. UpdateProgress(completed, realContentLength);
  698. }
  699. }
  700. WriteEndLine(outputStream);
  701. outputStream.Flush();
  702. }
  703. if (endRequestBody != null)
  704. {
  705. endRequestBody(null);
  706. }
  707. }
  708. catch (Exception ex)
  709. {
  710. if (endRequestBody != null)
  711. {
  712. endRequestBody(ex);
  713. }
  714. else
  715. {
  716. throw;
  717. }
  718. }
  719. finally
  720. {
  721. if (fileStream != null)
  722. {
  723. fileStream.Close();
  724. fileStream.Dispose();
  725. fileStream = null;
  726. }
  727. if (outputStream != null)
  728. {
  729. outputStream.Flush();
  730. outputStream.Close();
  731. outputStream.Dispose();
  732. outputStream = null;
  733. }
  734. }
  735. }
  736. }
  737. }