366N/A/*
366N/A * CDDL HEADER START
366N/A *
366N/A * The contents of this file are subject to the terms of the
911N/A * Common Development and Distribution License (the "License").
366N/A * You may not use this file except in compliance with the License.
366N/A *
366N/A * See LICENSE.txt included in this distribution for the specific
366N/A * language governing permissions and limitations under the License.
366N/A *
366N/A * When distributing Covered Code, include this CDDL HEADER in each
366N/A * file and include the License file at LICENSE.txt.
366N/A * If applicable, add the following below this CDDL HEADER, with the
366N/A * fields enclosed by brackets "[]" replaced with your own identifying
366N/A * information: Portions Copyright [yyyy] [name of copyright owner]
366N/A *
366N/A * CDDL HEADER END
366N/A */
366N/A
366N/A/*
366N/A * Copyright (c) 2007, 2011, Oracle and/or its affiliates. All rights reserved.
366N/A */
366N/A
366N/Apackage org.opensolaris.opengrok.history;
366N/A
366N/Aimport java.io.BufferedInputStream;
366N/Aimport java.io.File;
366N/Aimport java.io.IOException;
366N/Aimport java.io.InputStream;
366N/Aimport java.util.ArrayList;
366N/Aimport java.util.List;
366N/Aimport java.util.logging.Level;
366N/Aimport java.util.logging.Logger;
366N/A
366N/Aimport javax.xml.parsers.DocumentBuilder;
493N/Aimport javax.xml.parsers.DocumentBuilderFactory;
366N/Aimport javax.xml.parsers.ParserConfigurationException;
366N/Aimport javax.xml.parsers.SAXParser;
493N/Aimport javax.xml.parsers.SAXParserFactory;
366N/A
911N/Aimport org.opensolaris.opengrok.configuration.Configuration;
911N/Aimport org.opensolaris.opengrok.util.Executor;
911N/Aimport org.opensolaris.opengrok.util.IOUtils;
911N/Aimport org.w3c.dom.Document;
366N/Aimport org.w3c.dom.Node;
366N/Aimport org.xml.sax.Attributes;
366N/Aimport org.xml.sax.SAXException;
366N/Aimport org.xml.sax.ext.DefaultHandler2;
366N/A
366N/A/**
366N/A * Access to a Subversion repository.
366N/A *
366N/A * <b>TODO</b> The current implementation does <b>not</b> support nested
493N/A * repositories as described in http://svnbook.red-bean.com/en/1.0/ch07s03.html
366N/A *
366N/A * @author Trond Norbye
493N/A */
366N/Apublic class SubversionRepository extends Repository {
366N/A private static final long serialVersionUID = 1L;
366N/A /** The property name used to obtain the client command for this repository. */
366N/A public static final String CMD_PROPERTY_KEY =
366N/A Configuration.PROPERTY_KEY_PREFIX + "history.Subversion";
493N/A /** The command to use to access the repository if none was given explicitly */
366N/A public static final String CMD_FALLBACK = "svn";
366N/A /** The system property name to obtain the subversion user configuration
* directory to use, i.e. the value for the svn option --config-dir. Gets
* ignored, if not set.
*/
public static final String CONFIG_DIRECTORY_KEY =
Configuration.PROPERTY_KEY_PREFIX + "history.Subversion.configdir";
private static final Logger logger =
Logger.getLogger(SubversionRepository.class.getName());
/**
* Pointer to the repository path. Is {@code null} until
* {@link #setDirectoryName(String)} got called.
*/
protected String reposPath;
private String configDir;
/**
* Create a new instance of type {@code Subversion}.
*/
public SubversionRepository() {
type = "Subversion";
datePattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
}
private static String getValue(Node node) {
if (node == null) {
return null;
}
StringBuffer sb = new StringBuffer();
Node n = node.getFirstChild();
while (n != null) {
if (n.getNodeType() == Node.TEXT_NODE) {
sb.append(n.getNodeValue());
}
n = n.getNextSibling();
}
return sb.toString();
}
/**
* {@inheritDoc}
*/
@Override
public void setDirectoryName(String directoryName) {
super.setDirectoryName(directoryName);
if (isWorking()) {
// set to true if we manage to find the root directory
Boolean rootFound = Boolean.FALSE;
List<String> cmd = new ArrayList<String>();
cmd.add(this.cmd);
cmd.add("info");
if (configDir != null) {
cmd.add(configDir);
}
cmd.add("--xml");
File directory = new File(getDirectoryName());
Executor executor = new Executor(cmd, directory);
if (executor.exec() == 0) {
try {
DocumentBuilderFactory factory =
DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(executor.getOutputStream());
String url =
getValue(document.getElementsByTagName("url").item(0));
if (url == null) {
logger.warning("svn info did not contain an URL for '"
+ directoryName + "'. Assuming remote repository.");
setRemote(true);
} else {
if (!url.startsWith("file")) {
setRemote(true);
}
}
String root =
getValue(document.getElementsByTagName("root").item(0));
if (url != null && root != null) {
reposPath = url.substring(root.length());
rootFound = Boolean.TRUE;
}
} catch (SAXException saxe) {
logger.warning("Parser error parsing svn output: "
+ saxe.getMessage());
logger.log(Level.FINE, "setDirectoryName", saxe);
} catch (ParserConfigurationException pce) {
logger.warning("Parser configuration error parsing svn output: "
+ pce.getMessage());
logger.log(Level.FINE, "setDirectoryName", pce);
} catch (IOException ioe) {
logger.warning("IOException reading from svn process: "
+ ioe.getMessage());
logger.log(Level.FINE, "setDirectoryName", ioe);
}
} else {
logger.warning("Failed to execute svn info for '"
+ directoryName + "'. Repository disabled.");
}
setWorking(rootFound);
}
}
/**
* Get an executor to be used for retrieving the history log for the
* named file.
*
* @param file The file to retrieve history for (canonical path incl. source
* root)
* @param sinceRevision the revision number immediately preceding the first
* revision we want, or {@code null} to fetch the entire history
* @return An Executor ready to be started
*/
Executor getHistoryLogExecutor(final File file, String sinceRevision) {
String abs = file.getAbsolutePath();
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("log");
cmd.add("--trust-server-cert");
cmd.add("--non-interactive");
if (configDir != null) {
cmd.add(configDir);
}
cmd.add("--xml");
cmd.add("-v");
if (sinceRevision != null) {
cmd.add("-r");
// We would like to use sinceRevision+1 here, but if no new
// revisions have been added after sinceRevision, it would fail
// because there is no such revision as sinceRevision+1. Instead,
// fetch the unneeded revision and remove it later.
cmd.add("BASE:" + sinceRevision);
}
cmd.add(escapeFileName(filename));
return new Executor(cmd, new File(directoryName));
}
/**
* {@inheritDoc}
*/
@Override
public InputStream getHistoryGet(String parent, String basename, String rev)
{
InputStream ret = null;
File directory = new File(directoryName);
String filename = (new File(parent, basename)).getAbsolutePath()
.substring(directoryName.length() + 1);
List<String> cmd = new ArrayList<String>();
ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
cmd.add(this.cmd);
cmd.add("cat");
if (configDir != null) {
cmd.add(configDir);
}
cmd.add("-r");
cmd.add(rev);
cmd.add(escapeFileName(filename));
Executor executor = new Executor(cmd, directory);
if (executor.exec() == 0) {
ret = executor.getOutputStream();
}
return ret;
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasHistoryForDirectories() {
return true;
}
/**
* {@inheritDoc}
*/
@Override
public History getHistory(File file) throws HistoryException {
return getHistory(file, null);
}
/**
* {@inheritDoc}
*/
@Override
public History getHistory(File file, String sinceRevision)
throws HistoryException
{
return new SubversionHistoryParser().parse(file, this, sinceRevision);
}
private static String escapeFileName(String name) {
if (name.length() == 0) {
return name;
}
return name + "@";
}
private static class AnnotateHandler extends DefaultHandler2 {
String rev;
String author;
final Annotation annotation;
final StringBuilder sb;
AnnotateHandler(String filename) {
annotation = new Annotation(filename);
sb = new StringBuilder();
}
@Override
public void startElement(String uri, String localName, String qname,
Attributes attr)
{
sb.setLength(0);
if ("entry".equals(qname)) {
rev = null;
author = null;
} else if ("commit".equals(qname)) {
rev = attr.getValue("revision");
}
}
@Override
public void endElement(String uri, String localName, String qname) {
if ("author".equals(qname)) {
author = sb.toString();
} else if ("entry".equals(qname)) {
annotation.addLine(rev, author);
}
}
@Override
public void characters(char[] arg0, int arg1, int arg2) {
sb.append(arg0, arg1, arg2);
}
}
/**
* {@inheritDoc}
*/
@Override
public Annotation annotate(File file, String revision) throws IOException {
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = null;
try {
saxParser = factory.newSAXParser();
} catch (Exception ex) {
throw new IOException("Failed to create SAX parser ("
+ ex.getMessage() + ")");
}
ArrayList<String> argv = new ArrayList<String>();
ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
argv.add(cmd);
argv.add("annotate");
argv.add("--trust-server-cert");
argv.add("--non-interactive");
if (configDir != null) {
argv.add(configDir);
}
argv.add("--xml");
if (revision != null) {
argv.add("-r");
argv.add(revision);
}
argv.add(escapeFileName(file.getName()));
ProcessBuilder pb = new ProcessBuilder(argv);
pb.directory(file.getParentFile());
Process process = null;
BufferedInputStream in = null;
Annotation ret = null;
try {
process = pb.start();
in = new BufferedInputStream(process.getInputStream());
AnnotateHandler handler = new AnnotateHandler(file.getName());
try {
saxParser.parse(in, handler);
ret = handler.annotation;
} catch (Exception e) {
logger.severe("An error occurred while parsing the command output (xml): "
+ e.getMessage());
logger.log(Level.FINE, "annotate", e);
}
} finally {
IOUtils.close(in);
if (process != null) {
try {
process.exitValue();
} catch (IllegalThreadStateException e) {
// the process is still running??? just kill it..
process.destroy();
}
}
}
return ret;
}
/**
* {@inheritDoc}
*/
@Override
public boolean fileHasAnnotation(File file) {
return true;
}
/**
* {@inheritDoc}
*/
@Override
public boolean fileHasHistory(File file) {
// @TODO: Research how to cheaply test if a file in a given
// SVN repo has history. If there is a cheap test, then this
// code can be refined, boosting performance.
return true;
}
/**
* {@inheritDoc}
*/
@Override
public void update() throws IOException {
File directory = new File(getDirectoryName());
List<String> cmd = new ArrayList<String>();
ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
cmd.add(this.cmd);
cmd.add("update");
cmd.add("--trust-server-cert");
cmd.add("--non-interactive");
if (configDir != null) {
cmd.add(configDir);
}
Executor executor = new Executor(cmd, directory);
if (executor.exec() != 0) {
throw new IOException(executor.getErrorString());
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean isRepositoryFor(File file) {
if (file.isDirectory()) {
File f = new File(file, ".svn");
return f.exists() && f.isDirectory();
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isWorking() {
if (working == null) {
ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
configDir = System.getProperty(CONFIG_DIRECTORY_KEY);
if (configDir != null) {
configDir = "--config-dir=" + configDir;
working = checkCmd(cmd, "--help", configDir);
} else {
working = checkCmd(cmd, "--help");
}
}
return working.booleanValue();
}
}