Merge lp:~tjoneslo/akiban-server/add-rest-model-hash into lp:~akiban-technologies/akiban-server/trunk

Proposed by Thomas Jones-Low
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
Reviewer Review Type Date Requested Status
Nathan Williams Approve
Thomas Jones-Low Needs Resubmitting
Review via email: mp+155763@code.launchpad.net

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.

To post a comment you must log in.
Revision history for this message
Nathan Williams (nwilliams) wrote :

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
b584c39f97dfe71ebceea3fdb860ed6c -

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.

review: Needs Fixing
Revision history for this message
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
c639efc1e98762233743a75e7798dd9c -

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.

review: Needs Resubmitting
Revision history for this message
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.

review: Needs Fixing
Revision history for this message
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.

review: Needs Resubmitting
Revision history for this message
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.

Revision history for this message
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.

review: Needs Resubmitting
Revision history for this message
Nathan Williams (nwilliams) wrote :

Ha! Even better than my suggestion. Excellent.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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

Subscribers

People subscribed via source and target branches