Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

LuceneService.java 42KB

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