org.sventon.service.javahl.JavaHLRepositoryService.java Source code

Java tutorial

Introduction

Here is the source code for org.sventon.service.javahl.JavaHLRepositoryService.java

Source

/*
 * ====================================================================
 * Copyright (c) 2005-2012 sventon project. All rights reserved.
 *
 * This software is licensed as described in the file LICENSE, which
 * you should have received as part of this distribution. The terms
 * are also available at http://www.sventon.org.
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 * ====================================================================
 */
package org.sventon.service.javahl;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.Validate;
import org.apache.commons.lang.mutable.MutableLong;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.sventon.*;
import org.sventon.diff.DiffException;
import org.sventon.diff.IdenticalFilesException;
import org.sventon.model.*;
import org.sventon.model.DirEntry;
import org.sventon.model.Properties;
import org.sventon.model.Revision;
import org.sventon.service.AbstractRepositoryService;
import org.tigris.subversion.javahl.*;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.*;

import static org.sventon.EncodingUtils.encodeUri;
import static org.tigris.subversion.javahl.PropertyData.*;

/**
 * JavaHLRepositoryService.
 *
 * @author jesper@sventon.org
 */
public class JavaHLRepositoryService extends AbstractRepositoryService {
    private static final String[] REV_PROP_NAMES = new String[] { REV_LOG, REV_AUTHOR, REV_DATE };

    /**
     * Logger for this class and subclasses.
     */
    final Log logger = LogFactory.getLog(getClass());

    @Override
    public LogEntry getLogEntry(SVNConnection connection, RepositoryName repositoryName, long revision)
            throws SventonException {
        final List<LogEntry> logEntries = getLogEntriesFromRepositoryRoot(connection, revision, revision);
        Validate.isTrue(logEntries.size() == 1,
                "Too many LogEntries for a given revision. One revision always relates to exactly one LogEntry");
        return logEntries.get(0);
    }

    @Override
    public List<LogEntry> getLogEntriesFromRepositoryRoot(SVNConnection connection, long fromRevision,
            long toRevision) throws SventonException {
        return getLogEntries(connection, null, fromRevision, toRevision, "/", 0, false, true);
    }

    @Override
    public List<LogEntry> getLogEntries(SVNConnection connection, RepositoryName repositoryName, long fromRevision,
            long toRevision, String path, long limit, boolean stopOnCopy, boolean includeChangedPaths)
            throws SventonException {

        return getLogEntriesInternal(connection, fromRevision, toRevision, encodeUri(path), limit, stopOnCopy,
                includeChangedPaths);
    }

    private List<LogEntry> getLogEntriesInternal(final SVNConnection connection, final long fromRevision,
            final long toRevision, final String path, final long limit, final boolean stopOnCopy,
            final boolean includeChangedPaths) throws SventonException {

        final SVNClientInterface client = (SVNClientInterface) connection.getDelegate();
        final List<LogEntry> logEntries = new ArrayList<LogEntry>();

        try {
            client.logMessages(connection.getRepositoryRootUrl().getFullPath(path),
                    JavaHLConverter.convertRevision(fromRevision),
                    JavaHLConverter.getRevisionRange(fromRevision, toRevision), stopOnCopy, includeChangedPaths,
                    false, REV_PROP_NAMES, (int) limit, new LogMessageCallback() {
                        @Override
                        public void singleMessage(ChangePath[] changePaths, long l, Map map, boolean b) {
                            final LogEntry logEntry = new LogEntry(l,
                                    JavaHLConverter.convertRevisionPropertyMap(map),
                                    JavaHLConverter.convertChangedPaths(changePaths));

                            logEntries.add(logEntry);
                        }
                    });
        } catch (ClientException e) {
            return translateException("Unable to get log entries for " + path + " at revision [" + fromRevision
                    + " .. " + toRevision + "]", e);
        }
        return logEntries;
    }

