You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

LuceneService.java 41KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205
  1. /*
  2. * Copyright 2012 gitblit.com.
  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. package com.gitblit.service;
  17. import static org.eclipse.jgit.treewalk.filter.TreeFilter.ANY_DIFF;
  18. import java.io.ByteArrayOutputStream;
  19. import java.io.File;
  20. import java.io.IOException;
  21. import java.io.InputStream;
  22. import java.text.MessageFormat;
  23. import java.text.ParseException;
  24. import java.util.ArrayList;
  25. import java.util.Collections;
  26. import java.util.Comparator;
  27. import java.util.HashMap;
  28. import java.util.LinkedHashSet;
  29. import java.util.List;
  30. import java.util.Map;
  31. import java.util.Set;
  32. import java.util.TreeMap;
  33. import java.util.TreeSet;
  34. import java.util.concurrent.ConcurrentHashMap;
  35. import org.apache.lucene.analysis.Analyzer;
  36. import org.apache.lucene.analysis.standard.StandardAnalyzer;
  37. import org.apache.lucene.document.DateTools;
  38. import org.apache.lucene.document.DateTools.Resolution;
  39. import org.apache.lucene.document.Document;
  40. import org.apache.lucene.document.Field;
  41. import org.apache.lucene.document.StringField;
  42. import org.apache.lucene.document.TextField;
  43. import org.apache.lucene.index.DirectoryReader;
  44. import org.apache.lucene.index.IndexReader;
  45. import org.apache.lucene.index.IndexWriter;
  46. import org.apache.lucene.index.IndexWriterConfig;
  47. import org.apache.lucene.index.IndexWriterConfig.OpenMode;
  48. import org.apache.lucene.index.MultiReader;
  49. import org.apache.lucene.index.Term;
  50. import org.apache.lucene.queryparser.classic.QueryParser;
  51. import org.apache.lucene.search.BooleanClause.Occur;
  52. import org.apache.lucene.search.BooleanQuery;
  53. import org.apache.lucene.search.IndexSearcher;
  54. import org.apache.lucene.search.Query;
  55. import org.apache.lucene.search.ScoreDoc;
  56. import org.apache.lucene.search.TopScoreDocCollector;
  57. import org.apache.lucene.search.highlight.Fragmenter;
  58. import org.apache.lucene.search.highlight.Highlighter;
  59. import org.apache.lucene.search.highlight.InvalidTokenOffsetsException;
  60. import org.apache.lucene.search.highlight.QueryScorer;
  61. import org.apache.lucene.search.highlight.SimpleHTMLFormatter;
  62. import org.apache.lucene.search.highlight.SimpleSpanFragmenter;
  63. import org.apache.lucene.store.Directory;
  64. import org.apache.lucene.store.FSDirectory;
  65. import org.eclipse.jgit.diff.DiffEntry.ChangeType;
  66. import org.eclipse.jgit.lib.Constants;
  67. import org.eclipse.jgit.lib.FileMode;
  68. import org.eclipse.jgit.lib.ObjectId;
  69. import org.eclipse.jgit.lib.ObjectLoader;
  70. import org.eclipse.jgit.lib.ObjectReader;
  71. import org.eclipse.jgit.lib.Repository;
  72. import org.eclipse.jgit.lib.RepositoryCache.FileKey;
  73. import org.eclipse.jgit.revwalk.RevCommit;
  74. import org.eclipse.jgit.revwalk.RevTree;
  75. import org.eclipse.jgit.revwalk.RevWalk;
  76. import org.eclipse.jgit.storage.file.FileBasedConfig;
  77. import org.eclipse.jgit.treewalk.EmptyTreeIterator;
  78. import org.eclipse.jgit.treewalk.TreeWalk;
  79. import org.eclipse.jgit.util.FS;
  80. import org.slf4j.Logger;
  81. import org.slf4j.LoggerFactory;
  82. import com.gitblit.Constants.SearchObjectType;
  83. import com.gitblit.IStoredSettings;
  84. import com.gitblit.Keys;
  85. import com.gitblit.manager.IRepositoryManager;
  86. import com.gitblit.models.PathModel.PathChangeModel;
  87. import com.gitblit.models.RefModel;
  88. import com.gitblit.models.RepositoryModel;
  89. import com.gitblit.models.SearchResult;
  90. import com.gitblit.utils.ArrayUtils;
  91. import com.gitblit.utils.JGitUtils;
  92. import com.gitblit.utils.StringUtils;
  93. /**
  94. * The Lucene service handles indexing and searching repositories.
  95. *
  96. * @author James Moger
  97. *
  98. */
  99. public class LuceneService implements Runnable {
  100. private static final int INDEX_VERSION = 6;
  101. private static final String FIELD_OBJECT_TYPE = "type";
  102. private static final String FIELD_PATH = "path";
  103. private static final String FIELD_COMMIT = "commit";
  104. private static final String FIELD_BRANCH = "branch";
  105. private static final String FIELD_SUMMARY = "summary";
  106. private static final String FIELD_CONTENT = "content";
  107. private static final String FIELD_AUTHOR = "author";
  108. private static final String FIELD_COMMITTER = "committer";
  109. private static final String FIELD_DATE = "date";
  110. private static final String FIELD_TAG = "tag";
  111. private static final String CONF_ALIAS = "aliases";
  112. private static final String CONF_BRANCH = "branches";
  113. private final Logger logger = LoggerFactory.getLogger(LuceneService.class);
  114. private final IStoredSettings storedSettings;
  115. private final IRepositoryManager repositoryManager;
  116. private final File repositoriesFolder;
  117. private final Map<String, IndexSearcher> searchers = new ConcurrentHashMap<String, IndexSearcher>();
  118. private final Map<String, IndexWriter> writers = new ConcurrentHashMap<String, IndexWriter>();
  119. private final String luceneIgnoreExtensions = "7z arc arj bin bmp dll doc docx exe gif gz jar jpg lib lzh odg odf odt pdf ppt png so swf xcf xls xlsx zip";
  120. private Set<String> excludedExtensions;
  121. public LuceneService(
  122. IStoredSettings settings,
  123. IRepositoryManager repositoryManager) {
  124. this.storedSettings = settings;
  125. this.repositoryManager = repositoryManager;
  126. this.repositoriesFolder = repositoryManager.getRepositoriesFolder();
  127. String exts = luceneIgnoreExtensions;
  128. if (settings != null) {
  129. exts = settings.getString(Keys.web.luceneIgnoreExtensions, exts);
  130. }
  131. excludedExtensions = new TreeSet<String>(StringUtils.getStringsFromValue(exts));
  132. }
  133. /**
  134. * Run is executed by the Gitblit executor service. Because this is called
  135. * by an executor service, calls will queue - i.e. there can never be
  136. * concurrent execution of repository index updates.
  137. */
  138. @Override
  139. public void run() {
  140. if (!storedSettings.getBoolean(Keys.web.allowLuceneIndexing, true)) {
  141. // Lucene indexing is disabled
  142. return;
  143. }
  144. // reload the excluded extensions
  145. String exts = storedSettings.getString(Keys.web.luceneIgnoreExtensions, luceneIgnoreExtensions);
  146. excludedExtensions = new TreeSet<String>(StringUtils.getStringsFromValue(exts));
  147. if (repositoryManager.isCollectingGarbage()) {
  148. // busy collecting garbage, try again later
  149. return;
  150. }
  151. for (String repositoryName: repositoryManager.getRepositoryList()) {
  152. RepositoryModel model = repositoryManager.getRepositoryModel(repositoryName);
  153. if (model.hasCommits && !ArrayUtils.isEmpty(model.indexedBranches)) {
  154. Repository repository = repositoryManager.getRepository(model.name);
  155. if (repository == null) {
  156. if (repositoryManager.isCollectingGarbage(model.name)) {
  157. logger.info("Skipping Lucene index of {}, busy garbage collecting", repositoryName);
  158. }
  159. continue;
  160. }
  161. index(model, repository);
  162. repository.close();
  163. System.gc();
  164. }
  165. }
  166. }
  167. /**
  168. * Synchronously indexes a repository. This may build a complete index of a
  169. * repository or it may update an existing index.
  170. *
  171. * @param displayName
  172. * the name of the repository
  173. * @param repository
  174. * the repository object
  175. */
  176. private void index(RepositoryModel model, Repository repository) {
  177. try {
  178. if (shouldReindex(repository)) {
  179. // (re)build the entire index
  180. IndexResult result = reindex(model, repository);
  181. if (result.success) {
  182. if (result.commitCount > 0) {
  183. logger.info("Built {} Lucene index from {} commits and {} files across {} branches in {} secs",
  184. model.name, result.commitCount, result.blobCount, result.branchCount, result.duration());
  185. }
  186. } else {
  187. String msg = "Could not build {0} Lucene index!";
  188. logger.error(MessageFormat.format(msg, model.name));
  189. }
  190. } else {
  191. // update the index with latest commits
  192. IndexResult result = updateIndex(model, repository);
  193. if (result.success) {
  194. if (result.commitCount > 0) {
  195. logger.info("Updated {} Lucene index with {} commits and {} files across {} branches in {} secs",
  196. model.name, result.commitCount, result.blobCount, result.branchCount, result.duration());
  197. }
  198. } else {
  199. logger.error("Could not update {} Lucene index!", model.name);
  200. }
  201. }
  202. } catch (Throwable t) {
  203. logger.error("Lucene indexing failure for {}", model.name, t);
  204. }
  205. }
  206. /**
  207. * Close the writer/searcher objects for a repository.
  208. *
  209. * @param repositoryName
  210. */
  211. public synchronized void close(String repositoryName) {
  212. try {
  213. IndexSearcher searcher = searchers.remove(repositoryName);
  214. if (searcher != null) {
  215. searcher.getIndexReader().close();
  216. }
  217. } catch (Exception e) {
  218. logger.error("Failed to close index searcher for {}", repositoryName, e);
  219. }
  220. try {
  221. IndexWriter writer = writers.remove(repositoryName);
  222. if (writer != null) {
  223. writer.close();
  224. }
  225. } catch (Exception e) {
  226. logger.error("Failed to close index writer for {}", repositoryName, e);
  227. }
  228. }
  229. /**
  230. * Close all Lucene indexers.
  231. *
  232. */
  233. public synchronized void close() {
  234. // close all writers
  235. for (String writer : writers.keySet()) {
  236. try {
  237. writers.get(writer).close();
  238. } catch (Throwable t) {
  239. logger.error("Failed to close Lucene writer for {}", writer, t);
  240. }
  241. }
  242. writers.clear();
  243. // close all searchers
  244. for (String searcher : searchers.keySet()) {
  245. try {
  246. searchers.get(searcher).getIndexReader().close();
  247. } catch (Throwable t) {
  248. logger.error("Failed to close Lucene searcher for {}", searcher, t);
  249. }
  250. }
  251. searchers.clear();
  252. }
  253. /**
  254. * Deletes the Lucene index for the specified repository.
  255. *
  256. * @param repositoryName
  257. * @return true, if successful
  258. */
  259. public boolean deleteIndex(String repositoryName) {
  260. // close any open writer/searcher
  261. close(repositoryName);
  262. // delete the index folder
  263. File repositoryFolder = FileKey.resolve(new File(repositoriesFolder, repositoryName), FS.DETECTED);
  264. LuceneRepoIndexStore luceneIndex = new LuceneRepoIndexStore(repositoryFolder, INDEX_VERSION);
  265. return luceneIndex.delete();
  266. }
  267. /**
  268. * Returns the author for the commit, if this information is available.
  269. *
  270. * @param commit
  271. * @return an author or unknown
  272. */
  273. private String getAuthor(RevCommit commit) {
  274. String name = "unknown";
  275. try {
  276. name = commit.getAuthorIdent().getName();
  277. if (StringUtils.isEmpty(name)) {
  278. name = commit.getAuthorIdent().getEmailAddress();
  279. }
  280. } catch (NullPointerException n) {
  281. }
  282. return name;
  283. }
  284. /**
  285. * Returns the committer for the commit, if this information is available.
  286. *
  287. * @param commit
  288. * @return an committer or unknown
  289. */
  290. private String getCommitter(RevCommit commit) {
  291. String name = "unknown";
  292. try {
  293. name = commit.getCommitterIdent().getName();
  294. if (StringUtils.isEmpty(name)) {
  295. name = commit.getCommitterIdent().getEmailAddress();
  296. }
  297. } catch (NullPointerException n) {
  298. }
  299. return name;
  300. }
  301. /**
  302. * Get the tree associated with the given commit.
  303. *
  304. * @param walk
  305. * @param commit
  306. * @return tree
  307. * @throws IOException
  308. */
  309. private RevTree getTree(final RevWalk walk, final RevCommit commit)
  310. throws IOException {
  311. final RevTree tree = commit.getTree();
  312. if (tree != null) {
  313. return tree;
  314. }
  315. walk.parseHeaders(commit);
  316. return commit.getTree();
  317. }
  318. /**
  319. * Construct a keyname from the branch.
  320. *
  321. * @param branchName
  322. * @return a keyname appropriate for the Git config file format
  323. */
  324. private String getBranchKey(String branchName) {
  325. return StringUtils.getSHA1(branchName);
  326. }
  327. /**
  328. * Returns the Lucene configuration for the specified repository.
  329. *
  330. * @param repository
  331. * @return a config object
  332. */
  333. private FileBasedConfig getConfig(Repository repository) {
  334. LuceneRepoIndexStore luceneIndex = new LuceneRepoIndexStore(repository.getDirectory(), INDEX_VERSION);
  335. FileBasedConfig config = new FileBasedConfig(luceneIndex.getConfigFile(), FS.detect());
  336. return config;
  337. }
  338. /**
  339. * Checks if an index exists for the repository, that is compatible with
  340. * INDEX_VERSION and the Lucene version.
  341. *
  342. * @param repository
  343. * @return true if no index is found for the repository, false otherwise.
  344. */
  345. private boolean shouldReindex(Repository repository) {
  346. return ! (new LuceneRepoIndexStore(repository.getDirectory(), INDEX_VERSION).hasIndex());
  347. }
  348. /**
  349. * This completely indexes the repository and will destroy any existing
  350. * index.
  351. *
  352. * @param repositoryName
  353. * @param repository
  354. * @return IndexResult
  355. */
  356. public IndexResult reindex(RepositoryModel model, Repository repository) {
  357. IndexResult result = new IndexResult();
  358. if (!deleteIndex(model.name)) {
  359. return result;
  360. }
  361. try {
  362. String [] encodings = storedSettings.getStrings(Keys.web.blobEncodings).toArray(new String[0]);
  363. FileBasedConfig config = getConfig(repository);
  364. Set<String> indexedCommits = new TreeSet<String>();
  365. IndexWriter writer = getIndexWriter(model.name);
  366. // build a quick lookup of tags
  367. Map<String, List<String>> tags = new HashMap<String, List<String>>();
  368. for (RefModel tag : JGitUtils.getTags(repository, false, -1)) {
  369. if (!tag.isAnnotatedTag()) {
  370. // skip non-annotated tags
  371. continue;
  372. }
  373. if (!tags.containsKey(tag.getReferencedObjectId().getName())) {
  374. tags.put(tag.getReferencedObjectId().getName(), new ArrayList<String>());
  375. }
  376. tags.get(tag.getReferencedObjectId().getName()).add(tag.displayName);
  377. }
  378. ObjectReader reader = repository.newObjectReader();
  379. // get the local branches
  380. List<RefModel> branches = JGitUtils.getLocalBranches(repository, true, -1);
  381. // sort them by most recently updated
  382. Collections.sort(branches, new Comparator<RefModel>() {
  383. @Override
  384. public int compare(RefModel ref1, RefModel ref2) {
  385. return ref2.getDate().compareTo(ref1.getDate());
  386. }
  387. });
  388. // reorder default branch to first position
  389. RefModel defaultBranch = null;
  390. ObjectId defaultBranchId = JGitUtils.getDefaultBranch(repository);
  391. for (RefModel branch : branches) {
  392. if (branch.getObjectId().equals(defaultBranchId)) {
  393. defaultBranch = branch;
  394. break;
  395. }
  396. }
  397. branches.remove(defaultBranch);
  398. branches.add(0, defaultBranch);
  399. // walk through each branch
  400. for (RefModel branch : branches) {
  401. boolean indexBranch = false;
  402. if (model.indexedBranches.contains(com.gitblit.Constants.DEFAULT_BRANCH)
  403. && branch.equals(defaultBranch)) {
  404. // indexing "default" branch
  405. indexBranch = true;
  406. } else if (branch.getName().startsWith(com.gitblit.Constants.R_META)) {
  407. // skip internal meta branches
  408. indexBranch = false;
  409. } else {
  410. // normal explicit branch check
  411. indexBranch = model.indexedBranches.contains(branch.getName());
  412. }
  413. // if this branch is not specifically indexed then skip
  414. if (!indexBranch) {
  415. continue;
  416. }
  417. String branchName = branch.getName();
  418. RevWalk revWalk = new RevWalk(reader);
  419. RevCommit tip = revWalk.parseCommit(branch.getObjectId());
  420. String tipId = tip.getId().getName();
  421. String keyName = getBranchKey(branchName);
  422. config.setString(CONF_ALIAS, null, keyName, branchName);
  423. config.setString(CONF_BRANCH, null, keyName, tipId);
  424. // index the blob contents of the tree
  425. TreeWalk treeWalk = new TreeWalk(repository);
  426. treeWalk.addTree(tip.getTree());
  427. treeWalk.setRecursive(true);
  428. Map<String, ObjectId> paths = new TreeMap<String, ObjectId>();
  429. while (treeWalk.next()) {
  430. // ensure path is not in a submodule
  431. if (treeWalk.getFileMode(0) != FileMode.GITLINK) {
  432. paths.put(treeWalk.getPathString(), treeWalk.getObjectId(0));
  433. }
  434. }
  435. ByteArrayOutputStream os = new ByteArrayOutputStream();
  436. byte[] tmp = new byte[32767];
  437. RevWalk commitWalk = new RevWalk(reader);
  438. commitWalk.markStart(tip);
  439. RevCommit commit;
  440. while ((paths.size() > 0) && (commit = commitWalk.next()) != null) {
  441. TreeWalk diffWalk = new TreeWalk(reader);
  442. int parentCount = commit.getParentCount();
  443. switch (parentCount) {
  444. case 0:
  445. diffWalk.addTree(new EmptyTreeIterator());
  446. break;
  447. case 1:
  448. diffWalk.addTree(getTree(commitWalk, commit.getParent(0)));
  449. break;
  450. default:
  451. // skip merge commits
  452. continue;
  453. }
  454. diffWalk.addTree(getTree(commitWalk, commit));
  455. diffWalk.setFilter(ANY_DIFF);
  456. diffWalk.setRecursive(true);
  457. while ((paths.size() > 0) && diffWalk.next()) {
  458. String path = diffWalk.getPathString();
  459. if (!paths.containsKey(path)) {
  460. continue;
  461. }
  462. // remove path from set
  463. ObjectId blobId = paths.remove(path);
  464. result.blobCount++;
  465. // index the blob metadata
  466. String blobAuthor = getAuthor(commit);
  467. String blobCommitter = getCommitter(commit);
  468. String blobDate = DateTools.timeToString(commit.getCommitTime() * 1000L,
  469. Resolution.MINUTE);
  470. Document doc = new Document();
  471. doc.add(new Field(FIELD_OBJECT_TYPE, SearchObjectType.blob.name(), StringField.TYPE_STORED));
  472. doc.add(new Field(FIELD_BRANCH, branchName, TextField.TYPE_STORED));
  473. doc.add(new Field(FIELD_COMMIT, commit.getName(), TextField.TYPE_STORED));
  474. doc.add(new Field(FIELD_PATH, path, TextField.TYPE_STORED));
  475. doc.add(new Field(FIELD_DATE, blobDate, StringField.TYPE_STORED));
  476. doc.add(new Field(FIELD_AUTHOR, blobAuthor, TextField.TYPE_STORED));
  477. doc.add(new Field(FIELD_COMMITTER, blobCommitter, TextField.TYPE_STORED));
  478. // determine extension to compare to the extension
  479. // blacklist
  480. String ext = null;
  481. String name = path.toLowerCase();
  482. if (name.indexOf('.') > -1) {
  483. ext = name.substring(name.lastIndexOf('.') + 1);
  484. }
  485. // index the blob content
  486. if (StringUtils.isEmpty(ext) || !excludedExtensions.contains(ext)) {
  487. ObjectLoader ldr = repository.open(blobId, Constants.OBJ_BLOB);
  488. InputStream in = ldr.openStream();
  489. int n;
  490. while ((n = in.read(tmp)) > 0) {
  491. os.write(tmp, 0, n);
  492. }
  493. in.close();
  494. byte[] content = os.toByteArray();
  495. String str = StringUtils.decodeString(content, encodings);
  496. doc.add(new Field(FIELD_CONTENT, str, TextField.TYPE_STORED));
  497. os.reset();
  498. }
  499. // add the blob to the index
  500. writer.addDocument(doc);
  501. }
  502. }
  503. os.close();
  504. // index the tip commit object
  505. if (indexedCommits.add(tipId)) {
  506. Document doc = createDocument(tip, tags.get(tipId));
  507. doc.add(new Field(FIELD_BRANCH, branchName, TextField.TYPE_STORED));
  508. writer.addDocument(doc);
  509. result.commitCount += 1;
  510. result.branchCount += 1;
  511. }
  512. // traverse the log and index the previous commit objects
  513. RevWalk historyWalk = new RevWalk(reader);
  514. historyWalk.markStart(historyWalk.parseCommit(tip.getId()));
  515. RevCommit rev;
  516. while ((rev = historyWalk.next()) != null) {
  517. String hash = rev.getId().getName();
  518. if (indexedCommits.add(hash)) {
  519. Document doc = createDocument(rev, tags.get(hash));
  520. doc.add(new Field(FIELD_BRANCH, branchName, TextField.TYPE_STORED));
  521. writer.addDocument(doc);
  522. result.commitCount += 1;
  523. }
  524. }
  525. }
  526. // finished
  527. reader.close();
  528. // commit all changes and reset the searcher
  529. config.save();
  530. writer.commit();
  531. resetIndexSearcher(model.name);
  532. result.success();
  533. } catch (Exception e) {
  534. logger.error("Exception while reindexing {}", model.name, e);
  535. }
  536. return result;
  537. }
  538. /**
  539. * Incrementally update the index with the specified commit for the
  540. * repository.
  541. *
  542. * @param repositoryName
  543. * @param repository
  544. * @param branch
  545. * the fully qualified branch name (e.g. refs/heads/master)
  546. * @param commit
  547. * @return true, if successful
  548. */
  549. private IndexResult index(String repositoryName, Repository repository,
  550. String branch, RevCommit commit) {
  551. IndexResult result = new IndexResult();
  552. try {
  553. String [] encodings = storedSettings.getStrings(Keys.web.blobEncodings).toArray(new String[0]);
  554. List<PathChangeModel> changedPaths = JGitUtils.getFilesInCommit(repository, commit);
  555. String revDate = DateTools.timeToString(commit.getCommitTime() * 1000L,
  556. Resolution.MINUTE);
  557. IndexWriter writer = getIndexWriter(repositoryName);
  558. for (PathChangeModel path : changedPaths) {
  559. if (path.isSubmodule()) {
  560. continue;
  561. }
  562. // delete the indexed blob
  563. deleteBlob(repositoryName, branch, path.name);
  564. // re-index the blob
  565. if (!ChangeType.DELETE.equals(path.changeType)) {
  566. result.blobCount++;
  567. Document doc = new Document();
  568. doc.add(new Field(FIELD_OBJECT_TYPE, SearchObjectType.blob.name(), StringField.TYPE_STORED));
  569. doc.add(new Field(FIELD_BRANCH, branch, TextField.TYPE_STORED));
  570. doc.add(new Field(FIELD_COMMIT, commit.getName(), TextField.TYPE_STORED));
  571. doc.add(new Field(FIELD_PATH, path.path, TextField.TYPE_STORED));
  572. doc.add(new Field(FIELD_DATE, revDate, StringField.TYPE_STORED));
  573. doc.add(new Field(FIELD_AUTHOR, getAuthor(commit), TextField.TYPE_STORED));
  574. doc.add(new Field(FIELD_COMMITTER, getCommitter(commit), TextField.TYPE_STORED));
  575. // determine extension to compare to the extension
  576. // blacklist
  577. String ext = null;
  578. String name = path.name.toLowerCase();
  579. if (name.indexOf('.') > -1) {
  580. ext = name.substring(name.lastIndexOf('.') + 1);
  581. }
  582. if (StringUtils.isEmpty(ext) || !excludedExtensions.contains(ext)) {
  583. // read the blob content
  584. String str = JGitUtils.getStringContent(repository, commit.getTree(),
  585. path.path, encodings);
  586. if (str != null) {
  587. doc.add(new Field(FIELD_CONTENT, str, TextField.TYPE_STORED));
  588. writer.addDocument(doc);
  589. }
  590. }
  591. }
  592. }
  593. writer.commit();
  594. // get any annotated commit tags
  595. List<String> commitTags = new ArrayList<String>();
  596. for (RefModel ref : JGitUtils.getTags(repository, false, -1)) {
  597. if (ref.isAnnotatedTag() && ref.getReferencedObjectId().equals(commit.getId())) {
  598. commitTags.add(ref.displayName);
  599. }
  600. }
  601. // create and write the Lucene document
  602. Document doc = createDocument(commit, commitTags);
  603. doc.add(new Field(FIELD_BRANCH, branch, TextField.TYPE_STORED));
  604. result.commitCount++;
  605. result.success = index(repositoryName, doc);
  606. } catch (Exception e) {
  607. logger.error("Exception while indexing commit {} in {}", commit.getId().getName(), repositoryName, e);
  608. }
  609. return result;
  610. }
  611. /**
  612. * Delete a blob from the specified branch of the repository index.
  613. *
  614. * @param repositoryName
  615. * @param branch
  616. * @param path
  617. * @throws Exception
  618. * @return true, if deleted, false if no record was deleted
  619. */
  620. public boolean deleteBlob(String repositoryName, String branch, String path) throws Exception {
  621. String pattern = MessageFormat.format("{0}:'{'0} AND {1}:\"'{'1'}'\" AND {2}:\"'{'2'}'\"", FIELD_OBJECT_TYPE, FIELD_BRANCH, FIELD_PATH);
  622. String q = MessageFormat.format(pattern, SearchObjectType.blob.name(), branch, path);
  623. StandardAnalyzer analyzer = new StandardAnalyzer();
  624. QueryParser qp = new QueryParser(FIELD_SUMMARY, analyzer);
  625. BooleanQuery query = new BooleanQuery.Builder().add(qp.parse(q), Occur.MUST).build();
  626. IndexWriter writer = getIndexWriter(repositoryName);
  627. int numDocsBefore = writer.numDocs();
  628. writer.deleteDocuments(query);
  629. writer.commit();
  630. int numDocsAfter = writer.numDocs();
  631. if (numDocsBefore == numDocsAfter) {
  632. logger.debug("no records found to delete {}", query.toString());
  633. return false;
  634. } else {
  635. logger.debug("deleted {} records with {}", numDocsBefore - numDocsAfter, query.toString());
  636. return true;
  637. }
  638. }
  639. /**
  640. * Updates a repository index incrementally from the last indexed commits.
  641. *
  642. * @param model
  643. * @param repository
  644. * @return IndexResult
  645. */
  646. private IndexResult updateIndex(RepositoryModel model, Repository repository) {
  647. IndexResult result = new IndexResult();
  648. try {
  649. FileBasedConfig config = getConfig(repository);
  650. config.load();
  651. // build a quick lookup of annotated tags
  652. Map<String, List<String>> tags = new HashMap<String, List<String>>();
  653. for (RefModel tag : JGitUtils.getTags(repository, false, -1)) {
  654. if (!tag.isAnnotatedTag()) {
  655. // skip non-annotated tags
  656. continue;
  657. }
  658. if (!tags.containsKey(tag.getObjectId().getName())) {
  659. tags.put(tag.getReferencedObjectId().getName(), new ArrayList<String>());
  660. }
  661. tags.get(tag.getReferencedObjectId().getName()).add(tag.displayName);
  662. }
  663. // detect branch deletion
  664. // first assume all branches are deleted and then remove each
  665. // existing branch from deletedBranches during indexing
  666. Set<String> deletedBranches = new TreeSet<String>();
  667. for (String alias : config.getNames(CONF_ALIAS)) {
  668. String branch = config.getString(CONF_ALIAS, null, alias);
  669. deletedBranches.add(branch);
  670. }
  671. // get the local branches
  672. List<RefModel> branches = JGitUtils.getLocalBranches(repository, true, -1);
  673. // sort them by most recently updated
  674. Collections.sort(branches, new Comparator<RefModel>() {
  675. @Override
  676. public int compare(RefModel ref1, RefModel ref2) {
  677. return ref2.getDate().compareTo(ref1.getDate());
  678. }
  679. });
  680. // reorder default branch to first position
  681. RefModel defaultBranch = null;
  682. ObjectId defaultBranchId = JGitUtils.getDefaultBranch(repository);
  683. for (RefModel branch : branches) {
  684. if (branch.getObjectId().equals(defaultBranchId)) {
  685. defaultBranch = branch;
  686. break;
  687. }
  688. }
  689. branches.remove(defaultBranch);
  690. branches.add(0, defaultBranch);
  691. // walk through each branches
  692. for (RefModel branch : branches) {
  693. String branchName = branch.getName();
  694. boolean indexBranch = false;
  695. if (model.indexedBranches.contains(com.gitblit.Constants.DEFAULT_BRANCH)
  696. && branch.equals(defaultBranch)) {
  697. // indexing "default" branch
  698. indexBranch = true;
  699. } else if (branch.getName().startsWith(com.gitblit.Constants.R_META)) {
  700. // ignore internal meta branches
  701. indexBranch = false;
  702. } else {
  703. // normal explicit branch check
  704. indexBranch = model.indexedBranches.contains(branch.getName());
  705. }
  706. // if this branch is not specifically indexed then skip
  707. if (!indexBranch) {
  708. continue;
  709. }
  710. // remove this branch from the deletedBranches set
  711. deletedBranches.remove(branchName);
  712. // determine last commit
  713. String keyName = getBranchKey(branchName);
  714. String lastCommit = config.getString(CONF_BRANCH, null, keyName);
  715. List<RevCommit> revs;
  716. if (StringUtils.isEmpty(lastCommit)) {
  717. // new branch/unindexed branch, get all commits on branch
  718. revs = JGitUtils.getRevLog(repository, branchName, 0, -1);
  719. } else {
  720. // pre-existing branch, get changes since last commit
  721. revs = JGitUtils.getRevLog(repository, lastCommit, branchName);
  722. }
  723. if (revs.size() > 0) {
  724. result.branchCount += 1;
  725. }
  726. // reverse the list of commits so we start with the first commit
  727. Collections.reverse(revs);
  728. for (RevCommit commit : revs) {
  729. // index a commit
  730. result.add(index(model.name, repository, branchName, commit));
  731. }
  732. // update the config
  733. config.setString(CONF_ALIAS, null, keyName, branchName);
  734. config.setString(CONF_BRANCH, null, keyName, branch.getObjectId().getName());
  735. config.save();
  736. }
  737. // the deletedBranches set will normally be empty by this point
  738. // unless a branch really was deleted and no longer exists
  739. if (deletedBranches.size() > 0) {
  740. for (String branch : deletedBranches) {
  741. IndexWriter writer = getIndexWriter(model.name);
  742. writer.deleteDocuments(new Term(FIELD_BRANCH, branch));
  743. writer.commit();
  744. }
  745. }
  746. result.success = true;
  747. } catch (Throwable t) {
  748. logger.error("Exception while updating {} Lucene index", model.name, t);
  749. }
  750. return result;
  751. }
  752. /**
  753. * Creates a Lucene document for a commit
  754. *
  755. * @param commit
  756. * @param tags
  757. * @return a Lucene document
  758. */
  759. private Document createDocument(RevCommit commit, List<String> tags) {
  760. Document doc = new Document();
  761. doc.add(new Field(FIELD_OBJECT_TYPE, SearchObjectType.commit.name(), StringField.TYPE_STORED));
  762. doc.add(new Field(FIELD_COMMIT, commit.getName(), TextField.TYPE_STORED));
  763. doc.add(new Field(FIELD_DATE, DateTools.timeToString(commit.getCommitTime() * 1000L,
  764. Resolution.MINUTE), StringField.TYPE_STORED));
  765. doc.add(new Field(FIELD_AUTHOR, getAuthor(commit), TextField.TYPE_STORED));
  766. doc.add(new Field(FIELD_COMMITTER, getCommitter(commit), TextField.TYPE_STORED));
  767. doc.add(new Field(FIELD_SUMMARY, commit.getShortMessage(), TextField.TYPE_STORED));
  768. doc.add(new Field(FIELD_CONTENT, commit.getFullMessage(), TextField.TYPE_STORED));
  769. if (!ArrayUtils.isEmpty(tags)) {
  770. doc.add(new Field(FIELD_TAG, StringUtils.flattenStrings(tags), TextField.TYPE_STORED));
  771. }
  772. return doc;
  773. }
  774. /**
  775. * Incrementally index an object for the repository.
  776. *
  777. * @param repositoryName
  778. * @param doc
  779. * @return true, if successful
  780. */
  781. private boolean index(String repositoryName, Document doc) {
  782. try {
  783. IndexWriter writer = getIndexWriter(repositoryName);
  784. writer.addDocument(doc);
  785. writer.commit();
  786. resetIndexSearcher(repositoryName);
  787. return true;
  788. } catch (Exception e) {
  789. logger.error("Exception while incrementally updating {} Lucene index", repositoryName, e);
  790. }
  791. return false;
  792. }
  793. private SearchResult createSearchResult(Document doc, float score, int hitId, int totalHits) throws ParseException {
  794. SearchResult result = new SearchResult();
  795. result.hitId = hitId;
  796. result.totalHits = totalHits;
  797. result.score = score;
  798. result.date = DateTools.stringToDate(doc.get(FIELD_DATE));
  799. result.summary = doc.get(FIELD_SUMMARY);
  800. result.author = doc.get(FIELD_AUTHOR);
  801. result.committer = doc.get(FIELD_COMMITTER);
  802. result.type = SearchObjectType.fromName(doc.get(FIELD_OBJECT_TYPE));
  803. result.branch = doc.get(FIELD_BRANCH);
  804. result.commitId = doc.get(FIELD_COMMIT);
  805. result.path = doc.get(FIELD_PATH);
  806. if (doc.get(FIELD_TAG) != null) {
  807. result.tags = StringUtils.getStringsFromValue(doc.get(FIELD_TAG));
  808. }
  809. return result;
  810. }
  811. private synchronized void resetIndexSearcher(String repository) throws IOException {
  812. IndexSearcher searcher = searchers.remove(repository);
  813. if (searcher != null) {
  814. searcher.getIndexReader().close();
  815. }
  816. }
  817. /**
  818. * Gets an index searcher for the repository.
  819. *
  820. * @param repository
  821. * @return
  822. * @throws IOException
  823. */
  824. private IndexSearcher getIndexSearcher(String repository) throws IOException {
  825. IndexSearcher searcher = searchers.get(repository);
  826. if (searcher == null) {
  827. IndexWriter writer = getIndexWriter(repository);
  828. searcher = new IndexSearcher(DirectoryReader.open(writer, true));
  829. searchers.put(repository, searcher);
  830. }
  831. return searcher;
  832. }
  833. /**
  834. * Gets an index writer for the repository. The index will be created if it
  835. * does not already exist or if forceCreate is specified.
  836. *
  837. * @param repository
  838. * @return an IndexWriter
  839. * @throws IOException
  840. */
  841. private IndexWriter getIndexWriter(String repository) throws IOException {
  842. IndexWriter indexWriter = writers.get(repository);
  843. if (indexWriter == null) {
  844. File repositoryFolder = FileKey.resolve(new File(repositoriesFolder, repository), FS.DETECTED);
  845. LuceneRepoIndexStore indexStore = new LuceneRepoIndexStore(repositoryFolder, INDEX_VERSION);
  846. indexStore.create();
  847. Directory directory = FSDirectory.open(indexStore.getPath());
  848. StandardAnalyzer analyzer = new StandardAnalyzer();
  849. IndexWriterConfig config = new IndexWriterConfig(analyzer);
  850. config.setOpenMode(OpenMode.CREATE_OR_APPEND);
  851. indexWriter = new IndexWriter(directory, config);
  852. writers.put(repository, indexWriter);
  853. }
  854. return indexWriter;
  855. }
  856. /**
  857. * Searches the specified repositories for the given text or query
  858. *
  859. * @param text
  860. * if the text is null or empty, null is returned
  861. * @param page
  862. * the page number to retrieve. page is 1-indexed.
  863. * @param pageSize
  864. * the number of elements to return for this page
  865. * @param repositories
  866. * a list of repositories to search. if no repositories are
  867. * specified null is returned.
  868. * @return a list of SearchResults in order from highest to the lowest score
  869. *
  870. */
  871. public List<SearchResult> search(String text, int page, int pageSize, List<String> repositories) {
  872. if (ArrayUtils.isEmpty(repositories)) {
  873. return null;
  874. }
  875. return search(text, page, pageSize, repositories.toArray(new String[0]));
  876. }
  877. /**
  878. * Searches the specified repositories for the given text or query
  879. *
  880. * @param text
  881. * if the text is null or empty, null is returned
  882. * @param page
  883. * the page number to retrieve. page is 1-indexed.
  884. * @param pageSize
  885. * the number of elements to return for this page
  886. * @param repositories
  887. * a list of repositories to search. if no repositories are
  888. * specified null is returned.
  889. * @return a list of SearchResults in order from highest to the lowest score
  890. *
  891. */
  892. public List<SearchResult> search(String text, int page, int pageSize, String... repositories) {
  893. if (StringUtils.isEmpty(text)) {
  894. return null;
  895. }
  896. if (ArrayUtils.isEmpty(repositories)) {
  897. return null;
  898. }
  899. Set<SearchResult> results = new LinkedHashSet<SearchResult>();
  900. StandardAnalyzer analyzer = new StandardAnalyzer();
  901. try {
  902. // default search checks summary and content
  903. BooleanQuery.Builder bldr = new BooleanQuery.Builder();
  904. QueryParser qp;
  905. qp = new QueryParser(FIELD_SUMMARY, analyzer);
  906. qp.setAllowLeadingWildcard(true);
  907. bldr.add(qp.parse(text), Occur.SHOULD);
  908. qp = new QueryParser(FIELD_CONTENT, analyzer);
  909. qp.setAllowLeadingWildcard(true);
  910. bldr.add(qp.parse(text), Occur.SHOULD);
  911. IndexSearcher searcher;
  912. if (repositories.length == 1) {
  913. // single repository search
  914. searcher = getIndexSearcher(repositories[0]);
  915. } else {
  916. // multiple repository search
  917. List<IndexReader> readers = new ArrayList<IndexReader>();
  918. for (String repository : repositories) {
  919. IndexSearcher repositoryIndex = getIndexSearcher(repository);
  920. readers.add(repositoryIndex.getIndexReader());
  921. }
  922. IndexReader[] rdrs = readers.toArray(new IndexReader[readers.size()]);
  923. MultiSourceReader reader = new MultiSourceReader(rdrs);
  924. searcher = new IndexSearcher(reader);
  925. }
  926. BooleanQuery query = bldr.build();
  927. Query rewrittenQuery = searcher.rewrite(query);
  928. logger.debug(rewrittenQuery.toString());
  929. TopScoreDocCollector collector = TopScoreDocCollector.create(5000);
  930. searcher.search(rewrittenQuery, collector);
  931. int offset = Math.max(0, (page - 1) * pageSize);
  932. ScoreDoc[] hits = collector.topDocs(offset, pageSize).scoreDocs;
  933. int totalHits = collector.getTotalHits();
  934. for (int i = 0; i < hits.length; i++) {
  935. int docId = hits[i].doc;
  936. Document doc = searcher.doc(docId);
  937. SearchResult result = createSearchResult(doc, hits[i].score, offset + i + 1, totalHits);
  938. if (repositories.length == 1) {
  939. // single repository search
  940. result.repository = repositories[0];
  941. } else {
  942. // multi-repository search
  943. MultiSourceReader reader = (MultiSourceReader) searcher.getIndexReader();
  944. int index = reader.getSourceIndex(docId);
  945. result.repository = repositories[index];
  946. }
  947. String content = doc.get(FIELD_CONTENT);
  948. result.fragment = getHighlightedFragment(analyzer, query, content, result);
  949. results.add(result);
  950. }
  951. } catch (Exception e) {
  952. logger.error("Exception while searching for {}", text, e);
  953. }
  954. return new ArrayList<SearchResult>(results);
  955. }
  956. /**
  957. *
  958. * @param analyzer
  959. * @param query
  960. * @param content
  961. * @param result
  962. * @return
  963. * @throws IOException
  964. * @throws InvalidTokenOffsetsException
  965. */
  966. private String getHighlightedFragment(Analyzer analyzer, Query query,
  967. String content, SearchResult result) throws IOException, InvalidTokenOffsetsException {
  968. if (content == null) {
  969. content = "";
  970. }
  971. int tabLength = storedSettings.getInteger(Keys.web.tabLength, 4);
  972. int fragmentLength = SearchObjectType.commit == result.type ? 512 : 150;
  973. QueryScorer scorer = new QueryScorer(query, "content");
  974. Fragmenter fragmenter = new SimpleSpanFragmenter(scorer, fragmentLength);
  975. // use an artificial delimiter for the token
  976. String termTag = "!!--[";
  977. String termTagEnd = "]--!!";
  978. SimpleHTMLFormatter formatter = new SimpleHTMLFormatter(termTag, termTagEnd);
  979. Highlighter highlighter = new Highlighter(formatter, scorer);
  980. highlighter.setTextFragmenter(fragmenter);
  981. String [] fragments = highlighter.getBestFragments(analyzer, "content", content, 3);
  982. if (ArrayUtils.isEmpty(fragments)) {
  983. if (SearchObjectType.blob == result.type) {
  984. return "";
  985. }
  986. // clip commit message
  987. String fragment = content;
  988. if (fragment.length() > fragmentLength) {
  989. fragment = fragment.substring(0, fragmentLength) + "...";
  990. }
  991. return "<pre class=\"text\">" + StringUtils.escapeForHtml(fragment, true, tabLength) + "</pre>";
  992. }
  993. // make sure we have unique fragments
  994. Set<String> uniqueFragments = new LinkedHashSet<String>();
  995. for (String fragment : fragments) {
  996. uniqueFragments.add(fragment);
  997. }
  998. fragments = uniqueFragments.toArray(new String[uniqueFragments.size()]);
  999. StringBuilder sb = new StringBuilder();
  1000. for (int i = 0, len = fragments.length; i < len; i++) {
  1001. String fragment = fragments[i];
  1002. String tag = "<pre class=\"text\">";
  1003. // resurrect the raw fragment from removing the artificial delimiters
  1004. String raw = fragment.replace(termTag, "").replace(termTagEnd, "");
  1005. // determine position of the raw fragment in the content
  1006. int pos = content.indexOf(raw);
  1007. // restore complete first line of fragment
  1008. int c = pos;
  1009. while (c > 0) {
  1010. c--;
  1011. if (content.charAt(c) == '\n') {
  1012. break;
  1013. }
  1014. }
  1015. if (c > 0) {
  1016. // inject leading chunk of first fragment line
  1017. fragment = content.substring(c + 1, pos) + fragment;
  1018. }
  1019. if (SearchObjectType.blob == result.type) {
  1020. // count lines as offset into the content for this fragment
  1021. int line = Math.max(1, StringUtils.countLines(content.substring(0, pos)));
  1022. // create fragment tag with line number and language
  1023. String lang = "";
  1024. String ext = StringUtils.getFileExtension(result.path).toLowerCase();
  1025. if (!StringUtils.isEmpty(ext)) {
  1026. // maintain leading space!
  1027. lang = " lang-" + ext;
  1028. }
  1029. tag = MessageFormat.format("<pre class=\"prettyprint linenums:{0,number,0}{1}\">", line, lang);
  1030. }
  1031. sb.append(tag);
  1032. // replace the artificial delimiter with html tags
  1033. String html = StringUtils.escapeForHtml(fragment, false);
  1034. html = html.replace(termTag, "<span class=\"highlight\">").replace(termTagEnd, "</span>");
  1035. sb.append(html);
  1036. sb.append("</pre>");
  1037. if (i < len - 1) {
  1038. sb.append("<span class=\"ellipses\">...</span><br/>");
  1039. }
  1040. }
  1041. return sb.toString();
  1042. }
  1043. /**
  1044. * Simple class to track the results of an index update.
  1045. */
  1046. private class IndexResult {
  1047. long startTime = System.currentTimeMillis();
  1048. long endTime = startTime;
  1049. boolean success;
  1050. int branchCount;
  1051. int commitCount;
  1052. int blobCount;
  1053. void add(IndexResult result) {
  1054. this.branchCount += result.branchCount;
  1055. this.commitCount += result.commitCount;
  1056. this.blobCount += result.blobCount;
  1057. }
  1058. void success() {
  1059. success = true;
  1060. endTime = System.currentTimeMillis();
  1061. }
  1062. float duration() {
  1063. return (endTime - startTime)/1000f;
  1064. }
  1065. }
  1066. /**
  1067. * Custom subclass of MultiReader to identify the source index for a given
  1068. * doc id. This would not be necessary of there was a public method to
  1069. * obtain this information.
  1070. *
  1071. */
  1072. private class MultiSourceReader extends MultiReader {
  1073. MultiSourceReader(IndexReader [] readers) throws IOException {
  1074. super(readers, false);
  1075. }
  1076. int getSourceIndex(int docId) {
  1077. int index = -1;
  1078. try {
  1079. index = super.readerIndex(docId);
  1080. } catch (Exception e) {
  1081. logger.error("Error getting source index", e);
  1082. }
  1083. return index;
  1084. }
  1085. }
  1086. }