tikhomirov@112: /* tikhomirov@112: * Copyright (c) 2011 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@112: import java.nio.ByteBuffer; tikhomirov@114: import java.util.ArrayList; tikhomirov@114: import java.util.Map; tikhomirov@112: import java.util.TreeMap; tikhomirov@112: tikhomirov@129: import org.tmatesoft.hg.repo.HgChangelog.Changeset; tikhomirov@112: import org.tmatesoft.hg.repo.HgRepository; 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@113: // present implementation is stateless, however, filter use pattern shall not assume that. In fact, Factory may us that tikhomirov@114: private final HgRepository repo; tikhomirov@112: private final boolean isExpanding; tikhomirov@112: private final TreeMap keywords; tikhomirov@112: private final int minBufferLen; tikhomirov@114: private final Path path; tikhomirov@114: private Changeset latestFileCset; 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@114: private KeywordFilter(HgRepository hgRepo, Path p, boolean expand) { tikhomirov@114: repo = hgRepo; tikhomirov@114: path = p; tikhomirov@112: isExpanding = expand; tikhomirov@112: keywords = new TreeMap(); tikhomirov@112: keywords.put("Id", "Id"); tikhomirov@112: keywords.put("Revision", "Revision"); tikhomirov@112: keywords.put("Author", "Author"); tikhomirov@113: keywords.put("Date", "Date"); tikhomirov@113: keywords.put("LastChangedRevision", "LastChangedRevision"); tikhomirov@113: keywords.put("LastChangedBy", "LastChangedBy"); tikhomirov@113: keywords.put("LastChangedDate", "LastChangedDate"); tikhomirov@113: keywords.put("Source", "Source"); tikhomirov@113: keywords.put("Header", "Header"); tikhomirov@113: tikhomirov@112: int l = 0; tikhomirov@112: for (String s : keywords.keySet()) { tikhomirov@112: if (s.length() > l) { tikhomirov@112: l = s.length(); tikhomirov@112: } tikhomirov@112: } tikhomirov@112: // FIXME later may implement #filter() not to read full kw value (just "$kw:"). However, limit of maxLen + 2 would keep valid. tikhomirov@112: // for buffers less then minBufferLen, there are chances #filter() implementation would never end tikhomirov@112: // (i.e. for input "$LongestKey"$ tikhomirov@112: minBufferLen = l + 2 + (isExpanding ? 0 : 120 /*any reasonable constant for max possible kw value length*/); 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@112: if (src.capacity() < minBufferLen) { tikhomirov@112: throw new IllegalStateException(String.format("Need buffer of at least %d bytes to ensure filter won't hang", minBufferLen)); tikhomirov@112: } tikhomirov@112: ByteBuffer rv = null; tikhomirov@112: int keywordStart = -1; tikhomirov@112: int x = src.position(); tikhomirov@119: int copyFrom = x; // needs to be updated each time we copy a slice, but not each time we modify source index (x) tikhomirov@112: while (x < src.limit()) { tikhomirov@112: if (keywordStart == -1) { tikhomirov@112: int i = indexOf(src, '$', x, false); tikhomirov@112: if (i == -1) { tikhomirov@112: if (rv == null) { tikhomirov@112: return src; tikhomirov@112: } else { tikhomirov@119: copySlice(src, copyFrom, src.limit(), rv); tikhomirov@112: rv.flip(); tikhomirov@112: src.position(src.limit()); tikhomirov@112: return rv; tikhomirov@112: } tikhomirov@112: } tikhomirov@112: keywordStart = i; tikhomirov@112: // fall-through tikhomirov@112: } tikhomirov@112: if (keywordStart >= 0) { tikhomirov@112: int i = indexOf(src, '$', keywordStart+1, true); tikhomirov@112: if (i == -1) { tikhomirov@112: // end of buffer reached tikhomirov@112: if (rv == null) { tikhomirov@112: if (keywordStart == x) { tikhomirov@112: // FIXME in fact, x might be equal to keywordStart and to src.position() here ('$' is first character in the buffer, tikhomirov@112: // and there are no other '$' not eols till the end of the buffer). This would lead to deadlock (filter won't consume any tikhomirov@112: // bytes). To prevent this, either shall copy bytes [keywordStart..buffer.limit()) to local buffer and use it on the next invocation, tikhomirov@112: // or add lookup of the keywords right after first '$' is found (do not wait for closing '$'). For now, large enough src buffer would be sufficient tikhomirov@112: // not to run into such situation tikhomirov@112: throw new IllegalStateException("Try src buffer of a greater size"); tikhomirov@112: } tikhomirov@121: rv = ByteBuffer.allocate(keywordStart - copyFrom); tikhomirov@112: } tikhomirov@112: // copy all from source till latest possible kw start tikhomirov@119: copySlice(src, copyFrom, keywordStart, rv); tikhomirov@112: rv.flip(); tikhomirov@112: // and tell caller we've consumed only to the potential kw start tikhomirov@112: src.position(keywordStart); tikhomirov@112: return rv; tikhomirov@112: } else if (src.get(i) == '$') { tikhomirov@112: // end of keyword, or start of a new one. tikhomirov@112: String keyword; tikhomirov@112: if ((keyword = matchKeyword(src, keywordStart, i)) != null) { tikhomirov@112: if (rv == null) { tikhomirov@113: // src.remaining(), not .capacity because src is not read, and remaining represents tikhomirov@113: // actual bytes count, while capacity - potential. tikhomirov@113: // Factor of 4 is pure guess and a HACK, need to be fixed with re-expanding buffer on demand tikhomirov@113: rv = ByteBuffer.allocate(isExpanding ? src.remaining() * 4 : src.remaining()); tikhomirov@112: } tikhomirov@119: copySlice(src, copyFrom, keywordStart+1, rv); tikhomirov@112: rv.put(keyword.getBytes()); tikhomirov@112: if (isExpanding) { tikhomirov@112: rv.put((byte) ':'); tikhomirov@112: rv.put((byte) ' '); tikhomirov@112: expandKeywordValue(keyword, rv); tikhomirov@112: rv.put((byte) ' '); tikhomirov@112: } tikhomirov@112: rv.put((byte) '$'); tikhomirov@112: keywordStart = -1; tikhomirov@112: x = i+1; tikhomirov@119: copyFrom = x; tikhomirov@112: continue; tikhomirov@112: } else { tikhomirov@112: if (rv != null) { tikhomirov@112: // we've already did some substitution, thus need to copy bytes we've scanned. tikhomirov@112: copySlice(src, x, i, rv); tikhomirov@119: copyFrom = i; tikhomirov@112: } // no else in attempt to avoid rv creation if no real kw would be found tikhomirov@112: keywordStart = i; tikhomirov@112: x = i; // '$' at i wasn't consumed, hence x points to i, not i+1. This is to avoid problems with case: "sdfsd $ asdfs $Id$ sdf" tikhomirov@112: continue; tikhomirov@112: } tikhomirov@112: } else { tikhomirov@112: assert src.get(i) == '\n' || src.get(i) == '\r'; tikhomirov@112: // line break tikhomirov@112: if (rv != null) { tikhomirov@112: copySlice(src, x, i+1, rv); tikhomirov@119: copyFrom = i+1; tikhomirov@112: } tikhomirov@112: x = i+1; tikhomirov@112: keywordStart = -1; // Wasn't keyword, really tikhomirov@112: continue; // try once again tikhomirov@112: } tikhomirov@112: } tikhomirov@112: } tikhomirov@112: if (keywordStart != -1) { tikhomirov@112: if (rv == null) { tikhomirov@112: // no expansion happened yet, and we have potential kw start tikhomirov@113: rv = ByteBuffer.allocate(keywordStart - src.position()); tikhomirov@112: copySlice(src, src.position(), keywordStart, rv); tikhomirov@112: } tikhomirov@112: src.position(keywordStart); tikhomirov@112: } tikhomirov@112: if (rv != null) { tikhomirov@112: rv.flip(); tikhomirov@112: return rv; tikhomirov@112: } tikhomirov@112: return src; tikhomirov@112: } tikhomirov@112: tikhomirov@112: /** tikhomirov@112: * @param keyword tikhomirov@112: * @param rv tikhomirov@112: */ tikhomirov@112: private void expandKeywordValue(String keyword, ByteBuffer rv) { tikhomirov@112: if ("Id".equals(keyword)) { tikhomirov@112: rv.put(identityString().getBytes()); tikhomirov@112: } else if ("Revision".equals(keyword)) { tikhomirov@114: rv.put(revision().getBytes()); tikhomirov@112: } else if ("Author".equals(keyword)) { tikhomirov@112: rv.put(username().getBytes()); tikhomirov@114: } else if ("Date".equals(keyword)) { tikhomirov@114: rv.put(date().getBytes()); tikhomirov@114: } else { tikhomirov@114: throw new IllegalStateException(String.format("Keyword %s is not yet supported", keyword)); tikhomirov@112: } tikhomirov@112: } tikhomirov@112: tikhomirov@112: private String matchKeyword(ByteBuffer src, int kwStart, int kwEnd) { tikhomirov@112: assert kwEnd - kwStart - 1 > 0; tikhomirov@112: assert src.get(kwStart) == src.get(kwEnd) && src.get(kwEnd) == '$'; tikhomirov@112: char[] chars = new char[kwEnd - kwStart - 1]; tikhomirov@112: int i; tikhomirov@112: for (i = 0; i < chars.length; i++) { tikhomirov@112: char c = (char) src.get(kwStart + 1 + i); tikhomirov@112: if (c == ':') { tikhomirov@112: break; tikhomirov@112: } tikhomirov@112: chars[i] = c; tikhomirov@112: } tikhomirov@112: String kw = new String(chars, 0, i); tikhomirov@114: // XXX may use subMap to look up keywords based on few available characters (not waiting till closing $) tikhomirov@114: // System.out.println(keywords.subMap("I", "J")); tikhomirov@114: // System.out.println(keywords.subMap("A", "B")); tikhomirov@114: // System.out.println(keywords.subMap("Au", "B")); tikhomirov@112: return keywords.get(kw); 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@114: // FIXME add cset's nodeid into Changeset class tikhomirov@114: int csetRev = repo.getFileNode(path).getChangesetLocalRevision(HgRepository.TIP); tikhomirov@114: return repo.getChangelog().getRevision(csetRev).shortNotation(); tikhomirov@112: } tikhomirov@112: tikhomirov@112: private String username() { tikhomirov@114: return getChangeset().user(); tikhomirov@114: } tikhomirov@114: tikhomirov@114: private String date() { tikhomirov@114: return String.format("%tY/% patterns = new ArrayList(); tikhomirov@114: for (Map.Entry e : cfg.getSection("keyword").entrySet()) { tikhomirov@114: if (!"ignore".equalsIgnoreCase(e.getValue())) { tikhomirov@114: patterns.add(e.getKey()); tikhomirov@114: } tikhomirov@114: } tikhomirov@114: matcher = new PathGlobMatcher(patterns.toArray(new String[patterns.size()])); tikhomirov@114: // TODO 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@118: return new KeywordFilter(repo, path, 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: }