    @Override
    public void export(final SVNConnection connection, final List<PathRevision> targets, final long pegRevision,
            final File exportDirectory) throws SventonException {
        final JavaHLConnection conn = (JavaHLConnection) connection;
        final SVNClientInterface client = conn.getDelegate();

        for (final PathRevision fileRevision : targets) {
            final String path = fileRevision.getPath();
            final String encodedPath = encodeUri(path);
            final long revision = fileRevision.getRevision().getNumber();
            final File revisionRootDir = new File(exportDirectory, String.valueOf(revision));

            Validate.isTrue(revisionRootDir.exists() || revisionRootDir.mkdirs(),
                    "Unable to create directory: " + revisionRootDir.getAbsolutePath());

            try {
                final File destination = new File(revisionRootDir, encodedPath);
                final String pathToExport = conn.getRepositoryRootUrl().getFullPath(encodedPath);

                logger.debug("Exporting file [" + pathToExport + "] revision [" + revision + "]");
                client.doExport(pathToExport, destination.getAbsolutePath(),
                        org.tigris.subversion.javahl.Revision.getInstance(revision),
                        org.tigris.subversion.javahl.Revision.getInstance(pegRevision), true, false, Depth.infinity,
                        null);
            } catch (ClientException ex) {
                translateException("Error exporting [" + encodedPath + "@" + revision + "]", ex);
            }
        }
    }

    @Override
    public void getFileContents(SVNConnection connection, String path, long revision, OutputStream output)
            throws SventonException {
        final JavaHLConnection conn = (JavaHLConnection) connection;
        final SVNClientInterface client = conn.getDelegate();
        final String encodedPath = encodeUri(path);

        try {
            final byte[] bytes = client.fileContent(conn.getRepositoryRootUrl().getFullPath(encodedPath),
                    org.tigris.subversion.javahl.Revision.getInstance(revision),
                    org.tigris.subversion.javahl.Revision.getInstance(revision));
            output.write(bytes);
        } catch (IOException e) {
            throw new SventonException(e.getMessage());
        } catch (ClientException ce) {
            translateException("Unable to get file contents: " + encodedPath, ce);
        }
    }

    @Override
    public Properties listProperties(SVNConnection connection, String path, long revision) throws SventonException {
        final JavaHLConnection conn = (JavaHLConnection) connection;
        final SVNClientInterface client = conn.getDelegate();
        final String encodedPath = encodeUri(path);
        final Properties properties = new Properties();

        try {
            client.properties(conn.getRepositoryRootUrl().getFullPath(encodedPath),
                    JavaHLConverter.convertRevision(revision), JavaHLConverter.convertRevision(revision),
                    Depth.empty, null, new ProplistCallback() {
                        @Override
                        public void singlePath(String path, Map prop) {
                            for (Object o : prop.keySet()) {
                                properties.put(new Property((String) o), new PropertyValue((String) prop.get(o)));
                            }
                        }
                    });
        } catch (ClientException e) {
            return translateException("Could not get properties for [" + encodedPath + "] at revision: " + revision,
                    e);
        }
        return properties;
    }

    @Override
    public Long getLatestRevision(SVNConnection connection) throws SventonException {
        final JavaHLConnection conn = (JavaHLConnection) connection;
        final SVNClientInterface client = conn.getDelegate();

        try {
            final MutableLong revision = new MutableLong();
            client.info2(conn.getRepositoryRootUrl().toString(), org.tigris.subversion.javahl.Revision.HEAD,
                    org.tigris.subversion.javahl.Revision.HEAD, Depth.empty, null, new InfoCallback() {
                        @Override
                        public void singleInfo(Info2 info2) {
                            revision.setValue(info2.getLastChangedRev());
                        }
                    });
            return revision.toLong();
        } catch (ClientException ce) {
            return translateException("Unable to get latest revision", ce);
        }
    }

    @Override
    public DirEntry.Kind getNodeKind(SVNConnection connection, String path, long revision) throws SventonException {

        final JavaHLConnection conn = (JavaHLConnection) connection;
        final SVNClientInterface client = conn.getDelegate();
        final String encodedPath = encodeUri(path);
        final List<DirEntry.Kind> nodeKinds = new ArrayList<DirEntry.Kind>();

        try {
            client.info2(conn.getRepositoryRootUrl().getFullPath(encodedPath),
                    org.tigris.subversion.javahl.Revision.getInstance(revision),
                    org.tigris.subversion.javahl.Revision.getInstance(revision), Depth.empty, null,
                    new InfoCallback() {
                        @Override
                        public void singleInfo(Info2 info2) {
                            nodeKinds.add(JavaHLConverter.convertNodeKind(info2.getKind()));
                        }
                    });
            Validate.isTrue(nodeKinds.size() == 1,
                    "Too many nodeKinds for a given entry. One entry always relates to exactly one nodeKind");
            return nodeKinds.get(0);
        } catch (ClientException ce) {
            return translateException("Cannot get nodeKind for [" + encodedPath + "@" + revision + "]", ce);
        }
    }

