/*
* 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) 2008, 2013, Oracle and/or its affiliates. All rights reserved.
*/
package org.opensolaris.opengrok.history;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.regex.Pattern;
import org.opensolaris.opengrok.OpenGrokLogger;
import org.opensolaris.opengrok.util.Executor;
import org.opensolaris.opengrok.util.IOUtils;
/**
* Access to a ClearCase repository.
*
*/
public class ClearCaseRepository extends Repository {
private static final long serialVersionUID = 1L;
/** The property name used to obtain the client command for this repository. */
public static final String CMD_PROPERTY_KEY =
"org.opensolaris.opengrok.history.ClearCase";
/** The command to use to access the repository if none was given explicitly */
public static final String CMD_FALLBACK = "cleartool";
private boolean verbose;
public ClearCaseRepository() {
type = "ClearCase";
datePattern = "yyyyMMdd.HHmmss";
}
/**
* Use verbose log messages, or just the summary
* @return true if verbose log messages are used for this repository
*/
public boolean isVerbose() {
return verbose;
}
/**
* Specify if verbose log messages or just the summary should be used
* @param verbose set to true if verbose messages should be used
*/
public void setVerbose(boolean verbose) {
this.verbose = verbose;
}
/**
* Get an executor to be used for retrieving the history log for the
* named file.
*
* @param file The file to retrieve history for
* @return An Executor ready to be started
*/
Executor getHistoryLogExecutor(final File file) throws IOException {
String abs = file.getCanonicalPath();
String filename = "";
if (abs.length() > directoryName.length()) {
filename = abs.substring(directoryName.length() + 1);
}
List<String> cmd = new ArrayList<String>();
ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
cmd.add(this.cmd);
cmd.add("lshistory");
if (file.isDirectory()) {
cmd.add("-dir");
}
cmd.add("-fmt");
cmd.add("%e\n%Nd\n%Fu (%u)\n%Vn\n%Nc\n.\n");
cmd.add(filename);
return new Executor(cmd, new File(getDirectoryName()));
}
@Override
public InputStream getHistoryGet(String parent, String basename, String rev)
{
InputStream ret = null;
File directory = new File(directoryName);
Process process = null;
try {
String filename = (new File(parent, basename)).getCanonicalPath()
.substring(directoryName.length() + 1);
final File tmp = File.createTempFile("opengrok", "tmp");
String tmpName = tmp.getCanonicalPath();
// cleartool can't get to a previously existing file
if (tmp.exists() && !tmp.delete()) {
OpenGrokLogger.getLogger().log(Level.WARNING,
"Failed to remove temporary file used by history cache");
}
String decorated = filename + "@@" + rev;
ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
String argv[] = {cmd, "get", "-to", tmpName, decorated};
process = Runtime.getRuntime().exec(argv, null, directory);
drainStream(process.getInputStream());
if(waitFor(process) != 0) {
return null;
}
ret = new BufferedInputStream(new FileInputStream(tmp)) {
@Override
public void close() throws IOException {
super.close();
// delete the temporary file on close
if (!tmp.delete()) {
// failed, lets do the next best thing then ..
// delete it on JVM exit
tmp.deleteOnExit();
}
}
};
} catch (Exception exp) {
OpenGrokLogger.getLogger().log(Level.SEVERE,
"Failed to get history: " + exp.getClass().toString(), exp);
} finally {
// Clean up zombie-processes...
if (process != null) {
try {
process.exitValue();
} catch (IllegalThreadStateException exp) {
// the process is still running??? just kill it..
process.destroy();
}
}
}
return ret;
}
/**
* Drain all data from a stream and close it.
* @param in the stream to drain
* @throws IOException if an I/O error occurs
*/
private static void drainStream(InputStream in) throws IOException {
while (true) {
long skipped = 0;
try {
skipped = in.skip(32768L);
} catch (IOException ioe) {
// ignored - stream isn't seekable, but skipped variable still
// has correct value.
OpenGrokLogger.getLogger().log(Level.FINEST,
"Stream not seekable", ioe);
}
if (skipped == 0 && in.read() == -1) {
// No bytes skipped, checked that we've reached EOF with read()
break;
}
}
IOUtils.close(in);
}
/**
* Annotate the specified file/revision.
*
* @param file file to annotate
* @param revision revision to annotate
* @return file annotation
*/
@Override
public Annotation annotate(File file, String revision) throws IOException {
ArrayList<String> argv = new ArrayList<String>();
ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
argv.add(cmd);
argv.add("annotate");
argv.add("-nheader");
argv.add("-out");
argv.add("-");
argv.add("-f");
argv.add("-fmt");
argv.add("%u|%Vn|");
if (revision != null) {
argv.add(revision);
}
argv.add(file.getName());
ProcessBuilder pb = new ProcessBuilder(argv);
pb.directory(file.getParentFile());
Process process = null;
try {
process = pb.start();
Annotation a = new Annotation(file.getName());
String line;
try (BufferedReader in = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
while ((line = in.readLine()) != null) {
String parts[] = line.split("\\|");
String aAuthor = parts[0];
String aRevision = parts[1];
aRevision = aRevision.replace('\\', '/');
a.addLine(aRevision, aAuthor, true);
}
}
return a;
} finally {
if (process != null) {
try {
process.exitValue();
} catch (IllegalThreadStateException e) {
process.destroy();
}
}
}
}
@Override
public boolean fileHasAnnotation(File file) {
return true;
}
private int waitFor(Process process) {
do {
try {
return process.waitFor();
} catch (InterruptedException exp) {
}
} while (true);
}
@SuppressWarnings("PMD.EmptyWhileStmt")
@Override
public void update() throws IOException {
Process process = null;
try {
File directory = new File(getDirectoryName());
// Check if this is a snapshot view
ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
String[] argv = {cmd, "catcs"};
process = Runtime.getRuntime().exec(argv, null, directory);
boolean snapshot = false;
String line;
try (BufferedReader in = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
while (!snapshot && (line = in.readLine()) != null) {
snapshot = line.startsWith("load");
}
if (waitFor(process) != 0) {
return;
}
}
if (snapshot) {
// It is a snapshot view, we need to update it manually
ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
argv = new String[]{cmd, "update", "-overwrite", "-f"};
process = Runtime.getRuntime().exec(argv, null, directory);
try (BufferedReader in = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
while ((line = in.readLine()) != null) {
// do nothing
}
}
if (waitFor(process) != 0) {
return;
}
}
} finally {
if (process != null) {
try {
process.exitValue();
} catch (IllegalThreadStateException e) {
process.destroy();
}
}
}
}
@Override
public boolean fileHasHistory(File file) {
// Todo: is there a cheap test for whether ClearCase has history
// available for a file?
// Otherwise, this is harmless, since ClearCase's commands will just
// print nothing if there is no history.
return true;
}
@Override
public boolean isWorking() {
if (working == null) {
ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
working = checkCmd(cmd, "-version");
}
return working.booleanValue();
}
@Override
boolean isRepositoryFor(File file) {
// if the parent contains a file named "view.dat" or
// the parent is named "vobs" or the canonical path
// is found in "cleartool lsvob -s"
File f = new File(file, "view.dat");
if (f.exists()) {
return true;
} else if (file.isDirectory() && file.getName().equalsIgnoreCase("vobs")) {
return true;
} else if (isWorking()) {
try {
String canonicalPath = file.getCanonicalPath();
for (String vob : getAllVobs()) {
if (canonicalPath.equalsIgnoreCase(vob)) {
return true;
}
}
} catch (IOException e) {
OpenGrokLogger.getLogger().log(Level.WARNING,
"Could not get canonical path for \""+file+"\"", e);
}
}
return false;
}
private static class VobsHolder {
public static String[] vobs = runLsvob();
}
private static String[] getAllVobs() {
return VobsHolder.vobs;
}
private static final ClearCaseRepository testRepo =
new ClearCaseRepository();
private static String[] runLsvob() {
if (testRepo.isWorking()) {
Executor exec = new Executor(new String[] {testRepo.cmd, "lsvob", "-s"});
int rc;
if ((rc = exec.exec(true)) == 0) {
String output = exec.getOutputString();
if (output == null) {
OpenGrokLogger.getLogger().log(Level.SEVERE,
"\"cleartool lsvob -s\" output was null");
return new String[0];
}
String sep = System.getProperty("line.separator");
String[] vobs = output.split(Pattern.quote(sep));
OpenGrokLogger.getLogger().log(Level.CONFIG, "Found VOBs: {0}",
Arrays.asList(vobs));
return vobs;
}
OpenGrokLogger.getLogger().log(Level.SEVERE,
"\"cleartool lsvob -s\" returned non-zero status: " + rc);
}
return new String[0];
}
@Override
boolean hasHistoryForDirectories() {
return true;
}
@Override
History getHistory(File file) throws HistoryException {
return new ClearCaseHistoryParser().parse(file, this);
}
}