SegmentReader.cs
上传用户:zhangkuixh
上传日期:2013-09-30
资源大小:5473k
文件大小:21k
源码类别:

搜索引擎

开发平台:

C#

  1. /*
  2.  * Copyright 2004 The Apache Software Foundation
  3.  * 
  4.  * Licensed under the Apache License, Version 2.0 (the "License");
  5.  * you may not use this file except in compliance with the License.
  6.  * You may obtain a copy of the License at
  7.  * 
  8.  * http://www.apache.org/licenses/LICENSE-2.0
  9.  * 
  10.  * Unless required by applicable law or agreed to in writing, software
  11.  * distributed under the License is distributed on an "AS IS" BASIS,
  12.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13.  * See the License for the specific language governing permissions and
  14.  * limitations under the License.
  15.  */
  16. using System;
  17. using Document = Lucene.Net.Documents.Document;
  18. using Field = Lucene.Net.Documents.Field;
  19. using DefaultSimilarity = Lucene.Net.Search.DefaultSimilarity;
  20. using Directory = Lucene.Net.Store.Directory;
  21. using IndexInput = Lucene.Net.Store.IndexInput;
  22. using IndexOutput = Lucene.Net.Store.IndexOutput;
  23. using BitVector = Lucene.Net.Util.BitVector;
  24. namespace Lucene.Net.Index
  25. {
  26. /// <version>  $Id: SegmentReader.java 329523 2005-10-30 05:37:11Z yonik $
  27. /// </version>
  28. class SegmentReader : IndexReader
  29. {
  30. private System.String segment;
  31. internal FieldInfos fieldInfos;
  32. private FieldsReader fieldsReader;
  33. internal TermInfosReader tis;
  34. internal TermVectorsReader termVectorsReaderOrig = null;
  35. internal System.LocalDataStoreSlot termVectorsLocal = System.Threading.Thread.AllocateDataSlot();
  36. internal BitVector deletedDocs = null;
  37. private bool deletedDocsDirty = false;
  38. private bool normsDirty = false;
  39. private bool undeleteAll = false;
  40. internal IndexInput freqStream;
  41. internal IndexInput proxStream;
  42. // Compound File Reader when based on a compound file segment
  43. internal CompoundFileReader cfsReader = null;
  44. private class Norm
  45. {
  46. private void  InitBlock(SegmentReader enclosingInstance)
  47. {
  48. this.enclosingInstance = enclosingInstance;
  49. }
  50. private SegmentReader enclosingInstance;
  51. public SegmentReader Enclosing_Instance
  52. {
  53. get
  54. {
  55. return enclosingInstance;
  56. }
  57. }
  58. public Norm(SegmentReader enclosingInstance, IndexInput in_Renamed, int number)
  59. {
  60. InitBlock(enclosingInstance);
  61. this.in_Renamed = in_Renamed;
  62. this.number = number;
  63. }
  64. public IndexInput in_Renamed;
  65. public byte[] bytes;
  66. public bool dirty;
  67. public int number;
  68. public void  ReWrite()
  69. {
  70. // NOTE: norms are re-written in regular directory, not cfs
  71. IndexOutput out_Renamed = Enclosing_Instance.Directory().CreateOutput(Enclosing_Instance.segment + ".tmp");
  72. try
  73. {
  74. out_Renamed.WriteBytes(bytes, Enclosing_Instance.MaxDoc());
  75. }
  76. finally
  77. {
  78. out_Renamed.Close();
  79. }
  80. System.String fileName;
  81. if (Enclosing_Instance.cfsReader == null)
  82. fileName = Enclosing_Instance.segment + ".f" + number;
  83. else
  84. {
  85. // use a different file name if we have compound format
  86. fileName = Enclosing_Instance.segment + ".s" + number;
  87. }
  88. Enclosing_Instance.Directory().RenameFile(Enclosing_Instance.segment + ".tmp", fileName);
  89. this.dirty = false;
  90. }
  91. }
  92. private System.Collections.Hashtable norms = System.Collections.Hashtable.Synchronized(new System.Collections.Hashtable());
  93. /// <summary>The class which implements SegmentReader. </summary>
  94. private static System.Type IMPL;
  95. public SegmentReader() : base(null)
  96. {
  97. }
  98. public static SegmentReader Get(SegmentInfo si)
  99. {
  100. return Get(si.dir, si, null, false, false);
  101. }
  102. public static SegmentReader Get(SegmentInfos sis, SegmentInfo si, bool closeDir)
  103. {
  104. return Get(si.dir, si, sis, closeDir, true);
  105. }
  106. public static SegmentReader Get(Directory dir, SegmentInfo si, SegmentInfos sis, bool closeDir, bool ownDir)
  107. {
  108. SegmentReader instance;
  109. try
  110. {
  111. instance = (SegmentReader) System.Activator.CreateInstance(IMPL);
  112. }
  113. catch (System.Exception e)
  114. {
  115. throw new System.SystemException("cannot load SegmentReader class: " + e);
  116. }
  117. instance.Init(dir, sis, closeDir, ownDir);
  118. instance.Initialize(si);
  119. return instance;
  120. }
  121. private void  Initialize(SegmentInfo si)
  122. {
  123. segment = si.name;
  124. // Use compound file directory for some files, if it exists
  125. Directory cfsDir = Directory();
  126. if (Directory().FileExists(segment + ".cfs"))
  127. {
  128. cfsReader = new CompoundFileReader(Directory(), segment + ".cfs");
  129. cfsDir = cfsReader;
  130. }
  131. // No compound file exists - use the multi-file format
  132. fieldInfos = new FieldInfos(cfsDir, segment + ".fnm");
  133. fieldsReader = new FieldsReader(cfsDir, segment, fieldInfos);
  134. tis = new TermInfosReader(cfsDir, segment, fieldInfos);
  135. // NOTE: the bitvector is stored using the regular directory, not cfs
  136. if (HasDeletions(si))
  137. deletedDocs = new BitVector(Directory(), segment + ".del");
  138. // make sure that all index files have been read or are kept open
  139. // so that if an index update removes them we'll still have them
  140. freqStream = cfsDir.OpenInput(segment + ".frq");
  141. proxStream = cfsDir.OpenInput(segment + ".prx");
  142. OpenNorms(cfsDir);
  143. if (fieldInfos.HasVectors())
  144. {
  145. // open term vector files only as needed
  146. termVectorsReaderOrig = new TermVectorsReader(cfsDir, segment, fieldInfos);
  147. }
  148. }
  149. ~SegmentReader()
  150. {
  151. // patch for pre-1.4.2 JVMs, whose ThreadLocals leak
  152. System.Threading.Thread.SetData(termVectorsLocal, null);
  153. }
  154. protected internal override void  DoCommit()
  155. {
  156. if (deletedDocsDirty)
  157. {
  158. // re-write deleted
  159. deletedDocs.Write(Directory(), segment + ".tmp");
  160. Directory().RenameFile(segment + ".tmp", segment + ".del");
  161. }
  162. if (undeleteAll && Directory().FileExists(segment + ".del"))
  163. {
  164. Directory().DeleteFile(segment + ".del");
  165. }
  166. if (normsDirty)
  167. {
  168. // re-write norms
  169. System.Collections.IEnumerator values = norms.Values.GetEnumerator();
  170. while (values.MoveNext())
  171. {
  172. Norm norm = (Norm) values.Current;
  173. if (norm.dirty)
  174. {
  175. norm.ReWrite();
  176. }
  177. }
  178. }
  179. deletedDocsDirty = false;
  180. normsDirty = false;
  181. undeleteAll = false;
  182. }
  183. protected internal override void  DoClose()
  184. {
  185. fieldsReader.Close();
  186. tis.Close();
  187. if (freqStream != null)
  188. freqStream.Close();
  189. if (proxStream != null)
  190. proxStream.Close();
  191. CloseNorms();
  192. if (termVectorsReaderOrig != null)
  193. termVectorsReaderOrig.Close();
  194. if (cfsReader != null)
  195. cfsReader.Close();
  196. }
  197. internal static bool HasDeletions(SegmentInfo si)
  198. {
  199. return si.dir.FileExists(si.name + ".del");
  200. }
  201. public override bool HasDeletions()
  202. {
  203. return deletedDocs != null;
  204. }
  205. internal static bool UsesCompoundFile(SegmentInfo si)
  206. {
  207. return si.dir.FileExists(si.name + ".cfs");
  208. }
  209. internal static bool HasSeparateNorms(SegmentInfo si)
  210. {
  211. System.String[] result = si.dir.List();
  212. System.String pattern = si.name + ".s";
  213. int patternLength = pattern.Length;
  214. for (int i = 0; i < result.Length; i++)
  215. {
  216. if (result[i].StartsWith(pattern) && System.Char.IsDigit(result[i][patternLength]))
  217. return true;
  218. }
  219. return false;
  220. }
  221. protected internal override void  DoDelete(int docNum)
  222. {
  223. if (deletedDocs == null)
  224. deletedDocs = new BitVector(MaxDoc());
  225. deletedDocsDirty = true;
  226. undeleteAll = false;
  227. deletedDocs.Set(docNum);
  228. }
  229. protected internal override void  DoUndeleteAll()
  230. {
  231. deletedDocs = null;
  232. deletedDocsDirty = false;
  233. undeleteAll = true;
  234. }
  235. internal virtual System.Collections.ArrayList Files()
  236. {
  237. System.Collections.ArrayList files = System.Collections.ArrayList.Synchronized(new System.Collections.ArrayList(16));
  238. for (int i = 0; i < IndexFileNames.INDEX_EXTENSIONS.Length; i++)
  239. {
  240. System.String name = segment + "." + IndexFileNames.INDEX_EXTENSIONS[i];
  241. if (Directory().FileExists(name))
  242. files.Add(name);
  243. }
  244. for (int i = 0; i < fieldInfos.Size(); i++)
  245. {
  246. FieldInfo fi = fieldInfos.FieldInfo(i);
  247. if (fi.isIndexed && !fi.omitNorms)
  248. {
  249. System.String name;
  250. if (cfsReader == null)
  251. name = segment + ".f" + i;
  252. else
  253. name = segment + ".s" + i;
  254. if (Directory().FileExists(name))
  255. files.Add(name);
  256. }
  257. }
  258. return files;
  259. }
  260. public override TermEnum Terms()
  261. {
  262. return tis.Terms();
  263. }
  264. public override TermEnum Terms(Term t)
  265. {
  266. return tis.Terms(t);
  267. }
  268. public override Document Document(int n)
  269. {
  270. lock (this)
  271. {
  272. if (IsDeleted(n))
  273. throw new System.ArgumentException("attempt to access a deleted document");
  274. return fieldsReader.Doc(n);
  275. }
  276. }
  277. public override bool IsDeleted(int n)
  278. {
  279. lock (this)
  280. {
  281. return (deletedDocs != null && deletedDocs.Get(n));
  282. }
  283. }
  284. public override TermDocs TermDocs()
  285. {
  286. return new SegmentTermDocs(this);
  287. }
  288. public override TermPositions TermPositions()
  289. {
  290. return new SegmentTermPositions(this);
  291. }
  292. public override int DocFreq(Term t)
  293. {
  294. TermInfo ti = tis.Get(t);
  295. if (ti != null)
  296. return ti.docFreq;
  297. else
  298. return 0;
  299. }
  300. public override int NumDocs()
  301. {
  302. int n = MaxDoc();
  303. if (deletedDocs != null)
  304. n -= deletedDocs.Count();
  305. return n;
  306. }
  307. public override int MaxDoc()
  308. {
  309. return fieldsReader.Size();
  310. }
  311. /// <seealso cref="IndexReader.GetFieldNames()">
  312. /// </seealso>
  313. /// <deprecated>  Replaced by {@link #GetFieldNames (IndexReader.FieldOption fldOption)}
  314. /// </deprecated>
  315. public override System.Collections.ICollection GetFieldNames()
  316. {
  317. // maintain a unique set of field names
  318. System.Collections.Hashtable fieldSet = new System.Collections.Hashtable();
  319. for (int i = 0; i < fieldInfos.Size(); i++)
  320. {
  321. FieldInfo fi = fieldInfos.FieldInfo(i);
  322. fieldSet.Add(fi.name, fi.name);
  323. }
  324. return fieldSet;
  325. }
  326. /// <seealso cref="IndexReader.GetFieldNames(boolean)">
  327. /// </seealso>
  328. /// <deprecated>  Replaced by {@link #GetFieldNames (IndexReader.FieldOption fldOption)}
  329. /// </deprecated>
  330. public override System.Collections.ICollection GetFieldNames(bool indexed)
  331. {
  332. // maintain a unique set of field names
  333. System.Collections.Hashtable fieldSet = new System.Collections.Hashtable();
  334. for (int i = 0; i < fieldInfos.Size(); i++)
  335. {
  336. FieldInfo fi = fieldInfos.FieldInfo(i);
  337. if (fi.isIndexed == indexed)
  338. fieldSet.Add(fi.name, fi.name);
  339. }
  340. return fieldSet;
  341. }
  342. /// <seealso cref="IndexReader.GetIndexedFieldNames(Field.TermVector tvSpec)">
  343. /// </seealso>
  344. /// <deprecated>  Replaced by {@link #GetFieldNames (IndexReader.FieldOption fldOption)}
  345. /// </deprecated>
  346. public override System.Collections.ICollection GetIndexedFieldNames(Field.TermVector tvSpec)
  347. {
  348. bool storedTermVector;
  349. bool storePositionWithTermVector;
  350. bool storeOffsetWithTermVector;
  351. if (tvSpec == Field.TermVector.NO)
  352. {
  353. storedTermVector = false;
  354. storePositionWithTermVector = false;
  355. storeOffsetWithTermVector = false;
  356. }
  357. else if (tvSpec == Field.TermVector.YES)
  358. {
  359. storedTermVector = true;
  360. storePositionWithTermVector = false;
  361. storeOffsetWithTermVector = false;
  362. }
  363. else if (tvSpec == Field.TermVector.WITH_POSITIONS)
  364. {
  365. storedTermVector = true;
  366. storePositionWithTermVector = true;
  367. storeOffsetWithTermVector = false;
  368. }
  369. else if (tvSpec == Field.TermVector.WITH_OFFSETS)
  370. {                                                                           
  371. storedTermVector = true;
  372. storePositionWithTermVector = false;
  373. storeOffsetWithTermVector = true;
  374. }
  375. else if (tvSpec == Field.TermVector.WITH_POSITIONS_OFFSETS)
  376. {
  377. storedTermVector = true;
  378. storePositionWithTermVector = true;
  379. storeOffsetWithTermVector = true;
  380. }
  381. else
  382. {
  383. throw new System.ArgumentException("unknown termVector parameter " + tvSpec);
  384. }
  385. // maintain a unique set of field names
  386. System.Collections.Hashtable fieldSet = new System.Collections.Hashtable();
  387. for (int i = 0; i < fieldInfos.Size(); i++)
  388. {
  389. FieldInfo fi = fieldInfos.FieldInfo(i);
  390. if (fi.isIndexed && fi.storeTermVector == storedTermVector && fi.storePositionWithTermVector == storePositionWithTermVector && fi.storeOffsetWithTermVector == storeOffsetWithTermVector)
  391. {
  392. fieldSet.Add(fi.name, fi.name);
  393. }
  394. }
  395. return fieldSet;
  396. }
  397. /// <seealso cref="IndexReader.GetFieldNames(IndexReader.FieldOption fldOption)">
  398. /// </seealso>
  399. public override System.Collections.ICollection GetFieldNames(IndexReader.FieldOption fieldOption)
  400. {
  401. System.Collections.Hashtable fieldSet = new System.Collections.Hashtable();
  402. for (int i = 0; i < fieldInfos.Size(); i++)
  403. {
  404. FieldInfo fi = fieldInfos.FieldInfo(i);
  405. if (fieldOption == IndexReader.FieldOption.ALL)
  406. {
  407. fieldSet.Add(fi.name, fi.name);
  408. }
  409. else if (!fi.isIndexed && fieldOption == IndexReader.FieldOption.UNINDEXED)
  410. {
  411. fieldSet.Add(fi.name, fi.name);
  412. }
  413. else if (fi.isIndexed && fieldOption == IndexReader.FieldOption.INDEXED)
  414. {
  415. fieldSet.Add(fi.name, fi.name);
  416. }
  417. else if (fi.isIndexed && fi.storeTermVector == false && fieldOption == IndexReader.FieldOption.INDEXED_NO_TERMVECTOR)
  418. {
  419. fieldSet.Add(fi.name, fi.name);
  420. }
  421. else if (fi.storeTermVector == true && fi.storePositionWithTermVector == false && fi.storeOffsetWithTermVector == false && fieldOption == IndexReader.FieldOption.TERMVECTOR)
  422. {
  423. fieldSet.Add(fi.name, fi.name);
  424. }
  425. else if (fi.isIndexed && fi.storeTermVector && fieldOption == IndexReader.FieldOption.INDEXED_WITH_TERMVECTOR)
  426. {
  427. fieldSet.Add(fi.name, fi.name);
  428. }
  429. else if (fi.storePositionWithTermVector && fi.storeOffsetWithTermVector == false && fieldOption == IndexReader.FieldOption.TERMVECTOR_WITH_POSITION)
  430. {
  431. fieldSet.Add(fi.name, fi.name);
  432. }
  433. else if (fi.storeOffsetWithTermVector && fi.storePositionWithTermVector == false && fieldOption == IndexReader.FieldOption.TERMVECTOR_WITH_OFFSET)
  434. {
  435. fieldSet.Add(fi.name, fi.name);
  436. }
  437. else if ((fi.storeOffsetWithTermVector && fi.storePositionWithTermVector) && fieldOption == IndexReader.FieldOption.TERMVECTOR_WITH_POSITION_OFFSET)
  438. {
  439. fieldSet.Add(fi.name, fi.name);
  440. }
  441. }
  442. return fieldSet;
  443. }
  444. public override bool HasNorms(System.String field)
  445. {
  446. lock (this)
  447. {
  448. return norms.ContainsKey(field);
  449. }
  450. }
  451. internal static byte[] CreateFakeNorms(int size)
  452. {
  453. byte[] ones = new byte[size];
  454.             byte[] byteArray = new byte[ones.Length];
  455.             for (int index = 0; index < ones.Length; index++)
  456.                 byteArray[index] = (byte) ones[index];
  457.             byte val = DefaultSimilarity.EncodeNorm(1.0f);
  458.             for (int index = 0; index < byteArray.Length; index++)
  459.                 byteArray.SetValue(val, index);
  460. return ones;
  461. }
  462. private byte[] ones;
  463. private byte[] FakeNorms()
  464. {
  465. if (ones == null)
  466. ones = CreateFakeNorms(MaxDoc());
  467. return ones;
  468. }
  469. // can return null if norms aren't stored
  470. protected internal virtual byte[] GetNorms(System.String field)
  471. {
  472. lock (this)
  473. {
  474. Norm norm = (Norm) norms[field];
  475. if (norm == null)
  476. return null; // not indexed, or norms not stored
  477. if (norm.bytes == null)
  478. {
  479. // value not yet read
  480. byte[] bytes = new byte[MaxDoc()];
  481. Norms(field, bytes, 0);
  482. norm.bytes = bytes; // cache it
  483. }
  484. return norm.bytes;
  485. }
  486. }
  487. // returns fake norms if norms aren't available
  488. public override byte[] Norms(System.String field)
  489. {
  490. lock (this)
  491. {
  492. byte[] bytes = GetNorms(field);
  493. if (bytes == null)
  494. bytes = FakeNorms();
  495. return bytes;
  496. }
  497. }
  498. protected internal override void  DoSetNorm(int doc, System.String field, byte value_Renamed)
  499. {
  500. Norm norm = (Norm) norms[field];
  501. if (norm == null)
  502. // not an indexed field
  503. return ;
  504. norm.dirty = true; // mark it dirty
  505. normsDirty = true;
  506. Norms(field)[doc] = value_Renamed; // set the value
  507. }
  508. /// <summary>Read norms into a pre-allocated array. </summary>
  509. public override void  Norms(System.String field, byte[] bytes, int offset)
  510. {
  511. lock (this)
  512. {
  513. Norm norm = (Norm) norms[field];
  514. if (norm == null)
  515. {
  516. Array.Copy(FakeNorms(), 0, bytes, offset, MaxDoc());
  517. return ;
  518. }
  519. if (norm.bytes != null)
  520. {
  521. // can copy from cache
  522. Array.Copy(norm.bytes, 0, bytes, offset, MaxDoc());
  523. return ;
  524. }
  525. IndexInput normStream = (IndexInput) norm.in_Renamed.Clone();
  526. try
  527. {
  528. // read from disk
  529. normStream.Seek(0);
  530. normStream.ReadBytes(bytes, offset, MaxDoc());
  531. }
  532. finally
  533. {
  534. normStream.Close();
  535. }
  536. }
  537. }
  538. private void  OpenNorms(Directory cfsDir)
  539. {
  540. for (int i = 0; i < fieldInfos.Size(); i++)
  541. {
  542. FieldInfo fi = fieldInfos.FieldInfo(i);
  543. if (fi.isIndexed && !fi.omitNorms)
  544. {
  545. // look first if there are separate norms in compound format
  546. System.String fileName = segment + ".s" + fi.number;
  547. Directory d = Directory();
  548. if (!d.FileExists(fileName))
  549. {
  550. fileName = segment + ".f" + fi.number;
  551. d = cfsDir;
  552. }
  553. norms[fi.name] = new Norm(this, d.OpenInput(fileName), fi.number);
  554. }
  555. }
  556. }
  557. private void  CloseNorms()
  558. {
  559. lock (norms.SyncRoot)
  560. {
  561. System.Collections.IEnumerator enumerator = norms.Values.GetEnumerator();
  562. while (enumerator.MoveNext())
  563. {
  564. Norm norm = (Norm) enumerator.Current;
  565. norm.in_Renamed.Close();
  566. }
  567. }
  568. }
  569. /// <summary> Create a clone from the initial TermVectorsReader and store it in the ThreadLocal.</summary>
  570. /// <returns> TermVectorsReader
  571. /// </returns>
  572. private TermVectorsReader GetTermVectorsReader()
  573. {
  574. TermVectorsReader tvReader = (TermVectorsReader) System.Threading.Thread.GetData(termVectorsLocal);
  575. if (tvReader == null)
  576. {
  577. tvReader = (TermVectorsReader) termVectorsReaderOrig.Clone();
  578. System.Threading.Thread.SetData(termVectorsLocal, tvReader);
  579. }
  580. return tvReader;
  581. }
  582. /// <summary>Return a term frequency vector for the specified document and field. The
  583. /// vector returned contains term numbers and frequencies for all terms in
  584. /// the specified field of this document, if the field had storeTermVector
  585. /// flag set.  If the flag was not set, the method returns null.
  586. /// </summary>
  587. /// <throws>  IOException </throws>
  588. public override TermFreqVector GetTermFreqVector(int docNumber, System.String field)
  589. {
  590. // Check if this field is invalid or has no stored term vector
  591. FieldInfo fi = fieldInfos.FieldInfo(field);
  592. if (fi == null || !fi.storeTermVector || termVectorsReaderOrig == null)
  593. return null;
  594. TermVectorsReader termVectorsReader = GetTermVectorsReader();
  595. if (termVectorsReader == null)
  596. return null;
  597. return termVectorsReader.Get(docNumber, field);
  598. }
  599. /// <summary>Return an array of term frequency vectors for the specified document.
  600. /// The array contains a vector for each vectorized field in the document.
  601. /// Each vector vector contains term numbers and frequencies for all terms
  602. /// in a given vectorized field.
  603. /// If no such fields existed, the method returns null.
  604. /// </summary>
  605. /// <throws>  IOException </throws>
  606. public override TermFreqVector[] GetTermFreqVectors(int docNumber)
  607. {
  608. if (termVectorsReaderOrig == null)
  609. return null;
  610. TermVectorsReader termVectorsReader = GetTermVectorsReader();
  611. if (termVectorsReader == null)
  612. return null;
  613. return termVectorsReader.Get(docNumber);
  614. }
  615.         static SegmentReader()
  616. {
  617. {
  618. try
  619. {
  620.                     System.String name = SupportClass.AppSettings.Get("Lucene.Net.SegmentReader.class", typeof(SegmentReader).FullName);
  621. IMPL = System.Type.GetType(name);
  622. }
  623. catch (System.Security.SecurityException)
  624. {
  625. try
  626. {
  627. IMPL = System.Type.GetType(typeof(SegmentReader).FullName);
  628. }
  629. catch (System.Exception e)
  630. {
  631. throw new System.SystemException("cannot load default SegmentReader class: " + e);
  632. }
  633. }
  634.                 catch (System.Exception e)
  635.                 {
  636.                     throw new System.SystemException("cannot load SegmentReader class: " + e);
  637.                 }
  638.             }
  639. }
  640. }
  641. }