    @Override
    public Map<String, DirEntryLock> getLocks(final SVNConnection connection, final String startPath,
            final boolean recursive) {
        final JavaHLConnection conn = (JavaHLConnection) connection;
        final SVNClientInterface client = conn.getDelegate();
        final HashMap<String, DirEntryLock> locks = new HashMap<String, DirEntryLock>();
        final String encodedStartPath = encodeUri(startPath);
        final MutableLong callbackCounter = new MutableLong(0);

        try {
            client.info2(conn.getRepositoryRootUrl().getFullPath(encodedStartPath),
                    org.tigris.subversion.javahl.Revision.HEAD, org.tigris.subversion.javahl.Revision.HEAD,
                    recursive ? Depth.infinity : Depth.immediates, null, new InfoCallback() {
                        @Override
                        public void singleInfo(Info2 info2) {
                            callbackCounter.increment();
                            final Lock lock = info2.getLock();
                            if (lock != null) {
                                final DirEntryLock entryLock = new DirEntryLock(lock.getToken(), lock.getPath(),
                                        lock.getOwner(), lock.getComment(), lock.getCreationDate(),
                                        lock.getExpirationDate());
                                locks.put(lock.getPath(), entryLock);
                            }
                        }
                    });
        } catch (ClientException e) {
            logger.debug("Unable to get locks for path [" + encodedStartPath + "]. Directory may not exist in HEAD",
                    e);
        }
        logger.debug("GetLocks call resulted in [" + callbackCounter + "] callbacks");
        return locks;
    }

    @Override
    public DirList list(final SVNConnection connection, final String path, final long revision)
            throws SventonException {
        final JavaHLConnection conn = (JavaHLConnection) connection;
        final SVNClientInterface client = conn.getDelegate();
        final String encodedPath = encodeUri(path);
        final List<DirEntry> dirEntries = new ArrayList<DirEntry>();

        try {
            client.list(conn.getRepositoryRootUrl().getFullPath(encodedPath),
                    org.tigris.subversion.javahl.Revision.getInstance(revision),
                    org.tigris.subversion.javahl.Revision.getInstance(revision), Depth.immediates,
                    org.tigris.subversion.javahl.DirEntry.Fields.all, false, new ListCallback() {
                        @Override
                        public void doEntry(org.tigris.subversion.javahl.DirEntry dirEntry, Lock lock) {
                            dirEntries.add(new DirEntry(path, dirEntry.getPath(), dirEntry.getLastAuthor(),
                                    dirEntry.getLastChanged(),
                                    JavaHLConverter.convertNodeKind(dirEntry.getNodeKind()),
                                    dirEntry.getLastChangedRevision().getNumber(), dirEntry.getSize()));
                        }
                    });
            // Skip the first entry as that's the one we passed in 'path'.
            return new DirList(dirEntries.subList(1, dirEntries.size()), new Properties());
        } catch (ClientException ce) {
            return translateException("Unable to list directory: " + encodedPath, ce);
        }
    }

    @Override
    public DirEntry getEntryInfo(SVNConnection connection, final String path, long revision)
            throws SventonException {
        final JavaHLConnection conn = (JavaHLConnection) connection;
        final SVNClientInterface client = conn.getDelegate();
        final String encodedPath = encodeUri(path);

        final List<DirEntry> dirEntries = new ArrayList<DirEntry>();
        try {
            client.info2(conn.getRepositoryRootUrl().getFullPath(encodedPath),
                    org.tigris.subversion.javahl.Revision.getInstance(revision),
                    org.tigris.subversion.javahl.Revision.getInstance(revision), Depth.empty, null,
                    new InfoCallback() {
                        @Override
                        public void singleInfo(Info2 info2) {
                            final String name = info2.getPath();
                            final String author = info2.getLastChangedAuthor();
                            final long lastChangedRev = info2.getLastChangedRev();
                            final Date date = info2.getLastChangedDate();
                            dirEntries.add(new DirEntry(FilenameUtils.getFullPath(path), name, author, date,
                                    JavaHLConverter.convertNodeKind(info2.getKind()), lastChangedRev,
                                    info2.getReposSize()));
                        }
                    });
            return dirEntries.get(0);
        } catch (ClientException ce) {
            return translateException("Cannot get info for [" + encodedPath + "@" + revision + "]", ce);
        }
    }

