Mercurial > hg4j
comparison src/org/tmatesoft/hg/internal/KeywordFilter.java @ 112:d488c7638b87
Prototype Filter support with keyword filter as a playground
author | Artem Tikhomirov <tikhomirov.artem@gmail.com> |
---|---|
date | Tue, 01 Feb 2011 00:21:08 +0100 |
parents | |
children | 67ae317408c9 |
comparison
equal
deleted
inserted
replaced
111:32e794c599d7 | 112:d488c7638b87 |
---|---|
1 /* | |
2 * Copyright (c) 2011 TMate Software Ltd | |
3 * | |
4 * This program is free software; you can redistribute it and/or modify | |
5 * it under the terms of the GNU General Public License as published by | |
6 * the Free Software Foundation; version 2 of the License. | |
7 * | |
8 * This program is distributed in the hope that it will be useful, | |
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
11 * GNU General Public License for more details. | |
12 * | |
13 * For information on how to redistribute this software under | |
14 * the terms of a license other than GNU General Public License | |
15 * contact TMate Software at support@svnkit.com | |
16 */ | |
17 package org.tmatesoft.hg.internal; | |
18 | |
19 import java.io.File; | |
20 import java.io.FileInputStream; | |
21 import java.io.FileOutputStream; | |
22 import java.nio.ByteBuffer; | |
23 import java.util.TreeMap; | |
24 | |
25 import org.tmatesoft.hg.core.Path; | |
26 import org.tmatesoft.hg.repo.HgRepository; | |
27 | |
28 /** | |
29 * | |
30 * @author Artem Tikhomirov | |
31 * @author TMate Software Ltd. | |
32 */ | |
33 public class KeywordFilter implements Filter { | |
34 private final boolean isExpanding; | |
35 private final TreeMap<String,String> keywords; | |
36 private final int minBufferLen; | |
37 | |
38 /** | |
39 * | |
40 * @param expand <code>true</code> to expand keywords, <code>false</code> to shrink | |
41 */ | |
42 private KeywordFilter(boolean expand) { | |
43 isExpanding = expand; | |
44 keywords = new TreeMap<String,String>(); | |
45 keywords.put("Id", "Id"); | |
46 keywords.put("Revision", "Revision"); | |
47 keywords.put("Author", "Author"); | |
48 int l = 0; | |
49 for (String s : keywords.keySet()) { | |
50 if (s.length() > l) { | |
51 l = s.length(); | |
52 } | |
53 } | |
54 // FIXME later may implement #filter() not to read full kw value (just "$kw:"). However, limit of maxLen + 2 would keep valid. | |
55 // for buffers less then minBufferLen, there are chances #filter() implementation would never end | |
56 // (i.e. for input "$LongestKey"$ | |
57 minBufferLen = l + 2 + (isExpanding ? 0 : 120 /*any reasonable constant for max possible kw value length*/); | |
58 } | |
59 | |
60 /** | |
61 * @param src buffer ready to be read | |
62 * @return buffer ready to be read and original buffer's position modified to reflect consumed bytes. IOW, if source buffer | |
63 * on return has remaining bytes, they are assumed not-read (not processed) and next chunk passed to filter is supposed to | |
64 * start with them | |
65 */ | |
66 public ByteBuffer filter(ByteBuffer src) { | |
67 if (src.capacity() < minBufferLen) { | |
68 throw new IllegalStateException(String.format("Need buffer of at least %d bytes to ensure filter won't hang", minBufferLen)); | |
69 } | |
70 ByteBuffer rv = null; | |
71 int keywordStart = -1; | |
72 int x = src.position(); | |
73 while (x < src.limit()) { | |
74 if (keywordStart == -1) { | |
75 int i = indexOf(src, '$', x, false); | |
76 if (i == -1) { | |
77 if (rv == null) { | |
78 return src; | |
79 } else { | |
80 copySlice(src, x, src.limit(), rv); | |
81 rv.flip(); | |
82 src.position(src.limit()); | |
83 return rv; | |
84 } | |
85 } | |
86 keywordStart = i; | |
87 // fall-through | |
88 } | |
89 if (keywordStart >= 0) { | |
90 int i = indexOf(src, '$', keywordStart+1, true); | |
91 if (i == -1) { | |
92 // end of buffer reached | |
93 if (rv == null) { | |
94 if (keywordStart == x) { | |
95 // FIXME in fact, x might be equal to keywordStart and to src.position() here ('$' is first character in the buffer, | |
96 // and there are no other '$' not eols till the end of the buffer). This would lead to deadlock (filter won't consume any | |
97 // bytes). To prevent this, either shall copy bytes [keywordStart..buffer.limit()) to local buffer and use it on the next invocation, | |
98 // 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 | |
99 // not to run into such situation | |
100 throw new IllegalStateException("Try src buffer of a greater size"); | |
101 } | |
102 rv = ByteBuffer.allocateDirect(keywordStart - x); | |
103 } | |
104 // copy all from source till latest possible kw start | |
105 copySlice(src, x, keywordStart, rv); | |
106 rv.flip(); | |
107 // and tell caller we've consumed only to the potential kw start | |
108 src.position(keywordStart); | |
109 return rv; | |
110 } else if (src.get(i) == '$') { | |
111 // end of keyword, or start of a new one. | |
112 String keyword; | |
113 if ((keyword = matchKeyword(src, keywordStart, i)) != null) { | |
114 if (rv == null) { | |
115 rv = ByteBuffer.allocateDirect(isExpanding ? src.capacity() * 4 : src.capacity()); | |
116 } | |
117 copySlice(src, x, keywordStart+1, rv); | |
118 rv.put(keyword.getBytes()); | |
119 if (isExpanding) { | |
120 rv.put((byte) ':'); | |
121 rv.put((byte) ' '); | |
122 expandKeywordValue(keyword, rv); | |
123 rv.put((byte) ' '); | |
124 } | |
125 rv.put((byte) '$'); | |
126 keywordStart = -1; | |
127 x = i+1; | |
128 continue; | |
129 } else { | |
130 if (rv != null) { | |
131 // we've already did some substitution, thus need to copy bytes we've scanned. | |
132 copySlice(src, x, i, rv); | |
133 } // no else in attempt to avoid rv creation if no real kw would be found | |
134 keywordStart = i; | |
135 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" | |
136 continue; | |
137 } | |
138 } else { | |
139 assert src.get(i) == '\n' || src.get(i) == '\r'; | |
140 // line break | |
141 if (rv != null) { | |
142 copySlice(src, x, i+1, rv); | |
143 } | |
144 x = i+1; | |
145 keywordStart = -1; // Wasn't keyword, really | |
146 continue; // try once again | |
147 } | |
148 } | |
149 } | |
150 if (keywordStart != -1) { | |
151 if (rv == null) { | |
152 // no expansion happened yet, and we have potential kw start | |
153 rv = ByteBuffer.allocateDirect(keywordStart - src.position()); | |
154 copySlice(src, src.position(), keywordStart, rv); | |
155 } | |
156 src.position(keywordStart); | |
157 } | |
158 if (rv != null) { | |
159 rv.flip(); | |
160 return rv; | |
161 } | |
162 return src; | |
163 } | |
164 | |
165 /** | |
166 * @param keyword | |
167 * @param rv | |
168 */ | |
169 private void expandKeywordValue(String keyword, ByteBuffer rv) { | |
170 if ("Id".equals(keyword)) { | |
171 rv.put(identityString().getBytes()); | |
172 } else if ("Revision".equals(keyword)) { | |
173 rv.put(revision()); | |
174 } else if ("Author".equals(keyword)) { | |
175 rv.put(username().getBytes()); | |
176 } | |
177 } | |
178 | |
179 private String matchKeyword(ByteBuffer src, int kwStart, int kwEnd) { | |
180 assert kwEnd - kwStart - 1 > 0; | |
181 assert src.get(kwStart) == src.get(kwEnd) && src.get(kwEnd) == '$'; | |
182 char[] chars = new char[kwEnd - kwStart - 1]; | |
183 int i; | |
184 for (i = 0; i < chars.length; i++) { | |
185 char c = (char) src.get(kwStart + 1 + i); | |
186 if (c == ':') { | |
187 break; | |
188 } | |
189 chars[i] = c; | |
190 } | |
191 String kw = new String(chars, 0, i); | |
192 return keywords.get(kw); | |
193 } | |
194 | |
195 // copies part of the src buffer, [from..to). doesn't modify src position | |
196 private static void copySlice(ByteBuffer src, int from, int to, ByteBuffer dst) { | |
197 if (to > src.limit()) { | |
198 throw new IllegalArgumentException("Bad right boundary"); | |
199 } | |
200 if (dst.remaining() < to - from) { | |
201 throw new IllegalArgumentException("Not enough room in the destination buffer"); | |
202 } | |
203 for (int i = from; i < to; i++) { | |
204 dst.put(src.get(i)); | |
205 } | |
206 } | |
207 | |
208 private static int indexOf(ByteBuffer b, char ch, int from, boolean newlineBreaks) { | |
209 for (int i = from; i < b.limit(); i++) { | |
210 byte c = b.get(i); | |
211 if (ch == c) { | |
212 return i; | |
213 } | |
214 if (newlineBreaks && (c == '\n' || c == '\r')) { | |
215 return i; | |
216 } | |
217 } | |
218 return -1; | |
219 } | |
220 | |
221 private String identityString() { | |
222 return "sample/file.txt, asd"; | |
223 } | |
224 | |
225 private byte[] revision() { | |
226 return "1234567890ab".getBytes(); | |
227 } | |
228 | |
229 private String username() { | |
230 /* ui.py: username() | |
231 Searched in this order: $HGUSER, [ui] section of hgrcs, $EMAIL | |
232 and stop searching if one of these is set. | |
233 If not found and ui.askusername is True, ask the user, else use | |
234 ($LOGNAME or $USER or $LNAME or $USERNAME) + "@full.hostname". | |
235 */ | |
236 return "<Sample> sample@sample.org"; | |
237 } | |
238 | |
239 public static class Factory implements Filter.Factory { | |
240 | |
241 public Filter create(HgRepository hgRepo, Path path) { | |
242 return new KeywordFilter(true); | |
243 } | |
244 } | |
245 | |
246 | |
247 public static void main(String[] args) throws Exception { | |
248 FileInputStream fis = new FileInputStream(new File("/temp/kwoutput.txt")); | |
249 FileOutputStream fos = new FileOutputStream(new File("/temp/kwoutput2.txt")); | |
250 ByteBuffer b = ByteBuffer.allocateDirect(256); | |
251 KeywordFilter kwFilter = new KeywordFilter(false); | |
252 while (fis.getChannel().read(b) != -1) { | |
253 b.flip(); // get ready to be read | |
254 ByteBuffer f = kwFilter.filter(b); | |
255 fos.getChannel().write(f); | |
256 if (b.hasRemaining()) { | |
257 ByteBuffer remaining = b.slice(); | |
258 b.clear(); | |
259 b.put(remaining); | |
260 } else { | |
261 b.clear(); | |
262 } | |
263 } | |
264 fis.close(); | |
265 fos.flush(); | |
266 fos.close(); | |
267 } | |
268 } |