tikhomirov@112: /* tikhomirov@710: * Copyright (c) 2011-2013 TMate Software Ltd tikhomirov@112: * tikhomirov@112: * This program is free software; you can redistribute it and/or modify tikhomirov@112: * it under the terms of the GNU General Public License as published by tikhomirov@112: * the Free Software Foundation; version 2 of the License. tikhomirov@112: * tikhomirov@112: * This program is distributed in the hope that it will be useful, tikhomirov@112: * but WITHOUT ANY WARRANTY; without even the implied warranty of tikhomirov@112: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the tikhomirov@112: * GNU General Public License for more details. tikhomirov@112: * tikhomirov@112: * For information on how to redistribute this software under tikhomirov@112: * the terms of a license other than GNU General Public License tikhomirov@130: * contact TMate Software at support@hg4j.com tikhomirov@112: */ tikhomirov@112: package org.tmatesoft.hg.internal; tikhomirov@112: tikhomirov@456: import static org.tmatesoft.hg.util.LogFacility.Severity.Error; tikhomirov@456: tikhomirov@112: import java.nio.ByteBuffer; tikhomirov@114: import java.util.ArrayList; tikhomirov@354: import java.util.Date; tikhomirov@711: import java.util.Map; tikhomirov@112: import java.util.TreeMap; tikhomirov@112: tikhomirov@354: import org.tmatesoft.hg.core.Nodeid; tikhomirov@154: import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; tikhomirov@112: import org.tmatesoft.hg.repo.HgRepository; tikhomirov@423: import org.tmatesoft.hg.repo.HgRuntimeException; tikhomirov@331: import org.tmatesoft.hg.util.Pair; tikhomirov@133: import org.tmatesoft.hg.util.Path; tikhomirov@112: tikhomirov@112: /** tikhomirov@112: * tikhomirov@112: * @author Artem Tikhomirov tikhomirov@112: * @author TMate Software Ltd. tikhomirov@112: */ tikhomirov@112: public class KeywordFilter implements Filter { tikhomirov@114: private final HgRepository repo; tikhomirov@112: private final boolean isExpanding; tikhomirov@711: private final Map keywords; tikhomirov@114: private final Path path; tikhomirov@154: private RawChangeset latestFileCset; tikhomirov@711: private final ByteVector unprocessedBuffer; tikhomirov@112: tikhomirov@112: /** tikhomirov@112: * tikhomirov@114: * @param hgRepo tikhomirov@114: * @param path tikhomirov@112: * @param expand true to expand keywords, false to shrink tikhomirov@112: */ tikhomirov@711: private KeywordFilter(HgRepository hgRepo, Path p, Map kw, boolean expand) { tikhomirov@114: repo = hgRepo; tikhomirov@114: path = p; tikhomirov@112: isExpanding = expand; tikhomirov@711: keywords = kw; tikhomirov@711: unprocessedBuffer = expand ? new ByteVector(0, 0) : new ByteVector(120, 50); tikhomirov@112: } tikhomirov@112: tikhomirov@112: /** tikhomirov@112: * @param src buffer ready to be read tikhomirov@112: * @return buffer ready to be read and original buffer's position modified to reflect consumed bytes. IOW, if source buffer tikhomirov@112: * on return has remaining bytes, they are assumed not-read (not processed) and next chunk passed to filter is supposed to tikhomirov@112: * start with them tikhomirov@112: */ tikhomirov@112: public ByteBuffer filter(ByteBuffer src) { tikhomirov@711: // when unprocessedBuffer is empty, we are looking for first $ in the input, tikhomirov@711: // when we've already got anything unprocessed, newline is of interest, too tikhomirov@711: int kwBreak = indexOf(src, '$', src.position(), !unprocessedBuffer.isEmpty()); tikhomirov@711: ByteBuffer outBuffer = null; tikhomirov@711: while (kwBreak != -1) { tikhomirov@711: if (unprocessedBuffer.isEmpty()) { tikhomirov@711: // both expand and collapse cases tikhomirov@711: assert src.get(kwBreak) == '$'; tikhomirov@711: tikhomirov@711: int end = indexOf(src, '$', kwBreak+1, true); tikhomirov@711: if (end == -1) { tikhomirov@711: for (int i = kwBreak; i < src.limit(); i++) { tikhomirov@711: unprocessedBuffer.add(src.get(i)); tikhomirov@112: } tikhomirov@711: src.limit(kwBreak); tikhomirov@711: kwBreak = -1; tikhomirov@711: // src up to kwBreak is left and returned either with outBuffer or alone tikhomirov@711: } else if (src.get(end) == '$') { tikhomirov@711: StringBuilder sb = new StringBuilder(end - kwBreak); tikhomirov@711: for (int i = kwBreak+1; i < end; i++) { tikhomirov@711: if (src.get(i) == ':' || src.get(i) == ' ') { tikhomirov@711: break; tikhomirov@112: } tikhomirov@711: sb.append((char) src.get(i)); tikhomirov@112: } tikhomirov@711: final String keyword = sb.toString(); tikhomirov@711: if (knownKeyword(keyword)) { tikhomirov@711: // copy src up to kw, including starting $keyword tikhomirov@711: outBuffer = append(outBuffer, src, kwBreak - src.position() + 1+keyword.length()); tikhomirov@711: // replace kwStart..end with new content tikhomirov@711: outBuffer = ensureCapacityFor(outBuffer, (isExpanding ? 200 : 1)); tikhomirov@711: if (isExpanding) { tikhomirov@711: outBuffer.put((byte) ':'); tikhomirov@711: outBuffer.put((byte) ' '); tikhomirov@711: outBuffer = expandKeywordValue(keyword, outBuffer); tikhomirov@711: outBuffer.put((byte) ' '); tikhomirov@112: } tikhomirov@711: outBuffer.put((byte) '$'); tikhomirov@711: // src is consumed up to end tikhomirov@711: src.position(end+1); tikhomirov@711: kwBreak = indexOf(src, '$', end+1, false); tikhomirov@112: } else { tikhomirov@711: // no (or unknown) keyword, try with '$' at src[end] tikhomirov@711: kwBreak = end; tikhomirov@112: } tikhomirov@112: } else { tikhomirov@711: // newline, ignore keyword start tikhomirov@711: kwBreak = indexOf(src, '$', end+1, false); tikhomirov@711: } tikhomirov@711: } else { tikhomirov@711: // we've got smth unprocessed, and we've matched either $ or NL tikhomirov@711: // the only chance to get here is when src is in the very start tikhomirov@711: if (src.get(kwBreak) == '$') { tikhomirov@711: // closed tag tikhomirov@711: for (int i = src.position(); i <= kwBreak; i++) { tikhomirov@711: // consume src: going to handle its [position*()..kwBreak] as part of unprocessedBuffer tikhomirov@711: unprocessedBuffer.add(src.get()); tikhomirov@112: } tikhomirov@711: StringBuilder sb = new StringBuilder(unprocessedBuffer.size()); tikhomirov@711: assert unprocessedBuffer.get(0) == '$'; tikhomirov@711: for (int i = 1; i < unprocessedBuffer.size(); i++) { tikhomirov@711: char ch = (char) unprocessedBuffer.get(i); tikhomirov@711: if (ch == ':' || ch == ' ') { tikhomirov@711: break; tikhomirov@711: } tikhomirov@711: sb.append(ch); tikhomirov@711: } tikhomirov@711: final String keyword = sb.toString(); tikhomirov@711: if (knownKeyword(keyword)) { tikhomirov@711: outBuffer = ensureCapacityFor(outBuffer, keyword.length() + (isExpanding ? 200 : 2)); tikhomirov@711: outBuffer.put((byte) '$'); tikhomirov@711: outBuffer.put(keyword.getBytes()); tikhomirov@711: if (isExpanding) { tikhomirov@711: outBuffer.put((byte) ':'); tikhomirov@711: outBuffer.put((byte) ' '); tikhomirov@711: outBuffer = expandKeywordValue(keyword, outBuffer); tikhomirov@711: outBuffer.put((byte) ' '); tikhomirov@711: } tikhomirov@711: outBuffer.put((byte) '$'); tikhomirov@711: } else { tikhomirov@711: outBuffer = append(outBuffer, unprocessedBuffer.toByteArray()); tikhomirov@711: } tikhomirov@711: // src part is consumed already, do nothing here, look for next possible kw tikhomirov@711: kwBreak = indexOf(src, '$', kwBreak+1, false); tikhomirov@711: } else { tikhomirov@711: // newline => tag without close tikhomirov@711: outBuffer = append(outBuffer, unprocessedBuffer.toByteArray()); tikhomirov@711: kwBreak = indexOf(src, '$', kwBreak+1, false); tikhomirov@112: } tikhomirov@711: unprocessedBuffer.clear(); tikhomirov@112: } tikhomirov@711: } while (kwBreak != -1); tikhomirov@711: if (outBuffer == null) { tikhomirov@711: return src; tikhomirov@112: } tikhomirov@711: outBuffer = ensureCapacityFor(outBuffer, src.remaining()); tikhomirov@711: outBuffer.put(src); tikhomirov@711: outBuffer.flip(); tikhomirov@711: return outBuffer; tikhomirov@711: } tikhomirov@711: private boolean knownKeyword(String kw) { tikhomirov@711: return keywords.containsKey(kw); tikhomirov@711: } tikhomirov@711: tikhomirov@711: private static ByteBuffer append(ByteBuffer out, byte[] data) { tikhomirov@711: out = ensureCapacityFor(out, data.length); tikhomirov@711: out.put(data); tikhomirov@711: return out; tikhomirov@711: } tikhomirov@711: private static ByteBuffer append(ByteBuffer out, ByteBuffer in, int count) { tikhomirov@711: out = ensureCapacityFor(out, count); tikhomirov@711: while (count-- > 0) { tikhomirov@711: out.put(in.get()); tikhomirov@711: } tikhomirov@711: return out; tikhomirov@711: } tikhomirov@711: private static ByteBuffer ensureCapacityFor(ByteBuffer out, int exansion) { tikhomirov@711: if (out == null || out.remaining() < exansion) { tikhomirov@711: ByteBuffer newOut = ByteBuffer.allocate(out == null ? exansion*2 : out.capacity() + exansion); tikhomirov@711: if (out != null) { tikhomirov@711: out.flip(); tikhomirov@711: newOut.put(out); tikhomirov@112: } tikhomirov@711: return newOut; tikhomirov@112: } tikhomirov@711: return out; tikhomirov@112: } tikhomirov@112: tikhomirov@711: private ByteBuffer expandKeywordValue(String keyword, ByteBuffer rv) { tikhomirov@711: byte[] toInject; tikhomirov@112: if ("Id".equals(keyword)) { tikhomirov@711: toInject = identityString().getBytes(); tikhomirov@112: } else if ("Revision".equals(keyword)) { tikhomirov@711: toInject = revision().getBytes(); tikhomirov@112: } else if ("Author".equals(keyword)) { tikhomirov@711: toInject = username().getBytes(); tikhomirov@114: } else if ("Date".equals(keyword)) { tikhomirov@711: toInject = date().getBytes(); tikhomirov@114: } else { tikhomirov@114: throw new IllegalStateException(String.format("Keyword %s is not yet supported", keyword)); tikhomirov@112: } tikhomirov@711: rv = ensureCapacityFor(rv, toInject.length); tikhomirov@711: rv.put(toInject); tikhomirov@711: return rv; tikhomirov@112: } tikhomirov@112: tikhomirov@112: // copies part of the src buffer, [from..to). doesn't modify src position tikhomirov@113: static void copySlice(ByteBuffer src, int from, int to, ByteBuffer dst) { tikhomirov@112: if (to > src.limit()) { tikhomirov@112: throw new IllegalArgumentException("Bad right boundary"); tikhomirov@112: } tikhomirov@112: if (dst.remaining() < to - from) { tikhomirov@112: throw new IllegalArgumentException("Not enough room in the destination buffer"); tikhomirov@112: } tikhomirov@112: for (int i = from; i < to; i++) { tikhomirov@112: dst.put(src.get(i)); tikhomirov@112: } tikhomirov@112: } tikhomirov@112: tikhomirov@112: private static int indexOf(ByteBuffer b, char ch, int from, boolean newlineBreaks) { tikhomirov@113: for (int i = from; i < b.limit(); i++) { tikhomirov@112: byte c = b.get(i); tikhomirov@112: if (ch == c) { tikhomirov@112: return i; tikhomirov@112: } tikhomirov@112: if (newlineBreaks && (c == '\n' || c == '\r')) { tikhomirov@112: return i; tikhomirov@112: } tikhomirov@112: } tikhomirov@112: return -1; tikhomirov@112: } tikhomirov@112: tikhomirov@112: private String identityString() { tikhomirov@114: return String.format("%s,v %s %s %s", path, revision(), date(), username()); tikhomirov@112: } tikhomirov@112: tikhomirov@114: private String revision() { tikhomirov@354: try { tikhomirov@418: // TODO post-1.0 Either add cset's nodeid into Changeset class or use own inspector tikhomirov@418: // when accessing changelog, see below, #getChangeset tikhomirov@367: int csetRev = repo.getFileNode(path).getChangesetRevisionIndex(HgRepository.TIP); tikhomirov@354: return repo.getChangelog().getRevision(csetRev).shortNotation(); tikhomirov@423: } catch (HgRuntimeException ex) { tikhomirov@490: repo.getSessionContext().getLog().dump(getClass(), Error, ex, null); tikhomirov@354: return Nodeid.NULL.shortNotation(); // XXX perhaps, might return anything better? Not sure how hg approaches this. tikhomirov@354: } tikhomirov@112: } tikhomirov@112: tikhomirov@112: private String username() { tikhomirov@354: try { tikhomirov@354: return getChangeset().user(); tikhomirov@423: } catch (HgRuntimeException ex) { tikhomirov@490: repo.getSessionContext().getLog().dump(getClass(), Error, ex, null); tikhomirov@354: return ""; tikhomirov@354: } tikhomirov@114: } tikhomirov@114: tikhomirov@114: private String date() { tikhomirov@354: Date d; tikhomirov@354: try { tikhomirov@354: d = getChangeset().date(); tikhomirov@423: } catch (HgRuntimeException ex) { tikhomirov@490: repo.getSessionContext().getLog().dump(getClass(), Error, ex, null); tikhomirov@354: d = new Date(0l); tikhomirov@354: } tikhomirov@354: return String.format("%tY/% keywords; tikhomirov@114: private HgRepository repo; tikhomirov@114: private Path.Matcher matcher; tikhomirov@711: tikhomirov@711: public Factory() { tikhomirov@711: keywords = new TreeMap(); tikhomirov@711: keywords.put("Id", "Id"); tikhomirov@711: keywords.put("Revision", "Revision"); tikhomirov@711: keywords.put("Author", "Author"); tikhomirov@711: keywords.put("Date", "Date"); tikhomirov@711: keywords.put("LastChangedRevision", "LastChangedRevision"); tikhomirov@711: keywords.put("LastChangedBy", "LastChangedBy"); tikhomirov@711: keywords.put("LastChangedDate", "LastChangedDate"); tikhomirov@711: keywords.put("Source", "Source"); tikhomirov@711: keywords.put("Header", "Header"); tikhomirov@711: } tikhomirov@112: tikhomirov@331: public void initialize(HgRepository hgRepo) { tikhomirov@114: repo = hgRepo; tikhomirov@114: ArrayList patterns = new ArrayList(); tikhomirov@331: for (Pair e : hgRepo.getConfiguration().getSection("keyword")) { tikhomirov@331: if (!"ignore".equalsIgnoreCase(e.second())) { tikhomirov@331: patterns.add(e.first()); tikhomirov@114: } tikhomirov@114: } tikhomirov@114: matcher = new PathGlobMatcher(patterns.toArray(new String[patterns.size()])); tikhomirov@423: // TODO post-1.0 read and respect keyword patterns from [keywordmaps] tikhomirov@114: } tikhomirov@114: tikhomirov@114: public Filter create(Path path, Options opts) { tikhomirov@114: if (matcher.accept(path)) { tikhomirov@711: return new KeywordFilter(repo, path, keywords, opts.getDirection() == Filter.Direction.FromRepo); tikhomirov@114: } tikhomirov@114: return null; tikhomirov@112: } tikhomirov@112: } tikhomirov@112: tikhomirov@114: // tikhomirov@114: // public static void main(String[] args) throws Exception { tikhomirov@114: // FileInputStream fis = new FileInputStream(new File("/temp/kwoutput.txt")); tikhomirov@114: // FileOutputStream fos = new FileOutputStream(new File("/temp/kwoutput2.txt")); tikhomirov@114: // ByteBuffer b = ByteBuffer.allocate(256); tikhomirov@114: // KeywordFilter kwFilter = new KeywordFilter(false); tikhomirov@114: // while (fis.getChannel().read(b) != -1) { tikhomirov@114: // b.flip(); // get ready to be read tikhomirov@114: // ByteBuffer f = kwFilter.filter(b); tikhomirov@114: // fos.getChannel().write(f); // XXX in fact, f may not be fully consumed tikhomirov@114: // if (b.hasRemaining()) { tikhomirov@114: // b.compact(); tikhomirov@114: // } else { tikhomirov@114: // b.clear(); tikhomirov@114: // } tikhomirov@114: // } tikhomirov@114: // fis.close(); tikhomirov@114: // fos.flush(); tikhomirov@114: // fos.close(); tikhomirov@114: // } tikhomirov@112: }