Java tutorial
/* * ==================================================================== * 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); } }