/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License (the "License").
* You may not use this file except in compliance with the License.
*
* See LICENSE.txt included in this distribution for the specific
* language governing permissions and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at LICENSE.txt.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information: Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*/
/*
* Copyright (c) 2005, 2015, Oracle and/or its affiliates. All rights reserved.
*/
package org.opensolaris.opengrok.history;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.lucene.document.DateTools;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.TopFieldDocs;
import org.opensolaris.opengrok.analysis.CompatibleAnalyser;
import org.opensolaris.opengrok.configuration.RuntimeEnvironment;
import org.opensolaris.opengrok.index.IndexDatabase;
import org.opensolaris.opengrok.logger.LoggerFactory;
import org.opensolaris.opengrok.search.QueryBuilder;
/**
* Generate SCM history for directory by using the Index database. (Please note
* that SCM systems that supports changesets consisting of multiple files should
* implement their own HistoryReader!)
*
* The sole purpose of this class is to produce history for generating RSS feed
* for directory changes.
*
* @author Chandan
* @author Lubos Kosco update for lucene 4.x
*/
public class DirectoryHistoryReader {
private static final Logger LOGGER = LoggerFactory.getLogger(DirectoryHistoryReader.class);
// This is a giant hash constructed in this class.
// It maps date -> author -> (comment, revision) -> [ list of files ]
private final Map<Date, Map<String, Map<List<String>, SortedSet<String>>>> hash
= new LinkedHashMap<>(); // set in put()
Iterator<Date> diter;
Date idate;
Iterator<String> aiter;
String iauthor;
Iterator<List<String>> citer;
List<String> icomment;
HistoryEntry currentEntry; // set in next()
History history; // set in the constructor
/**
* The main task of this method is to produce list of history entries for
* the specified directory and store them in @code history. This is done by
* searching the index to get recently changed files under in the directory
* tree under @code path and storing their histories in giant @code hash.
*
* @param path directory to generate history for
* @throws IOException when index cannot be accessed
*/
public DirectoryHistoryReader(String path) throws IOException {
//TODO can we introduce paging here ??? this class is used just for rss.jsp !
int hitsPerPage = RuntimeEnvironment.getInstance().getHitsPerPage();
int cachePages = RuntimeEnvironment.getInstance().getCachePages();
IndexReader ireader = null;
IndexSearcher searcher;
try {
// Prepare for index search.
String src_root = RuntimeEnvironment.getInstance().getSourceRootPath();
ireader = IndexDatabase.getIndexReader(path);
if (ireader == null) {
throw new IOException("Could not locate index database");
}
// The search results will be sorted by date.
searcher = new IndexSearcher(ireader);
SortField sfield = new SortField(QueryBuilder.DATE, SortField.Type.STRING, true);
Sort sort = new Sort(sfield);
QueryParser qparser = new QueryParser(QueryBuilder.PATH, new CompatibleAnalyser());
Query query;
ScoreDoc[] hits = null;
try {
// Get files under given directory by searching the index.
query = qparser.parse(path);
TopFieldDocs fdocs = searcher.search(query, hitsPerPage * cachePages, sort);
fdocs = searcher.search(query, fdocs.totalHits, sort);
hits = fdocs.scoreDocs;
} catch (ParseException e) {
LOGGER.log(Level.WARNING,
"An error occured while parsing search query", e);
}
if (hits != null) {
// Get maximum 40 (why ? XXX) files which were changed recently.
for (int i = 0; i < 40 && i < hits.length; i++) {
int docId = hits[i].doc;
Document doc = searcher.doc(docId);
String rpath = doc.get(QueryBuilder.PATH);
if (!rpath.startsWith(path)) {
continue;
}
Date cdate;
try {
cdate = DateTools.stringToDate(doc.get(QueryBuilder.DATE));
} catch (java.text.ParseException ex) {
LOGGER.log(Level.WARNING,
"Could not get date for " + path, ex);
cdate = new Date();
}
int ls = rpath.lastIndexOf('/');
if (ls != -1) {
String rparent = rpath.substring(0, ls);
String rbase = rpath.substring(ls + 1);
History hist = null;
try {
File f = new File(src_root + rparent, rbase);
hist = HistoryGuru.getInstance().getHistory(f);
} catch (HistoryException e) {
LOGGER.log(Level.WARNING,
"An error occured while getting history reader", e);
}
if (hist == null) {
put(cdate, "", "-", "", rpath);
} else {
// Put all history entries for this file into the giant hash.
readFromHistory(hist, rpath);
}
}
}
}
// Now go through the giant hash and produce history entries from it.
ArrayList<HistoryEntry> entries = new ArrayList<>();
while (next()) {
entries.add(currentEntry);
}
// This is why we are here. Store all the constructed history entries
// into history object.
history = new History(entries);
} finally {
if (ireader != null) {
try {
ireader.close();
} catch (Exception ex) {
LOGGER.log(Level.WARNING,
"An error occured while closing reader", ex);
}
}
}
}
public History getHistory() {
return history;
}
// Fill the giant hash with some data from one history entry.
private void put(Date date, String revision, String author, String comment, String path) {
long time = date.getTime();
date.setTime(time - (time % 3600000l));
Map<String, Map<List<String>, SortedSet<String>>> ac = hash.get(date);
if (ac == null) {
ac = new HashMap<>();
hash.put(date, ac);
}
Map<List<String>, SortedSet<String>> cf = ac.get(author);
if (cf == null) {
cf = new HashMap<>();
ac.put(author, cf);
}
// We are not going to modify the list so this is safe to do.
List<String> cr = new ArrayList<>();
cr.add(comment);
cr.add(revision);
SortedSet<String> fls = cf.get(cr);
if (fls == null) {
fls = new TreeSet<>();
cf.put(cr, fls);
}
fls.add(path);
}
/**
* Do one traversal step of the giant hash and produce history entry object
* and store it into @code currentEntry.
*
* @return true if history entry was successfully generated otherwise false
* @throws IOException
*/
private boolean next() throws IOException {
if (diter == null) {
diter = hash.keySet().iterator();
}
if (citer == null || !citer.hasNext()) {
if (aiter == null || !aiter.hasNext()) {
if (diter.hasNext()) {
aiter = hash.get(idate = diter.next()).keySet().iterator();
} else {
return false;
}
}
citer = hash.get(idate).get(iauthor = aiter.next()).keySet().iterator();
}
icomment = citer.next();
currentEntry = new HistoryEntry(icomment.get(1), idate, iauthor, null, icomment.get(0), true);
currentEntry.setFiles(hash.get(idate).get(iauthor).get(icomment));
return true;
}
/**
* Go through all history entries in @code hist for file @code path and
* store them in the giant hash.
*
* @param hist history to store
* @param rpath path of the file corresponding to the history
*/
private void readFromHistory(History hist, String rpath) {
for (HistoryEntry entry : hist.getHistoryEntries()) {
if (entry.isActive()) {
String comment = entry.getMessage();
String cauthor = entry.getAuthor();
Date cdate = entry.getDate();
String revision = entry.getRevision();
put(cdate, revision, cauthor, comment, rpath);
break;
}
}
}
}