    @Override
    public List<FileRevision> getFileRevisions(SVNConnection connection, String path, long revision)
            throws SventonException {
        final String encodedPath = encodeUri(path);
        final long toRevision = Revision.FIRST.getNumber();
        final List<LogEntry> entries = getLogEntriesInternal(connection, revision, toRevision, encodedPath, 100,
                false, true);
        final List<FileRevision> revisions = new ArrayList<FileRevision>();

        LogEntry.setPathAtRevisionInLogEntries(entries, path);

        for (LogEntry entry : entries) {
            final FileRevision fileRevision = new FileRevision(entry.getPathAtRevision(),
                    Revision.create(entry.getRevision()));
            revisions.add(fileRevision);
        }

        return revisions;
    }

    @Override
    public String diffUnified(final SVNConnection connection, final PathRevision from, final PathRevision to,
            final Revision pegRevision, final String charset) throws SventonException {
        final JavaHLConnection conn = (JavaHLConnection) connection;
        final SVNClientInterface client = conn.getDelegate();

        assertNotBinary(connection, from, to, pegRevision);

        try {
            final File outFile = createTempFileForDiff();

            try {
                final String fromPath = encodeUri(conn.getRepositoryRootUrl().getFullPath(from.getPath()));
                final String toPath = encodeUri(conn.getRepositoryRootUrl().getFullPath(to.getPath()));
                final long fromRevision = getProperRevision(from.getRevision(), pegRevision).getNumber();
                final long toRevision = getProperRevision(to.getRevision(), pegRevision).getNumber();
                final org.tigris.subversion.javahl.Revision fromRev = org.tigris.subversion.javahl.Revision
                        .getInstance(fromRevision);
                final org.tigris.subversion.javahl.Revision toRev = org.tigris.subversion.javahl.Revision
                        .getInstance(toRevision);

                client.diff(fromPath, fromRev, toPath, toRev, null, outFile.getAbsolutePath(), Depth.empty, null,
                        false, false, true);
                final String diffResultString = FileUtils.readFileToString(outFile, charset);
                if ("".equals(diffResultString)) {
                    throw new IdenticalFilesException(from.getPath() + ", " + to.getPath());
                }
                return new TextFile(stripUnifiedDiffHeader(diffResultString)).getContent();
            } catch (ClientException ce) {
                return translateException("Unable to produce unified diff", ce);
            } finally {
                if (!outFile.delete()) {
                    outFile.deleteOnExit();
                }
            }
        } catch (IOException ioex) {
            throw new DiffException("Unable to produce unified diff", ioex);
        }
    }

    private String stripUnifiedDiffHeader(final String diffResult) {
        final String startMarker = "@@";
        if (!diffResult.contains(startMarker))
            return diffResult;
        return diffResult.substring(diffResult.indexOf(startMarker));
    }

    private File createTempFileForDiff() throws IOException {
        return File.createTempFile("sventon-temp", ".diff");
    }

    @Override
    public List<DiffStatus> diffPaths(final SVNConnection connection, final PathRevision from,
            final PathRevision to) throws SventonException {
        final JavaHLConnection conn = (JavaHLConnection) connection;
        final SVNClientInterface client = conn.getDelegate();

        final List<DiffStatus> result = new ArrayList<DiffStatus>();

        try {
            final String fromPath = encodeUri(conn.getRepositoryRootUrl().getFullPath(from.getPath()));
            final String toPath = encodeUri(conn.getRepositoryRootUrl().getFullPath(to.getPath()));

            final org.tigris.subversion.javahl.Revision fromRev = org.tigris.subversion.javahl.Revision
                    .getInstance(from.getRevision().getNumber());
            final org.tigris.subversion.javahl.Revision toRev = org.tigris.subversion.javahl.Revision
                    .getInstance(to.getRevision().getNumber());

            client.diffSummarize(fromPath, fromRev, toPath, toRev, Depth.infinity, null, true,
                    new DiffSummaryReceiver() {
                        @Override
                        public void onSummary(DiffSummary diffSummary) {
                            ChangeType type = null;
                            try {
                                type = ChangeType.parse(diffSummary.getDiffKind().toString());
                            } catch (IllegalArgumentException e) {
                                logger.debug("Skipping diffKind: " + diffSummary.getDiffKind());
                            }
                            result.add(new DiffStatus(type, diffSummary.getPath(), diffSummary.propsChanged()));
                        }
                    });
        } catch (ClientException ce) {
            return translateException("Could not calculate diff for [" + from + "/" + to + "]", ce);
        }
        return result;
    }

