Merge lp:~tjoneslo/akiban-server/add-rest-model-hash into lp:~akiban-technologies/akiban-server/trunk
- add-rest-model-hash
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Nathan Williams |
Approved revision: | 2609 |
Merged at revision: | 2606 |
Proposed branch: | lp:~tjoneslo/akiban-server/add-rest-model-hash |
Merge into: | lp:~akiban-technologies/akiban-server/trunk |
Diff against target: |
419 lines (+201/-36) 10 files modified
src/main/java/com/akiban/rest/resources/ModelResource.java (+24/-0) src/main/java/com/akiban/server/entity/model/Space.java (+29/-2) src/main/java/com/akiban/server/service/security/SecurityServiceImpl.java (+3/-18) src/main/java/com/akiban/server/types3/mcompat/mfuncs/MD5.java (+4/-16) src/main/java/com/akiban/util/MessageDigestWriter.java (+52/-0) src/main/java/com/akiban/util/Strings.java (+6/-0) src/test/java/com/akiban/util/MessageDigestWriterTest.java (+67/-0) src/test/java/com/akiban/util/StringsTest.java (+12/-0) src/test/resources/com/akiban/rest/space/hash-test.expected (+3/-0) src/test/resources/com/akiban/rest/space/hash-test.get (+1/-0) |
To merge this branch: | bzr merge lp:~tjoneslo/akiban-server/add-rest-model-hash |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Nathan Williams | Approve | ||
Thomas Jones-Low | Needs Resubmitting | ||
Review via email: mp+155763@code.launchpad.net |
Commit message
Description of the change
Per the Story, add a /model/hash REST API.
Returns a MD5 hash of the current space in the server as a Json object for user consumption.
Added a MessageDigestWriter (no one already written) to use the existing Space#JsonGenerate process for least amount of duplicated code.
Add test for new writer. Add simple test for REST API.
Thomas Jones-Low (tjoneslo) wrote : | # |
Updated the md5 generation (was building based on the UTF-16 java characters, now using UTF-8 as expected.
echo -n "This is a test string" |md5sum
c639efc1e987622
It now also matches the results produced by MySQL, Python, and two online MD5 hash generators.
Updated the MessageDigestWriter to use getFormatMD5 for the string value.
Refactor the formatMD5() into the Strings class. Also update the MD5 function to use the same.
Nathan Williams (nwilliams) wrote : | # |
I think formatMD5 needs another parameter for upper vs lowercase to preserve the SercurityService needs. Just moving the old SecurityService implementation to Strings would probably suffice.
Thomas Jones-Low (tjoneslo) wrote : | # |
Move the RestServiceFiles hash test to the rest.space test to get a consistent "space" to generate hash values for run to run.
Fix the SecurityService requirements for MD5 string to be in upper case in some cases.
Nathan Williams (nwilliams) wrote : | # |
Sorry to be picky, and maybe this is premature optimization, but the new code is pretty wasteful. Construct a BigInteger, toString() on that, maybe construct another sting with leading zero, and append that into a StringBuilder, toString() on that, and then toUppper or toLower it. That's six lines and a lot of intermediate steps. The previous, simple loop was five and avoided it all.
Feel free to push back.
Thomas Jones-Low (tjoneslo) wrote : | # |
Tired of re-writing the same function, do what any good programmer does. Find someone else's implementation and make a one line call to that. Plus test to verify the leading 0 is still there.
Nathan Williams (nwilliams) wrote : | # |
Ha! Even better than my suggestion. Excellent.
Preview Diff
1 | === modified file 'src/main/java/com/akiban/rest/resources/ModelResource.java' |
2 | --- src/main/java/com/akiban/rest/resources/ModelResource.java 2013-03-26 16:44:27 +0000 |
3 | +++ src/main/java/com/akiban/rest/resources/ModelResource.java 2013-03-28 14:27:28 +0000 |
4 | @@ -99,6 +99,30 @@ |
5 | }) |
6 | .build(); |
7 | } |
8 | + |
9 | + @GET |
10 | + @Path("/hash" + OPTIONAL_SCHEMA) |
11 | + @Produces(MEDIATYPE_JSON_JAVASCRIPT) |
12 | + public Response hashOfSpace(@Context HttpServletRequest request, |
13 | + @PathParam("schema") String schemaParam) { |
14 | + final String schema = getSchemaName(request, schemaParam); |
15 | + checkSchemaAccessible(reqs.securityService, request, schema); |
16 | + return RestResponseBuilder |
17 | + .forRequest(request) |
18 | + .body(new RestResponseBuilder.BodyGenerator() { |
19 | + @Override |
20 | + public void write(PrintWriter writer) throws Exception { |
21 | + try (Session session = reqs.sessionService.createSession(); |
22 | + CloseableTransaction txn = reqs.transactionService.beginCloseableTransaction(session)) { |
23 | + Space space = spaceForAIS(session, schema); |
24 | + String json = space.toHash(); |
25 | + writer.write(json); |
26 | + txn.commit(); |
27 | + } |
28 | + } |
29 | + }) |
30 | + .build(); |
31 | + } |
32 | |
33 | @POST |
34 | @Path("/parse/{table}") |
35 | |
36 | === modified file 'src/main/java/com/akiban/server/entity/model/Space.java' |
37 | --- src/main/java/com/akiban/server/entity/model/Space.java 2013-03-22 20:05:57 +0000 |
38 | +++ src/main/java/com/akiban/server/entity/model/Space.java 2013-03-28 14:27:28 +0000 |
39 | @@ -17,6 +17,7 @@ |
40 | |
41 | package com.akiban.server.entity.model; |
42 | |
43 | +import com.akiban.util.MessageDigestWriter; |
44 | import com.google.common.base.Function; |
45 | import org.codehaus.jackson.JsonGenerator; |
46 | import org.codehaus.jackson.map.JsonMappingException; |
47 | @@ -27,6 +28,8 @@ |
48 | import java.io.InputStreamReader; |
49 | import java.io.Reader; |
50 | import java.io.StringWriter; |
51 | +import java.io.Writer; |
52 | +import java.security.NoSuchAlgorithmException; |
53 | import java.util.Collections; |
54 | import java.util.HashSet; |
55 | import java.util.Map; |
56 | @@ -102,19 +105,43 @@ |
57 | } |
58 | |
59 | public String toJson() { |
60 | + StringWriter writer = new StringWriter(); |
61 | + jsonGenerate (writer); |
62 | + return writer.toString(); |
63 | + } |
64 | + |
65 | + public String toHash () { |
66 | try { |
67 | StringWriter writer = new StringWriter(); |
68 | + MessageDigestWriter md5writer = new MessageDigestWriter(); |
69 | + JsonGenerator generator = createJsonGenerator(writer); |
70 | + generator.writeStartObject(); |
71 | + generator.writeFieldName("hash"); |
72 | + jsonGenerate (md5writer); |
73 | + generator.writeString(md5writer.getFormatMD5()); |
74 | + generator.writeEndObject(); |
75 | + generator.flush(); |
76 | + writer.toString(); |
77 | + return writer.toString(); |
78 | + } catch (NoSuchAlgorithmException e) { |
79 | + throw new RuntimeException(e); |
80 | + } catch (IOException e) { |
81 | + throw new RuntimeException(e); |
82 | + } |
83 | + } |
84 | + |
85 | + private void jsonGenerate(Writer writer) { |
86 | + try { |
87 | JsonGenerator generator = createJsonGenerator(writer); |
88 | generateJson(generator); |
89 | generator.flush(); |
90 | writer.flush(); |
91 | - return writer.toString(); |
92 | } |
93 | catch (IOException e) { |
94 | throw new RuntimeException(e); |
95 | } |
96 | } |
97 | - |
98 | + |
99 | Space() {} |
100 | |
101 | private Map<String, Entity> entities = Collections.emptyMap(); |
102 | |
103 | === modified file 'src/main/java/com/akiban/server/service/security/SecurityServiceImpl.java' |
104 | --- src/main/java/com/akiban/server/service/security/SecurityServiceImpl.java 2013-03-22 20:05:57 +0000 |
105 | +++ src/main/java/com/akiban/server/service/security/SecurityServiceImpl.java 2013-03-28 14:27:28 +0000 |
106 | @@ -31,7 +31,7 @@ |
107 | import com.akiban.server.store.SchemaManager; |
108 | import com.akiban.sql.server.ServerCallContextStack; |
109 | import com.akiban.sql.server.ServerQueryContext; |
110 | -import com.akiban.sql.server.ServerSession; |
111 | +import com.akiban.util.Strings; |
112 | |
113 | import com.google.inject.Inject; |
114 | import org.slf4j.Logger; |
115 | @@ -52,9 +52,7 @@ |
116 | import java.util.ArrayList; |
117 | import java.util.Arrays; |
118 | import java.util.Collection; |
119 | -import java.util.HashMap; |
120 | import java.util.List; |
121 | -import java.util.Map; |
122 | import java.util.Properties; |
123 | |
124 | public class SecurityServiceImpl implements SecurityService, Service { |
125 | @@ -259,7 +257,6 @@ |
126 | public void deleteUser(String name) { |
127 | Connection conn = null; |
128 | PreparedStatement stmt = null; |
129 | - boolean success = false; |
130 | try { |
131 | conn = openConnection(); |
132 | stmt = conn.prepareStatement(DELETE_USER_USER_ROLES_SQL); |
133 | @@ -287,7 +284,6 @@ |
134 | public void changeUserPassword(String name, String password) { |
135 | Connection conn = null; |
136 | PreparedStatement stmt = null; |
137 | - boolean success = false; |
138 | try { |
139 | conn = openConnection(); |
140 | stmt = conn.prepareStatement(CHANGE_USER_PASSWORD_SQL); |
141 | @@ -315,7 +311,6 @@ |
142 | User user = null; |
143 | Connection conn = null; |
144 | PreparedStatement stmt = null; |
145 | - boolean success = false; |
146 | try { |
147 | conn = openConnection(); |
148 | stmt = conn.prepareStatement(GET_USER_SQL); |
149 | @@ -347,7 +342,6 @@ |
150 | User user = null; |
151 | Connection conn = null; |
152 | PreparedStatement stmt = null; |
153 | - boolean success = false; |
154 | try { |
155 | conn = openConnection(); |
156 | stmt = conn.prepareStatement(GET_USER_SQL); |
157 | @@ -434,18 +428,11 @@ |
158 | } |
159 | } |
160 | |
161 | - protected final char[] HEX_UC = "0123456789ABCDEF".toCharArray(); |
162 | - protected final char[] HEX_LC = "0123456789abcdef".toCharArray(); |
163 | - |
164 | protected String formatMD5(byte[] md5, boolean forDigest) { |
165 | StringBuilder str = new StringBuilder(); |
166 | str.append(forDigest ? "MD5:" : "md5"); |
167 | - final char[] hex = forDigest ? HEX_UC : HEX_LC; |
168 | - for (int i = 0; i < md5.length; i++) { |
169 | - int b = md5[i] & 0xFF; |
170 | - str.append(hex[b >> 4]); |
171 | - str.append(hex[b & 0xF]); |
172 | - } |
173 | + // Strings#formatMD5 wants toLowerCase for second parameter, inverse of the forDigest flag |
174 | + str.append(Strings.formatMD5(md5, !forDigest)); |
175 | return str.toString(); |
176 | } |
177 | |
178 | @@ -453,7 +440,6 @@ |
179 | public void clearAll(Session session) { |
180 | Connection conn = null; |
181 | Statement stmt = null; |
182 | - boolean success = false; |
183 | try { |
184 | conn = openConnection(); |
185 | stmt = conn.createStatement(); |
186 | @@ -461,7 +447,6 @@ |
187 | stmt.execute("DELETE FROM users"); |
188 | stmt.execute("DELETE FROM roles"); |
189 | conn.commit(); |
190 | - success = true; |
191 | } |
192 | catch (SQLException ex) { |
193 | throw new SecurityException("Error adding role", ex); |
194 | |
195 | === modified file 'src/main/java/com/akiban/server/types3/mcompat/mfuncs/MD5.java' |
196 | --- src/main/java/com/akiban/server/types3/mcompat/mfuncs/MD5.java 2013-03-22 20:05:57 +0000 |
197 | +++ src/main/java/com/akiban/server/types3/mcompat/mfuncs/MD5.java 2013-03-28 14:27:28 +0000 |
198 | @@ -26,6 +26,8 @@ |
199 | import com.akiban.server.types3.pvalue.PValueTarget; |
200 | import com.akiban.server.types3.texpressions.TInputSetBuilder; |
201 | import com.akiban.server.types3.texpressions.TScalarBase; |
202 | +import com.akiban.util.Strings; |
203 | + |
204 | import java.security.MessageDigest; |
205 | import java.security.NoSuchAlgorithmException; |
206 | |
207 | @@ -48,22 +50,8 @@ |
208 | { |
209 | MessageDigest md = MessageDigest.getInstance("MD5"); |
210 | byte ret[] = md.digest(inputs.get(0).getBytes()); |
211 | - StringBuilder retStr = new StringBuilder(32); |
212 | - int lo, hi; |
213 | - |
214 | - for (byte b : ret) |
215 | - { |
216 | - lo = b & 0x0f; |
217 | - hi = (b & 0xf0) >>> 4; |
218 | - |
219 | - retStr.append((char)(hi > 9 |
220 | - ? 'a' + hi - 10 |
221 | - : '0' + hi)); |
222 | - retStr.append((char)(lo > 9 |
223 | - ? 'a' + lo - 10 |
224 | - : '0' + lo)); |
225 | - } |
226 | - output.putString(retStr.toString(), null); |
227 | + |
228 | + output.putString(Strings.formatMD5(ret, true), null); |
229 | } |
230 | catch (NoSuchAlgorithmException ex) |
231 | { |
232 | |
233 | === added file 'src/main/java/com/akiban/util/MessageDigestWriter.java' |
234 | --- src/main/java/com/akiban/util/MessageDigestWriter.java 1970-01-01 00:00:00 +0000 |
235 | +++ src/main/java/com/akiban/util/MessageDigestWriter.java 2013-03-28 14:27:28 +0000 |
236 | @@ -0,0 +1,52 @@ |
237 | +/** |
238 | + * Copyright (C) 2009-2013 Akiban Technologies, Inc. |
239 | + * |
240 | + * This program is free software: you can redistribute it and/or modify |
241 | + * it under the terms of the GNU Affero General Public License as published by |
242 | + * the Free Software Foundation, either version 3 of the License, or |
243 | + * (at your option) any later version. |
244 | + * |
245 | + * This program is distributed in the hope that it will be useful, |
246 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
247 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
248 | + * GNU Affero General Public License for more details. |
249 | + * |
250 | + * You should have received a copy of the GNU Affero General Public License |
251 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
252 | + */ |
253 | +package com.akiban.util; |
254 | + |
255 | +import java.io.IOException; |
256 | +import java.io.Writer; |
257 | +import java.security.MessageDigest; |
258 | +import java.security.NoSuchAlgorithmException; |
259 | + |
260 | +public class MessageDigestWriter extends Writer { |
261 | + |
262 | + private final MessageDigest digest; |
263 | + |
264 | + public MessageDigestWriter () throws NoSuchAlgorithmException { |
265 | + digest = MessageDigest.getInstance("MD5"); |
266 | + digest.reset(); |
267 | + } |
268 | + @Override |
269 | + public void write(char[] cbuf, int off, int len) throws IOException { |
270 | + byte[] bytes = String.valueOf(cbuf).substring(off, (len+off)).getBytes("UTF-8"); |
271 | + digest.update(bytes, 0, bytes.length); |
272 | + } |
273 | + |
274 | + @Override |
275 | + public void flush() throws IOException { |
276 | + } |
277 | + |
278 | + @Override |
279 | + public void close() throws IOException { |
280 | + } |
281 | + |
282 | + public MessageDigest getDigest() { |
283 | + return digest; |
284 | + } |
285 | + public String getFormatMD5() { |
286 | + return Strings.formatMD5(digest.digest(), true); |
287 | + } |
288 | +} |
289 | |
290 | === modified file 'src/main/java/com/akiban/util/Strings.java' |
291 | --- src/main/java/com/akiban/util/Strings.java 2013-03-22 20:05:57 +0000 |
292 | +++ src/main/java/com/akiban/util/Strings.java 2013-03-28 14:27:28 +0000 |
293 | @@ -20,6 +20,8 @@ |
294 | import com.akiban.server.error.InvalidParameterValueException; |
295 | import com.google.common.collect.ArrayListMultimap; |
296 | import com.google.common.collect.Multimap; |
297 | + |
298 | +import org.apache.commons.codec.binary.Hex; |
299 | import org.slf4j.Logger; |
300 | import org.slf4j.LoggerFactory; |
301 | |
302 | @@ -420,4 +422,8 @@ |
303 | throw new RuntimeException(e); |
304 | } |
305 | } |
306 | + |
307 | + public static String formatMD5(byte[] md5, boolean toLowerCase) { |
308 | + return new String(Hex.encodeHex(md5, toLowerCase)); |
309 | + } |
310 | } |
311 | |
312 | === added file 'src/test/java/com/akiban/util/MessageDigestWriterTest.java' |
313 | --- src/test/java/com/akiban/util/MessageDigestWriterTest.java 1970-01-01 00:00:00 +0000 |
314 | +++ src/test/java/com/akiban/util/MessageDigestWriterTest.java 2013-03-28 14:27:28 +0000 |
315 | @@ -0,0 +1,67 @@ |
316 | +/** |
317 | + * Copyright (C) 2009-2013 Akiban Technologies, Inc. |
318 | + * |
319 | + * This program is free software: you can redistribute it and/or modify |
320 | + * it under the terms of the GNU Affero General Public License as published by |
321 | + * the Free Software Foundation, either version 3 of the License, or |
322 | + * (at your option) any later version. |
323 | + * |
324 | + * This program is distributed in the hope that it will be useful, |
325 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
326 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
327 | + * GNU Affero General Public License for more details. |
328 | + * |
329 | + * You should have received a copy of the GNU Affero General Public License |
330 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
331 | + */ |
332 | +package com.akiban.util; |
333 | + |
334 | +import static org.junit.Assert.*; |
335 | + |
336 | +import java.io.IOException; |
337 | +import java.security.NoSuchAlgorithmException; |
338 | + |
339 | +import org.junit.Test; |
340 | + |
341 | +public class MessageDigestWriterTest { |
342 | + |
343 | + |
344 | + @Test |
345 | + public void testGetDigest() throws NoSuchAlgorithmException { |
346 | + @SuppressWarnings("resource") |
347 | + MessageDigestWriter writer = new MessageDigestWriter(); |
348 | + assertNotNull(writer.getDigest()); |
349 | + } |
350 | + |
351 | + @Test |
352 | + public void testToString() throws IOException, NoSuchAlgorithmException { |
353 | + String test = "This is a test string"; |
354 | + MessageDigestWriter writer = new MessageDigestWriter(); |
355 | + writer.write(test); |
356 | + writer.close(); |
357 | + assertEquals(writer.getFormatMD5(), "c639efc1e98762233743a75e7798dd9c"); |
358 | + } |
359 | + |
360 | + @Test |
361 | + public void testoffset() throws IOException, NoSuchAlgorithmException { |
362 | + // Test my off-by-one problem is not present here |
363 | + String test1 = "This is a test string"; |
364 | + String test2 = "test string"; |
365 | + |
366 | + MessageDigestWriter writerA = new MessageDigestWriter(); |
367 | + |
368 | + writerA.write(test2); |
369 | + writerA.close(); |
370 | + String digest1 = writerA.getFormatMD5(); |
371 | + |
372 | + MessageDigestWriter writerB = new MessageDigestWriter(); |
373 | + char[] chars = test1.toCharArray(); |
374 | + writerB.write(chars, 10, 11); |
375 | + writerB.close(); |
376 | + String digest2 = writerB.getFormatMD5(); |
377 | + assertEquals(digest1, "6f8db599de986fab7a21625b7916589c"); |
378 | + assertEquals(digest1, digest2); |
379 | + |
380 | + } |
381 | + |
382 | +} |
383 | |
384 | === modified file 'src/test/java/com/akiban/util/StringsTest.java' |
385 | --- src/test/java/com/akiban/util/StringsTest.java 2013-03-22 20:05:57 +0000 |
386 | +++ src/test/java/com/akiban/util/StringsTest.java 2013-03-28 14:27:28 +0000 |
387 | @@ -45,4 +45,16 @@ |
388 | |
389 | assertEquals("bytes", new WrappingByteSource(expected), actual); |
390 | } |
391 | + |
392 | + @Test |
393 | + public void formatMD5() { |
394 | + byte[] md5 = {0x01, 0x39, (byte) 0xef, (byte) 0xc1, (byte) 0xe9, (byte) 0x86, 0x22, 0x33, 0x74, 0x3a, 0x75, 0x77, (byte) 0x98, (byte) 0xdd, (byte) 0x9c}; |
395 | + |
396 | + String expected = "0139efc1e9862233743a757798dd9c"; |
397 | + |
398 | + String actual = Strings.formatMD5(md5, true); |
399 | + |
400 | + assertEquals ("bytes", expected, actual); |
401 | + |
402 | + } |
403 | } |
404 | |
405 | === added file 'src/test/resources/com/akiban/rest/space/hash-test.expected' |
406 | --- src/test/resources/com/akiban/rest/space/hash-test.expected 1970-01-01 00:00:00 +0000 |
407 | +++ src/test/resources/com/akiban/rest/space/hash-test.expected 2013-03-28 14:27:28 +0000 |
408 | @@ -0,0 +1,3 @@ |
409 | +{ |
410 | + "hash": "92159a7225ef31a02dcead921284f737" |
411 | +} |
412 | \ No newline at end of file |
413 | |
414 | === added file 'src/test/resources/com/akiban/rest/space/hash-test.get' |
415 | --- src/test/resources/com/akiban/rest/space/hash-test.get 1970-01-01 00:00:00 +0000 |
416 | +++ src/test/resources/com/akiban/rest/space/hash-test.get 2013-03-28 14:27:28 +0000 |
417 | @@ -0,0 +1,1 @@ |
418 | +/model/hash/test/ |
419 | \ No newline at end of file |
Looks like something is slightly off with our computation. Perhaps it's the incremental digest updating (random guess)?
$ echo "This is a test string" |md5sum ebceea3fdb860ed 6c -
b584c39f97dfe71
The digestResult variable and toString() behavior is a little funny. Seems like always printing the digest would be useful (toString being for debug, etc).
There is also a helper in SecurityService for formatting an md5 byte array. Perhaps slicing that out into util.Strings or such would be good.