001: /*
002: * ====================================================================
003: * Copyright (c) 2004-2008 TMate Software Ltd. All rights reserved.
004: *
005: * This software is licensed as described in the file COPYING, which
006: * you should have received as part of this distribution. The terms
007: * are also available at http://svnkit.com/license.html
008: * If newer versions of this license are posted there, you may use a
009: * newer version instead, at your option.
010: * ====================================================================
011: */
012: package org.tmatesoft.svn.core;
013:
014: import java.io.File;
015: import java.io.IOException;
016: import java.io.OutputStream;
017: import java.io.RandomAccessFile;
018: import java.io.UnsupportedEncodingException;
019: import java.util.ArrayList;
020: import java.util.Date;
021: import java.util.List;
022: import java.util.Map;
023:
024: import org.tmatesoft.svn.core.internal.util.SVNTimeUtil;
025: import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
026: import org.tmatesoft.svn.core.internal.wc.SVNEventFactory;
027: import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
028: import org.tmatesoft.svn.core.io.ISVNFileRevisionHandler;
029: import org.tmatesoft.svn.core.io.SVNFileRevision;
030: import org.tmatesoft.svn.core.io.diff.SVNDeltaProcessor;
031: import org.tmatesoft.svn.core.io.diff.SVNDiffWindow;
032: import org.tmatesoft.svn.core.wc.ISVNAnnotateHandler;
033: import org.tmatesoft.svn.core.wc.ISVNEventHandler;
034: import org.tmatesoft.svn.core.wc.SVNDiffOptions;
035: import org.tmatesoft.svn.core.wc.SVNEvent;
036:
037: import de.regnis.q.sequence.QSequenceDifferenceBlock;
038: import de.regnis.q.sequence.line.QSequenceLine;
039: import de.regnis.q.sequence.line.QSequenceLineCache;
040: import de.regnis.q.sequence.line.QSequenceLineMedia;
041: import de.regnis.q.sequence.line.QSequenceLineRAFileData;
042: import de.regnis.q.sequence.line.QSequenceLineResult;
043: import de.regnis.q.sequence.line.simplifier.QSequenceLineDummySimplifier;
044: import de.regnis.q.sequence.line.simplifier.QSequenceLineEOLUnifyingSimplifier;
045: import de.regnis.q.sequence.line.simplifier.QSequenceLineSimplifier;
046: import de.regnis.q.sequence.line.simplifier.QSequenceLineTeeSimplifier;
047: import de.regnis.q.sequence.line.simplifier.QSequenceLineWhiteSpaceReducingSimplifier;
048: import de.regnis.q.sequence.line.simplifier.QSequenceLineWhiteSpaceSkippingSimplifier;
049:
050: /**
051: * The <b>SVNAnnotationGenerator</b> class is used to annotate files - that is
052: * to place author and revision information in-line for the specified
053: * file.
054: *
055: * <p>
056: * Since <b>SVNAnnotationGenerator</b> implements <b>ISVNFileRevisionHandler</b>,
057: * it is merely passed to a {@link org.tmatesoft.svn.core.io.SVNRepository#getFileRevisions(String, long, long, ISVNFileRevisionHandler) getFileRevisions()}
058: * method of <b>SVNRepository</b>. After that you handle the resultant annotated
059: * file line-by-line providing an <b>ISVNAnnotateHandler</b> implementation to the {@link #reportAnnotations(ISVNAnnotateHandler, String) reportAnnotations()}
060: * method:
061: * <pre class="javacode">
062: * <span class="javakeyword">import</span> org.tmatesoft.svn.core.SVNAnnotationGenerator;
063: * <span class="javakeyword">import</span> org.tmatesoft.svn.core.io.SVNRepositoryFactory;
064: * <span class="javakeyword">import</span> org.tmatesoft.svn.core.io.SVNRepository;
065: * <span class="javakeyword">import</span> org.tmatesoft.svn.core.wc.SVNAnnotateHandler;
066: * ...
067: *
068: * File tmpFile;
069: * SVNRepository repos;
070: * ISVNAnnotateHandler annotateHandler;
071: * ISVNEventHandler cancelHandler;
072: * <span class="javakeyword">long</span> startRev = 0;
073: * <span class="javakeyword">long</span> endRev = 150;
074: * ...
075: *
076: * SVNAnnotationGenerator generator = <span class="javakeyword">new</span> SVNAnnotationGenerator(path, tmpFile, cancelHandler);
077: * <span class="javakeyword">try</span> {
078: * repos.getFileRevisions(<span class="javastring">""</span>, startRev, endRev, generator);
079: * generator.reportAnnotations(annotateHandler, <span class="javakeyword">null</span>);
080: * } <span class="javakeyword">finally</span> {
081: * generator.dispose();
082: * }
083: * ...</pre>
084: *
085: * @version 1.1.1
086: * @author TMate Software Ltd.
087: */
088: public class SVNAnnotationGenerator implements ISVNFileRevisionHandler {
089:
090: private File myTmpDirectory;
091: private String myPath;
092:
093: private long myCurrentRevision;
094: private String myCurrentAuthor;
095: private Date myCurrentDate;
096:
097: private File myPreviousFile;
098: private File myCurrentFile;
099:
100: private List myLines;
101: private SVNDeltaProcessor myDeltaProcessor;
102: private ISVNEventHandler myCancelBaton;
103: private long myStartRevision;
104: private boolean myIsForce;
105: private SVNDiffOptions myDiffOptions;
106: private QSequenceLineSimplifier mySimplifier;
107:
108: /**
109: * Constructs an annotation generator object.
110: * <p>
111: * This constructor is equivalent to
112: * <code>SVNAnnotationGenerator(path, tmpDirectory, startRevision, false, cancelBaton)</code>.
113: *
114: * @param path a file path (relative to a repository location)
115: * @param tmpDirectory a revision to stop at
116: * @param startRevision a start revision to begin annotation with
117: * @param cancelBaton a baton which is used to check if an operation
118: * is cancelled
119: */
120: public SVNAnnotationGenerator(String path, File tmpDirectory,
121: long startRevision, ISVNEventHandler cancelBaton) {
122: this (path, tmpDirectory, startRevision, false, cancelBaton);
123:
124: }
125:
126: /**
127: * Constructs an annotation generator object.
128: *
129: * @param path a file path (relative to a repository location)
130: * @param tmpDirectory a revision to stop at
131: * @param startRevision a start revision to begin annotation with
132: * @param force forces binary files processing
133: * @param cancelBaton a baton which is used to check if an operation
134: * is cancelled
135: */
136: public SVNAnnotationGenerator(String path, File tmpDirectory,
137: long startRevision, boolean force,
138: ISVNEventHandler cancelBaton) {
139: this (path, tmpDirectory, startRevision, force,
140: new SVNDiffOptions(), cancelBaton);
141: }
142:
143: /**
144: * Constructs an annotation generator object.
145: *
146: * @param path a file path (relative to a repository location)
147: * @param tmpDirectory a revision to stop at
148: * @param startRevision a start revision to begin annotation with
149: * @param force forces binary files processing
150: * @param diffOptions diff options
151: * @param cancelBaton a baton which is used to check if an operation
152: * is cancelled
153: */
154: public SVNAnnotationGenerator(String path, File tmpDirectory,
155: long startRevision, boolean force,
156: SVNDiffOptions diffOptions, ISVNEventHandler cancelBaton) {
157: myTmpDirectory = tmpDirectory;
158: myCancelBaton = cancelBaton;
159: myPath = path;
160: myIsForce = force;
161: if (!myTmpDirectory.isDirectory()) {
162: myTmpDirectory.mkdirs();
163: }
164: myLines = new ArrayList();
165: myDeltaProcessor = new SVNDeltaProcessor();
166: myStartRevision = startRevision;
167: myDiffOptions = diffOptions;
168: }
169:
170: /**
171: *
172: * @param fileRevision
173: * @throws SVNException if one of the following occurs:
174: * <ul>
175: * <li>the file is binary (not text)
176: * <li>operation is cancelled
177: * </ul>
178: */
179: public void openRevision(SVNFileRevision fileRevision)
180: throws SVNException {
181: Map propDiff = fileRevision.getPropertiesDelta();
182: String newMimeType = (String) (propDiff != null ? propDiff
183: .get(SVNProperty.MIME_TYPE) : null);
184: if (!myIsForce && SVNProperty.isBinaryMimeType(newMimeType)) {
185: SVNErrorMessage err = SVNErrorMessage
186: .create(
187: SVNErrorCode.CLIENT_IS_BINARY_FILE,
188: "Cannot calculate blame information for binary file ''{0}''",
189: myPath);
190: SVNErrorManager.error(err);
191: }
192: myCurrentRevision = fileRevision.getRevision();
193: boolean known = fileRevision.getRevision() >= myStartRevision;
194: if (myCancelBaton != null) {
195: SVNEvent event = SVNEventFactory.createAnnotateEvent(
196: myPath, myCurrentRevision);
197: myCancelBaton.handleEvent(event, ISVNEventHandler.UNKNOWN);
198: myCancelBaton.checkCancelled();
199: }
200: Map props = fileRevision.getRevisionProperties();
201: if (known && props != null
202: && props.get(SVNRevisionProperty.AUTHOR) != null) {
203: myCurrentAuthor = props.get(SVNRevisionProperty.AUTHOR)
204: .toString();
205: } else {
206: myCurrentAuthor = null;
207: }
208: if (known && props != null
209: && props.get(SVNRevisionProperty.DATE) != null) {
210: myCurrentDate = SVNTimeUtil.parseDate(fileRevision
211: .getRevisionProperties().get(
212: SVNRevisionProperty.DATE).toString());
213: } else {
214: myCurrentDate = null;
215: }
216: if (myPreviousFile == null) {
217: // create previous file.
218: myPreviousFile = SVNFileUtil.createUniqueFile(
219: myTmpDirectory, "annotate", ".tmp");
220: SVNFileUtil.createEmptyFile(myPreviousFile);
221: }
222: }
223:
224: /**
225: * Does nothing.
226: *
227: * @param token
228: * @throws SVNException
229: */
230: public void closeRevision(String token) throws SVNException {
231: }
232:
233: public void applyTextDelta(String token, String baseChecksum)
234: throws SVNException {
235: if (myCurrentFile != null) {
236: myCurrentFile.delete();
237: } else {
238: myCurrentFile = SVNFileUtil.createUniqueFile(
239: myTmpDirectory, "annotate", ".tmp");
240: ;
241: }
242: myDeltaProcessor.applyTextDelta(myPreviousFile, myCurrentFile,
243: false);
244: }
245:
246: public OutputStream textDeltaChunk(String token,
247: SVNDiffWindow diffWindow) throws SVNException {
248: return myDeltaProcessor.textDeltaChunk(diffWindow);
249: }
250:
251: public void textDeltaEnd(String token) throws SVNException {
252: myDeltaProcessor.textDeltaEnd();
253:
254: RandomAccessFile left = null;
255: RandomAccessFile right = null;
256: try {
257: left = new RandomAccessFile(myPreviousFile, "r");
258: right = new RandomAccessFile(myCurrentFile, "r");
259:
260: ArrayList newLines = new ArrayList();
261: int oldStart = 0;
262: int newStart = 0;
263:
264: final QSequenceLineResult result = QSequenceLineMedia
265: .createBlocks(new QSequenceLineRAFileData(left),
266: new QSequenceLineRAFileData(right),
267: createSimplifier());
268: try {
269: List blocksList = result.getBlocks();
270: for (int i = 0; i < blocksList.size(); i++) {
271: QSequenceDifferenceBlock block = (QSequenceDifferenceBlock) blocksList
272: .get(i);
273: copyOldLinesToNewLines(oldStart, newStart, block
274: .getLeftFrom()
275: - oldStart, myLines, newLines, result
276: .getRightCache());
277: // copy all from right.
278: for (int j = block.getRightFrom(); j <= block
279: .getRightTo(); j++) {
280: LineInfo line = new LineInfo();
281: line.revision = myCurrentDate != null ? myCurrentRevision
282: : -1;
283: line.author = myCurrentAuthor;
284: line.line = result.getRightCache().getLine(j)
285: .getContentBytes();
286: line.date = myCurrentDate;
287: newLines.add(line);
288: }
289: oldStart = block.getLeftTo() + 1;
290: newStart = block.getRightTo() + 1;
291: }
292: copyOldLinesToNewLines(oldStart, newStart, myLines
293: .size()
294: - oldStart, myLines, newLines, result
295: .getRightCache());
296: myLines = newLines;
297: } finally {
298: result.close();
299: }
300: } catch (Throwable e) {
301: SVNErrorMessage err = SVNErrorMessage.create(
302: SVNErrorCode.UNKNOWN,
303: "Exception while generating annotation: {0}", e
304: .getMessage());
305: SVNErrorManager.error(err, e);
306: } finally {
307: if (left != null) {
308: try {
309: left.close();
310: } catch (IOException e) {
311: //
312: }
313: }
314: if (right != null) {
315: try {
316: right.close();
317: } catch (IOException e) {
318: //
319: }
320: }
321: }
322: SVNFileUtil.rename(myCurrentFile, myPreviousFile);
323: }
324:
325: private static void copyOldLinesToNewLines(int oldStart,
326: int newStart, int count, List oldLines, List newLines,
327: QSequenceLineCache newCache) throws IOException {
328: for (int index = 0; index < count; index++) {
329: final LineInfo line = (LineInfo) oldLines.get(oldStart
330: + index);
331: final QSequenceLine sequenceLine = newCache
332: .getLine(newStart + index);
333: line.line = sequenceLine.getContentBytes();
334:
335: newLines.add(line);
336: }
337: }
338:
339: /**
340: * Dispatches file lines along with author & revision info to the provided
341: * annotation handler.
342: *
343: * <p>
344: * If <code>inputEncoding</code> is <span class="javakeyword">null</span> then
345: * <span class="javastring">"file.encoding"</span> system property is used.
346: *
347: * @param handler an annotation handler that processes file lines with
348: * author & revision info
349: * @param inputEncoding a desired character set (encoding) of text lines
350: * @throws SVNException
351: */
352: public void reportAnnotations(ISVNAnnotateHandler handler,
353: String inputEncoding) throws SVNException {
354: if (myLines == null || handler == null) {
355: return;
356: }
357: inputEncoding = inputEncoding == null ? System
358: .getProperty("file.encoding") : inputEncoding;
359: for (int i = 0; i < myLines.size(); i++) {
360: LineInfo info = (LineInfo) myLines.get(i);
361: String lineAsString;
362: byte[] bytes = info.line;
363: int length = bytes.length;
364: if (bytes.length >= 2 && bytes[length - 2] == '\r'
365: && bytes[length - 1] == '\n') {
366: length -= 2;
367: } else if (bytes.length >= 1
368: && (bytes[length - 1] == '\r' || bytes[length - 1] == '\n')) {
369: length -= 1;
370: }
371: try {
372: lineAsString = new String(info.line, 0, length,
373: inputEncoding);
374: } catch (UnsupportedEncodingException e) {
375: lineAsString = new String(info.line, 0, length);
376: }
377: handler.handleLine(info.date, info.revision, info.author,
378: lineAsString);
379: }
380: }
381:
382: /**
383: * Finalizes an annotation operation releasing resources involved
384: * by this generator. Should be called after {@link #reportAnnotations(ISVNAnnotateHandler, String) reportAnnotations()}.
385: *
386: */
387: public void dispose() {
388: myLines = null;
389: if (myCurrentFile != null) {
390: myCurrentFile.delete();
391: }
392: if (myPreviousFile != null) {
393: myPreviousFile.delete();
394: }
395: }
396:
397: private QSequenceLineSimplifier createSimplifier() {
398: if (mySimplifier == null) {
399: QSequenceLineSimplifier first = myDiffOptions
400: .isIgnoreEOLStyle() ? (QSequenceLineSimplifier) new QSequenceLineEOLUnifyingSimplifier()
401: : (QSequenceLineSimplifier) new QSequenceLineDummySimplifier();
402: QSequenceLineSimplifier second = new QSequenceLineDummySimplifier();
403: if (myDiffOptions.isIgnoreAllWhitespace()) {
404: second = new QSequenceLineWhiteSpaceSkippingSimplifier();
405: } else if (myDiffOptions.isIgnoreAmountOfWhitespace()) {
406: second = new QSequenceLineWhiteSpaceReducingSimplifier();
407: }
408: mySimplifier = new QSequenceLineTeeSimplifier(first, second);
409: }
410: return mySimplifier;
411: }
412:
413: private static class LineInfo {
414: public byte[] line;
415: public long revision;
416: public String author;
417: public Date date;
418: }
419:
420: }
|