    @Override
    public AnnotatedTextFile blame(SVNConnection connection, String path, long revision, String charset,
            Colorer colorer) throws SventonException {
        final JavaHLConnection conn = (JavaHLConnection) connection;
        final SVNClientInterface client = conn.getDelegate();
        final String blamePath = encodeUri(conn.getRepositoryRootUrl().getFullPath(path));

        try {
            logger.debug("Blaming file [" + blamePath + "] revision [" + revision + "]");

            final AnnotatedTextFile annotatedTextFile = new AnnotatedTextFile(path, charset, colorer);
            final org.tigris.subversion.javahl.Revision startRev = org.tigris.subversion.javahl.Revision
                    .getInstance(0);
            final org.tigris.subversion.javahl.Revision endRev = org.tigris.subversion.javahl.Revision
                    .getInstance(revision);
            client.blame(blamePath, endRev, startRev, endRev, false, false, new BlameCallback2() {
                @Override
                public void singleLine(Date date, long revision, String author, Date mergedDate,
                        long mergedRevision, String mergedAuthor, String mergedPath, String line) {
                    annotatedTextFile.addRow(date, revision, author, line);
                }
            });

            try {
                annotatedTextFile.colorize();
            } catch (IOException ioex) {
                logger.warn("Unable to colorize [" + blamePath + "]", ioex);
            }
            return annotatedTextFile;
        } catch (ClientException ce) {
            return translateException("Error blaming [" + blamePath + "@" + revision + "]", ce);
        }
    }

    @Override
    public Revision translateRevision(SVNConnection connection, Revision revision, long headRevision)
            throws SventonException {
        final long revisionNumber = revision.getNumber();

        if (revision.isHeadRevision() || revisionNumber == headRevision) {
            return Revision.createHeadRevision(headRevision);
        }

        if (revisionNumber < 0) {
            final Date date = revision.getDate();
            if (date != null) {
                final JavaHLConnection conn = (JavaHLConnection) connection;
                final SVNClientInterface client = conn.getDelegate();
                try {
                    final MutableLong revAtDate = new MutableLong();
                    client.info2(conn.getRepositoryRootUrl().getUrl(),
                            org.tigris.subversion.javahl.Revision.getInstance(date),
                            org.tigris.subversion.javahl.Revision.getInstance(date), Depth.empty, null,
                            new InfoCallback() {
                                @Override
                                public void singleInfo(Info2 info2) {
                                    revAtDate.setValue(info2.getRev());
                                    // TODO: Is it better to look at last changed rev?
                                    //                    revAtDate.setValue(info2.getLastChangedRev());
                                }
                            });
                    return Revision.create(date, revAtDate.longValue());
                } catch (ClientException ex) {
                    return translateException("Unable to translate revision: " + revision, ex);
                }
            } else {
                logger.warn("Unexpected revision: " + revision);
                return Revision.createHeadRevision(headRevision);
            }
        }
        return Revision.create(revisionNumber);
    }

    @Override
    public List<Long> getRevisionsForPath(SVNConnection connection, String path, long fromRevision, long toRevision,
            boolean stopOnCopy, long limit) throws SventonException {
        final List<Long> list = new ArrayList<Long>();
        final String encodedPath = encodeUri(path);
        for (LogEntry entry : getLogEntriesInternal(connection, fromRevision, toRevision, encodedPath, limit,
                stopOnCopy, false)) {
            list.add(entry.getRevision());
        }
        return list;
    }

    private <T extends Object> T translateException(String errorMessage, ClientException exception)
            throws SventonException {
        // TODO: Filter exceptions here and translate to sventon specific versions of auth required etc.

        // TODO: Find a better way instead of parsing error messages!

        if (exception.getMessage().contains("Authorization failed")) {
            throw new AuthenticationException(exception.getMessage(), exception);
        }

        if (exception.getMessage().contains("non-existent in revision")) {
            throw new NoSuchRevisionException("Unable to get node kind: " + exception.getMessage());
        }

        if (exception.getMessage().contains("was not found in the repository")) {
            throw new DirEntryNotFoundException("Entry not found: " + exception.getMessage());
        }

        throw new SventonException(errorMessage, exception);
    }

}