305N/A/*
305N/A * CDDL HEADER START
305N/A *
305N/A * The contents of this file are subject to the terms of the
305N/A * Common Development and Distribution License (the "License").
305N/A * You may not use this file except in compliance with the License.
305N/A *
305N/A * See LICENSE.txt included in this distribution for the specific
305N/A * language governing permissions and limitations under the License.
305N/A *
305N/A * When distributing Covered Code, include this CDDL HEADER in each
305N/A * file and include the License file at LICENSE.txt.
305N/A * If applicable, add the following below this CDDL HEADER, with the
305N/A * fields enclosed by brackets "[]" replaced with your own identifying
305N/A * information: Portions Copyright [yyyy] [name of copyright owner]
305N/A *
305N/A * CDDL HEADER END
305N/A */
305N/A
305N/A/*
1460N/A * Copyright (c) 2008, 2012, Oracle and/or its affiliates. All rights reserved.
305N/A */
305N/A
305N/Apackage org.opensolaris.opengrok.history;
305N/A
305N/Aimport java.beans.Encoder;
305N/Aimport java.beans.Expression;
305N/Aimport java.beans.PersistenceDelegate;
305N/Aimport java.beans.XMLDecoder;
305N/Aimport java.beans.XMLEncoder;
394N/Aimport java.io.BufferedInputStream;
305N/Aimport java.io.BufferedOutputStream;
305N/Aimport java.io.File;
394N/Aimport java.io.FileInputStream;
305N/Aimport java.io.FileOutputStream;
305N/Aimport java.io.IOException;
743N/Aimport java.util.ArrayList;
1460N/Aimport java.util.Collections;
1460N/Aimport java.util.Date;
743N/Aimport java.util.HashMap;
743N/Aimport java.util.List;
743N/Aimport java.util.Map;
416N/Aimport java.util.logging.Level;
1327N/Aimport java.util.logging.Logger;
335N/Aimport java.util.zip.GZIPInputStream;
335N/Aimport java.util.zip.GZIPOutputStream;
1327N/A
1470N/Aimport org.opensolaris.opengrok.configuration.Configuration;
305N/Aimport org.opensolaris.opengrok.configuration.RuntimeEnvironment;
1195N/Aimport org.opensolaris.opengrok.util.IOUtils;
305N/A
305N/Aclass FileHistoryCache implements HistoryCache {
1470N/A private static final Logger logger =
1470N/A Logger.getLogger(FileHistoryCache.class.getName());
335N/A private final Object lock = new Object();
305N/A
305N/A static class FilePersistenceDelegate extends PersistenceDelegate {
815N/A @Override
305N/A protected Expression instantiate(Object oldInstance, Encoder out) {
305N/A File f = (File)oldInstance;
1470N/A return new Expression(oldInstance, f.getClass(), "new",
1470N/A new Object[] { f.toString() });
305N/A }
305N/A }
715N/A
815N/A @Override
715N/A public void initialize() {
715N/A // nothing to do
715N/A }
715N/A
815N/A @Override
795N/A public void optimize() {
795N/A // nothing to do
795N/A }
795N/A
887N/A @Override
887N/A public boolean supportsRepository(Repository repository) {
887N/A // all repositories are supported
887N/A return true;
887N/A }
887N/A
305N/A /**
305N/A * Get a <code>File</code> object describing the cache file.
305N/A *
305N/A * @param file the file to find the cache for
305N/A * @return file that might contain cached history for <code>file</code>
305N/A */
615N/A private static File getCachedFile(File file) throws HistoryException {
1470N/A Configuration cfg = RuntimeEnvironment.getConfig();
1190N/A
305N/A StringBuilder sb = new StringBuilder();
1470N/A sb.append(cfg.getDataRoot());
305N/A sb.append(File.separatorChar);
305N/A sb.append("historycache");
305N/A
305N/A try {
1470N/A String add = cfg.getPathRelativeToSourceRoot(file, 0);
1016N/A if (add.length() == 0) {
1016N/A add = File.separator;
702N/A }
1016N/A sb.append(add);
1016N/A sb.append(".gz");
1016N/A } catch (IOException e) {
1327N/A throw new HistoryException("Failed to get path relative to source root for '"
1327N/A + file + "'", e);
305N/A }
1190N/A
305N/A return new File(sb.toString());
305N/A }
1190N/A
305N/A /**
305N/A * Read history from a file.
305N/A */
1461N/A @SuppressWarnings("resource")
305N/A private static History readCache(File file) throws IOException {
509N/A final FileInputStream in = new FileInputStream(file);
1461N/A XMLDecoder d = null;
509N/A try {
1461N/A d = new XMLDecoder(new BufferedInputStream(new GZIPInputStream(in)));
509N/A Object obj = d.readObject();
509N/A return (History) obj;
509N/A } finally {
1461N/A if (d != null) {
1461N/A d.close();
1461N/A } else {
1461N/A IOUtils.close(in);
1461N/A }
509N/A }
305N/A }
1190N/A
744N/A private void storeFile(History history, File file) throws HistoryException {
1190N/A
307N/A File cache = getCachedFile(file);
452N/A
307N/A File dir = cache.getParentFile();
460N/A if (!dir.isDirectory() && !dir.mkdirs()) {
615N/A throw new HistoryException(
1327N/A "Unable to create cache directory '" + dir + "'");
305N/A }
305N/A
305N/A // We have a problem that multiple threads may access the cache layer
305N/A // at the same time. Since I would like to avoid read-locking, I just
305N/A // serialize the write access to the cache file. The generation of the
305N/A // cache file would most likely be executed during index generation, and
305N/A // that happens sequencial anyway....
305N/A // Generate the file with a temporary name and move it into place when
305N/A // I'm done so I don't have to protect the readers for partially updated
305N/A // files...
615N/A final File output;
509N/A try {
615N/A output = File.createTempFile("oghist", null, dir);
1461N/A @SuppressWarnings("resource")
615N/A final FileOutputStream out = new FileOutputStream(output);
1461N/A XMLEncoder e = null;
615N/A try {
1461N/A e = new XMLEncoder(new BufferedOutputStream(new GZIPOutputStream(out)));
615N/A e.setPersistenceDelegate(File.class, new FilePersistenceDelegate());
615N/A e.writeObject(history);
615N/A } finally {
1461N/A if (e != null) {
1461N/A e.close();
1461N/A } else {
1461N/A IOUtils.close(out);
1461N/A }
615N/A }
615N/A } catch (IOException ioe) {
615N/A throw new HistoryException("Failed to write history", ioe);
509N/A }
305N/A synchronized (lock) {
307N/A if (!cache.delete() && cache.exists()) {
357N/A if (!output.delete()) {
1327N/A logger.warning("Failed to remove temporary history cache file");
357N/A }
615N/A throw new HistoryException(
1327N/A "Cache file exists, and I could not delete it");
305N/A }
307N/A if (!output.renameTo(cache)) {
357N/A if (!output.delete()) {
1327N/A logger.warning("Failed to remove temporary history cache file");
357N/A }
1327N/A throw new HistoryException("Failed to rename cache tmpfile");
305N/A }
305N/A }
305N/A }
305N/A
815N/A @Override
743N/A public void store(History history, Repository repository)
743N/A throws HistoryException {
743N/A
743N/A if (history.getHistoryEntries() == null) {
743N/A return;
743N/A }
743N/A
743N/A HashMap<String, List<HistoryEntry>> map =
743N/A new HashMap<String, List<HistoryEntry>>();
743N/A
743N/A for (HistoryEntry e : history.getHistoryEntries()) {
743N/A for (String s : e.getFiles()) {
743N/A List<HistoryEntry> list = map.get(s);
743N/A if (list == null) {
743N/A list = new ArrayList<HistoryEntry>();
743N/A map.put(s, list);
743N/A }
743N/A list.add(e);
743N/A }
743N/A }
743N/A
1470N/A File root = RuntimeEnvironment.getConfig().getSourceRootFile();
743N/A for (Map.Entry<String, List<HistoryEntry>> e : map.entrySet()) {
743N/A for (HistoryEntry ent : e.getValue()) {
743N/A ent.strip();
743N/A }
743N/A History hist = new History();
743N/A hist.setHistoryEntries(e.getValue());
743N/A File file = new File(root, e.getKey());
743N/A if (!file.isDirectory()) {
744N/A storeFile(hist, file);
743N/A }
743N/A }
743N/A }
743N/A
815N/A @Override
1466N/A public History get(File file, Repository repository, boolean withFiles,
1466N/A Boolean isDir) throws HistoryException
1466N/A {
305N/A File cache = getCachedFile(file);
678N/A if (isUpToDate(file, cache)) {
305N/A try {
305N/A return readCache(cache);
305N/A } catch (Exception e) {
1327N/A logger.warning("Error when reading cache file '" + cache
1327N/A + "': " + e.getMessage());
1327N/A logger.log(Level.FINE, "get", e);
305N/A }
305N/A }
766N/A
677N/A final History history;
305N/A long time;
305N/A try {
305N/A time = System.currentTimeMillis();
766N/A history = repository.getHistory(file);
305N/A time = System.currentTimeMillis() - time;
305N/A } catch (UnsupportedOperationException e) {
305N/A // In this case, we've found a file for which the SCM has no history
305N/A // An example is a non-SCCS file somewhere in an SCCS-controlled
305N/A // workspace.
305N/A return null;
305N/A }
305N/A
1466N/A if (isDir == null) {
1466N/A isDir = Boolean.valueOf(file.isDirectory());
1466N/A }
1466N/A if (!isDir.booleanValue()) {
1190N/A // Don't cache history-information for directories, since the
305N/A // history information on the directory may change if a file in
305N/A // a sub-directory change. This will cause us to present a stale
1190N/A // history log until a the current directory is updated and
1190N/A // invalidates the cache entry.
1470N/A Configuration cfg = RuntimeEnvironment.getConfig();
1470N/A if ((cache != null)
1470N/A && (cache.exists() || (time > cfg.getHistoryCacheTime())))
1470N/A {
615N/A // retrieving the history takes too long, cache it!
744N/A storeFile(history, file);
305N/A }
615N/A }
305N/A return history;
305N/A }
678N/A
678N/A /**
678N/A * Check if the cache is up to date for the specified file.
678N/A * @param file the file to check
678N/A * @param cachedFile the file which contains the cached history for
678N/A * the file
678N/A * @return {@code true} if the cache is up to date, {@code false} otherwise
678N/A */
1461N/A private static boolean isUpToDate(File file, File cachedFile) {
678N/A return cachedFile != null && cachedFile.exists() &&
678N/A file.lastModified() <= cachedFile.lastModified();
678N/A }
678N/A
678N/A /**
764N/A * Check if the directory is in the cache.
764N/A * @param directory the directory to check
764N/A * @return {@code true} if the directory is in the cache
678N/A */
815N/A @Override
764N/A public boolean hasCacheForDirectory(File directory, Repository repository)
715N/A throws HistoryException {
764N/A assert directory.isDirectory();
764N/A Repository repos = HistoryGuru.getInstance().getRepository(directory);
764N/A if (repos == null) {
764N/A return true;
687N/A }
1470N/A Configuration cfg = RuntimeEnvironment.getConfig();
1470N/A File dir = cfg.getDataRootFile();
764N/A dir = new File(dir, "historycache");
1016N/A try {
1470N/A dir = new File(dir, cfg
1470N/A .getPathRelativeToSourceRoot(new File(repos.getDirectoryName()), 0));
1016N/A } catch (IOException e) {
1327N/A throw new HistoryException("Could not resolve '"
1327N/A + repos.getDirectoryName() + "' relative to source root", e);
1016N/A }
764N/A return dir.exists();
678N/A }
761N/A
815N/A @Override
761N/A public String getLatestCachedRevision(Repository repository) {
761N/A return null;
761N/A }
864N/A
864N/A @Override
1474N/A public Map<String, Date> getLastModifiedTimes(File directory,
1474N/A Repository repository, Map<String,String> path2rev)
1474N/A {
1460N/A // We don't have a good way to get this information from the file
1460N/A // cache, so leave it to the caller to find a reasonable time to
1460N/A // display (typically the last modified time on the file system).
1460N/A return Collections.emptyMap();
1460N/A }
1460N/A
1460N/A @Override
1475N/A public Map<String, Date> getLastModifiedTimes(Map<String, String> path2rev)
1475N/A {
1475N/A return null;
1475N/A }
1475N/A
1475N/A @Override
969N/A public void clear(Repository repository) {
969N/A // We only expect this method to be called if the cache supports
969N/A // incremental update, so it's not implemented here for now.
969N/A throw new UnsupportedOperationException();
969N/A }
969N/A
969N/A @Override
864N/A public String getInfo() {
864N/A return getClass().getSimpleName();
864N/A }
1475N/A
305N/A}