From 2ff34355e484098c0503a25858eaa816a463b613 Mon Sep 17 00:00:00 2001 From: Arbel Deutsch Peled Date: Sat, 28 Nov 2015 23:59:56 +0200 Subject: [PATCH 01/15] Bulletin Board Server configuration support. Bulletin Board Server signature testing. --- .../httpserver/BulletinBoardHttpServer.java | 3 +- .../sqlserver/BulletinBoardSQLServer.java | 3 +- .../sqlserver/SQLiteBulletinBoardServer.java | 5 +- .../webapp/BulletinBoardWebApp.java | 64 ++++++++++++++---- .../src/main/webapp/WEB-INF/web.xml | 9 +++ .../BulletinBoardServerTest.java | 23 ------- .../GenericBulletinBoardServerTest.java | 66 +++++++++++++++---- .../SQLiteServerIntegrationTest.java | 7 +- .../bulletinboard/BulletinBoardServer.java | 4 +- 9 files changed, 125 insertions(+), 59 deletions(-) delete mode 100644 bulletin-board-server/src/test/java/meerkat/bulletinboard/BulletinBoardServerTest.java diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/httpserver/BulletinBoardHttpServer.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/httpserver/BulletinBoardHttpServer.java index 032ea27..9989706 100644 --- a/bulletin-board-server/src/main/java/meerkat/bulletinboard/httpserver/BulletinBoardHttpServer.java +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/httpserver/BulletinBoardHttpServer.java @@ -1,6 +1,5 @@ package meerkat.bulletinboard.httpserver; -import java.io.File; import java.io.IOException; import javax.servlet.ServletConfig; @@ -16,7 +15,7 @@ import meerkat.protobuf.BulletinBoardAPI.*; public class BulletinBoardHttpServer extends HttpServlet { - public final static File DEFAULT_MEERKAT_DB = new File("local-instances/meerkat.db"); + public final static String DEFAULT_MEERKAT_DB = "local-instances/meerkat.db"; /** * Auto-generated UID. diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/BulletinBoardSQLServer.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/BulletinBoardSQLServer.java index 4cb8f38..b8fc3cd 100644 --- a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/BulletinBoardSQLServer.java +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/BulletinBoardSQLServer.java @@ -1,6 +1,5 @@ package meerkat.bulletinboard.sqlserver; -import java.io.File; import java.util.Arrays; import java.util.List; @@ -39,7 +38,7 @@ public abstract class BulletinBoardSQLServer implements BulletinBoardServer{ * 2. Call this procedure */ @Override - public void init(File meerkatDB) throws CommunicationException { + public void init(String meerkatDB) throws CommunicationException { // TODO write signature reading part. digest = new SHA256Digest(); diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/SQLiteBulletinBoardServer.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/SQLiteBulletinBoardServer.java index 95f1948..d4aa881 100644 --- a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/SQLiteBulletinBoardServer.java +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/SQLiteBulletinBoardServer.java @@ -1,6 +1,5 @@ package meerkat.bulletinboard.sqlserver; -import java.io.File; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -41,10 +40,10 @@ public class SQLiteBulletinBoardServer extends BulletinBoardSQLServer { } @Override - public void init(File meerkatDB) throws CommunicationException { + public void init(String meerkatDB) throws CommunicationException { try{ - String dbString = "jdbc:sqlite:" + meerkatDB.getPath(); + String dbString = "jdbc:sqlite:" + meerkatDB; connection = DriverManager.getConnection(dbString); createSchema(); diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/webapp/BulletinBoardWebApp.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/webapp/BulletinBoardWebApp.java index 1b8883a..69f2ba4 100644 --- a/bulletin-board-server/src/main/java/meerkat/bulletinboard/webapp/BulletinBoardWebApp.java +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/webapp/BulletinBoardWebApp.java @@ -1,12 +1,14 @@ package meerkat.bulletinboard.webapp; -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import meerkat.bulletinboard.BulletinBoardServer; @@ -18,26 +20,55 @@ import meerkat.protobuf.BulletinBoardAPI.BulletinBoardMessageList; import meerkat.protobuf.BulletinBoardAPI.MessageFilterList; import meerkat.rest.Constants; -import java.io.File; - @Path("/sqlserver") -public class BulletinBoardWebApp implements BulletinBoardServer { +public class BulletinBoardWebApp implements BulletinBoardServer, ServletContextListener{ + + private static final String BULLETIN_BOARD_ATTRIBUTE_NAME = "bulletinBoard"; + + @Context ServletContext servletContext; BulletinBoardServer bulletinBoard; - @PostConstruct + /** + * This is the servlet init method. + */ + public void init(){ + bulletinBoard = (BulletinBoardServer) servletContext.getAttribute(BULLETIN_BOARD_ATTRIBUTE_NAME); + } + + /** + * This is the BulletinBoard init method. + */ @Override - public void init(File meerkatDB) throws CommunicationException { - bulletinBoard = new SQLiteBulletinBoardServer(); + public void init(String meerkatDB) throws CommunicationException { bulletinBoard.init(meerkatDB); } + @Override + public void contextInitialized(ServletContextEvent servletContextEvent) { + ServletContext servletContext = servletContextEvent.getServletContext(); + String meerkatDB = servletContext.getInitParameter("meerkatdb"); + String dbType = servletContext.getInitParameter("dbtype"); + + if (dbType.compareTo("SQLite") == 0){ + bulletinBoard = new SQLiteBulletinBoardServer(); + } + + try { + init(meerkatDB); + servletContext.setAttribute(BULLETIN_BOARD_ATTRIBUTE_NAME, bulletinBoard); + } catch (CommunicationException e) { + System.err.println(e.getMessage()); + } + } + @Path("postmessage") @POST @Consumes(Constants.MEDIATYPE_PROTOBUF) @Produces(Constants.MEDIATYPE_PROTOBUF) @Override public BoolMsg postMessage(BulletinBoardMessage msg) throws CommunicationException { + init(); return bulletinBoard.postMessage(msg); } @@ -47,13 +78,17 @@ public class BulletinBoardWebApp implements BulletinBoardServer { @Produces(Constants.MEDIATYPE_PROTOBUF) @Override public BulletinBoardMessageList readMessages(MessageFilterList filterList) throws CommunicationException { + init(); return bulletinBoard.readMessages(filterList); } @Override - @PreDestroy - public void close() throws CommunicationException { - bulletinBoard.close(); + public void close(){ + try { + bulletinBoard.close(); + } catch (CommunicationException e) { + System.err.println(e.getMessage()); + } } @GET @@ -62,4 +97,11 @@ public class BulletinBoardWebApp implements BulletinBoardServer { return "This BulletinBoard is up and running!\n Please consult the API documents to perform queries."; } + @Override + public void contextDestroyed(ServletContextEvent servletContextEvent) { + ServletContext servletContext = servletContextEvent.getServletContext(); + bulletinBoard = (BulletinBoardServer) servletContext.getAttribute(BULLETIN_BOARD_ATTRIBUTE_NAME); + close(); + } + } diff --git a/bulletin-board-server/src/main/webapp/WEB-INF/web.xml b/bulletin-board-server/src/main/webapp/WEB-INF/web.xml index cc90843..5f513e9 100644 --- a/bulletin-board-server/src/main/webapp/WEB-INF/web.xml +++ b/bulletin-board-server/src/main/webapp/WEB-INF/web.xml @@ -14,4 +14,13 @@ Jersey Hello World /* + + meerkatdb + meerkatdb + + dbtype + SQLite + + meerkat.bulletinboard.webapp.BulletinBoardWebApp + diff --git a/bulletin-board-server/src/test/java/meerkat/bulletinboard/BulletinBoardServerTest.java b/bulletin-board-server/src/test/java/meerkat/bulletinboard/BulletinBoardServerTest.java deleted file mode 100644 index e0801af..0000000 --- a/bulletin-board-server/src/test/java/meerkat/bulletinboard/BulletinBoardServerTest.java +++ /dev/null @@ -1,23 +0,0 @@ -package meerkat.bulletinboard; - -import java.io.File; -import java.nio.file.Files; -import java.nio.file.NoSuchFileException; -import java.nio.file.Path; -import java.nio.file.Paths; - -import org.junit.Test; - -import meerkat.bulletinboard.sqlserver.SQLiteBulletinBoardServer; - -public class BulletinBoardServerTest { - - @Test - public void testAllServers() throws Exception { - GenericBulletinBoardServerTest bbst = new GenericBulletinBoardServerTest(); - - bbst.init(SQLiteBulletinBoardServer.class); - bbst.bulkTest(); - bbst.close(); - } -} diff --git a/bulletin-board-server/src/test/java/meerkat/bulletinboard/GenericBulletinBoardServerTest.java b/bulletin-board-server/src/test/java/meerkat/bulletinboard/GenericBulletinBoardServerTest.java index 36a998b..fb26df8 100644 --- a/bulletin-board-server/src/test/java/meerkat/bulletinboard/GenericBulletinBoardServerTest.java +++ b/bulletin-board-server/src/test/java/meerkat/bulletinboard/GenericBulletinBoardServerTest.java @@ -1,6 +1,5 @@ package meerkat.bulletinboard; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; @@ -8,7 +7,6 @@ import java.security.InvalidKeyException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; import java.security.SignatureException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; @@ -28,36 +26,57 @@ import meerkat.protobuf.BulletinBoardAPI.UnsignedBulletinBoardMessage; import static org.junit.Assert.*; import static org.hamcrest.CoreMatchers.*; -public class GenericBulletinBoardServerTest { - private BulletinBoardServer bulletinBoardServer; +public abstract class GenericBulletinBoardServerTest { + protected BulletinBoardServer bulletinBoardServer; private ECDSASignature signers[]; private Random random; private static String KEYFILE_EXAMPLE = "/certs/enduser-certs/user1-key-with-password-secret.p12"; - private static String KEYFILE_PASSWORD = "secret"; + private static String KEYFILE_EXAMPLE3 = "/certs/enduser-certs/user3-key-with-password-shh.p12"; + + private static String KEYFILE_PASSWORD1 = "secret"; + private static String KEYFILE_PASSWORD3 = "shh"; public static String CERT1_PEM_EXAMPLE = "/certs/enduser-certs/user1.crt"; + public static String CERT3_PEM_EXAMPLE = "/certs/enduser-certs/user3.crt"; -// private static String KEYFILE_EXAMPLE2 = "/certs/enduser-certs/user2-key.pem"; - - public void init(Class cls) throws InstantiationException, IllegalAccessException, CertificateException, KeyStoreException, NoSuchAlgorithmException, IOException, UnrecoverableKeyException, CommunicationException{ - bulletinBoardServer = (BulletinBoardServer) cls.newInstance(); - - bulletinBoardServer.init(File.createTempFile("meerkat-test", "db")); + /** + * + * @param cls + * @throws InstantiationException + * @throws IllegalAccessException + * @throws CertificateException + * @throws KeyStoreException + * @throws NoSuchAlgorithmException + * @throws IOException + * @throws UnrecoverableKeyException + * @throws CommunicationException + */ + public void init(String meerkatDB) throws InstantiationException, IllegalAccessException, CertificateException, KeyStoreException, NoSuchAlgorithmException, IOException, UnrecoverableKeyException, CommunicationException{ + + bulletinBoardServer.init(meerkatDB); signers = new ECDSASignature[2]; signers[0] = new ECDSASignature(); signers[1] = new ECDSASignature(); InputStream keyStream = getClass().getResourceAsStream(KEYFILE_EXAMPLE); - char[] password = KEYFILE_PASSWORD.toCharArray(); + char[] password = KEYFILE_PASSWORD1.toCharArray(); - KeyStore.Builder keyStore = signers[0].getPKCS12KeyStoreBuilder(keyStream, password); - signers[0].loadSigningCertificate(keyStore); + KeyStore.Builder keyStoreBuilder = signers[0].getPKCS12KeyStoreBuilder(keyStream, password); + signers[0].loadSigningCertificate(keyStoreBuilder); signers[0].loadVerificationCertificates(getClass().getResourceAsStream(CERT1_PEM_EXAMPLE)); + keyStream = getClass().getResourceAsStream(KEYFILE_EXAMPLE3); + password = KEYFILE_PASSWORD3.toCharArray(); + + keyStoreBuilder = signers[1].getPKCS12KeyStoreBuilder(keyStream, password); + signers[1].loadSigningCertificate(keyStoreBuilder); + + signers[1].loadVerificationCertificates(getClass().getResourceAsStream(CERT3_PEM_EXAMPLE)); + random = new Random(0); // We use insecure randomness in tests for repeatability } @@ -122,6 +141,11 @@ public class GenericBulletinBoardServerTest { if (i % 2 == 1){ signers[0].updateContent(msgBuilder.getMsg()); msgBuilder.addSig(signers[0].sign()); + + if (i % 4 == 1){ + signers[1].updateContent(msgBuilder.getMsg()); + msgBuilder.addSig(signers[1].sign()); + } } // Post message. @@ -166,6 +190,20 @@ public class GenericBulletinBoardServerTest { signers[0].initVerify(msg.getSig(0)); signers[0].updateContent(msg.getMsg()); assertTrue("Signature did not verify!", signers[0].verify()); + + if (msg.getEntryNum() % 4 == 1){ + signers[1].initVerify(msg.getSig(1)); + signers[1].updateContent(msg.getMsg()); + assertTrue("Signature did not verify!", signers[1].verify()); + + assertThat(msg.getSigCount(), is(2)); + } + else{ + assertThat(msg.getSigCount(), is(1)); + } + } + else{ + assertThat(msg.getSigCount(), is(0)); } } diff --git a/bulletin-board-server/src/test/java/meerkat/bulletinboard/SQLiteServerIntegrationTest.java b/bulletin-board-server/src/test/java/meerkat/bulletinboard/SQLiteServerIntegrationTest.java index e090529..95c916d 100644 --- a/bulletin-board-server/src/test/java/meerkat/bulletinboard/SQLiteServerIntegrationTest.java +++ b/bulletin-board-server/src/test/java/meerkat/bulletinboard/SQLiteServerIntegrationTest.java @@ -22,7 +22,7 @@ import javax.ws.rs.core.Response; public class SQLiteServerIntegrationTest { private static String PROP_GETTY_URL = "gretty.httpBaseURI"; - private static String DEFAULT_BASE_URL = "localhost:8081"; + private static String DEFAULT_BASE_URL = "http://localhost:8081"; private static String BASE_URL = System.getProperty(PROP_GETTY_URL, DEFAULT_BASE_URL); private static String SQL_SERVER_POST = "sqlserver/postmessage"; private static String SQL_SERVER_GET = "sqlserver/readmessages"; @@ -32,6 +32,7 @@ public class SQLiteServerIntegrationTest { @Before public void setup() throws Exception { + System.err.println("Registering client"); client = ClientBuilder.newClient(); client.register(ProtobufMessageBodyReader.class); client.register(ProtobufMessageBodyWriter.class); @@ -64,7 +65,11 @@ public class SQLiteServerIntegrationTest { // Test writing mechanism System.err.println("******** Testing: " + SQL_SERVER_POST); + System.err.println(BASE_URL); + System.err.println(SQL_SERVER_POST); + System.err.println(client.getConfiguration()); webTarget = client.target(BASE_URL).path(SQL_SERVER_POST); + System.err.println(webTarget.getUri()); msg = BulletinBoardMessage.newBuilder() .setMsg(UnsignedBulletinBoardMessage.newBuilder() diff --git a/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardServer.java b/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardServer.java index 298c290..da53c1f 100644 --- a/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardServer.java +++ b/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardServer.java @@ -3,8 +3,6 @@ package meerkat.bulletinboard; import meerkat.comm.CommunicationException; import meerkat.protobuf.BulletinBoardAPI.*; -import java.io.File; - /** * Created by Arbel on 07/11/15. * @@ -19,7 +17,7 @@ public interface BulletinBoardServer{ * It also establishes the connection to the DB. * @throws CommunicationException on DB connection error. */ - public void init(File meerkatDB) throws CommunicationException; + public void init(String meerkatDB) throws CommunicationException; /** * Post a message to bulletin board. From a31d88bd128debaf38734380b2b95eef6bef0394 Mon Sep 17 00:00:00 2001 From: Arbel Deutsch Peled Date: Sat, 5 Dec 2015 14:25:02 +0200 Subject: [PATCH 02/15] First implementation of simple BB Client --- .../SimpleBulletinBoardClient.java | 163 ++++++++++++++++++ ...tinBoard.java => BulletinBoardClient.java} | 20 ++- settings.gradle | 2 + 3 files changed, 178 insertions(+), 7 deletions(-) create mode 100644 bulletin-board-client/src/main/java/meerkat/bulletinboard/SimpleBulletinBoardClient.java rename meerkat-common/src/main/java/meerkat/bulletinboard/{BulletinBoard.java => BulletinBoardClient.java} (67%) diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/SimpleBulletinBoardClient.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/SimpleBulletinBoardClient.java new file mode 100644 index 0000000..aa61726 --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/SimpleBulletinBoardClient.java @@ -0,0 +1,163 @@ +package meerkat.bulletinboard; + +import com.google.protobuf.ByteString; +import meerkat.comm.CommunicationException; +import meerkat.crypto.Digest; +import meerkat.crypto.concrete.SHA256Digest; +import meerkat.protobuf.BulletinBoardAPI.*; +import meerkat.rest.*; + +import java.util.List; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; + +/** + * Created by Arbel Deutsch Peled on 05-Dec-15. + */ +public class SimpleBulletinBoardClient implements BulletinBoardClient { + + //TODO: Make this general + private static String SQL_SERVER_POST = "sqlserver/postmessage"; + private static String SQL_SERVER_GET = "sqlserver/readmessages"; + + private List meerkatDBs; + + private Client client; + + private Digest digest; + + /** + * Stores database locations and initializes the web Client + * @param meerkatDBs is the list of database locations + */ + @Override + public void init(List meerkatDBs) { + + this.meerkatDBs = meerkatDBs; + + client = ClientBuilder.newClient(); + client.register(ProtobufMessageBodyReader.class); + client.register(ProtobufMessageBodyWriter.class); + + digest = new SHA256Digest(); + + } + + /** + * Post message to all DBs + * Make only one try per DB. + * @param msg is the message, + * @return the message ID for later retrieval + * @throws CommunicationException + */ + @Override + public MessageID postMessage(BulletinBoardMessage msg) throws CommunicationException { + + WebTarget webTarget; + Response response; + + // Post message to all databases + try { + for (String db : meerkatDBs) { + webTarget = client.target(db).path(SQL_SERVER_POST); + response = webTarget.request(Constants.MEDIATYPE_PROTOBUF).post(Entity.entity(msg, Constants.MEDIATYPE_PROTOBUF)); + + // Only consider valid responses + if (response.getStatusInfo() == Response.Status.OK + || response.getStatusInfo() == Response.Status.CREATED) { + response.readEntity(BoolMsg.class).getValue(); + } + } + } catch (Exception e) { // Occurs only when server replies with valid status but invalid data + throw new CommunicationException("Error accessing database: " + e.getMessage()); + } + + // Calculate the correct message ID and return it + digest.reset(); + digest.update(msg.getMsg()); + return MessageID.newBuilder().setID(ByteString.copyFrom(digest.digest())).build(); + } + + /** + * Access each database and search for a given message ID + * Return the number of databases in which the message was found + * Only try once per DB + * Ignore communication exceptions in specific databases + * @param id is the requested message ID + * @return the number of DBs in which retrieval was successful + */ + @Override + public float getRedundancy(MessageID id) { + WebTarget webTarget; + Response response; + + MessageFilterList filterList = MessageFilterList.newBuilder() + .addFilter(MessageFilter.newBuilder() + .setType(FilterType.MSG_ID) + .setId(id.getID()) + .build()) + .build(); + + int count = 0; + + for (String db : meerkatDBs) { + try { + webTarget = client.target(db).path(SQL_SERVER_GET); + + response = webTarget.request(Constants.MEDIATYPE_PROTOBUF).post(Entity.entity(filterList, Constants.MEDIATYPE_PROTOBUF)); + + if (response.readEntity(BulletinBoardMessageList.class).getMessageCount() > 0){ + count++; + } + + } catch (Exception e) {} + } + + return count; + } + + /** + * Go through the DBs and try to retrieve messages according to the specified filter + * If at the operation is successful for some DB: return the results and stop iterating + * If no operation is successful: return null (NOT blank list) + * @param filterList return only messages that match the filters (null means no filtering). + * @return + */ + @Override + public List readMessages(MessageFilterList filterList) { + WebTarget webTarget; + Response response; + BulletinBoardMessageList messageList; + + // Replace null filter list with blank one. + if (filterList == null){ + filterList = MessageFilterList.newBuilder().build(); + } + + for (String db : meerkatDBs) { + try { + webTarget = client.target(db).path(SQL_SERVER_GET); + + response = webTarget.request(Constants.MEDIATYPE_PROTOBUF).post(Entity.entity(filterList, Constants.MEDIATYPE_PROTOBUF)); + + messageList =response.readEntity(BulletinBoardMessageList.class); + + if (messageList != null){ + return messageList.getMessageList(); + } + + } catch (Exception e) {} + } + + return null; + } + + @Override + public void registerNewMessageCallback(MessageCallback callback, MessageFilterList filterList) { + callback.handleNewMessage(readMessages(filterList)); + } +} diff --git a/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoard.java b/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardClient.java similarity index 67% rename from meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoard.java rename to meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardClient.java index 0efd6a7..2e466b3 100644 --- a/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoard.java +++ b/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardClient.java @@ -8,7 +8,14 @@ import java.util.List; /** * Created by talm on 24/10/15. */ -public interface BulletinBoard { +public interface BulletinBoardClient { + + /** + * Initialize the client to use some specified servers + * @param meerkatDBs is the list of database locations + */ + void init(List meerkatDBs); + /** * Post a message to the bulletin board * @param msg @@ -27,22 +34,21 @@ public interface BulletinBoard { * Note that if messages haven't been "fully posted", this might return a different * set of messages in different calls. However, messages that are fully posted * are guaranteed to be included. - * @param filter return only messages that match the filter (null means no filtering). - * @param max maximum number of messages to return (0=no limit) + * @param filterList return only messages that match the filters (null means no filtering). * @return */ - List readMessages(MessageFilter filter, int max); + List readMessages(MessageFilterList filterList); interface MessageCallback { - void handleNewMessage(BulletinBoardMessage msg); + void handleNewMessage(List msg); } /** * Register a callback that will be called with each new message that is posted. * The callback will be called only once for each message. * @param callback - * @param filter only call back for messages that match the filter. + * @param filterList only call back for messages that match the filter. */ - void registerNewMessageCallback(MessageCallback callback, MessageFilter filter); + void registerNewMessageCallback(MessageCallback callback, MessageFilterList filterList); } diff --git a/settings.gradle b/settings.gradle index e4ef054..99f4c5e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,3 +3,5 @@ include 'voting-booth' include 'bulletin-board-server' include 'polling-station' include 'restful-api-common' +include 'bulletin-board-client' + From 679d18f4a2c08685861751afeae8770d05a90b39 Mon Sep 17 00:00:00 2001 From: Arbel Deutsch Peled Date: Sun, 6 Dec 2015 20:33:45 +0200 Subject: [PATCH 03/15] Added BB client intergration test (broken) Fixed MsgID retrieval in BB server --- build.gradle | 1 - .../SimpleBulletinBoardClient.java | 4 +- .../BulletinBoardClientIntegrationTest.java | 109 ++++++++++++++++++ bulletin-board-server/build.gradle | 16 +-- .../sqlserver/SQLiteBulletinBoardServer.java | 2 +- .../util/BulletinBoardMessageComparator.java | 49 ++++++++ 6 files changed, 170 insertions(+), 11 deletions(-) create mode 100644 bulletin-board-client/src/test/java/BulletinBoardClientIntegrationTest.java create mode 100644 meerkat-common/src/main/java/meerkat/util/BulletinBoardMessageComparator.java diff --git a/build.gradle b/build.gradle index 2791de4..9f070e3 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,4 @@ - subprojects { proj -> proj.afterEvaluate { // Used to generate initial maven-dir layout diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/SimpleBulletinBoardClient.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/SimpleBulletinBoardClient.java index aa61726..9cf6dd4 100644 --- a/bulletin-board-client/src/main/java/meerkat/bulletinboard/SimpleBulletinBoardClient.java +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/SimpleBulletinBoardClient.java @@ -102,7 +102,7 @@ public class SimpleBulletinBoardClient implements BulletinBoardClient { .build()) .build(); - int count = 0; + float count = 0; for (String db : meerkatDBs) { try { @@ -117,7 +117,7 @@ public class SimpleBulletinBoardClient implements BulletinBoardClient { } catch (Exception e) {} } - return count; + return count / ((float) meerkatDBs.size()); } /** diff --git a/bulletin-board-client/src/test/java/BulletinBoardClientIntegrationTest.java b/bulletin-board-client/src/test/java/BulletinBoardClientIntegrationTest.java new file mode 100644 index 0000000..f9699c1 --- /dev/null +++ b/bulletin-board-client/src/test/java/BulletinBoardClientIntegrationTest.java @@ -0,0 +1,109 @@ +import com.google.protobuf.ByteString; +import meerkat.bulletinboard.BulletinBoardClient; +import meerkat.bulletinboard.SimpleBulletinBoardClient; +import meerkat.comm.CommunicationException; +import meerkat.protobuf.BulletinBoardAPI.*; +import meerkat.protobuf.Crypto; + +import meerkat.util.BulletinBoardMessageComparator; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.*; +import static org.hamcrest.CoreMatchers.*; + +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; + +/** + * Created by Arbel Deutsch Peled on 05-Dec-15. + */ +public class BulletinBoardClientIntegrationTest { + + private BulletinBoardClient bulletinBoardClient; + + private static String PROP_GETTY_URL = "gretty.httpBaseURI"; + private static String DEFAULT_BASE_URL = "http://localhost:8081"; + private static String BASE_URL = System.getProperty(PROP_GETTY_URL, DEFAULT_BASE_URL); + + @Before + public void init(){ + + bulletinBoardClient = new SimpleBulletinBoardClient(); + + List testDB = new LinkedList(); + testDB.add(BASE_URL); + + bulletinBoardClient.init(testDB); + + } + + @Test + public void postTest(){ + + byte[] b1 = {(byte) 1, (byte) 2, (byte) 3, (byte) 4}; + byte[] b2 = {(byte) 11, (byte) 12, (byte) 13, (byte) 14}; + byte[] b3 = {(byte) 21, (byte) 22, (byte) 23, (byte) 24}; + byte[] b4 = {(byte) 4, (byte) 5, (byte) 100, (byte) -50, (byte) 0}; + + BulletinBoardMessage msg; + + MessageFilterList filterList; + List msgList; + + MessageID messageID; + + Comparator msgComparator = new BulletinBoardMessageComparator(); + + msg = BulletinBoardMessage.newBuilder() + .setMsg(UnsignedBulletinBoardMessage.newBuilder() + .addTag("Signature") + .addTag("Trustee") + .setData(ByteString.copyFrom(b1)) + .build()) + .addSig(Crypto.Signature.newBuilder() + .setType(Crypto.SignatureType.DSA) + .setData(ByteString.copyFrom(b2)) + .setSignerId(ByteString.copyFrom(b3)) + .build()) + .addSig(Crypto.Signature.newBuilder() + .setType(Crypto.SignatureType.ECDSA) + .setData(ByteString.copyFrom(b3)) + .setSignerId(ByteString.copyFrom(b2)) + .build()) + .build(); + + try { + messageID = bulletinBoardClient.postMessage(msg); + } catch (CommunicationException e) { + System.err.println("Error posting to BB Server: " + e.getMessage()); + assert false; + return; + } + + assertThat(bulletinBoardClient.getRedundancy(messageID), is((float) 1.00)); + + filterList = MessageFilterList.newBuilder() + .addFilter( + MessageFilter.newBuilder() + .setType(FilterType.TAG) + .setTag("Signature") + .build() + ) +// .addFilter( +// MessageFilter.newBuilder() +// .setType(FilterType.TAG) +// .setTag("Trustee") +// .build() +// ) + .build(); + + msgList = bulletinBoardClient.readMessages(filterList); + + assertThat(msgList.size(), is(1)); + + assertThat(msgComparator.compare(msgList.iterator().next(), msg), is(0)); + + } + +} diff --git a/bulletin-board-server/build.gradle b/bulletin-board-server/build.gradle index 790f0d3..593dfbc 100644 --- a/bulletin-board-server/build.gradle +++ b/bulletin-board-server/build.gradle @@ -2,9 +2,10 @@ plugins { id "us.kirchmeier.capsule" version "1.0.1" id 'com.google.protobuf' version '0.7.0' - id "org.akhikhl.gretty" version "1.2.4" + id 'org.akhikhl.gretty' version "1.2.4" } +apply plugin: 'org.akhikhl.gretty' apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'idea' @@ -45,7 +46,9 @@ dependencies { // Jersey for RESTful API compile 'org.glassfish.jersey.containers:jersey-container-servlet:2.22.+' compile 'org.xerial:sqlite-jdbc:3.7.+' - + + // Servlets + compile 'javax.servlet:javax.servlet-api:3.0.+' // Logging compile 'org.slf4j:slf4j-api:1.7.7' @@ -68,13 +71,11 @@ test { exclude '**/*IntegrationTest*' } -task debugIntegrationTest(type: Test){ - include '**/*IntegrationTest*' - debug = true -} - task integrationTest(type: Test) { include '**/*IntegrationTest*' +// debug = true + outputs.upToDateWhen { false } + } gretty { @@ -82,6 +83,7 @@ gretty { contextPath = '/' integrationTestTask = 'integrationTest' loggingLevel = 'TRACE' + debugPort = 5006 } diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/SQLiteBulletinBoardServer.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/SQLiteBulletinBoardServer.java index d4aa881..bfbbc1a 100644 --- a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/SQLiteBulletinBoardServer.java +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/SQLiteBulletinBoardServer.java @@ -167,7 +167,7 @@ public class SQLiteBulletinBoardServer extends BulletinBoardSQLServer { sqlSuffix += " LIMIT = ?"; break; case FilterType.MSG_ID_VALUE: - sql += " MsgTableMsgId = ?"; + sql += " MsgTable.MsgId = ?"; break; case FilterType.SIGNER_ID_VALUE: sql += " SignatureTable.SignerId = ?"; diff --git a/meerkat-common/src/main/java/meerkat/util/BulletinBoardMessageComparator.java b/meerkat-common/src/main/java/meerkat/util/BulletinBoardMessageComparator.java new file mode 100644 index 0000000..77a6663 --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/util/BulletinBoardMessageComparator.java @@ -0,0 +1,49 @@ +package meerkat.util; + +import meerkat.protobuf.BulletinBoardAPI; +import meerkat.protobuf.BulletinBoardAPI.*; +import meerkat.protobuf.Crypto.*; + +import java.util.Comparator; +import java.util.List; + +/** + * Created by Arbel Deutsch Peled on 05-Dec-15. + * This class implements a comparison between BulletinBoardMessage instances that disregards: + * 1. The entry number (since this can be different between database instances) + * 2. The order of the signatures + */ +public class BulletinBoardMessageComparator implements Comparator { + + /** + * Compare the messages + * @param msg1 + * @param msg2 + * @return 0 if the messages are equivalent (see above) and -1 otherwise. + */ + @Override + public int compare(BulletinBoardMessage msg1, BulletinBoardMessage msg2) { + + List msg1Sigs = msg1.getSigList(); + List msg2Sigs = msg2.getSigList(); + + // Compare unsigned message + if (!msg1.getMsg().equals(msg2.getMsg())){ + return -1; + } + + // Compare signatures + + if (msg1Sigs.size() != msg2Sigs.size()){ + return -1; + } + + for (Signature sig : msg1Sigs){ + if (!msg2Sigs.contains(sig)) { + return -1; + } + } + + return 0; + } +} From 0b847dcaf4ac104f06a6b8c6c1bd59e098017610 Mon Sep 17 00:00:00 2001 From: Arbel Deutsch Peled Date: Tue, 8 Dec 2015 08:17:30 +0200 Subject: [PATCH 04/15] Added a new version of the BB Server that allows for search of messages containing multiple tags and/or signatures. Added tests for both Server types. --- .../EnhancedSQLiteBulletinBoardServer.java | 190 ++++++++++ ...EnhancedSQLiteBulletinBoardServerTest.java | 54 +++ .../GenericBulletinBoardServerTest.java | 327 +++++++++++++----- .../SQLiteBulletinBoardServerTest.java | 67 ++++ 4 files changed, 552 insertions(+), 86 deletions(-) create mode 100644 bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/EnhancedSQLiteBulletinBoardServer.java create mode 100644 bulletin-board-server/src/test/java/meerkat/bulletinboard/EnhancedSQLiteBulletinBoardServerTest.java create mode 100644 bulletin-board-server/src/test/java/meerkat/bulletinboard/SQLiteBulletinBoardServerTest.java diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/EnhancedSQLiteBulletinBoardServer.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/EnhancedSQLiteBulletinBoardServer.java new file mode 100644 index 0000000..6baac36 --- /dev/null +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/EnhancedSQLiteBulletinBoardServer.java @@ -0,0 +1,190 @@ +package meerkat.bulletinboard.sqlserver; + +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.comm.CommunicationException; +import meerkat.protobuf.BulletinBoardAPI; +import meerkat.protobuf.BulletinBoardAPI.*; +import meerkat.protobuf.Crypto.*; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Created by Arbel Deutsch Peled on 07-Dec-15. + * This server version allows for reading of messages with several tags and/or signatures + * The superclass only allows one constraint per type. + */ +public class EnhancedSQLiteBulletinBoardServer extends SQLiteBulletinBoardServer { + + /** + * This class implements a comparator for the MessageFilter class + * The comparison is done solely by comparing the type of the filter + * This is used to sort the filters by type + */ + public class FilterTypeComparator implements Comparator{ + + @Override + public int compare(MessageFilter filter1, MessageFilter filter2) { + return filter1.getTypeValue() - filter2.getTypeValue(); + } + } + + @Override + public BulletinBoardMessageList readMessages(MessageFilterList filterList) throws CommunicationException { + + PreparedStatement pstmt; + ResultSet messages, signatures; + + long entryNum; + BulletinBoardMessageList.Builder resultListBuilder = BulletinBoardMessageList.newBuilder(); + BulletinBoardMessage.Builder messageBuilder; + + String sql; + String sqlSuffix = ""; + + List filters = new ArrayList(filterList.getFilterList()); + int i; + + boolean tagsRequired = false; + boolean signaturesRequired = false; + + boolean isFirstFilter = true; + + Collections.sort(filters, new FilterTypeComparator()); + + // Check if Tag/Signature tables are required for filtering purposes. + + sql = "SELECT MsgTable.EntryNum, MsgTable.Msg FROM MsgTable"; + + // Add conditions. + + if (!filters.isEmpty()){ + sql += " WHERE"; + + for (MessageFilter filter : filters){ + + if (filter.getType().getNumber() != FilterType.MAX_MESSAGES_VALUE){ + if (isFirstFilter){ + isFirstFilter = false; + } else{ + sql += " AND"; + } + } + + switch (filter.getType().getNumber()){ + case FilterType.EXACT_ENTRY_VALUE: + sql += " MsgTable.EntryNum = ?"; + break; + case FilterType.MAX_ENTRY_VALUE: + sql += " MsgTable.EntryNum <= ?"; + break; + case FilterType.MAX_MESSAGES_VALUE: + sqlSuffix += " LIMIT = ?"; + break; + case FilterType.MSG_ID_VALUE: + sql += " MsgTable.MsgId = ?"; + break; + case FilterType.SIGNER_ID_VALUE: + sql += " EXISTS (SELECT 1 FROM SignatureTable" + + " WHERE SignatureTable.SignerId = ? AND SignatureTable.EntryNum = MsgTable.EntryNum)"; + break; + case FilterType.TAG_VALUE: + sql += " EXISTS (SELECT 1 FROM TagTable" + + " INNER JOIN MsgTagTable ON TagTable.TagId = MsgTagTable.TagId" + + " WHERE TagTable.Tag = ? AND MsgTagTable.EntryNum = MsgTable.EntryNum)"; + break; + } + } + + sql += sqlSuffix; + } + + // Make query. + + try { + pstmt = connection.prepareStatement(sql); + + // Specify values for filters. + + i = 1; + for (MessageFilter filter : filters){ + + switch (filter.getType().getNumber()){ + + case FilterType.EXACT_ENTRY_VALUE: // Go through. + case FilterType.MAX_ENTRY_VALUE: + pstmt.setLong(i, filter.getEntry()); + i++; + break; + + case FilterType.MSG_ID_VALUE: // Go through. + case FilterType.SIGNER_ID_VALUE: + pstmt.setBytes(i, filter.getId().toByteArray()); + i++; + break; + + case FilterType.TAG_VALUE: + pstmt.setString(i, filter.getTag()); + i++; + break; + + // The max-messages condition is applied as a suffix. Therefore, it is treated differently. + case FilterType.MAX_MESSAGES_VALUE: + pstmt.setLong(filters.size(), filter.getMaxMessages()); + i++; + break; + + } + } + + // Run query. + + messages = pstmt.executeQuery(); + + // Compile list of messages. + + sql = "SELECT Signature FROM SignatureTable WHERE EntryNum = ?"; + pstmt = connection.prepareStatement(sql); + + while (messages.next()){ + + // Get entry number and retrieve signatures. + + entryNum = messages.getLong(1); + pstmt.setLong(1, entryNum); + signatures = pstmt.executeQuery(); + + // Create message and append signatures. + + messageBuilder = BulletinBoardMessage.newBuilder() + .setEntryNum(entryNum) + .setMsg(UnsignedBulletinBoardMessage.parseFrom(messages.getBytes(2))); + + while (signatures.next()){ + messageBuilder.addSig(Signature.parseFrom(signatures.getBytes(1))); + } + + // Finalize message and add to message list. + + resultListBuilder.addMessage(messageBuilder.build()); + + } + + pstmt.close(); + + } catch (SQLException e){ + throw new CommunicationException("Error reading messages from DB: " + e.getMessage()); + } catch (InvalidProtocolBufferException e) { + throw new CommunicationException("Invalid data from DB: " + e.getMessage()); + } + + //Combine results and return. + + return resultListBuilder.build(); + } +} diff --git a/bulletin-board-server/src/test/java/meerkat/bulletinboard/EnhancedSQLiteBulletinBoardServerTest.java b/bulletin-board-server/src/test/java/meerkat/bulletinboard/EnhancedSQLiteBulletinBoardServerTest.java new file mode 100644 index 0000000..8d643e4 --- /dev/null +++ b/bulletin-board-server/src/test/java/meerkat/bulletinboard/EnhancedSQLiteBulletinBoardServerTest.java @@ -0,0 +1,54 @@ +package meerkat.bulletinboard; + +import meerkat.bulletinboard.sqlserver.EnhancedSQLiteBulletinBoardServer; +import meerkat.comm.CommunicationException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; + +import static org.junit.Assert.fail; + +/** + * Created by Arbel Deutsch Peled on 07-Dec-15. + */ +public class EnhancedSQLiteBulletinBoardServerTest{ + + private String testFilename = "EnhancedSQLiteDBTest.db"; + + private GenericBulletinBoardServerTest serverTest; + + @Before + public void init(){ + + File old = new File(testFilename); + old.delete(); + + BulletinBoardServer bulletinBoardServer = new EnhancedSQLiteBulletinBoardServer(); + try { + bulletinBoardServer.init(testFilename); + + } catch (CommunicationException e) { + System.err.println("Failed to initialize server " + e.getMessage()); + fail("Failed to initialize server " + e.getMessage()); + return; + } + + serverTest = new GenericBulletinBoardServerTest(); + serverTest.init(bulletinBoardServer); + } + + @Test + public void bulkTest() { + System.err.println("Testing Enhanced SQLite Server"); + serverTest.testInsert(); + serverTest.testSimpleTagAndSignature(); + } + + @After + public void close() { + serverTest.close(); + } + +} diff --git a/bulletin-board-server/src/test/java/meerkat/bulletinboard/GenericBulletinBoardServerTest.java b/bulletin-board-server/src/test/java/meerkat/bulletinboard/GenericBulletinBoardServerTest.java index fb26df8..eca9273 100644 --- a/bulletin-board-server/src/test/java/meerkat/bulletinboard/GenericBulletinBoardServerTest.java +++ b/bulletin-board-server/src/test/java/meerkat/bulletinboard/GenericBulletinBoardServerTest.java @@ -26,12 +26,14 @@ import meerkat.protobuf.BulletinBoardAPI.UnsignedBulletinBoardMessage; import static org.junit.Assert.*; import static org.hamcrest.CoreMatchers.*; -public abstract class GenericBulletinBoardServerTest { +public class GenericBulletinBoardServerTest { + protected BulletinBoardServer bulletinBoardServer; private ECDSASignature signers[]; + private ByteString[] signerIDs; private Random random; - + private static String KEYFILE_EXAMPLE = "/certs/enduser-certs/user1-key-with-password-secret.p12"; private static String KEYFILE_EXAMPLE3 = "/certs/enduser-certs/user3-key-with-password-shh.p12"; @@ -40,10 +42,9 @@ public abstract class GenericBulletinBoardServerTest { public static String CERT1_PEM_EXAMPLE = "/certs/enduser-certs/user1.crt"; public static String CERT3_PEM_EXAMPLE = "/certs/enduser-certs/user3.crt"; - + /** - * - * @param cls + * @param bulletinBoardServer is an initialized server. * @throws InstantiationException * @throws IllegalAccessException * @throws CertificateException @@ -53,19 +54,23 @@ public abstract class GenericBulletinBoardServerTest { * @throws UnrecoverableKeyException * @throws CommunicationException */ - public void init(String meerkatDB) throws InstantiationException, IllegalAccessException, CertificateException, KeyStoreException, NoSuchAlgorithmException, IOException, UnrecoverableKeyException, CommunicationException{ + public void init(BulletinBoardServer bulletinBoardServer) { - bulletinBoardServer.init(meerkatDB); + this.bulletinBoardServer = bulletinBoardServer; signers = new ECDSASignature[2]; + signerIDs = new ByteString[signers.length]; signers[0] = new ECDSASignature(); signers[1] = new ECDSASignature(); InputStream keyStream = getClass().getResourceAsStream(KEYFILE_EXAMPLE); char[] password = KEYFILE_PASSWORD1.toCharArray(); - - KeyStore.Builder keyStoreBuilder = signers[0].getPKCS12KeyStoreBuilder(keyStream, password); - signers[0].loadSigningCertificate(keyStoreBuilder); + + KeyStore.Builder keyStoreBuilder = null; + try { + keyStoreBuilder = signers[0].getPKCS12KeyStoreBuilder(keyStream, password); + + signers[0].loadSigningCertificate(keyStoreBuilder); signers[0].loadVerificationCertificates(getClass().getResourceAsStream(CERT1_PEM_EXAMPLE)); @@ -76,6 +81,40 @@ public abstract class GenericBulletinBoardServerTest { signers[1].loadSigningCertificate(keyStoreBuilder); signers[1].loadVerificationCertificates(getClass().getResourceAsStream(CERT3_PEM_EXAMPLE)); + + } catch (IOException e) { + System.err.println("Failed reading from signature file " + e.getMessage()); + fail("Failed reading from signature file " + e.getMessage()); + } catch (CertificateException e) { + System.err.println("Failed reading certificate " + e.getMessage()); + fail("Failed reading certificate " + e.getMessage()); + } catch (KeyStoreException e) { + System.err.println("Failed reading keystore " + e.getMessage()); + fail("Failed reading keystore " + e.getMessage()); + } catch (NoSuchAlgorithmException e) { + System.err.println("Couldn't find signing algorithm " + e.getMessage()); + fail("Couldn't find signing algorithm " + e.getMessage()); + } catch (UnrecoverableKeyException e) { + System.err.println("Couldn't find signing key " + e.getMessage()); + fail("Couldn't find signing key " + e.getMessage()); + } + + // Get signer IDs + // TODO: remove this after creation of getSignerID method + + UnsignedBulletinBoardMessage msg = UnsignedBulletinBoardMessage.newBuilder().build(); + + for (int i = 0 ; i < signers.length ; i++) { + + try { + signers[i].updateContent(UnsignedBulletinBoardMessage.newBuilder().build()); + signerIDs[i] = signers[i].sign().getSignerId(); + } catch (SignatureException e) { + System.err.println("Error signing message" + e.getMessage()); + fail("Error signing message" + e.getMessage()); + return; + } + } random = new Random(0); // We use insecure randomness in tests for repeatability } @@ -87,96 +126,134 @@ public abstract class GenericBulletinBoardServerTest { private String randomString(){ return new BigInteger(130, random).toString(32); } - - public void bulkTest() throws CommunicationException, SignatureException, InvalidKeyException, CertificateException, IOException{ - - final int TAG_NUM = 5; // Number of tags. - final int MESSAGE_NUM = 32; // Number of messages (2^TAG_NUM). + + private final int TAG_NUM = 5; // Number of tags. + private final int MESSAGE_NUM = 32; // Number of messages (2^TAG_NUM). + + + private String[] tags; + private byte[][] data; + + /** + * Tests writing of several messages with multiple tags and signatures. + * @throws CommunicationException + * @throws SignatureException + * @throws InvalidKeyException + * @throws CertificateException + * @throws IOException + */ + public void testInsert() { + final int BYTES_PER_MESSAGE_DATA = 50; // Message size. - - String[] tags = new String[TAG_NUM]; - byte[][] data = new byte[MESSAGE_NUM][BYTES_PER_MESSAGE_DATA]; - + + tags = new String[TAG_NUM]; + data = new byte[MESSAGE_NUM][BYTES_PER_MESSAGE_DATA]; + UnsignedBulletinBoardMessage.Builder unsignedMsgBuilder; BulletinBoardMessage.Builder msgBuilder; - - int i,j; - + + int i, j; + // Generate random data. - - for (i = 1 ; i <= MESSAGE_NUM ; i++){ - for (j = 0 ; j < BYTES_PER_MESSAGE_DATA ; j++){ - data[i-1][j] = randomByte(); + + for (i = 1; i <= MESSAGE_NUM; i++) { + for (j = 0; j < BYTES_PER_MESSAGE_DATA; j++) { + data[i - 1][j] = randomByte(); } } - - for (i = 0 ; i < TAG_NUM ; i++){ + + for (i = 0; i < TAG_NUM; i++) { tags[i] = randomString(); } - + // Build messages. - - for (i = 1 ; i <= MESSAGE_NUM ; i++){ + + for (i = 1; i <= MESSAGE_NUM; i++) { unsignedMsgBuilder = UnsignedBulletinBoardMessage.newBuilder() - .setData(ByteString.copyFrom(data[i-1])); - + .setData(ByteString.copyFrom(data[i - 1])); + // Add tags based on bit-representation of message number. - + int copyI = i; - for (j = 0 ; j < TAG_NUM ; j++){ - if (copyI % 2 == 1){ + for (j = 0; j < TAG_NUM; j++) { + if (copyI % 2 == 1) { unsignedMsgBuilder.addTag(tags[j]); } - + copyI >>>= 1; } - + // Build message. - + msgBuilder = BulletinBoardMessage.newBuilder() .setMsg(unsignedMsgBuilder.build()); - + // Add signatures. - - if (i % 2 == 1){ - signers[0].updateContent(msgBuilder.getMsg()); - msgBuilder.addSig(signers[0].sign()); - - if (i % 4 == 1){ - signers[1].updateContent(msgBuilder.getMsg()); - msgBuilder.addSig(signers[1].sign()); - } - } - - // Post message. - - bulletinBoardServer.postMessage(msgBuilder.build()); + + try { + + if (i % 2 == 1) { + signers[0].updateContent(msgBuilder.getMsg()); + msgBuilder.addSig(signers[0].sign()); + + if (i % 4 == 1) { + signers[1].updateContent(msgBuilder.getMsg()); + msgBuilder.addSig(signers[1].sign()); + } + } + + } catch (SignatureException e) { + fail(e.getMessage()); + } + + // Post message. + + try { + bulletinBoardServer.postMessage(msgBuilder.build()); + } catch (CommunicationException e) { + fail(e.getMessage()); + } } - + } + + /** + * Tests retrieval of messages written in {@Link #testInsert()} + * Only queries using one tag filter + */ + public void testSimpleTagAndSignature(){ + + List messages; + // Check tag mechanism - for (i = 0 ; i < TAG_NUM ; i++){ + for (int i = 0 ; i < TAG_NUM ; i++){ // Retrieve messages having tag i - - List messages = - bulletinBoardServer.readMessages( - MessageFilterList.newBuilder() - .addFilter(MessageFilter.newBuilder() - .setType(FilterType.TAG) - .setTag(tags[i]) + + try { + + messages = bulletinBoardServer.readMessages( + MessageFilterList.newBuilder() + .addFilter(MessageFilter.newBuilder() + .setType(FilterType.TAG) + .setTag(tags[i]) + .build() + ) .build() - ) - .build() ) .getMessageList(); - + + } catch (CommunicationException e) { + fail(e.getMessage()); + return; + } + // Assert that the number of retrieved messages is correct. assertThat(messages.size(), is(MESSAGE_NUM / 2)); // Assert the identity of the messages. - + for (BulletinBoardMessage msg : messages){ // Assert serial number and raw data. @@ -185,34 +262,112 @@ public abstract class GenericBulletinBoardServerTest { assertThat(msg.getMsg().getData().toByteArray(), is(data[(int) msg.getEntryNum() - 1])); // Assert signatures. - - if (msg.getEntryNum() % 2 == 1){ - signers[0].initVerify(msg.getSig(0)); - signers[0].updateContent(msg.getMsg()); - assertTrue("Signature did not verify!", signers[0].verify()); - - if (msg.getEntryNum() % 4 == 1){ - signers[1].initVerify(msg.getSig(1)); - signers[1].updateContent(msg.getMsg()); - assertTrue("Signature did not verify!", signers[1].verify()); - - assertThat(msg.getSigCount(), is(2)); - } - else{ - assertThat(msg.getSigCount(), is(1)); - } - } - else{ - assertThat(msg.getSigCount(), is(0)); + + try { + + if (msg.getEntryNum() % 2 == 1) { + signers[0].initVerify(msg.getSig(0)); + signers[0].updateContent(msg.getMsg()); + assertTrue("Signature did not verify!", signers[0].verify()); + + if (msg.getEntryNum() % 4 == 1) { + signers[1].initVerify(msg.getSig(1)); + signers[1].updateContent(msg.getMsg()); + assertTrue("Signature did not verify!", signers[1].verify()); + + assertThat(msg.getSigCount(), is(2)); + } else { + assertThat(msg.getSigCount(), is(1)); + } + } else { + assertThat(msg.getSigCount(), is(0)); + } + } catch (Exception e) { + fail(e.getMessage()); } } } } + + /** + * Tests retrieval of messages written in {@Link #testInsert()} using multiple tags/signature filters. + */ + public void testEnhancedTagsAndSignatures(){ + + List messages; + MessageFilterList.Builder filterListBuilder = MessageFilterList.newBuilder(); + + int expectedMsgCount = MESSAGE_NUM; + + // Check multiple tag filters. + + for (int i = 0 ; i < TAG_NUM ; i++) { + + filterListBuilder.addFilter( + MessageFilter.newBuilder() + .setType(FilterType.TAG) + .setTag(tags[i]) + .build() + ); + + try { + messages = bulletinBoardServer.readMessages(filterListBuilder.build()).getMessageList(); + } catch (CommunicationException e) { + System.err.println("Failed retrieving multi-tag messages from DB: " + e.getMessage()); + fail("Failed retrieving multi-tag messages from DB: " + e.getMessage()); + return; + } + + expectedMsgCount /= 2; + + assertThat(messages.size(), is(expectedMsgCount)); + + for (BulletinBoardMessage msg : messages) { + for (int j = 0 ; j < i ; j++) { + assertThat((msg.getEntryNum() >>> j) % 2, is((long) 1)); + } + } + } + + // Check multiple signature filters. + + filterListBuilder = MessageFilterList.newBuilder() + .addFilter(MessageFilter.newBuilder() + .setType(FilterType.SIGNER_ID) + .setId(signerIDs[0]) + .build()) + .addFilter(MessageFilter.newBuilder() + .setType(FilterType.SIGNER_ID) + .setId(signerIDs[1]) + .build() + ); + + try { + messages = bulletinBoardServer.readMessages(filterListBuilder.build()).getMessageList(); + } catch (CommunicationException e) { + System.err.println("Failed retrieving multi-signature message from DB: " + e.getMessage()); + fail("Failed retrieving multi-signature message from DB: " + e.getMessage()); + return; + } + + assertThat(messages.size(), is(MESSAGE_NUM / 4)); + + for (BulletinBoardMessage message : messages) { + assertThat(message.getEntryNum() % 4, is((long) 1)); + } + + } public void close(){ signers[0].clearSigningKey(); signers[1].clearSigningKey(); + try { + bulletinBoardServer.close(); + } catch (CommunicationException e) { + System.err.println("Error closing server " + e.getMessage()); + fail("Error closing server " + e.getMessage()); + } } } diff --git a/bulletin-board-server/src/test/java/meerkat/bulletinboard/SQLiteBulletinBoardServerTest.java b/bulletin-board-server/src/test/java/meerkat/bulletinboard/SQLiteBulletinBoardServerTest.java new file mode 100644 index 0000000..1981ea3 --- /dev/null +++ b/bulletin-board-server/src/test/java/meerkat/bulletinboard/SQLiteBulletinBoardServerTest.java @@ -0,0 +1,67 @@ +package meerkat.bulletinboard; + +import meerkat.bulletinboard.sqlserver.SQLiteBulletinBoardServer; +import meerkat.comm.CommunicationException; +import meerkat.protobuf.*; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.security.*; +import java.security.cert.CertificateException; + +import static org.junit.Assert.fail; + +/** + * Created by Arbel Deutsch Peled on 07-Dec-15. + */ +public class SQLiteBulletinBoardServerTest{ + + private String testFilename = "SQLiteDBTest.db"; + + private GenericBulletinBoardServerTest serverTest; + + @Before + public void init(){ + + File old = new File(testFilename); + old.delete(); + + BulletinBoardServer bulletinBoardServer = new SQLiteBulletinBoardServer(); + try { + bulletinBoardServer.init(testFilename); + + } catch (CommunicationException e) { + System.err.println(e.getMessage()); + fail(e.getMessage()); + return; + } + + serverTest = new GenericBulletinBoardServerTest(); + try { + serverTest.init(bulletinBoardServer); + } catch (Exception e) { + System.err.println(e.getMessage()); + fail(e.getMessage()); + } + } + + @Test + public void bulkTest() { + try { + serverTest.testInsert(); + serverTest.testSimpleTagAndSignature(); + } catch (Exception e) { + System.err.println(e.getMessage()); + fail(e.getMessage()); + } + } + + @After + public void close() { + serverTest.close(); + } + +} From 3f21f30f3585274ecf4ba1f9156522489849c63b Mon Sep 17 00:00:00 2001 From: Arbel Deutsch Peled Date: Tue, 8 Dec 2015 09:11:22 +0200 Subject: [PATCH 05/15] Added getSignerID method to signatures. Added timing output for Server tests. --- .../GenericBulletinBoardServerTest.java | 75 +++++++++++-------- .../SQLiteBulletinBoardServerTest.java | 25 +++++++ .../java/meerkat/crypto/DigitalSignature.java | 5 ++ .../crypto/concrete/ECDSASignature.java | 5 ++ 4 files changed, 79 insertions(+), 31 deletions(-) diff --git a/bulletin-board-server/src/test/java/meerkat/bulletinboard/GenericBulletinBoardServerTest.java b/bulletin-board-server/src/test/java/meerkat/bulletinboard/GenericBulletinBoardServerTest.java index eca9273..3572a42 100644 --- a/bulletin-board-server/src/test/java/meerkat/bulletinboard/GenericBulletinBoardServerTest.java +++ b/bulletin-board-server/src/test/java/meerkat/bulletinboard/GenericBulletinBoardServerTest.java @@ -2,6 +2,8 @@ package meerkat.bulletinboard; import java.io.IOException; import java.io.InputStream; +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadMXBean; import java.math.BigInteger; import java.security.InvalidKeyException; import java.security.KeyStore; @@ -30,7 +32,6 @@ public class GenericBulletinBoardServerTest { protected BulletinBoardServer bulletinBoardServer; private ECDSASignature signers[]; - private ByteString[] signerIDs; private Random random; @@ -43,6 +44,14 @@ public class GenericBulletinBoardServerTest { public static String CERT1_PEM_EXAMPLE = "/certs/enduser-certs/user1.crt"; public static String CERT3_PEM_EXAMPLE = "/certs/enduser-certs/user3.crt"; + private final int TAG_NUM = 5; // Number of tags. + private final int MESSAGE_NUM = 32; // Number of messages (2^TAG_NUM). + + private String[] tags; + private byte[][] data; + + private final ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); // Used to time the tests + /** * @param bulletinBoardServer is an initialized server. * @throws InstantiationException @@ -55,11 +64,13 @@ public class GenericBulletinBoardServerTest { * @throws CommunicationException */ public void init(BulletinBoardServer bulletinBoardServer) { - + + System.err.println("Starting to initialize GenericBulletinBoardServerTest"); + long start = threadBean.getCurrentThreadCpuTime(); + this.bulletinBoardServer = bulletinBoardServer; signers = new ECDSASignature[2]; - signerIDs = new ByteString[signers.length]; signers[0] = new ECDSASignature(); signers[1] = new ECDSASignature(); @@ -98,42 +109,22 @@ public class GenericBulletinBoardServerTest { System.err.println("Couldn't find signing key " + e.getMessage()); fail("Couldn't find signing key " + e.getMessage()); } - - // Get signer IDs - // TODO: remove this after creation of getSignerID method - - UnsignedBulletinBoardMessage msg = UnsignedBulletinBoardMessage.newBuilder().build(); - - for (int i = 0 ; i < signers.length ; i++) { - - try { - signers[i].updateContent(UnsignedBulletinBoardMessage.newBuilder().build()); - signerIDs[i] = signers[i].sign().getSignerId(); - } catch (SignatureException e) { - System.err.println("Error signing message" + e.getMessage()); - fail("Error signing message" + e.getMessage()); - return; - } - } random = new Random(0); // We use insecure randomness in tests for repeatability + + long end = threadBean.getCurrentThreadCpuTime(); + System.err.println("Finished initializing GenericBulletinBoardServerTest"); + System.err.println("Time of operation: " + (end - start)); } private byte randomByte(){ return (byte) random.nextInt(); } - + private String randomString(){ return new BigInteger(130, random).toString(32); } - private final int TAG_NUM = 5; // Number of tags. - private final int MESSAGE_NUM = 32; // Number of messages (2^TAG_NUM). - - - private String[] tags; - private byte[][] data; - /** * Tests writing of several messages with multiple tags and signatures. * @throws CommunicationException @@ -144,6 +135,9 @@ public class GenericBulletinBoardServerTest { */ public void testInsert() { + System.err.println("Starting to insert messages to DB"); + long start = threadBean.getCurrentThreadCpuTime(); + final int BYTES_PER_MESSAGE_DATA = 50; // Message size. tags = new String[TAG_NUM]; @@ -214,6 +208,11 @@ public class GenericBulletinBoardServerTest { fail(e.getMessage()); } } + + long end = threadBean.getCurrentThreadCpuTime(); + System.err.println("Finished inserting messages to DB"); + System.err.println("Time of operation: " + (end - start)); + } /** @@ -222,6 +221,9 @@ public class GenericBulletinBoardServerTest { */ public void testSimpleTagAndSignature(){ + System.err.println("Starting to test tag and signature mechanism"); + long start = threadBean.getCurrentThreadCpuTime(); + List messages; // Check tag mechanism @@ -288,7 +290,11 @@ public class GenericBulletinBoardServerTest { } } - + + long end = threadBean.getCurrentThreadCpuTime(); + System.err.println("Finished testing tag and signature mechanism"); + System.err.println("Time of operation: " + (end - start)); + } /** @@ -296,6 +302,9 @@ public class GenericBulletinBoardServerTest { */ public void testEnhancedTagsAndSignatures(){ + System.err.println("Starting to test multiple tags and signatures"); + long start = threadBean.getCurrentThreadCpuTime(); + List messages; MessageFilterList.Builder filterListBuilder = MessageFilterList.newBuilder(); @@ -336,11 +345,11 @@ public class GenericBulletinBoardServerTest { filterListBuilder = MessageFilterList.newBuilder() .addFilter(MessageFilter.newBuilder() .setType(FilterType.SIGNER_ID) - .setId(signerIDs[0]) + .setId(signers[0].getSignerID()) .build()) .addFilter(MessageFilter.newBuilder() .setType(FilterType.SIGNER_ID) - .setId(signerIDs[1]) + .setId(signers[1].getSignerID()) .build() ); @@ -358,6 +367,10 @@ public class GenericBulletinBoardServerTest { assertThat(message.getEntryNum() % 4, is((long) 1)); } + long end = threadBean.getCurrentThreadCpuTime(); + System.err.println("Finished testing multiple tags and signatures"); + System.err.println("Time of operation: " + (end - start)); + } public void close(){ diff --git a/bulletin-board-server/src/test/java/meerkat/bulletinboard/SQLiteBulletinBoardServerTest.java b/bulletin-board-server/src/test/java/meerkat/bulletinboard/SQLiteBulletinBoardServerTest.java index 1981ea3..c071807 100644 --- a/bulletin-board-server/src/test/java/meerkat/bulletinboard/SQLiteBulletinBoardServerTest.java +++ b/bulletin-board-server/src/test/java/meerkat/bulletinboard/SQLiteBulletinBoardServerTest.java @@ -9,6 +9,8 @@ import org.junit.Test; import java.io.File; import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadMXBean; import java.security.*; import java.security.cert.CertificateException; @@ -23,9 +25,14 @@ public class SQLiteBulletinBoardServerTest{ private GenericBulletinBoardServerTest serverTest; + private final ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); // Used to time the tests + @Before public void init(){ + System.err.println("Starting to initialize SQLiteBulletinBoardServerTest"); + long start = threadBean.getCurrentThreadCpuTime(); + File old = new File(testFilename); old.delete(); @@ -46,10 +53,17 @@ public class SQLiteBulletinBoardServerTest{ System.err.println(e.getMessage()); fail(e.getMessage()); } + + long end = threadBean.getCurrentThreadCpuTime(); + System.err.println("Finished initializing SQLiteBulletinBoardServerTest"); + System.err.println("Time of operation: " + (end - start)); } @Test public void bulkTest() { + System.err.println("Starting bulkTest of SQLiteBulletinBoardServerTest"); + long start = threadBean.getCurrentThreadCpuTime(); + try { serverTest.testInsert(); serverTest.testSimpleTagAndSignature(); @@ -57,11 +71,22 @@ public class SQLiteBulletinBoardServerTest{ System.err.println(e.getMessage()); fail(e.getMessage()); } + + long end = threadBean.getCurrentThreadCpuTime(); + System.err.println("Finished bulkTest of SQLiteBulletinBoardServerTest"); + System.err.println("Time of operation: " + (end - start)); } @After public void close() { + System.err.println("Starting to close SQLiteBulletinBoardServerTest"); + long start = threadBean.getCurrentThreadCpuTime(); + serverTest.close(); + + long end = threadBean.getCurrentThreadCpuTime(); + System.err.println("Finished closing SQLiteBulletinBoardServerTest"); + System.err.println("Time of operation: " + (end - start)); } } diff --git a/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java b/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java index 5386c5f..e7b64e5 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java +++ b/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java @@ -1,5 +1,6 @@ package meerkat.crypto; +import com.google.protobuf.ByteString; import com.google.protobuf.Message; import java.io.IOException; @@ -82,6 +83,10 @@ public interface DigitalSignature { throws IOException, CertificateException, UnrecoverableKeyException; + /** + * @return the signer ID if it exists; null otherwise. + */ + public ByteString getSignerID(); /** * Clear the signing key (will require authentication to use again). diff --git a/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java index 615b06e..887b8e8 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java +++ b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java @@ -300,6 +300,11 @@ public class ECDSASignature extends GlobalCryptoSetup implements DigitalSignatur throw new UnrecoverableKeyException("Didn't find valid private key entry in keystore!"); } + @Override + public ByteString getSignerID() { + return loadedSigningKeyId; + } + public void clearSigningKey() { try { // TODO: Check if this really clears the key from memory From 76c5e6681f11b35ad2600a71f3108b46f9a21647 Mon Sep 17 00:00:00 2001 From: Arbel Deutsch Peled Date: Wed, 9 Dec 2015 14:47:18 +0200 Subject: [PATCH 06/15] Made SQL Servers generic. Added MySQL Server and test. Added partial H2 Server code. --- bulletin-board-server/build.gradle | 4 + .../httpserver/BulletinBoardHttpServer.java | 103 ------ .../sqlserver/BulletinBoardSQLServer.java | 336 +++++++++++++++--- .../EnhancedSQLiteBulletinBoardServer.java | 190 ---------- .../sqlserver/H2QueryProvider.java | 111 ++++++ .../sqlserver/MySQLQueryProvider.java | 106 ++++++ .../sqlserver/SQLiteBulletinBoardServer.java | 266 -------------- .../sqlserver/SQLiteQueryProvider.java | 109 ++++++ .../webapp/BulletinBoardWebApp.java | 5 +- ...EnhancedSQLiteBulletinBoardServerTest.java | 54 --- .../GenericBulletinBoardServerTest.java | 34 +- .../H2BulletinBoardServerTest.java | 122 +++++++ .../MySQLBulletinBoardServerTest.java | 127 +++++++ .../SQLiteBulletinBoardServerTest.java | 20 +- gradle/wrapper/gradle-wrapper.jar | Bin 53637 -> 0 bytes gradle/wrapper/gradle-wrapper.properties | 6 - 16 files changed, 900 insertions(+), 693 deletions(-) delete mode 100644 bulletin-board-server/src/main/java/meerkat/bulletinboard/httpserver/BulletinBoardHttpServer.java delete mode 100644 bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/EnhancedSQLiteBulletinBoardServer.java create mode 100644 bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/H2QueryProvider.java create mode 100644 bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/MySQLQueryProvider.java delete mode 100644 bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/SQLiteBulletinBoardServer.java create mode 100644 bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/SQLiteQueryProvider.java delete mode 100644 bulletin-board-server/src/test/java/meerkat/bulletinboard/EnhancedSQLiteBulletinBoardServerTest.java create mode 100644 bulletin-board-server/src/test/java/meerkat/bulletinboard/H2BulletinBoardServerTest.java create mode 100644 bulletin-board-server/src/test/java/meerkat/bulletinboard/MySQLBulletinBoardServerTest.java delete mode 100644 gradle/wrapper/gradle-wrapper.jar delete mode 100644 gradle/wrapper/gradle-wrapper.properties diff --git a/bulletin-board-server/build.gradle b/bulletin-board-server/build.gradle index 593dfbc..5989499 100644 --- a/bulletin-board-server/build.gradle +++ b/bulletin-board-server/build.gradle @@ -45,7 +45,11 @@ dependencies { // Jersey for RESTful API compile 'org.glassfish.jersey.containers:jersey-container-servlet:2.22.+' + + // JDBC connections compile 'org.xerial:sqlite-jdbc:3.7.+' + compile 'mysql:mysql-connector-java:5.1.+' + compile 'com.h2database:h2:1.0.+' // Servlets compile 'javax.servlet:javax.servlet-api:3.0.+' diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/httpserver/BulletinBoardHttpServer.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/httpserver/BulletinBoardHttpServer.java deleted file mode 100644 index 9989706..0000000 --- a/bulletin-board-server/src/main/java/meerkat/bulletinboard/httpserver/BulletinBoardHttpServer.java +++ /dev/null @@ -1,103 +0,0 @@ -package meerkat.bulletinboard.httpserver; - -import java.io.IOException; - -import javax.servlet.ServletConfig; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import meerkat.bulletinboard.BulletinBoardServer; -import meerkat.bulletinboard.sqlserver.SQLiteBulletinBoardServer; -import meerkat.comm.CommunicationException; -import meerkat.protobuf.BulletinBoardAPI.*; - -public class BulletinBoardHttpServer extends HttpServlet { - - public final static String DEFAULT_MEERKAT_DB = "local-instances/meerkat.db"; - - /** - * Auto-generated UID. - */ - private static final long serialVersionUID = -1263665607729456165L; - - BulletinBoardServer bbs; - - @Override - public void init(ServletConfig config) throws ServletException { - //TODO: Make this generic - bbs = new SQLiteBulletinBoardServer(); - - try { - bbs.init(DEFAULT_MEERKAT_DB); - } catch (CommunicationException e) { - // TODO Log error - throw new ServletException("Servlet failed to initialize: " + e.getMessage()); - } - } - - /** - * This procedure handles (POST) requests to post messages to the Bulletin Board. - */ - @Override - protected void doPost( HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - BulletinBoardMessage message; - - try{ - message = BulletinBoardMessage.newBuilder() - .mergeFrom(request.getInputStream()) - .build(); - } catch(Exception e){ - //TODO: Log invalid request - return; - } - - try { - bbs.postMessage(message); - } catch (CommunicationException e) { - // TODO Log DB communication error - } - - } - - /** - * This procedure handles (GET) requests which request data from the Bulletin Board. - */ - @Override - protected void doGet( HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - BulletinBoardMessage message; - - try{ - message = BulletinBoardMessage.newBuilder() - .mergeFrom(request.getInputStream()) - .build(); - } catch(Exception e){ - //TODO: Log invalid request - return; - } - - try { - bbs.postMessage(message); - } catch (CommunicationException e) { - // TODO Log DB communication error - } - - } - - @Override - public void destroy() { - - try { - bbs.close(); - } catch (CommunicationException e) { - // TODO Log DB communication error - } - - } - -} diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/BulletinBoardSQLServer.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/BulletinBoardSQLServer.java index b8fc3cd..2689f70 100644 --- a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/BulletinBoardSQLServer.java +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/BulletinBoardSQLServer.java @@ -1,16 +1,11 @@ package meerkat.bulletinboard.sqlserver; -import java.util.Arrays; -import java.util.List; +import java.sql.*; +import java.util.*; +import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.ProtocolStringList; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; - import meerkat.bulletinboard.BulletinBoardServer; import meerkat.comm.CommunicationException; import meerkat.protobuf.BulletinBoardAPI.*; @@ -19,8 +14,77 @@ import meerkat.protobuf.Crypto.SignatureVerificationKey; import meerkat.crypto.Digest; import meerkat.crypto.concrete.SHA256Digest; -public abstract class BulletinBoardSQLServer implements BulletinBoardServer{ - +/** + * This is a generic SQL implementation of the BulletinBoardServer API. + */ +public class BulletinBoardSQLServer implements BulletinBoardServer{ + + /** + * This interface provides the required implementation-specific data to enable an access to an actual SQL server. + * It accounts for differences in languages between SQL DB types and for the different addresses needed to access them. + */ + public interface SQLQueryProvider { + + /** + * Allowed query types. + * Note that each query returned has to comply with the placeholder ("?") requirements written in its comment. + */ + public static enum QueryType { + FIND_MSG_ID, // Placeholders for: MsgId + INSERT_MSG, // Placeholders for: MsgId, Msg + INSERT_NEW_TAG, // Placeholders for: Tag + CONNECT_TAG, // Placeholders for: EntryNum, Tag + ADD_SIGNATURE, // Placeholders for: EntryNum, SignerId, Signature + GET_SIGNATURES, // Placeholders for: EntryNum + GET_MESSAGES // Placeholders for: N/A + } + + /** + * This function translates a QueryType into an actual SQL query. + * @param queryType is the type of query requested + * @return a string representation of the query for the specific type of SQL database implemented. + */ + public String getSQLString(QueryType queryType) throws IllegalArgumentException; + + public String getCondition(FilterType filterType) throws IllegalArgumentException; + + /** + * @return the string needed in order to connect to the DB. + */ + public String getConnectionString(); + + /** + * This is used to get a list of queries that together create the schema needed for the DB. + * Note that these queries should not assume anything about the current state of the DB. + * In particular: they should not erase any existing tables and/or entries. + * @return The list of queries. + */ + public List getSchemaCreationCommands(); + + /** + * This is used to get a list of queries that together delete the schema needed for the DB. + * This is useful primarily for tests, in which we want to make sure we start with a clean DB. + * @return The list of queries. + */ + public List getSchemaDeletionCommands(); + + } + + /** + * This class implements a comparator for the MessageFilter class + * The comparison is done solely by comparing the type of the filter + * This is used to sort the filters by type + */ + public class FilterTypeComparator implements Comparator { + + @Override + public int compare(MessageFilter filter1, MessageFilter filter2) { + return filter1.getTypeValue() - filter2.getTypeValue(); + } + } + + protected SQLQueryProvider sqlQueryProvider; + protected Connection connection; protected Digest digest; @@ -30,18 +94,52 @@ public abstract class BulletinBoardSQLServer implements BulletinBoardServer{ protected List> pollingCommitteeSignatureVerificationKeyArray; protected int minCommiteeSignatures; - + /** - * This method initializes the signatures but does not implement the DB connection. - * Any full (non-abstract) extension of this class should - * 1. Establish a DB connection, and - * 2. Call this procedure + * This constructor sets the type of SQL language in use. + * @param sqlQueryProvider is the provider of the SQL query strings required for actual operation of the server. + */ + public BulletinBoardSQLServer(SQLQueryProvider sqlQueryProvider) { + this.sqlQueryProvider = sqlQueryProvider; + } + + private void createSchema() throws SQLException { + + final int TIMEOUT = 20; + + Statement statement = connection.createStatement(); + statement.setQueryTimeout(TIMEOUT); + + for (String command : sqlQueryProvider.getSchemaCreationCommands()) { + statement.executeUpdate(command); + } + + statement.close(); + } + + /** + * This method initializes the signatures, connects to the DB and creates the schema (if required). */ @Override public void init(String meerkatDB) throws CommunicationException { // TODO write signature reading part. digest = new SHA256Digest(); + + try{ + + String dbString = sqlQueryProvider.getConnectionString(); + connection = DriverManager.getConnection(dbString); + } catch (SQLException e) { + throw new CommunicationException("Couldn't form a connection with the database " + e.getMessage()); + } + + try { + createSchema(); + } catch (SQLException e) { + throw new CommunicationException("Couldn't create schema " + e.getMessage()); + } + } /** @@ -61,7 +159,29 @@ public abstract class BulletinBoardSQLServer implements BulletinBoardServer{ * This procedure makes sure that all tags in the given list have an entry in the tags table. * @param tags */ - protected abstract void insertNewTags(String[] tags) throws SQLException; + protected void insertNewTags(String[] tags) throws SQLException { + + PreparedStatement pstmt; + String sql; + + try { + + sql = sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.INSERT_NEW_TAG); + pstmt = connection.prepareStatement(sql); + + for (String tag : tags){ + pstmt.setString(1, tag); + pstmt.addBatch(); + } + + pstmt.executeBatch(); + pstmt.close(); + + } catch (SQLException e){ + throw new SQLException("Error adding new tags to table: " + e.getMessage()); + } + + } /** * This procedure is used to convert a boolean to a BoolMsg. @@ -73,7 +193,7 @@ public abstract class BulletinBoardSQLServer implements BulletinBoardServer{ .setValue(b) .build(); } - + @Override public BoolMsg postMessage(BulletinBoardMessage msg) throws CommunicationException { @@ -104,7 +224,7 @@ public abstract class BulletinBoardSQLServer implements BulletinBoardServer{ try { - sql = "SELECT EntryNum From MsgTable WHERE MsgId = ?"; + sql = sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.FIND_MSG_ID); pstmt = connection.prepareStatement(sql); pstmt.setBytes(1, msgID); rs = pstmt.executeQuery(); @@ -115,8 +235,8 @@ public abstract class BulletinBoardSQLServer implements BulletinBoardServer{ } else{ - sql = "INSERT INTO MsgTable (MsgId, Msg) VALUES(?,?)"; - pstmt = connection.prepareStatement(sql); + sql = sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.INSERT_MSG); + pstmt = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); pstmt.setBytes(1, msgID); pstmt.setBytes(2, msg.getMsg().toByteArray()); pstmt.executeUpdate(); @@ -150,7 +270,7 @@ public abstract class BulletinBoardSQLServer implements BulletinBoardServer{ // Connect message to tags. try{ - sql = "INSERT OR IGNORE INTO MsgTagTable (TagId, EntryNum) SELECT TagTable.TagId, ? AS EntryNum FROM TagTable WHERE Tag = ?"; + sql = sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.CONNECT_TAG); pstmt = connection.prepareStatement(sql); pstmt.setLong(1, entryNum); @@ -176,7 +296,7 @@ public abstract class BulletinBoardSQLServer implements BulletinBoardServer{ // Connect message to signatures. try{ - sql = "INSERT OR IGNORE INTO SignatureTable (EntryNum, SignerId, Signature) VALUES (?,?,?)"; + sql = sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.ADD_SIGNATURE); pstmt = connection.prepareStatement(sql); pstmt.setLong(1, entryNum); @@ -197,41 +317,147 @@ public abstract class BulletinBoardSQLServer implements BulletinBoardServer{ return boolToBoolMsg(true); } - - public String testPrint(){ - - String s = ""; - - try { - - Statement statement = connection.createStatement(); - ResultSet rs = statement.executeQuery("select * from MsgTable"); - while (rs.next()) { - // read the result set - s += "entry = " + rs.getInt("EntryNum") + " \n"; - s += "id = " + Arrays.toString(rs.getBytes("MsgId")) + " \n"; - s += "msg = " + Arrays.toString(rs.getBytes("Msg")) + " \n"; - s += "signer ID = " + Arrays.toString(rs.getBytes("SignerId")) + "\t\n
"; + + @Override + public BulletinBoardMessageList readMessages(MessageFilterList filterList) throws CommunicationException { + PreparedStatement pstmt; + ResultSet messages, signatures; + + long entryNum; + BulletinBoardMessageList.Builder resultListBuilder = BulletinBoardMessageList.newBuilder(); + BulletinBoardMessage.Builder messageBuilder; + + String sql; + + List filters = new ArrayList(filterList.getFilterList()); + int i; + + boolean tagsRequired = false; + boolean signaturesRequired = false; + + boolean isFirstFilter = true; + + Collections.sort(filters, new FilterTypeComparator()); + + // Check if Tag/Signature tables are required for filtering purposes. + + sql = sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.GET_MESSAGES); + + // Add conditions. + + if (!filters.isEmpty()){ + sql += " WHERE "; + + for (MessageFilter filter : filters){ + + if (filter.getType().getNumber() != FilterType.MAX_MESSAGES_VALUE){ + if (isFirstFilter){ + isFirstFilter = false; + } else{ + sql += " AND "; + } + } + + sql += sqlQueryProvider.getCondition(filter.getType()); } - - rs = statement.executeQuery("select * from TagTable"); - while (rs.next()) { - // read the result set - s += "Tag = " + rs.getString("Tag") + " \n"; - s += "TagId = " + rs.getInt("TagId") + "\t\n
"; - } - - rs = statement.executeQuery("select * from MsgTagTable"); - while (rs.next()) { - // read the result set - s += "MsgId = " + Arrays.toString(rs.getBytes("MsgId")) + " \n"; - s += "TagId = " + rs.getInt("TagId") + "\t\n
"; - } - } catch(SQLException e){ - s += "Error reading from DB"; + } - - return s; + + // Make query. + + try { + pstmt = connection.prepareStatement(sql); + + // Specify values for filters. + + i = 1; + for (MessageFilter filter : filters){ + + switch (filter.getType().getNumber()){ + + case FilterType.EXACT_ENTRY_VALUE: // Go through. + case FilterType.MAX_ENTRY_VALUE: + pstmt.setLong(i, filter.getEntry()); + i++; + break; + + case FilterType.MSG_ID_VALUE: // Go through. + case FilterType.SIGNER_ID_VALUE: + pstmt.setBytes(i, filter.getId().toByteArray()); + i++; + break; + + case FilterType.TAG_VALUE: + pstmt.setString(i, filter.getTag()); + i++; + break; + + // The max-messages condition is applied as a suffix. Therefore, it is treated differently. + case FilterType.MAX_MESSAGES_VALUE: + pstmt.setLong(filters.size(), filter.getMaxMessages()); + i++; + break; + + } + } + + // Run query. + + messages = pstmt.executeQuery(); + + // Compile list of messages. + + sql = sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.GET_SIGNATURES); + pstmt = connection.prepareStatement(sql); + + while (messages.next()){ + + // Get entry number and retrieve signatures. + + entryNum = messages.getLong(1); + pstmt.setLong(1, entryNum); + signatures = pstmt.executeQuery(); + + // Create message and append signatures. + + messageBuilder = BulletinBoardMessage.newBuilder() + .setEntryNum(entryNum) + .setMsg(UnsignedBulletinBoardMessage.parseFrom(messages.getBytes(2))); + + while (signatures.next()){ + messageBuilder.addSig(Signature.parseFrom(signatures.getBytes(1))); + } + + // Finalize message and add to message list. + + resultListBuilder.addMessage(messageBuilder.build()); + + } + + pstmt.close(); + + } catch (SQLException e){ + throw new CommunicationException("Error reading messages from DB: " + e.getMessage()); + } catch (InvalidProtocolBufferException e) { + throw new CommunicationException("Invalid data from DB: " + e.getMessage()); + } + + //Combine results and return. + + return resultListBuilder.build(); + } + + @Override + public void close() throws CommunicationException { + + try{ + connection.close(); + } catch (SQLException e) { + + throw new CommunicationException("Couldn't close connection to the database"); + + } + } } diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/EnhancedSQLiteBulletinBoardServer.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/EnhancedSQLiteBulletinBoardServer.java deleted file mode 100644 index 6baac36..0000000 --- a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/EnhancedSQLiteBulletinBoardServer.java +++ /dev/null @@ -1,190 +0,0 @@ -package meerkat.bulletinboard.sqlserver; - -import com.google.protobuf.InvalidProtocolBufferException; -import meerkat.comm.CommunicationException; -import meerkat.protobuf.BulletinBoardAPI; -import meerkat.protobuf.BulletinBoardAPI.*; -import meerkat.protobuf.Crypto.*; - -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -/** - * Created by Arbel Deutsch Peled on 07-Dec-15. - * This server version allows for reading of messages with several tags and/or signatures - * The superclass only allows one constraint per type. - */ -public class EnhancedSQLiteBulletinBoardServer extends SQLiteBulletinBoardServer { - - /** - * This class implements a comparator for the MessageFilter class - * The comparison is done solely by comparing the type of the filter - * This is used to sort the filters by type - */ - public class FilterTypeComparator implements Comparator{ - - @Override - public int compare(MessageFilter filter1, MessageFilter filter2) { - return filter1.getTypeValue() - filter2.getTypeValue(); - } - } - - @Override - public BulletinBoardMessageList readMessages(MessageFilterList filterList) throws CommunicationException { - - PreparedStatement pstmt; - ResultSet messages, signatures; - - long entryNum; - BulletinBoardMessageList.Builder resultListBuilder = BulletinBoardMessageList.newBuilder(); - BulletinBoardMessage.Builder messageBuilder; - - String sql; - String sqlSuffix = ""; - - List filters = new ArrayList(filterList.getFilterList()); - int i; - - boolean tagsRequired = false; - boolean signaturesRequired = false; - - boolean isFirstFilter = true; - - Collections.sort(filters, new FilterTypeComparator()); - - // Check if Tag/Signature tables are required for filtering purposes. - - sql = "SELECT MsgTable.EntryNum, MsgTable.Msg FROM MsgTable"; - - // Add conditions. - - if (!filters.isEmpty()){ - sql += " WHERE"; - - for (MessageFilter filter : filters){ - - if (filter.getType().getNumber() != FilterType.MAX_MESSAGES_VALUE){ - if (isFirstFilter){ - isFirstFilter = false; - } else{ - sql += " AND"; - } - } - - switch (filter.getType().getNumber()){ - case FilterType.EXACT_ENTRY_VALUE: - sql += " MsgTable.EntryNum = ?"; - break; - case FilterType.MAX_ENTRY_VALUE: - sql += " MsgTable.EntryNum <= ?"; - break; - case FilterType.MAX_MESSAGES_VALUE: - sqlSuffix += " LIMIT = ?"; - break; - case FilterType.MSG_ID_VALUE: - sql += " MsgTable.MsgId = ?"; - break; - case FilterType.SIGNER_ID_VALUE: - sql += " EXISTS (SELECT 1 FROM SignatureTable" - + " WHERE SignatureTable.SignerId = ? AND SignatureTable.EntryNum = MsgTable.EntryNum)"; - break; - case FilterType.TAG_VALUE: - sql += " EXISTS (SELECT 1 FROM TagTable" - + " INNER JOIN MsgTagTable ON TagTable.TagId = MsgTagTable.TagId" - + " WHERE TagTable.Tag = ? AND MsgTagTable.EntryNum = MsgTable.EntryNum)"; - break; - } - } - - sql += sqlSuffix; - } - - // Make query. - - try { - pstmt = connection.prepareStatement(sql); - - // Specify values for filters. - - i = 1; - for (MessageFilter filter : filters){ - - switch (filter.getType().getNumber()){ - - case FilterType.EXACT_ENTRY_VALUE: // Go through. - case FilterType.MAX_ENTRY_VALUE: - pstmt.setLong(i, filter.getEntry()); - i++; - break; - - case FilterType.MSG_ID_VALUE: // Go through. - case FilterType.SIGNER_ID_VALUE: - pstmt.setBytes(i, filter.getId().toByteArray()); - i++; - break; - - case FilterType.TAG_VALUE: - pstmt.setString(i, filter.getTag()); - i++; - break; - - // The max-messages condition is applied as a suffix. Therefore, it is treated differently. - case FilterType.MAX_MESSAGES_VALUE: - pstmt.setLong(filters.size(), filter.getMaxMessages()); - i++; - break; - - } - } - - // Run query. - - messages = pstmt.executeQuery(); - - // Compile list of messages. - - sql = "SELECT Signature FROM SignatureTable WHERE EntryNum = ?"; - pstmt = connection.prepareStatement(sql); - - while (messages.next()){ - - // Get entry number and retrieve signatures. - - entryNum = messages.getLong(1); - pstmt.setLong(1, entryNum); - signatures = pstmt.executeQuery(); - - // Create message and append signatures. - - messageBuilder = BulletinBoardMessage.newBuilder() - .setEntryNum(entryNum) - .setMsg(UnsignedBulletinBoardMessage.parseFrom(messages.getBytes(2))); - - while (signatures.next()){ - messageBuilder.addSig(Signature.parseFrom(signatures.getBytes(1))); - } - - // Finalize message and add to message list. - - resultListBuilder.addMessage(messageBuilder.build()); - - } - - pstmt.close(); - - } catch (SQLException e){ - throw new CommunicationException("Error reading messages from DB: " + e.getMessage()); - } catch (InvalidProtocolBufferException e) { - throw new CommunicationException("Invalid data from DB: " + e.getMessage()); - } - - //Combine results and return. - - return resultListBuilder.build(); - } -} diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/H2QueryProvider.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/H2QueryProvider.java new file mode 100644 index 0000000..d80b302 --- /dev/null +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/H2QueryProvider.java @@ -0,0 +1,111 @@ +package meerkat.bulletinboard.sqlserver; + +import meerkat.protobuf.BulletinBoardAPI.FilterType; + +import java.util.LinkedList; +import java.util.List; + +/** + * Created by Arbel Deutsch Peled on 09-Dec-15. + */ + +public class H2QueryProvider implements BulletinBoardSQLServer.SQLQueryProvider { + + private String dbConnectionString; + + public H2QueryProvider(String dbAddress) { + dbConnectionString = "jdbc:h2:" + dbAddress + ";MODE=MYSQL"; + } + + + @Override + public String getSQLString(QueryType queryType) throws IllegalArgumentException{ + + switch(queryType) { + case ADD_SIGNATURE: + return "INSERT IGNORE INTO SignatureTable (EntryNum, SignerId, Signature) VALUES (?,?,?)"; + case CONNECT_TAG: + return "INSERT IGNORE INTO MsgTagTable (TagId, EntryNum)" + + " SELECT TagTable.TagId, ? AS EntryNum FROM TagTable WHERE Tag = ?"; + case FIND_MSG_ID: + return "SELECT EntryNum From MsgTable WHERE MsgId = ?"; + case GET_MESSAGES: + return "SELECT MsgTable.EntryNum, MsgTable.Msg FROM MsgTable"; + case GET_SIGNATURES: + return "SELECT Signature FROM SignatureTable WHERE EntryNum = ?"; + case INSERT_MSG: + return "INSERT INTO MsgTable (MsgId, Msg) VALUES(?,?)"; + case INSERT_NEW_TAG: + return "INSERT IGNORE INTO TagTable(Tag) VALUES (?)"; + default: + throw new IllegalArgumentException("Cannot serve a query of type " + queryType); + } + + } + + @Override + public String getCondition(FilterType filterType) throws IllegalArgumentException { + + switch(filterType) { + case EXACT_ENTRY: + return "MsgTable.EntryNum = ?"; + case MAX_ENTRY: + return "MsgTable.EntryNum <= ?"; + case MAX_MESSAGES: + return "LIMIT ?"; + case MSG_ID: + return "MsgTable.MsgId = ?"; + case SIGNER_ID: + return "EXISTS (SELECT 1 FROM SignatureTable" + + " WHERE SignatureTable.SignerId = ? AND SignatureTable.EntryNum = MsgTable.EntryNum)"; + case TAG: + return "EXISTS (SELECT 1 FROM TagTable" + + " INNER JOIN MsgTagTable ON TagTable.TagId = MsgTagTable.TagId" + + " WHERE TagTable.Tag = ? AND MsgTagTable.EntryNum = MsgTable.EntryNum)"; + default: + throw new IllegalArgumentException("Cannot serve a filter of type " + filterType); + } + + } + + @Override + public String getConnectionString() { + return dbConnectionString; + } + + + @Override + public List getSchemaCreationCommands() { + List list = new LinkedList(); + + list.add("CREATE TABLE IF NOT EXISTS MsgTable (EntryNum INT NOT NULL AUTO_INCREMENT PRIMARY KEY, MsgId TINYBLOB UNIQUE, Msg BLOB)"); + + list.add("CREATE TABLE IF NOT EXISTS TagTable (TagId INT NOT NULL AUTO_INCREMENT PRIMARY KEY, Tag varchar(50) UNIQUE)"); + + list.add("CREATE TABLE IF NOT EXISTS MsgTagTable (EntryNum INT, TagId INT," + + " FOREIGN KEY (EntryNum) REFERENCES MsgTable(EntryNum)," + + " FOREIGN KEY (TagId) REFERENCES TagTable(TagId)," + + " UNIQUE (EntryNum, TagID))"); + + list.add("CREATE TABLE IF NOT EXISTS SignatureTable (EntryNum INT, SignerId TINYBLOB, Signature TINYBLOB UNIQUE," + + " FOREIGN KEY (EntryNum) REFERENCES MsgTable(EntryNum))"); + + list.add("CREATE UNIQUE INDEX IF NOT EXISTS SignerIdIndex ON SignatureTable(SignerId)"); + + return list; + } + + @Override + public List getSchemaDeletionCommands() { + List list = new LinkedList(); + + list.add("DROP INDEX IF EXISTS SignerIdIndex"); + list.add("DROP TABLE IF EXISTS MsgTagTable"); + list.add("DROP TABLE IF EXISTS SignatureTable"); + list.add("DROP TABLE IF EXISTS TagTable"); + list.add("DROP TABLE IF EXISTS MsgTable"); + + return list; + } + +} diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/MySQLQueryProvider.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/MySQLQueryProvider.java new file mode 100644 index 0000000..18d8189 --- /dev/null +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/MySQLQueryProvider.java @@ -0,0 +1,106 @@ +package meerkat.bulletinboard.sqlserver; + +import meerkat.protobuf.BulletinBoardAPI.FilterType; + +import java.util.LinkedList; +import java.util.List; + +/** + * Created by Arbel Deutsch Peled on 09-Dec-15. + */ + +public class MySQLQueryProvider implements BulletinBoardSQLServer.SQLQueryProvider { + + String dbConnectionString; + + public MySQLQueryProvider(String dbAddress, String username, String password) { + dbConnectionString = "jdbc:mysql:" + dbAddress + "?user=" + username + "&password=" + password; + } + + @Override + public String getSQLString(QueryType queryType) throws IllegalArgumentException{ + + switch(queryType) { + case ADD_SIGNATURE: + return "INSERT IGNORE INTO SignatureTable (EntryNum, SignerId, Signature) VALUES (?,?,?)"; + case CONNECT_TAG: + return "INSERT IGNORE INTO MsgTagTable (TagId, EntryNum)" + + " SELECT TagTable.TagId, ? AS EntryNum FROM TagTable WHERE Tag = ?"; + case FIND_MSG_ID: + return "SELECT EntryNum From MsgTable WHERE MsgId = ?"; + case GET_MESSAGES: + return "SELECT MsgTable.EntryNum, MsgTable.Msg FROM MsgTable"; + case GET_SIGNATURES: + return "SELECT Signature FROM SignatureTable WHERE EntryNum = ?"; + case INSERT_MSG: + return "INSERT INTO MsgTable (MsgId, Msg) VALUES(?,?)"; + case INSERT_NEW_TAG: + return "INSERT IGNORE INTO TagTable(Tag) VALUES (?)"; + default: + throw new IllegalArgumentException("Cannot serve a query of type " + queryType); + } + + } + + @Override + public String getCondition(FilterType filterType) throws IllegalArgumentException { + + switch(filterType) { + case EXACT_ENTRY: + return "MsgTable.EntryNum = ?"; + case MAX_ENTRY: + return "MsgTable.EntryNum <= ?"; + case MAX_MESSAGES: + return "LIMIT ?"; + case MSG_ID: + return "MsgTable.MsgId = ?"; + case SIGNER_ID: + return "EXISTS (SELECT 1 FROM SignatureTable" + + " WHERE SignatureTable.SignerId = ? AND SignatureTable.EntryNum = MsgTable.EntryNum)"; + case TAG: + return "EXISTS (SELECT 1 FROM TagTable" + + " INNER JOIN MsgTagTable ON TagTable.TagId = MsgTagTable.TagId" + + " WHERE TagTable.Tag = ? AND MsgTagTable.EntryNum = MsgTable.EntryNum)"; + default: + throw new IllegalArgumentException("Cannot serve a filter of type " + filterType); + } + + } + + @Override + public String getConnectionString() { + return dbConnectionString; + } + + + @Override + public List getSchemaCreationCommands() { + List list = new LinkedList(); + + list.add("CREATE TABLE IF NOT EXISTS MsgTable (EntryNum INT NOT NULL AUTO_INCREMENT PRIMARY KEY, MsgId TINYBLOB, Msg BLOB, UNIQUE(MsgId(50)))"); + + list.add("CREATE TABLE IF NOT EXISTS TagTable (TagId INT NOT NULL AUTO_INCREMENT PRIMARY KEY, Tag varchar(50), UNIQUE(Tag))"); + + list.add("CREATE TABLE IF NOT EXISTS MsgTagTable (EntryNum INT, TagId INT," + + " CONSTRAINT FOREIGN KEY (EntryNum) REFERENCES MsgTable(EntryNum)," + + " CONSTRAINT FOREIGN KEY (TagId) REFERENCES TagTable(TagId)," + + " CONSTRAINT UNIQUE (EntryNum, TagID))"); + + list.add("CREATE TABLE IF NOT EXISTS SignatureTable (EntryNum INT, SignerId TINYBLOB, Signature TINYBLOB, UNIQUE(Signature(150))," + + " INDEX(SignerId(50)), CONSTRAINT FOREIGN KEY (EntryNum) REFERENCES MsgTable(EntryNum))"); + + return list; + } + + @Override + public List getSchemaDeletionCommands() { + List list = new LinkedList(); + + list.add("DROP TABLE IF EXISTS MsgTagTable"); + list.add("DROP TABLE IF EXISTS SignatureTable"); + list.add("DROP TABLE IF EXISTS TagTable"); + list.add("DROP TABLE IF EXISTS MsgTable"); + + return list; + } +} diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/SQLiteBulletinBoardServer.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/SQLiteBulletinBoardServer.java deleted file mode 100644 index bfbbc1a..0000000 --- a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/SQLiteBulletinBoardServer.java +++ /dev/null @@ -1,266 +0,0 @@ -package meerkat.bulletinboard.sqlserver; - -import java.sql.DriverManager; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.List; - -import com.google.protobuf.InvalidProtocolBufferException; - -import meerkat.protobuf.BulletinBoardAPI.*; -import meerkat.protobuf.Crypto.Signature; -import meerkat.bulletinboard.sqlserver.BulletinBoardSQLServer; -import meerkat.comm.CommunicationException; - -public class SQLiteBulletinBoardServer extends BulletinBoardSQLServer { - - protected static final int TIMEOUT = 20; - - /** - * This procedure initializes: - * 1. The database connection - * 2. The database tables (if they do not yet exist). - */ - - private void createSchema() throws SQLException { - Statement statement = connection.createStatement(); - statement.setQueryTimeout(TIMEOUT); - - statement.executeUpdate("CREATE TABLE IF NOT EXISTS MsgTable (EntryNum INTEGER PRIMARY KEY, MsgId BLOB UNIQUE, Msg BLOB)"); - - statement.executeUpdate("CREATE TABLE IF NOT EXISTS TagTable (TagId INTEGER PRIMARY KEY, Tag varchar(50) UNIQUE)"); - statement.executeUpdate("CREATE TABLE IF NOT EXISTS MsgTagTable (EntryNum BLOB, TagId INTEGER, FOREIGN KEY (EntryNum) REFERENCES MsgTable(EntryNum), FOREIGN KEY (TagId) REFERENCES TagTable(TagId), UNIQUE (EntryNum, TagID))"); - - statement.executeUpdate("CREATE TABLE IF NOT EXISTS SignatureTable (EntryNum BLOB, SignerId BLOB, Signature BLOB UNIQUE, FOREIGN KEY (EntryNum) REFERENCES MsgTable(EntryNum))"); - statement.executeUpdate("CREATE INDEX IF NOT EXISTS SignerIndex ON SignatureTable(SignerId)"); - - statement.close(); - } - - @Override - public void init(String meerkatDB) throws CommunicationException { - - try{ - String dbString = "jdbc:sqlite:" + meerkatDB; - connection = DriverManager.getConnection(dbString); - createSchema(); - - super.init(meerkatDB); - - } catch (SQLException e) { - - throw new CommunicationException("Couldn't form a connection with the database" + e.getMessage()); - - } - - } - - public void close() throws CommunicationException{ - - try{ - connection.close(); - } catch (SQLException e) { - - throw new CommunicationException("Couldn't close connection to the database"); - - } - - } - - @Override - protected void insertNewTags(String[] tags) throws SQLException { - - PreparedStatement pstmt; - String sql; - - try { - - sql = "INSERT OR IGNORE INTO TagTable(Tag) VALUES (?)"; - pstmt = connection.prepareStatement(sql); - - for (String tag : tags){ - pstmt.setString(1, tag); - pstmt.addBatch(); - } - - pstmt.executeBatch(); - pstmt.close(); - - } catch (SQLException e){ - throw new SQLException("Error adding new tags to table: " + e.getMessage()); - } - - } - - @Override - public BoolMsg postMessage(BulletinBoardMessage msg) throws CommunicationException { - return super.postMessage(msg); - } - - @Override - public BulletinBoardMessageList readMessages(MessageFilterList filterList) throws CommunicationException{ - - PreparedStatement pstmt; - ResultSet messages, signatures; - - long entryNum; - BulletinBoardMessageList.Builder resultListBuilder = BulletinBoardMessageList.newBuilder(); - BulletinBoardMessage.Builder messageBuilder; - - String sql; - String sqlSuffix = ""; - - List filters = filterList.getFilterList(); - int i; - - boolean tagsRequired = false; - boolean signaturesRequired = false; - - boolean isFirstFilter = true; - - // Check if Tag/Signature tables are required for filtering purposes. - - for (MessageFilter filter : filters){ - if (filter.getType() == FilterType.TAG){ - tagsRequired = true; - } else if (filter.getType() == FilterType.SIGNER_ID){ - signaturesRequired = true; - } - } - - sql = "SELECT MsgTable.EntryNum, MsgTable.Msg FROM MsgTable"; - - if (tagsRequired){ - sql += " INNER JOIN MsgTagTable ON MsgTable.EntryNum = MsgTagTable.EntryNum"; - sql += " INNER JOIN TagTable ON TagTable.TagId = MsgTagTable.TagId"; - } - - if (signaturesRequired){ - sql += " INNER JOIN SignatureTable ON SignatureTable.EntryNum = MsgTable.EntryNum"; - } - - // Add conditions. - - if (!filters.isEmpty()){ - sql += " WHERE"; - - for (MessageFilter filter : filters){ - - if (filter.getType().getNumber() != FilterType.MAX_MESSAGES_VALUE){ - if (isFirstFilter){ - isFirstFilter = false; - } else{ - sql += " AND"; - } - } - - switch (filter.getType().getNumber()){ - case FilterType.EXACT_ENTRY_VALUE: - sql += " MsgTable.EntryNum = ?"; - break; - case FilterType.MAX_ENTRY_VALUE: - sql += " MsgTable.EntryNum <= ?"; - break; - case FilterType.MAX_MESSAGES_VALUE: - sqlSuffix += " LIMIT = ?"; - break; - case FilterType.MSG_ID_VALUE: - sql += " MsgTable.MsgId = ?"; - break; - case FilterType.SIGNER_ID_VALUE: - sql += " SignatureTable.SignerId = ?"; - break; - case FilterType.TAG_VALUE: - sql += " TagTable.Tag = ?"; - break; - } - } - - sql += sqlSuffix; - } - - // Make query. - - try { - pstmt = connection.prepareStatement(sql); - - // Specify values for filters. - - i = 1; - for (MessageFilter filter : filters){ - - switch (filter.getType().getNumber()){ - - case FilterType.EXACT_ENTRY_VALUE: // Go through. - case FilterType.MAX_ENTRY_VALUE: - pstmt.setLong(i, filter.getEntry()); - i++; - break; - - case FilterType.MSG_ID_VALUE: // Go through. - case FilterType.SIGNER_ID_VALUE: - pstmt.setBytes(i, filter.getId().toByteArray()); - i++; - break; - - case FilterType.TAG_VALUE: - pstmt.setString(i, filter.getTag()); - break; - - // The max-messages condition is applied as a suffix. Therefore, it is treated differently. - case FilterType.MAX_MESSAGES_VALUE: - pstmt.setLong(filters.size(), filter.getMaxMessages()); - break; - - } - } - - // Run query. - - messages = pstmt.executeQuery(); - - // Compile list of messages. - - sql = "SELECT Signature FROM SignatureTable WHERE EntryNum = ?"; - pstmt = connection.prepareStatement(sql); - - while (messages.next()){ - - // Get entry number and retrieve signatures. - - entryNum = messages.getLong(1); - pstmt.setLong(1, entryNum); - signatures = pstmt.executeQuery(); - - // Create message and append signatures. - - messageBuilder = BulletinBoardMessage.newBuilder() - .setEntryNum(entryNum) - .setMsg(UnsignedBulletinBoardMessage.parseFrom(messages.getBytes(2))); - - while (signatures.next()){ - messageBuilder.addSig(Signature.parseFrom(signatures.getBytes(1))); - } - - // Finalize message and add to message list. - - resultListBuilder.addMessage(messageBuilder.build()); - - } - - pstmt.close(); - - } catch (SQLException e){ - throw new CommunicationException("Error reading messages from DB: " + e.getMessage()); - } catch (InvalidProtocolBufferException e) { - throw new CommunicationException("Invalid data from DB: " + e.getMessage()); - } - - //Combine results and return. - - return resultListBuilder.build(); - } - -} diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/SQLiteQueryProvider.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/SQLiteQueryProvider.java new file mode 100644 index 0000000..5978c9d --- /dev/null +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/SQLiteQueryProvider.java @@ -0,0 +1,109 @@ +package meerkat.bulletinboard.sqlserver; + +import meerkat.protobuf.BulletinBoardAPI.*; + +import java.util.LinkedList; +import java.util.List; + +import static meerkat.protobuf.BulletinBoardAPI.FilterType; + +/** + * Created by Arbel Deutsch Peled on 09-Dec-15. + */ + +public class SQLiteQueryProvider implements BulletinBoardSQLServer.SQLQueryProvider { + + String dbConnectionString; + + public SQLiteQueryProvider(String dbAddress) { + dbConnectionString = "jdbc:sqlite:" + dbAddress; + } + + @Override + public String getSQLString(QueryType queryType) throws IllegalArgumentException{ + + switch(queryType) { + case ADD_SIGNATURE: + return "INSERT OR IGNORE INTO SignatureTable (EntryNum, SignerId, Signature) VALUES (?,?,?)"; + case CONNECT_TAG: + return "INSERT OR IGNORE INTO MsgTagTable (TagId, EntryNum)" + + " SELECT TagTable.TagId, ? AS EntryNum FROM TagTable WHERE Tag = ?"; + case FIND_MSG_ID: + return "SELECT EntryNum From MsgTable WHERE MsgId = ?"; + case GET_MESSAGES: + return "SELECT MsgTable.EntryNum, MsgTable.Msg FROM MsgTable"; + case GET_SIGNATURES: + return "SELECT Signature FROM SignatureTable WHERE EntryNum = ?"; + case INSERT_MSG: + return "INSERT INTO MsgTable (MsgId, Msg) VALUES(?,?)"; + case INSERT_NEW_TAG: + return "INSERT OR IGNORE INTO TagTable(Tag) VALUES (?)"; + default: + throw new IllegalArgumentException("Cannot serve a query of type " + queryType); + } + + } + + @Override + public String getCondition(FilterType filterType) throws IllegalArgumentException { + + switch(filterType) { + case EXACT_ENTRY: + return "MsgTable.EntryNum = ?"; + case MAX_ENTRY: + return "MsgTable.EntryNum <= ?"; + case MAX_MESSAGES: + return "LIMIT = ?"; + case MSG_ID: + return "MsgTable.MsgId = ?"; + case SIGNER_ID: + return "EXISTS (SELECT 1 FROM SignatureTable" + + " WHERE SignatureTable.SignerId = ? AND SignatureTable.EntryNum = MsgTable.EntryNum)"; + case TAG: + return "EXISTS (SELECT 1 FROM TagTable" + + " INNER JOIN MsgTagTable ON TagTable.TagId = MsgTagTable.TagId" + + " WHERE TagTable.Tag = ? AND MsgTagTable.EntryNum = MsgTable.EntryNum)"; + default: + throw new IllegalArgumentException("Cannot serve a filter of type " + filterType); + } + + } + + @Override + public String getConnectionString() { + return dbConnectionString; + } + + + @Override + public List getSchemaCreationCommands() { + List list = new LinkedList(); + + list.add("CREATE TABLE IF NOT EXISTS MsgTable (EntryNum INTEGER PRIMARY KEY, MsgId BLOB UNIQUE, Msg BLOB)"); + + list.add("CREATE TABLE IF NOT EXISTS TagTable (TagId INTEGER PRIMARY KEY, Tag varchar(50) UNIQUE)"); + list.add("CREATE TABLE IF NOT EXISTS MsgTagTable (EntryNum BLOB, TagId INTEGER, FOREIGN KEY (EntryNum)" + + " REFERENCES MsgTable(EntryNum), FOREIGN KEY (TagId) REFERENCES TagTable(TagId), UNIQUE (EntryNum, TagID))"); + + list.add("CREATE TABLE IF NOT EXISTS SignatureTable (EntryNum BLOB, SignerId BLOB, Signature BLOB UNIQUE," + + " FOREIGN KEY (EntryNum) REFERENCES MsgTable(EntryNum))"); + list.add("CREATE INDEX IF NOT EXISTS SignerIndex ON SignatureTable(SignerId)"); + + return list; + } + + @Override + public List getSchemaDeletionCommands() { + List list = new LinkedList(); + + list.add("DROP TABLE IF EXISTS MsgTagTable"); + + list.add("DROP INDEX IF EXISTS SignerIndex"); + list.add("DROP TABLE IF EXISTS SignatureTable"); + + list.add("DROP TABLE IF EXISTS TagTable"); + list.add("DROP TABLE IF EXISTS MsgTable"); + + return list; + } +} diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/webapp/BulletinBoardWebApp.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/webapp/BulletinBoardWebApp.java index 69f2ba4..5dae671 100644 --- a/bulletin-board-server/src/main/java/meerkat/bulletinboard/webapp/BulletinBoardWebApp.java +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/webapp/BulletinBoardWebApp.java @@ -12,7 +12,8 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import meerkat.bulletinboard.BulletinBoardServer; -import meerkat.bulletinboard.sqlserver.SQLiteBulletinBoardServer; +import meerkat.bulletinboard.sqlserver.BulletinBoardSQLServer; +import meerkat.bulletinboard.sqlserver.SQLiteQueryProvider; import meerkat.comm.CommunicationException; import meerkat.protobuf.BulletinBoardAPI.BoolMsg; import meerkat.protobuf.BulletinBoardAPI.BulletinBoardMessage; @@ -51,7 +52,7 @@ public class BulletinBoardWebApp implements BulletinBoardServer, ServletContextL String dbType = servletContext.getInitParameter("dbtype"); if (dbType.compareTo("SQLite") == 0){ - bulletinBoard = new SQLiteBulletinBoardServer(); + bulletinBoard = new BulletinBoardSQLServer(new SQLiteQueryProvider(meerkatDB)); } try { diff --git a/bulletin-board-server/src/test/java/meerkat/bulletinboard/EnhancedSQLiteBulletinBoardServerTest.java b/bulletin-board-server/src/test/java/meerkat/bulletinboard/EnhancedSQLiteBulletinBoardServerTest.java deleted file mode 100644 index 8d643e4..0000000 --- a/bulletin-board-server/src/test/java/meerkat/bulletinboard/EnhancedSQLiteBulletinBoardServerTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package meerkat.bulletinboard; - -import meerkat.bulletinboard.sqlserver.EnhancedSQLiteBulletinBoardServer; -import meerkat.comm.CommunicationException; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import java.io.File; - -import static org.junit.Assert.fail; - -/** - * Created by Arbel Deutsch Peled on 07-Dec-15. - */ -public class EnhancedSQLiteBulletinBoardServerTest{ - - private String testFilename = "EnhancedSQLiteDBTest.db"; - - private GenericBulletinBoardServerTest serverTest; - - @Before - public void init(){ - - File old = new File(testFilename); - old.delete(); - - BulletinBoardServer bulletinBoardServer = new EnhancedSQLiteBulletinBoardServer(); - try { - bulletinBoardServer.init(testFilename); - - } catch (CommunicationException e) { - System.err.println("Failed to initialize server " + e.getMessage()); - fail("Failed to initialize server " + e.getMessage()); - return; - } - - serverTest = new GenericBulletinBoardServerTest(); - serverTest.init(bulletinBoardServer); - } - - @Test - public void bulkTest() { - System.err.println("Testing Enhanced SQLite Server"); - serverTest.testInsert(); - serverTest.testSimpleTagAndSignature(); - } - - @After - public void close() { - serverTest.close(); - } - -} diff --git a/bulletin-board-server/src/test/java/meerkat/bulletinboard/GenericBulletinBoardServerTest.java b/bulletin-board-server/src/test/java/meerkat/bulletinboard/GenericBulletinBoardServerTest.java index 3572a42..61354e7 100644 --- a/bulletin-board-server/src/test/java/meerkat/bulletinboard/GenericBulletinBoardServerTest.java +++ b/bulletin-board-server/src/test/java/meerkat/bulletinboard/GenericBulletinBoardServerTest.java @@ -32,6 +32,7 @@ public class GenericBulletinBoardServerTest { protected BulletinBoardServer bulletinBoardServer; private ECDSASignature signers[]; + private ByteString[] signerIDs; private Random random; @@ -71,6 +72,7 @@ public class GenericBulletinBoardServerTest { this.bulletinBoardServer = bulletinBoardServer; signers = new ECDSASignature[2]; + signerIDs = new ByteString[signers.length]; signers[0] = new ECDSASignature(); signers[1] = new ECDSASignature(); @@ -81,17 +83,21 @@ public class GenericBulletinBoardServerTest { try { keyStoreBuilder = signers[0].getPKCS12KeyStoreBuilder(keyStream, password); - signers[0].loadSigningCertificate(keyStoreBuilder); - - signers[0].loadVerificationCertificates(getClass().getResourceAsStream(CERT1_PEM_EXAMPLE)); - - keyStream = getClass().getResourceAsStream(KEYFILE_EXAMPLE3); - password = KEYFILE_PASSWORD3.toCharArray(); - - keyStoreBuilder = signers[1].getPKCS12KeyStoreBuilder(keyStream, password); - signers[1].loadSigningCertificate(keyStoreBuilder); - - signers[1].loadVerificationCertificates(getClass().getResourceAsStream(CERT3_PEM_EXAMPLE)); + signers[0].loadSigningCertificate(keyStoreBuilder); + + signers[0].loadVerificationCertificates(getClass().getResourceAsStream(CERT1_PEM_EXAMPLE)); + + keyStream = getClass().getResourceAsStream(KEYFILE_EXAMPLE3); + password = KEYFILE_PASSWORD3.toCharArray(); + + keyStoreBuilder = signers[1].getPKCS12KeyStoreBuilder(keyStream, password); + signers[1].loadSigningCertificate(keyStoreBuilder); + + signers[1].loadVerificationCertificates(getClass().getResourceAsStream(CERT3_PEM_EXAMPLE)); + + for (int i = 0 ; i < signers.length ; i++) { + signerIDs[i] = signers[i].getSignerID(); + } } catch (IOException e) { System.err.println("Failed reading from signature file " + e.getMessage()); @@ -334,7 +340,7 @@ public class GenericBulletinBoardServerTest { assertThat(messages.size(), is(expectedMsgCount)); for (BulletinBoardMessage msg : messages) { - for (int j = 0 ; j < i ; j++) { + for (int j = 0 ; j <= i ; j++) { assertThat((msg.getEntryNum() >>> j) % 2, is((long) 1)); } } @@ -345,11 +351,11 @@ public class GenericBulletinBoardServerTest { filterListBuilder = MessageFilterList.newBuilder() .addFilter(MessageFilter.newBuilder() .setType(FilterType.SIGNER_ID) - .setId(signers[0].getSignerID()) + .setId(signerIDs[0]) .build()) .addFilter(MessageFilter.newBuilder() .setType(FilterType.SIGNER_ID) - .setId(signers[1].getSignerID()) + .setId(signerIDs[1]) .build() ); diff --git a/bulletin-board-server/src/test/java/meerkat/bulletinboard/H2BulletinBoardServerTest.java b/bulletin-board-server/src/test/java/meerkat/bulletinboard/H2BulletinBoardServerTest.java new file mode 100644 index 0000000..6210dd4 --- /dev/null +++ b/bulletin-board-server/src/test/java/meerkat/bulletinboard/H2BulletinBoardServerTest.java @@ -0,0 +1,122 @@ +package meerkat.bulletinboard; + +import meerkat.bulletinboard.sqlserver.BulletinBoardSQLServer; +import meerkat.bulletinboard.sqlserver.BulletinBoardSQLServer.SQLQueryProvider; +import meerkat.bulletinboard.sqlserver.H2QueryProvider; +import meerkat.comm.CommunicationException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadMXBean; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; + +import static org.junit.Assert.fail; + +/** + * Created by Arbel Deutsch Peled on 07-Dec-15. + */ +public class H2BulletinBoardServerTest { + + private final String dbAddress = "~/meerkatTest"; + + private GenericBulletinBoardServerTest serverTest; + + private final ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); // Used to time the tests + +// @Before + public void init(){ + + System.err.println("Starting to initialize H2BulletinBoardServerTest"); + long start = threadBean.getCurrentThreadCpuTime(); + + SQLQueryProvider queryProvider = new H2QueryProvider(dbAddress); + + try { + + Connection conn = DriverManager.getConnection(queryProvider.getConnectionString()); + Statement stmt = conn.createStatement(); + + List deletionQueries = queryProvider.getSchemaDeletionCommands(); + + for (String deletionQuery : deletionQueries) { + stmt.execute(deletionQuery); + } + + } catch (SQLException e) { + System.err.println(e.getMessage()); + fail(e.getMessage()); + } + + BulletinBoardServer bulletinBoardServer = new BulletinBoardSQLServer(queryProvider); + try { + bulletinBoardServer.init(""); + + } catch (CommunicationException e) { + System.err.println(e.getMessage()); + fail(e.getMessage()); + return; + } + + serverTest = new GenericBulletinBoardServerTest(); + try { + serverTest.init(bulletinBoardServer); + } catch (Exception e) { + System.err.println(e.getMessage()); + fail(e.getMessage()); + } + + long end = threadBean.getCurrentThreadCpuTime(); + System.err.println("Finished initializing H2BulletinBoardServerTest"); + System.err.println("Time of operation: " + (end - start)); + } + +// @Test + public void bulkTest() { + System.err.println("Starting bulkTest of H2BulletinBoardServerTest"); + long start = threadBean.getCurrentThreadCpuTime(); + + try { + serverTest.testInsert(); + } catch (Exception e) { + System.err.println(e.getMessage()); + fail(e.getMessage()); + } + + try{ + serverTest.testSimpleTagAndSignature(); + } catch (Exception e) { + System.err.println(e.getMessage()); + fail(e.getMessage()); + } + + try{ + serverTest.testEnhancedTagsAndSignatures(); + } catch (Exception e) { + System.err.println(e.getMessage()); + fail(e.getMessage()); + } + + long end = threadBean.getCurrentThreadCpuTime(); + System.err.println("Finished bulkTest of H2BulletinBoardServerTest"); + System.err.println("Time of operation: " + (end - start)); + } + +// @After + public void close() { + System.err.println("Starting to close H2BulletinBoardServerTest"); + long start = threadBean.getCurrentThreadCpuTime(); + + serverTest.close(); + + long end = threadBean.getCurrentThreadCpuTime(); + System.err.println("Finished closing H2BulletinBoardServerTest"); + System.err.println("Time of operation: " + (end - start)); + } + +} diff --git a/bulletin-board-server/src/test/java/meerkat/bulletinboard/MySQLBulletinBoardServerTest.java b/bulletin-board-server/src/test/java/meerkat/bulletinboard/MySQLBulletinBoardServerTest.java new file mode 100644 index 0000000..af1cfb4 --- /dev/null +++ b/bulletin-board-server/src/test/java/meerkat/bulletinboard/MySQLBulletinBoardServerTest.java @@ -0,0 +1,127 @@ +package meerkat.bulletinboard; + +import meerkat.bulletinboard.sqlserver.BulletinBoardSQLServer; +import meerkat.bulletinboard.sqlserver.BulletinBoardSQLServer.SQLQueryProvider; +import meerkat.bulletinboard.sqlserver.MySQLQueryProvider; +import meerkat.bulletinboard.sqlserver.SQLiteQueryProvider; +import meerkat.comm.CommunicationException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadMXBean; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.LinkedList; +import java.util.List; + +import static org.junit.Assert.fail; + +/** + * Created by Arbel Deutsch Peled on 07-Dec-15. + */ +public class MySQLBulletinBoardServerTest { + + private final String dbAddress = "//localhost:3306/meerkat"; + private final String username = "arbel"; + private final String password = "mypass"; + + private GenericBulletinBoardServerTest serverTest; + + private final ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); // Used to time the tests + + @Before + public void init(){ + + System.err.println("Starting to initialize MySQLBulletinBoardServerTest"); + long start = threadBean.getCurrentThreadCpuTime(); + + SQLQueryProvider queryProvider = new MySQLQueryProvider(dbAddress,username,password); + + try { + + Connection conn = DriverManager.getConnection(queryProvider.getConnectionString()); + Statement stmt = conn.createStatement(); + + List deletionQueries = queryProvider.getSchemaDeletionCommands(); + + for (String deletionQuery : deletionQueries) { + stmt.execute(deletionQuery); + } + + } catch (SQLException e) { + System.err.println(e.getMessage()); + fail(e.getMessage()); + } + + BulletinBoardServer bulletinBoardServer = new BulletinBoardSQLServer(queryProvider); + try { + bulletinBoardServer.init(""); + + } catch (CommunicationException e) { + System.err.println(e.getMessage()); + fail(e.getMessage()); + return; + } + + serverTest = new GenericBulletinBoardServerTest(); + try { + serverTest.init(bulletinBoardServer); + } catch (Exception e) { + System.err.println(e.getMessage()); + fail(e.getMessage()); + } + + long end = threadBean.getCurrentThreadCpuTime(); + System.err.println("Finished initializing MySQLBulletinBoardServerTest"); + System.err.println("Time of operation: " + (end - start)); + } + + @Test + public void bulkTest() { + System.err.println("Starting bulkTest of MySQLBulletinBoardServerTest"); + long start = threadBean.getCurrentThreadCpuTime(); + + try { + serverTest.testInsert(); + } catch (Exception e) { + System.err.println(e.getMessage()); + fail(e.getMessage()); + } + + try{ + serverTest.testSimpleTagAndSignature(); + } catch (Exception e) { + System.err.println(e.getMessage()); + fail(e.getMessage()); + } + + try{ + serverTest.testEnhancedTagsAndSignatures(); + } catch (Exception e) { + System.err.println(e.getMessage()); + fail(e.getMessage()); + } + + long end = threadBean.getCurrentThreadCpuTime(); + System.err.println("Finished bulkTest of MySQLBulletinBoardServerTest"); + System.err.println("Time of operation: " + (end - start)); + } + + @After + public void close() { + System.err.println("Starting to close MySQLBulletinBoardServerTest"); + long start = threadBean.getCurrentThreadCpuTime(); + + serverTest.close(); + + long end = threadBean.getCurrentThreadCpuTime(); + System.err.println("Finished closing MySQLBulletinBoardServerTest"); + System.err.println("Time of operation: " + (end - start)); + } + +} diff --git a/bulletin-board-server/src/test/java/meerkat/bulletinboard/SQLiteBulletinBoardServerTest.java b/bulletin-board-server/src/test/java/meerkat/bulletinboard/SQLiteBulletinBoardServerTest.java index c071807..1d7aae0 100644 --- a/bulletin-board-server/src/test/java/meerkat/bulletinboard/SQLiteBulletinBoardServerTest.java +++ b/bulletin-board-server/src/test/java/meerkat/bulletinboard/SQLiteBulletinBoardServerTest.java @@ -1,6 +1,7 @@ package meerkat.bulletinboard; -import meerkat.bulletinboard.sqlserver.SQLiteBulletinBoardServer; +import meerkat.bulletinboard.sqlserver.BulletinBoardSQLServer; +import meerkat.bulletinboard.sqlserver.SQLiteQueryProvider; import meerkat.comm.CommunicationException; import meerkat.protobuf.*; import org.junit.After; @@ -36,9 +37,9 @@ public class SQLiteBulletinBoardServerTest{ File old = new File(testFilename); old.delete(); - BulletinBoardServer bulletinBoardServer = new SQLiteBulletinBoardServer(); + BulletinBoardServer bulletinBoardServer = new BulletinBoardSQLServer(new SQLiteQueryProvider(testFilename)); try { - bulletinBoardServer.init(testFilename); + bulletinBoardServer.init(""); } catch (CommunicationException e) { System.err.println(e.getMessage()); @@ -66,12 +67,25 @@ public class SQLiteBulletinBoardServerTest{ try { serverTest.testInsert(); + } catch (Exception e) { + System.err.println(e.getMessage()); + fail(e.getMessage()); + } + + try{ serverTest.testSimpleTagAndSignature(); } catch (Exception e) { System.err.println(e.getMessage()); fail(e.getMessage()); } + try{ + serverTest.testEnhancedTagsAndSignatures(); + } catch (Exception e) { + System.err.println(e.getMessage()); + fail(e.getMessage()); + } + long end = threadBean.getCurrentThreadCpuTime(); System.err.println("Finished bulkTest of SQLiteBulletinBoardServerTest"); System.err.println("Time of operation: " + (end - start)); diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 05ef575b0cd0173fc735f2857ce4bd594ce4f6bd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53637 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 t9m@SZ!E*7&jsr?!t7TL-WYI4eM@gAug8 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 Date: Thu, 10 Dec 2015 15:08:41 +0200 Subject: [PATCH 07/15] Created (untested version of) a Threaded Bulletin Board Client. Overhauled Bulletin Board Client interface to accommodate this. Deprecated the Simple Bulletin Board Client. Made the path to the server methods generic (defined in the Constants class of the rest package). --- .../bulletinboard/BulletinClientJob.java | 82 +++++++ .../BulletinClientJobResult.java | 29 +++ .../bulletinboard/BulletinClientWorker.java | 213 ++++++++++++++++++ .../SimpleBulletinBoardClient.java | 38 ++-- .../ThreadedBulletinBoardClient.java | 118 ++++++++++ .../callbacks/ClientFutureCallback.java | 25 ++ .../GetRedundancyFutureCallback.java | 38 ++++ .../callbacks/PostMessageFutureCallback.java | 44 ++++ .../callbacks/ReadMessagesFutureCallback.java | 38 ++++ bulletin-board-server/build.gradle | 2 + .../webapp/BulletinBoardWebApp.java | 6 +- .../SQLiteServerIntegrationTest.java | 14 +- meerkat-common/build.gradle | 3 + .../bulletinboard/BulletinBoardClient.java | 30 +-- .../main/proto/meerkat/BulletinBoardAPI.proto | 3 + .../src/main/proto/meerkat/voting.proto | 12 + .../src/main/java/meerkat/rest/Constants.java | 4 + 17 files changed, 648 insertions(+), 51 deletions(-) create mode 100644 bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientJob.java create mode 100644 bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientJobResult.java create mode 100644 bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientWorker.java create mode 100644 bulletin-board-client/src/main/java/meerkat/bulletinboard/ThreadedBulletinBoardClient.java create mode 100644 bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/ClientFutureCallback.java create mode 100644 bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/GetRedundancyFutureCallback.java create mode 100644 bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/PostMessageFutureCallback.java create mode 100644 bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/ReadMessagesFutureCallback.java diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientJob.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientJob.java new file mode 100644 index 0000000..b63ca50 --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientJob.java @@ -0,0 +1,82 @@ +package meerkat.bulletinboard; + +import com.google.protobuf.Message; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * Created by Arbel Deutsch Peled on 09-Dec-15. + * + * This class specifies the job that is required of a Bulletin Board Client Worker + */ +public class BulletinClientJob { + + public static enum JobType{ + POST_MESSAGE, // Post a message to servers + READ_MESSAGES, // Read messages according to some given filter (any server will do) + GET_REDUNDANCY // Check the redundancy of a specific message in the databases + } + + private List serverAddresses; + + private int minServers; // The minimal number of servers the job must be successful on for the job to be completed + + private final JobType jobType; + + private final Message payload; // The information associated with the job type + + private int maxRetry; // Number of retries for this job; set to -1 for infinite retries + + public BulletinClientJob(List serverAddresses, int minServers, JobType jobType, Message payload, int maxRetry) { + this.serverAddresses = serverAddresses; + this.minServers = minServers; + this.jobType = jobType; + this.payload = payload; + this.maxRetry = maxRetry; + } + + public List getServerAddresses() { + return serverAddresses; + } + + public int getMinServers() { + return minServers; + } + + public JobType getJobType() { + return jobType; + } + + public Message getPayload() { + return payload; + } + + public int getMaxRetry() { + return maxRetry; + } + + public Iterator getAddressIterator() { + return serverAddresses.iterator(); + } + + public void shuffleAddresses() { + Collections.shuffle(serverAddresses); + } + + public void decMinServers(){ + minServers--; + } + + public void decMaxRetry(){ + if (maxRetry > 0) { + maxRetry--; + } + } + + public boolean isRetry(){ + return (maxRetry != 0); + } + +} \ No newline at end of file diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientJobResult.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientJobResult.java new file mode 100644 index 0000000..be0501b --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientJobResult.java @@ -0,0 +1,29 @@ +package meerkat.bulletinboard; + +import com.google.protobuf.Message; + +/** + * Created by Arbel Deutsch Peled on 09-Dec-15. + * + * This class contains the end status and result of a Bulletin Board Client Job. + */ +public final class BulletinClientJobResult { + + private final BulletinClientJob job; // Stores the job the result refers to + + private final Message result; // The result of the job; valid only if success==true + + public BulletinClientJobResult(BulletinClientJob job, Message result) { + this.job = job; + this.result = result; + } + + public BulletinClientJob getJob() { + return job; + } + + public Message getResult() { + return result; + } + +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientWorker.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientWorker.java new file mode 100644 index 0000000..eb67672 --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientWorker.java @@ -0,0 +1,213 @@ +package meerkat.bulletinboard; + +import com.google.protobuf.ByteString; +import com.google.protobuf.Message; +import meerkat.comm.CommunicationException; +import meerkat.crypto.Digest; +import meerkat.crypto.concrete.SHA256Digest; +import meerkat.protobuf.BulletinBoardAPI.*; +import meerkat.rest.Constants; +import meerkat.rest.ProtobufMessageBodyReader; +import meerkat.rest.ProtobufMessageBodyWriter; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; +import java.util.Iterator; +import java.util.concurrent.Callable; + +/** + * Created by Arbel Deutsch Peled on 09-Dec-15. + * + * This class implements the actual communication with the Bulletin Board Servers. + * It is meant to be used in a multi-threaded environment. + */ +//TODO: Maybe make this abstract and inherit from it. +public class BulletinClientWorker implements Callable { + + private final BulletinClientJob job; // The requested job to be handled + + public BulletinClientWorker(BulletinClientJob job){ + this.job = job; + } + + // This resource enabled creation of a single Client per thread. + private static final ThreadLocal clientLocal = + new ThreadLocal () { + @Override protected Client initialValue() { + Client client; + client = ClientBuilder.newClient(); + client.register(ProtobufMessageBodyReader.class); + client.register(ProtobufMessageBodyWriter.class); + + return client; + } + }; + + // This resource enables creation of a single Digest per thread. + private static final ThreadLocal digestLocal = + new ThreadLocal () { + @Override protected Digest initialValue() { + Digest digest; + digest = new SHA256Digest(); //TODO: Make this generic. + + return digest; + } + }; + + /** + * This method carries out the actual communication with the servers via HTTP Post + * It accesses the servers according to the job it received and updates said job as it goes + * The method will only iterate once through the server list, removing servers from the list when they are no longer required + * In a POST_MESSAGE job: successful post to a server results in removing the server from the list + * In a GET_REDUNDANCY job: no server is removed from the list and the (absolute) number of servers in which the message was found is returned + * In a READ_MESSAGES job: successful retrieval from any server terminates the method and returns the received values; The list is not changed + * @return The original job, modified to fit the current state and the required output (if any) of the operation + * @throws IllegalArgumentException + * @throws CommunicationException + */ + public BulletinClientJobResult call() throws IllegalArgumentException, CommunicationException{ + + Client client = clientLocal.get(); + Digest digest = digestLocal.get(); + + WebTarget webTarget; + Response response; + + job.shuffleAddresses(); // This is done to randomize the order of access to servers primarily for READ operations + + String requestPath; + Message msg; + + BulletinBoardMessageList msgList; + + int count = 0; // Used to count number of servers which contain the required message in a GET_REDUNDANCY request. + + // Prepare the request. + switch(job.getJobType()) { + + case POST_MESSAGE: + // Make sure the payload is a BulletinBoardMessage + if (!(job.getPayload() instanceof BulletinBoardMessage)) { + throw new IllegalArgumentException("Cannot post an object that is not an instance of BulletinBoardMessage"); + } + + msg = job.getPayload(); + requestPath = Constants.POST_MESSAGE_PATH; + break; + + case READ_MESSAGES: + // Make sure the payload is a MessageFilterList + if (!(job.getPayload() instanceof MessageFilterList)) { + throw new IllegalArgumentException("Read failed: an instance of MessageFilterList is required as payload for a READ_MESSAGES operation"); + } + + msg = job.getPayload(); + requestPath = Constants.READ_MESSAGES_PATH; + break; + + case GET_REDUNDANCY: + // Make sure the payload is a BulletinBoardMessage + if (!(job.getPayload() instanceof BulletinBoardMessage)) { + throw new IllegalArgumentException("Cannot search for an object that is not an instance of BulletinBoardMessage"); + } + + requestPath = Constants.READ_MESSAGES_PATH; + + // Create a MsgID from the + digest.update((BulletinBoardMessage) job.getPayload()); + msg = MessageFilterList.newBuilder() + .addFilter(MessageFilter.newBuilder() + .setType(FilterType.MSG_ID) + .setId(ByteString.copyFrom(digest.digest())) + .build() + ).build(); + + break; + + default: + throw new IllegalArgumentException("Unsupported job type"); + + } + + // Iterate through servers + + Iterator addressIterator = job.getAddressIterator(); + + while (addressIterator.hasNext()) { + + // Send request to Server + String address = addressIterator.next(); + webTarget = client.target(address).path(Constants.BULLETIN_BOARD_SERVER_PATH).path(requestPath); + response = webTarget.request(Constants.MEDIATYPE_PROTOBUF).post(Entity.entity(msg, Constants.MEDIATYPE_PROTOBUF)); + + // Retrieve answer + switch(job.getJobType()) { + + case POST_MESSAGE: + try { + + response.readEntity(BoolMsg.class); // If a BoolMsg entity is returned: the post was successful + addressIterator.remove(); // Post to this server succeeded: remove server from list + job.decMinServers(); + + } catch (ProcessingException | IllegalStateException e) {} // Post to this server failed: retry next time + finally { + response.close(); + } + break; + + case GET_REDUNDANCY: + try { + msgList = response.readEntity(BulletinBoardMessageList.class); // If a BulletinBoardMessageList is returned: the read was successful + + if (msgList.getMessageList().size() > 0){ // Message was found in the server. + count++; + } + } catch (ProcessingException | IllegalStateException e) {} // Read failed: try with next server + finally { + response.close(); + } + break; + + case READ_MESSAGES: + try { + msgList = response.readEntity(BulletinBoardMessageList.class); // If a BulletinBoardMessageList is returned: the read was successful + return new BulletinClientJobResult(job, msgList); // Return the result + } catch (ProcessingException | IllegalStateException e) {} // Read failed: try with next server + finally { + response.close(); + } + break; + + } + + } + + // Return result (if haven't done so yet) + switch(job.getJobType()) { + + case POST_MESSAGE: + // The job now contains the information required to ascertain whether enough server posts have succeeded + // It also contains the list of servers in which the post was not successful + return new BulletinClientJobResult(job, null); + + case GET_REDUNDANCY: + // Return the number of servers in which the message was found + // The job now contains the list of these servers + return new BulletinClientJobResult(job, IntMsg.newBuilder().setValue(count).build()); + + case READ_MESSAGES: + // A successful operation would have already returned an output + // Therefore: no server access was successful + throw new CommunicationException("Could not access any server"); + + default: // This is required for successful compilation + throw new IllegalArgumentException("Unsupported job type"); + + } + } +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/SimpleBulletinBoardClient.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/SimpleBulletinBoardClient.java index 9cf6dd4..f340cae 100644 --- a/bulletin-board-client/src/main/java/meerkat/bulletinboard/SimpleBulletinBoardClient.java +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/SimpleBulletinBoardClient.java @@ -5,6 +5,8 @@ import meerkat.comm.CommunicationException; import meerkat.crypto.Digest; import meerkat.crypto.concrete.SHA256Digest; import meerkat.protobuf.BulletinBoardAPI.*; +import meerkat.protobuf.Voting; +import meerkat.protobuf.Voting.BulletinBoardClientParams; import meerkat.rest.*; import java.util.List; @@ -18,11 +20,7 @@ import javax.ws.rs.core.Response; /** * Created by Arbel Deutsch Peled on 05-Dec-15. */ -public class SimpleBulletinBoardClient implements BulletinBoardClient { - - //TODO: Make this general - private static String SQL_SERVER_POST = "sqlserver/postmessage"; - private static String SQL_SERVER_GET = "sqlserver/readmessages"; +public class SimpleBulletinBoardClient{ //implements BulletinBoardClient { private List meerkatDBs; @@ -32,12 +30,12 @@ public class SimpleBulletinBoardClient implements BulletinBoardClient { /** * Stores database locations and initializes the web Client - * @param meerkatDBs is the list of database locations + * @param clientParams contains the data needed to access the DBs */ - @Override - public void init(List meerkatDBs) { +// @Override + public void init(Voting.BulletinBoardClientParams clientParams) { - this.meerkatDBs = meerkatDBs; + meerkatDBs = clientParams.getBulletinBoardAddressList(); client = ClientBuilder.newClient(); client.register(ProtobufMessageBodyReader.class); @@ -54,7 +52,7 @@ public class SimpleBulletinBoardClient implements BulletinBoardClient { * @return the message ID for later retrieval * @throws CommunicationException */ - @Override +// @Override public MessageID postMessage(BulletinBoardMessage msg) throws CommunicationException { WebTarget webTarget; @@ -63,7 +61,7 @@ public class SimpleBulletinBoardClient implements BulletinBoardClient { // Post message to all databases try { for (String db : meerkatDBs) { - webTarget = client.target(db).path(SQL_SERVER_POST); + webTarget = client.target(db).path(Constants.BULLETIN_BOARD_SERVER_PATH).path(Constants.POST_MESSAGE_PATH); response = webTarget.request(Constants.MEDIATYPE_PROTOBUF).post(Entity.entity(msg, Constants.MEDIATYPE_PROTOBUF)); // Only consider valid responses @@ -90,7 +88,7 @@ public class SimpleBulletinBoardClient implements BulletinBoardClient { * @param id is the requested message ID * @return the number of DBs in which retrieval was successful */ - @Override +// @Override public float getRedundancy(MessageID id) { WebTarget webTarget; Response response; @@ -106,7 +104,7 @@ public class SimpleBulletinBoardClient implements BulletinBoardClient { for (String db : meerkatDBs) { try { - webTarget = client.target(db).path(SQL_SERVER_GET); + webTarget = client.target(db).path(Constants.BULLETIN_BOARD_SERVER_PATH).path(Constants.READ_MESSAGES_PATH); response = webTarget.request(Constants.MEDIATYPE_PROTOBUF).post(Entity.entity(filterList, Constants.MEDIATYPE_PROTOBUF)); @@ -127,7 +125,7 @@ public class SimpleBulletinBoardClient implements BulletinBoardClient { * @param filterList return only messages that match the filters (null means no filtering). * @return */ - @Override +// @Override public List readMessages(MessageFilterList filterList) { WebTarget webTarget; Response response; @@ -140,11 +138,11 @@ public class SimpleBulletinBoardClient implements BulletinBoardClient { for (String db : meerkatDBs) { try { - webTarget = client.target(db).path(SQL_SERVER_GET); + webTarget = client.target(db).path(Constants.BULLETIN_BOARD_SERVER_PATH).path(Constants.READ_MESSAGES_PATH); response = webTarget.request(Constants.MEDIATYPE_PROTOBUF).post(Entity.entity(filterList, Constants.MEDIATYPE_PROTOBUF)); - messageList =response.readEntity(BulletinBoardMessageList.class); + messageList = response.readEntity(BulletinBoardMessageList.class); if (messageList != null){ return messageList.getMessageList(); @@ -156,8 +154,8 @@ public class SimpleBulletinBoardClient implements BulletinBoardClient { return null; } - @Override - public void registerNewMessageCallback(MessageCallback callback, MessageFilterList filterList) { - callback.handleNewMessage(readMessages(filterList)); - } +// @Override +// public void registerNewMessageCallback(MessageCallback callback, MessageFilterList filterList) { +// callback.handleNewMessage(readMessages(filterList)); +// } } diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/ThreadedBulletinBoardClient.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/ThreadedBulletinBoardClient.java new file mode 100644 index 0000000..dd1ab0f --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/ThreadedBulletinBoardClient.java @@ -0,0 +1,118 @@ +package meerkat.bulletinboard; + +import com.google.common.util.concurrent.*; +import com.google.protobuf.ByteString; +import meerkat.bulletinboard.callbacks.GetRedundancyFutureCallback; +import meerkat.bulletinboard.callbacks.PostMessageFutureCallback; +import meerkat.bulletinboard.callbacks.ReadMessagesFutureCallback; +import meerkat.comm.CommunicationException; +import meerkat.crypto.Digest; +import meerkat.crypto.concrete.SHA256Digest; +import meerkat.protobuf.BulletinBoardAPI.*; +import meerkat.protobuf.Voting; + +import java.util.List; +import java.util.concurrent.Executors; + +/** + * Created by Arbel Deutsch Peled on 05-Dec-15. + * Thread-based implementation of a Bulletin Board Client. + * Features: + * 1. Handles tasks concurrently. + * 2. Retries submitting + */ +public class ThreadedBulletinBoardClient implements BulletinBoardClient { + + private final static int THREAD_NUM = 10; + ListeningExecutorService listeningExecutor; + + private Digest digest; + + private List meerkatDBs; + private String postSubAddress; + private String readSubAddress; + + private final static int READ_MESSAGES_RETRY_NUM = 1; + + private int minAbsoluteRedundancy; + + /** + * Stores database locations and initializes the web Client + * Stores the required minimum redundancy. + * Starts the Thread Pool. + * @param clientParams contains the required information + */ + @Override + public void init(Voting.BulletinBoardClientParams clientParams) { + + meerkatDBs = clientParams.getBulletinBoardAddressList(); + + minAbsoluteRedundancy = (int) (clientParams.getMinRedundancy() * meerkatDBs.size()); + + listeningExecutor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(THREAD_NUM)); + + digest = new SHA256Digest(); + + } + + /** + * Post message to all DBs + * Retry failed DBs + * @param msg is the message, + * @return the message ID for later retrieval + * @throws CommunicationException + */ + @Override + public MessageID postMessage(BulletinBoardMessage msg, ClientCallback callback){ + + // Create job + BulletinClientJob job = new BulletinClientJob(meerkatDBs, minAbsoluteRedundancy, BulletinClientJob.JobType.POST_MESSAGE, msg, -1); + + // Submit job and create callback + Futures.addCallback(listeningExecutor.submit(new BulletinClientWorker(job)), new PostMessageFutureCallback(listeningExecutor, callback)); + + // Calculate the correct message ID and return it + digest.reset(); + digest.update(msg.getMsg()); + return MessageID.newBuilder().setID(ByteString.copyFrom(digest.digest())).build(); + } + + /** + * Access each database and search for a given message ID + * Return the number of databases in which the message was found + * Only try once per DB + * Ignore communication exceptions in specific databases + * @param id is the requested message ID + * @return the number of DBs in which retrieval was successful + */ + @Override + public void getRedundancy(MessageID id, ClientCallback callback) { + + // Create job + BulletinClientJob job = new BulletinClientJob(meerkatDBs, minAbsoluteRedundancy, BulletinClientJob.JobType.GET_REDUNDANCY, id, 1); + + // Submit job and create callback + Futures.addCallback(listeningExecutor.submit(new BulletinClientWorker(job)), new GetRedundancyFutureCallback(listeningExecutor, callback)); + + } + + /** + * Go through the DBs and try to retrieve messages according to the specified filter + * If at the operation is successful for some DB: return the results and stop iterating + * If no operation is successful: return null (NOT blank list) + * @param filterList return only messages that match the filters (null means no filtering). + * @return + */ + @Override + public void readMessages(MessageFilterList filterList, ClientCallback> callback) { + + // Create job + BulletinClientJob job = new BulletinClientJob(meerkatDBs, minAbsoluteRedundancy, BulletinClientJob.JobType.GET_REDUNDANCY, + filterList, READ_MESSAGES_RETRY_NUM); + + // Submit job and create callback + Futures.addCallback(listeningExecutor.submit(new BulletinClientWorker(job)), new ReadMessagesFutureCallback(listeningExecutor, callback)); + + } + +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/ClientFutureCallback.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/ClientFutureCallback.java new file mode 100644 index 0000000..7c5b7b0 --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/ClientFutureCallback.java @@ -0,0 +1,25 @@ +package meerkat.bulletinboard.callbacks; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListeningExecutorService; +import meerkat.bulletinboard.BulletinClientJob; +import meerkat.bulletinboard.BulletinClientJobResult; +import meerkat.bulletinboard.BulletinClientWorker; +import meerkat.protobuf.BulletinBoardAPI; + +import java.util.List; + +/** + * This is a future callback used to listen to workers and run on job finish + * Depending on the type of job and the finishing status of the worker: a decision is made whether to retry or return an error + */ +public abstract class ClientFutureCallback implements FutureCallback { + + protected ListeningExecutorService listeningExecutor; + + ClientFutureCallback(ListeningExecutorService listeningExecutor) { + this.listeningExecutor = listeningExecutor; + } + +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/GetRedundancyFutureCallback.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/GetRedundancyFutureCallback.java new file mode 100644 index 0000000..518ed77 --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/GetRedundancyFutureCallback.java @@ -0,0 +1,38 @@ +package meerkat.bulletinboard.callbacks; + +import com.google.common.util.concurrent.ListeningExecutorService; +import meerkat.bulletinboard.BulletinBoardClient; +import meerkat.bulletinboard.BulletinClientJobResult; +import meerkat.protobuf.BulletinBoardAPI.*; + +import java.util.List; + +/** + * This is a future callback used to listen to workers and run on job finish + * Depending on the type of job and the finishing status of the worker: a decision is made whether to retry or return an error + */ +public class GetRedundancyFutureCallback extends ClientFutureCallback { + + private BulletinBoardClient.ClientCallback callback; + + public GetRedundancyFutureCallback(ListeningExecutorService listeningExecutor, + BulletinBoardClient.ClientCallback callback) { + super(listeningExecutor); + this.callback = callback; + } + + @Override + public void onSuccess(BulletinClientJobResult result) { + + int absoluteRedundancy = ((IntMsg) result.getResult()).getValue(); + int totalServers = result.getJob().getServerAddresses().size(); + + callback.handleCallback( ((float) absoluteRedundancy) / ((float) totalServers) ); + + } + + @Override + public void onFailure(Throwable t) { + callback.handleFailure(t); + } +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/PostMessageFutureCallback.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/PostMessageFutureCallback.java new file mode 100644 index 0000000..7e2a855 --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/PostMessageFutureCallback.java @@ -0,0 +1,44 @@ +package meerkat.bulletinboard.callbacks; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListeningExecutorService; +import meerkat.bulletinboard.BulletinBoardClient; +import meerkat.bulletinboard.BulletinClientJob; +import meerkat.bulletinboard.BulletinClientJobResult; +import meerkat.bulletinboard.BulletinClientWorker; +import meerkat.protobuf.BulletinBoardAPI; + +import java.util.List; + +/** + * This is a future callback used to listen to workers and run on job finish + * Depending on the type of job and the finishing status of the worker: a decision is made whether to retry or return an error + */ +public class PostMessageFutureCallback extends ClientFutureCallback { + + private BulletinBoardClient.ClientCallback callback; + + public PostMessageFutureCallback(ListeningExecutorService listeningExecutor, + BulletinBoardClient.ClientCallback callback) { + super(listeningExecutor); + this.callback = callback; + } + + @Override + public void onSuccess(BulletinClientJobResult result) { + + BulletinClientJob job = result.getJob(); + + job.decMaxRetry(); + + // If redundancy is below threshold: retry + if (job.getMinServers() > 0 && job.isRetry()) { + Futures.addCallback(listeningExecutor.submit(new BulletinClientWorker(job)), this); + } + } + + @Override + public void onFailure(Throwable t) { + callback.handleFailure(t); + } +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/ReadMessagesFutureCallback.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/ReadMessagesFutureCallback.java new file mode 100644 index 0000000..4c43ba2 --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/ReadMessagesFutureCallback.java @@ -0,0 +1,38 @@ +package meerkat.bulletinboard.callbacks; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListeningExecutorService; +import meerkat.bulletinboard.BulletinBoardClient; +import meerkat.bulletinboard.BulletinClientJob; +import meerkat.bulletinboard.BulletinClientJobResult; +import meerkat.bulletinboard.BulletinClientWorker; +import meerkat.protobuf.BulletinBoardAPI; + +import java.util.List; + +/** + * This is a future callback used to listen to workers and run on job finish + * Depending on the type of job and the finishing status of the worker: a decision is made whether to retry or return an error + */ +public class ReadMessagesFutureCallback extends ClientFutureCallback { + + private BulletinBoardClient.ClientCallback> callback; + + public ReadMessagesFutureCallback(ListeningExecutorService listeningExecutor, + BulletinBoardClient.ClientCallback> callback) { + super(listeningExecutor); + this.callback = callback; + } + + @Override + public void onSuccess(BulletinClientJobResult result) { + + callback.handleCallback(((BulletinBoardAPI.BulletinBoardMessageList) result.getResult()).getMessageList()); + + } + + @Override + public void onFailure(Throwable t) { + callback.handleFailure(t); + } +} diff --git a/bulletin-board-server/build.gradle b/bulletin-board-server/build.gradle index 5989499..7adda9d 100644 --- a/bulletin-board-server/build.gradle +++ b/bulletin-board-server/build.gradle @@ -72,7 +72,9 @@ dependencies { test { + exclude '**/*SQLite*Test*' exclude '**/*IntegrationTest*' + outputs.upToDateWhen { false } } task integrationTest(type: Test) { diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/webapp/BulletinBoardWebApp.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/webapp/BulletinBoardWebApp.java index 5dae671..1a8abaf 100644 --- a/bulletin-board-server/src/main/java/meerkat/bulletinboard/webapp/BulletinBoardWebApp.java +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/webapp/BulletinBoardWebApp.java @@ -21,7 +21,7 @@ import meerkat.protobuf.BulletinBoardAPI.BulletinBoardMessageList; import meerkat.protobuf.BulletinBoardAPI.MessageFilterList; import meerkat.rest.Constants; -@Path("/sqlserver") +@Path(Constants.BULLETIN_BOARD_SERVER_PATH) public class BulletinBoardWebApp implements BulletinBoardServer, ServletContextListener{ private static final String BULLETIN_BOARD_ATTRIBUTE_NAME = "bulletinBoard"; @@ -63,7 +63,7 @@ public class BulletinBoardWebApp implements BulletinBoardServer, ServletContextL } } - @Path("postmessage") + @Path(Constants.POST_MESSAGE_PATH) @POST @Consumes(Constants.MEDIATYPE_PROTOBUF) @Produces(Constants.MEDIATYPE_PROTOBUF) @@ -73,7 +73,7 @@ public class BulletinBoardWebApp implements BulletinBoardServer, ServletContextL return bulletinBoard.postMessage(msg); } - @Path("readmessages") + @Path(Constants.READ_MESSAGES_PATH) @POST @Consumes(Constants.MEDIATYPE_PROTOBUF) @Produces(Constants.MEDIATYPE_PROTOBUF) diff --git a/bulletin-board-server/src/test/java/meerkat/bulletinboard/SQLiteServerIntegrationTest.java b/bulletin-board-server/src/test/java/meerkat/bulletinboard/SQLiteServerIntegrationTest.java index 95c916d..2a4d6d9 100644 --- a/bulletin-board-server/src/test/java/meerkat/bulletinboard/SQLiteServerIntegrationTest.java +++ b/bulletin-board-server/src/test/java/meerkat/bulletinboard/SQLiteServerIntegrationTest.java @@ -24,8 +24,6 @@ public class SQLiteServerIntegrationTest { private static String PROP_GETTY_URL = "gretty.httpBaseURI"; private static String DEFAULT_BASE_URL = "http://localhost:8081"; private static String BASE_URL = System.getProperty(PROP_GETTY_URL, DEFAULT_BASE_URL); - private static String SQL_SERVER_POST = "sqlserver/postmessage"; - private static String SQL_SERVER_GET = "sqlserver/readmessages"; Client client; // Connection connection; @@ -64,11 +62,8 @@ public class SQLiteServerIntegrationTest { // Test writing mechanism - System.err.println("******** Testing: " + SQL_SERVER_POST); - System.err.println(BASE_URL); - System.err.println(SQL_SERVER_POST); - System.err.println(client.getConfiguration()); - webTarget = client.target(BASE_URL).path(SQL_SERVER_POST); + System.err.println("******** Testing: " + Constants.POST_MESSAGE_PATH); + webTarget = client.target(BASE_URL).path(Constants.BULLETIN_BOARD_SERVER_PATH).path(Constants.POST_MESSAGE_PATH); System.err.println(webTarget.getUri()); msg = BulletinBoardMessage.newBuilder() @@ -114,9 +109,8 @@ public class SQLiteServerIntegrationTest { // Test reading mechanism - System.err.println("******** Testing: " + SQL_SERVER_GET); - webTarget = client.target(BASE_URL).path(SQL_SERVER_GET); - + System.err.println("******** Testing: " + Constants.READ_MESSAGES_PATH); + webTarget = client.target(BASE_URL).path(Constants.BULLETIN_BOARD_SERVER_PATH).path(Constants.READ_MESSAGES_PATH); filterList = MessageFilterList.newBuilder() .addFilter( MessageFilter.newBuilder() diff --git a/meerkat-common/build.gradle b/meerkat-common/build.gradle index d2fe0fd..6aa93cb 100644 --- a/meerkat-common/build.gradle +++ b/meerkat-common/build.gradle @@ -45,6 +45,9 @@ dependencies { // Google protobufs compile 'com.google.protobuf:protobuf-java:3.+' + // ListeningExecutor + compile 'com.google.guava:guava:11.0.+' + // Crypto compile 'org.factcenter.qilin:qilin:1.1+' compile 'org.bouncycastle:bcprov-jdk15on:1.53' diff --git a/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardClient.java b/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardClient.java index 2e466b3..1577527 100644 --- a/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardClient.java +++ b/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardClient.java @@ -1,6 +1,8 @@ package meerkat.bulletinboard; import meerkat.comm.*; +import meerkat.protobuf.Voting.*; + import static meerkat.protobuf.BulletinBoardAPI.*; import java.util.List; @@ -10,24 +12,29 @@ import java.util.List; */ public interface BulletinBoardClient { + interface ClientCallback { + void handleCallback(T msg); + void handleFailure(Throwable t); + } + /** * Initialize the client to use some specified servers - * @param meerkatDBs is the list of database locations + * @param clientParams contains the parameters required for the client setup */ - void init(List meerkatDBs); + void init(BulletinBoardClientParams clientParams); /** * Post a message to the bulletin board * @param msg */ - MessageID postMessage(BulletinBoardMessage msg) throws CommunicationException; + MessageID postMessage(BulletinBoardMessage msg, ClientCallback callback); /** * Check how "safe" a given message is * @param id * @return a normalized "redundancy score" from 0 (local only) to 1 (fully published) */ - float getRedundancy(MessageID id); + void getRedundancy(MessageID id, ClientCallback callback); /** * Read all messages posted matching the given filter @@ -35,20 +42,7 @@ public interface BulletinBoardClient { * set of messages in different calls. However, messages that are fully posted * are guaranteed to be included. * @param filterList return only messages that match the filters (null means no filtering). - * @return */ - List readMessages(MessageFilterList filterList); - - interface MessageCallback { - void handleNewMessage(List msg); - } - - /** - * Register a callback that will be called with each new message that is posted. - * The callback will be called only once for each message. - * @param callback - * @param filterList only call back for messages that match the filter. - */ - void registerNewMessageCallback(MessageCallback callback, MessageFilterList filterList); + void readMessages(MessageFilterList filterList, ClientCallback> callback); } diff --git a/meerkat-common/src/main/proto/meerkat/BulletinBoardAPI.proto b/meerkat-common/src/main/proto/meerkat/BulletinBoardAPI.proto index 28fc948..4830afc 100644 --- a/meerkat-common/src/main/proto/meerkat/BulletinBoardAPI.proto +++ b/meerkat-common/src/main/proto/meerkat/BulletinBoardAPI.proto @@ -10,6 +10,9 @@ message BoolMsg { bool value = 1; } +message IntMsg { + int32 value = 1; +} message MessageID { // The ID of a message for unique retrieval. diff --git a/meerkat-common/src/main/proto/meerkat/voting.proto b/meerkat-common/src/main/proto/meerkat/voting.proto index beb4e0c..9837cce 100644 --- a/meerkat-common/src/main/proto/meerkat/voting.proto +++ b/meerkat-common/src/main/proto/meerkat/voting.proto @@ -52,6 +52,16 @@ message BallotAnswerTranslationTable { bytes data = 1; } +// Data required in order to access the Bulletin Board Servers +message BulletinBoardClientParams { + + // Addresses of all Bulletin Board Servers + repeated string bulletinBoardAddress = 1; + + // Threshold fraction of successful servers posts before a post task is considered complete + float minRedundancy = 2; +} + message ElectionParams { // TODO: different sets of keys for different roles? repeated SignatureVerificationKey trusteeVerificationKeys = 1; @@ -75,4 +85,6 @@ message ElectionParams { // Translation table between answers and plaintext encoding BallotAnswerTranslationTable answerTranslationTable = 7; + // Data required in order to access the Bulletin Board Servers + BulletinBoardClientParams bulletinBoardClientParams = 8; } diff --git a/restful-api-common/src/main/java/meerkat/rest/Constants.java b/restful-api-common/src/main/java/meerkat/rest/Constants.java index 2c04248..73ed7d1 100644 --- a/restful-api-common/src/main/java/meerkat/rest/Constants.java +++ b/restful-api-common/src/main/java/meerkat/rest/Constants.java @@ -5,4 +5,8 @@ package meerkat.rest; */ public interface Constants { public static final String MEDIATYPE_PROTOBUF = "application/x-protobuf"; + + public static final String BULLETIN_BOARD_SERVER_PATH = "/bbserver"; + public static final String READ_MESSAGES_PATH = "/readmessages"; + public static final String POST_MESSAGE_PATH = "/postmessage"; } From 520697d121367dee8561d3cc3967a26865ac5978 Mon Sep 17 00:00:00 2001 From: Arbel Deutsch Peled Date: Sat, 12 Dec 2015 11:54:52 +0200 Subject: [PATCH 08/15] Added named parameters to the BulletinBoardSQLServer. Added support for H2 SQL engine. Further generalization of the BulletinBoardSQLServer. --- bulletin-board-server/build.gradle | 9 +- .../sqlserver/BulletinBoardSQLServer.java | 420 +++++++++--------- .../sqlserver/H2QueryProvider.java | 108 ++++- .../sqlserver/MySQLQueryProvider.java | 78 +++- .../sqlserver/SQLiteQueryProvider.java | 56 ++- .../sqlserver/mappers/EntryNumMapper.java | 18 + .../sqlserver/mappers/MessageMapper.java | 32 ++ .../sqlserver/mappers/SignatureMapper.java | 28 ++ .../GenericBulletinBoardServerTest.java | 3 +- .../H2BulletinBoardServerTest.java | 20 +- .../MySQLBulletinBoardServerTest.java | 7 +- .../main/proto/meerkat/BulletinBoardAPI.proto | 4 + 12 files changed, 502 insertions(+), 281 deletions(-) create mode 100644 bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/EntryNumMapper.java create mode 100644 bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/MessageMapper.java create mode 100644 bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/SignatureMapper.java diff --git a/bulletin-board-server/build.gradle b/bulletin-board-server/build.gradle index 7adda9d..eb197c9 100644 --- a/bulletin-board-server/build.gradle +++ b/bulletin-board-server/build.gradle @@ -47,6 +47,7 @@ dependencies { compile 'org.glassfish.jersey.containers:jersey-container-servlet:2.22.+' // JDBC connections + compile 'org.springframework:spring-jdbc:4.2.+' compile 'org.xerial:sqlite-jdbc:3.7.+' compile 'mysql:mysql-connector-java:5.1.+' compile 'com.h2database:h2:1.0.+' @@ -73,8 +74,14 @@ dependencies { test { exclude '**/*SQLite*Test*' + exclude '**/*H2*Test*' + exclude '**/*MySql*Test' exclude '**/*IntegrationTest*' - outputs.upToDateWhen { false } +} + +task dbTest(type: Test) { + include '**/*H2*Test*' + include '**/*MySql*Test' } task integrationTest(type: Test) { diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/BulletinBoardSQLServer.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/BulletinBoardSQLServer.java index 2689f70..ae2095a 100644 --- a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/BulletinBoardSQLServer.java +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/BulletinBoardSQLServer.java @@ -3,10 +3,12 @@ package meerkat.bulletinboard.sqlserver; import java.sql.*; import java.util.*; -import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.ProtocolStringList; import meerkat.bulletinboard.BulletinBoardServer; +import meerkat.bulletinboard.sqlserver.mappers.EntryNumMapper; +import meerkat.bulletinboard.sqlserver.mappers.MessageMapper; +import meerkat.bulletinboard.sqlserver.mappers.SignatureMapper; import meerkat.comm.CommunicationException; import meerkat.protobuf.BulletinBoardAPI.*; import meerkat.protobuf.Crypto.Signature; @@ -14,6 +16,13 @@ import meerkat.protobuf.Crypto.SignatureVerificationKey; import meerkat.crypto.Digest; import meerkat.crypto.concrete.SHA256Digest; +import javax.sql.DataSource; + +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; + /** * This is a generic SQL implementation of the BulletinBoardServer API. */ @@ -27,16 +36,80 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ /** * Allowed query types. - * Note that each query returned has to comply with the placeholder ("?") requirements written in its comment. + * Note that each query returned has to comply with the parameter names specified ny getParamNames */ public static enum QueryType { - FIND_MSG_ID, // Placeholders for: MsgId - INSERT_MSG, // Placeholders for: MsgId, Msg - INSERT_NEW_TAG, // Placeholders for: Tag - CONNECT_TAG, // Placeholders for: EntryNum, Tag - ADD_SIGNATURE, // Placeholders for: EntryNum, SignerId, Signature - GET_SIGNATURES, // Placeholders for: EntryNum - GET_MESSAGES // Placeholders for: N/A + + FIND_MSG_ID(new String[] {"MsgId"}), + INSERT_MSG(new String[] {"MsgId","Msg"}), + INSERT_NEW_TAG(new String[] {"Tag"}), + CONNECT_TAG(new String[] {"EntryNum","Tag"}), + ADD_SIGNATURE(new String[] {"EntryNum","SignerId","Signature"}), + GET_SIGNATURES(new String[] {"EntryNum"}), + GET_MESSAGES(new String[] {}); + + private String[] paramNames; + + private QueryType(String[] paramNames) { + this.paramNames = paramNames; + } + + public String[] getParamNames() { + return paramNames; + } + + } + + /** + * This enum provides the standard translation between a filter type and the corresponding parameter name in the SQL query + */ + public static enum FilterTypeParam { + + ENTRY_NUM("EntryNum", Types.INTEGER), + MSG_ID("MsgId", Types.BLOB), + SIGNER_ID("SignerId", Types.BLOB), + TAG("Tag", Types.VARCHAR), + LIMIT("Limit", Types.INTEGER); + + private FilterTypeParam(String paramName, int paramType) { + this.paramName = paramName; + this.paramType = paramType; + } + + private String paramName; + private int paramType; + + public static FilterTypeParam getFilterTypeParamName(FilterType filterType) { + switch (filterType) { + + case MSG_ID: + return MSG_ID; + + case EXACT_ENTRY: // Go through + case MAX_ENTRY: + return ENTRY_NUM; + + case SIGNER_ID: + return SIGNER_ID; + + case TAG: + return TAG; + + case MAX_MESSAGES: + return LIMIT; + + default: + return null; + } + } + + public String getParamName() { + return paramName; + } + + public int getParamType() { + return paramType; + } } /** @@ -46,12 +119,21 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ */ public String getSQLString(QueryType queryType) throws IllegalArgumentException; - public String getCondition(FilterType filterType) throws IllegalArgumentException; + /** + * Used to retrieve a condition to add to an SQL statement that will make the result comply with the filter type + * @param filterType is the filter type + * @param serialNum is a unique number used to identify the condition variables from other condition instances + * @return The SQL string for the condition + * @throws IllegalArgumentException if the filter type used is not supported + */ + public String getCondition(FilterType filterType, int serialNum) throws IllegalArgumentException; + + public String getConditionParamTypeName(FilterType filterType) throws IllegalArgumentException; /** * @return the string needed in order to connect to the DB. */ - public String getConnectionString(); + public DataSource getDataSource(); /** * This is used to get a list of queries that together create the schema needed for the DB. @@ -70,6 +152,30 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ } + private Object getParam(MessageFilter messageFilter) { + + switch (messageFilter.getType()) { + + case MSG_ID: // Go through + case SIGNER_ID: + return messageFilter.getId().toByteArray(); + + case EXACT_ENTRY: // Go through + case MAX_ENTRY: + return messageFilter.getEntry(); + + case TAG: + return messageFilter.getTag(); + + case MAX_MESSAGES: + return messageFilter.getMaxMessages(); + + default: + return null; + } + + } + /** * This class implements a comparator for the MessageFilter class * The comparison is done solely by comparing the type of the filter @@ -85,7 +191,7 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ protected SQLQueryProvider sqlQueryProvider; - protected Connection connection; + protected NamedParameterJdbcTemplate jdbcTemplate; protected Digest digest; @@ -103,18 +209,19 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ this.sqlQueryProvider = sqlQueryProvider; } + /** + * This method creates the schema in the given DB to prepare for future transactions + * It does not assume anything about the current state of the database + * @throws SQLException + */ private void createSchema() throws SQLException { final int TIMEOUT = 20; - Statement statement = connection.createStatement(); - statement.setQueryTimeout(TIMEOUT); - for (String command : sqlQueryProvider.getSchemaCreationCommands()) { - statement.executeUpdate(command); + jdbcTemplate.update(command,(Map) null); } - statement.close(); } /** @@ -126,13 +233,7 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ digest = new SHA256Digest(); - try{ - - String dbString = sqlQueryProvider.getConnectionString(); - connection = DriverManager.getConnection(dbString); - } catch (SQLException e) { - throw new CommunicationException("Couldn't form a connection with the database " + e.getMessage()); - } + jdbcTemplate = new NamedParameterJdbcTemplate(sqlQueryProvider.getDataSource()); try { createSchema(); @@ -161,26 +262,18 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ */ protected void insertNewTags(String[] tags) throws SQLException { - PreparedStatement pstmt; String sql; - try { + sql = sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.INSERT_NEW_TAG); + Map namedParameters[] = new HashMap[tags.length]; - sql = sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.INSERT_NEW_TAG); - pstmt = connection.prepareStatement(sql); - - for (String tag : tags){ - pstmt.setString(1, tag); - pstmt.addBatch(); - } - - pstmt.executeBatch(); - pstmt.close(); - - } catch (SQLException e){ - throw new SQLException("Error adding new tags to table: " + e.getMessage()); + for (int i = 0 ; i < tags.length ; i++){ + namedParameters[i] = new HashMap(); + namedParameters[i].put("Tag", tags[i]); } + jdbcTemplate.batchUpdate(sql, namedParameters); + } /** @@ -200,10 +293,10 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ if (!verifyMessage(msg)) { return boolToBoolMsg(false); } - - PreparedStatement pstmt; - ResultSet rs; + String sql; + Map[] namedParameterArray; + byte[] msgID; long entryNum; @@ -221,36 +314,28 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ msgID = digest.digest(); // Add message to table if needed and store entry number of message. - - try { + - sql = sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.FIND_MSG_ID); - pstmt = connection.prepareStatement(sql); - pstmt.setBytes(1, msgID); - rs = pstmt.executeQuery(); - - if (rs.next()){ - - entryNum = rs.getLong(1); - - } else{ - - sql = sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.INSERT_MSG); - pstmt = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); - pstmt.setBytes(1, msgID); - pstmt.setBytes(2, msg.getMsg().toByteArray()); - pstmt.executeUpdate(); - - rs = pstmt.getGeneratedKeys(); - rs.next(); - entryNum = rs.getLong(1); - - } - - pstmt.close(); - - } catch (SQLException e) { - throw new CommunicationException("Error inserting into MsgTable: " + e.getMessage()); + sql = sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.FIND_MSG_ID); + Map namedParameters = new HashMap(); + namedParameters.put("MsgId",msgID); + + List entryNums = jdbcTemplate.query(sql, new MapSqlParameterSource(namedParameters), new EntryNumMapper()); + + if (entryNums.size() > 0){ + + entryNum = entryNums.get(0); + + } else{ + + sql = sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.INSERT_MSG); + namedParameters.put("Msg", msg.getMsg().toByteArray()); + + KeyHolder keyHolder = new GeneratedKeyHolder(); + jdbcTemplate.update(sql,new MapSqlParameterSource(namedParameters),keyHolder); + + entryNum = keyHolder.getKey().longValue(); + } // Retrieve tags and store new ones in tag table. @@ -268,24 +353,18 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ } // Connect message to tags. - - try{ - sql = sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.CONNECT_TAG); - pstmt = connection.prepareStatement(sql); - - pstmt.setLong(1, entryNum); - - for (String tag : tags){ - pstmt.setString(2, tag); - pstmt.addBatch(); - } - - pstmt.executeBatch(); - pstmt.close(); - - } catch (SQLException e) { - throw new CommunicationException("Error Linking tags: " + e.getMessage()); + + sql = sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.CONNECT_TAG); + + namedParameterArray = new HashMap[tags.length]; + + for (int i = 0 ; i < tags.length ; i++) { + namedParameterArray[i] = new HashMap(); + namedParameterArray[i].put("EntryNum", entryNum); + namedParameterArray[i].put("Tag", tags[i]); } + + jdbcTemplate.batchUpdate(sql, namedParameterArray); // Retrieve signatures. @@ -294,170 +373,111 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ signatures = signatureList.toArray(signatures); // Connect message to signatures. - - try{ - sql = sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.ADD_SIGNATURE); - pstmt = connection.prepareStatement(sql); - - pstmt.setLong(1, entryNum); - - for (Signature sig : signatures){ - - pstmt.setBytes(2, sig.getSignerId().toByteArray()); - pstmt.setBytes(3, sig.toByteArray()); - pstmt.addBatch(); - } - - pstmt.executeBatch(); - pstmt.close(); - - } catch (SQLException e) { - throw new CommunicationException("Error Linking tags: " + e.getMessage()); + + sql = sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.ADD_SIGNATURE); + + namedParameterArray = new HashMap[signatures.length]; + + for (int i = 0 ; i < signatures.length ; i++) { + namedParameterArray[i] = new HashMap(); + namedParameterArray[i].put("EntryNum", entryNum); + namedParameterArray[i].put("SignerId", signatures[i].getSignerId().toByteArray()); + namedParameterArray[i].put("Signature", signatures[i].toByteArray()); } + jdbcTemplate.batchUpdate(sql,namedParameterArray); + return boolToBoolMsg(true); } @Override public BulletinBoardMessageList readMessages(MessageFilterList filterList) throws CommunicationException { - PreparedStatement pstmt; - ResultSet messages, signatures; - long entryNum; BulletinBoardMessageList.Builder resultListBuilder = BulletinBoardMessageList.newBuilder(); - BulletinBoardMessage.Builder messageBuilder; String sql; + MapSqlParameterSource namedParameters; + int paramNum; + + MessageMapper messageMapper = new MessageMapper(); + SignatureMapper signatureMapper = new SignatureMapper(); List filters = new ArrayList(filterList.getFilterList()); - int i; - - boolean tagsRequired = false; - boolean signaturesRequired = false; boolean isFirstFilter = true; Collections.sort(filters, new FilterTypeComparator()); - // Check if Tag/Signature tables are required for filtering purposes. + // Check if Tag/Signature tables are required for filtering purposes sql = sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.GET_MESSAGES); - // Add conditions. + // Add conditions - if (!filters.isEmpty()){ + namedParameters = new MapSqlParameterSource(); + + if (!filters.isEmpty()) { sql += " WHERE "; - for (MessageFilter filter : filters){ + for (paramNum = 0 ; paramNum < filters.size() ; paramNum++) { - if (filter.getType().getNumber() != FilterType.MAX_MESSAGES_VALUE){ - if (isFirstFilter){ + MessageFilter filter = filters.get(paramNum); + + if (filter.getType().getNumber() != FilterType.MAX_MESSAGES_VALUE) { + if (isFirstFilter) { isFirstFilter = false; - } else{ + } else { sql += " AND "; } } - sql += sqlQueryProvider.getCondition(filter.getType()); + sql += sqlQueryProvider.getCondition(filter.getType(), paramNum); + + SQLQueryProvider.FilterTypeParam filterTypeParam = SQLQueryProvider.FilterTypeParam.getFilterTypeParamName(filter.getType()); + + namedParameters.addValue( + filterTypeParam.getParamName() + Integer.toString(paramNum), + getParam(filter), + filterTypeParam.getParamType(), + sqlQueryProvider.getConditionParamTypeName(filter.getType())); + } } - // Make query. + // Run query - try { - pstmt = connection.prepareStatement(sql); + List msgBuilders = jdbcTemplate.query(sql, namedParameters, messageMapper); - // Specify values for filters. - i = 1; - for (MessageFilter filter : filters){ + // Compile list of messages - switch (filter.getType().getNumber()){ + sql = sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.GET_SIGNATURES); - case FilterType.EXACT_ENTRY_VALUE: // Go through. - case FilterType.MAX_ENTRY_VALUE: - pstmt.setLong(i, filter.getEntry()); - i++; - break; + for (BulletinBoardMessage.Builder msgBuilder : msgBuilders) { - case FilterType.MSG_ID_VALUE: // Go through. - case FilterType.SIGNER_ID_VALUE: - pstmt.setBytes(i, filter.getId().toByteArray()); - i++; - break; + // Retrieve signatures - case FilterType.TAG_VALUE: - pstmt.setString(i, filter.getTag()); - i++; - break; + namedParameters = new MapSqlParameterSource(); + namedParameters.addValue("EntryNum", msgBuilder.getEntryNum()); - // The max-messages condition is applied as a suffix. Therefore, it is treated differently. - case FilterType.MAX_MESSAGES_VALUE: - pstmt.setLong(filters.size(), filter.getMaxMessages()); - i++; - break; + List signatures = jdbcTemplate.query(sql, namedParameters, signatureMapper); - } - } + // Append signatures + msgBuilder.addAllSig(signatures); - // Run query. + // Finalize message and add to message list. - messages = pstmt.executeQuery(); + resultListBuilder.addMessage(msgBuilder.build()); - // Compile list of messages. - - sql = sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.GET_SIGNATURES); - pstmt = connection.prepareStatement(sql); - - while (messages.next()){ - - // Get entry number and retrieve signatures. - - entryNum = messages.getLong(1); - pstmt.setLong(1, entryNum); - signatures = pstmt.executeQuery(); - - // Create message and append signatures. - - messageBuilder = BulletinBoardMessage.newBuilder() - .setEntryNum(entryNum) - .setMsg(UnsignedBulletinBoardMessage.parseFrom(messages.getBytes(2))); - - while (signatures.next()){ - messageBuilder.addSig(Signature.parseFrom(signatures.getBytes(1))); - } - - // Finalize message and add to message list. - - resultListBuilder.addMessage(messageBuilder.build()); - - } - - pstmt.close(); - - } catch (SQLException e){ - throw new CommunicationException("Error reading messages from DB: " + e.getMessage()); - } catch (InvalidProtocolBufferException e) { - throw new CommunicationException("Invalid data from DB: " + e.getMessage()); } //Combine results and return. - return resultListBuilder.build(); + } @Override - public void close() throws CommunicationException { - - try{ - connection.close(); - } catch (SQLException e) { - - throw new CommunicationException("Couldn't close connection to the database"); - - } - - } + public void close() {} } diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/H2QueryProvider.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/H2QueryProvider.java index d80b302..cf3fd68 100644 --- a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/H2QueryProvider.java +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/H2QueryProvider.java @@ -1,7 +1,12 @@ package meerkat.bulletinboard.sqlserver; import meerkat.protobuf.BulletinBoardAPI.FilterType; +import org.h2.jdbcx.JdbcDataSource; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.sql.DataSource; import java.util.LinkedList; import java.util.List; @@ -11,10 +16,14 @@ import java.util.List; public class H2QueryProvider implements BulletinBoardSQLServer.SQLQueryProvider { - private String dbConnectionString; + private String dbAddress; + private String username; + private String password; - public H2QueryProvider(String dbAddress) { - dbConnectionString = "jdbc:h2:" + dbAddress + ";MODE=MYSQL"; + public H2QueryProvider(String dbAddress, String username, String password) { + this.dbAddress = dbAddress; + this.username = username; + this.password = password; } @@ -23,20 +32,33 @@ public class H2QueryProvider implements BulletinBoardSQLServer.SQLQueryProvider switch(queryType) { case ADD_SIGNATURE: - return "INSERT IGNORE INTO SignatureTable (EntryNum, SignerId, Signature) VALUES (?,?,?)"; + return "INSERT INTO SignatureTable (EntryNum, SignerId, Signature)" + + " SELECT DISTINCT :EntryNum AS Entry, :SignerId AS Id, :Signature AS Sig FROM UtilityTable AS Temp" + + " WHERE NOT EXISTS" + + " (SELECT 1 FROM SignatureTable AS SubTable WHERE SubTable.SignerId = :SignerId AND SubTable.EntryNum = :EntryNum)"; + case CONNECT_TAG: - return "INSERT IGNORE INTO MsgTagTable (TagId, EntryNum)" - + " SELECT TagTable.TagId, ? AS EntryNum FROM TagTable WHERE Tag = ?"; + return "INSERT INTO MsgTagTable (TagId, EntryNum)" + + " SELECT DISTINCT TagTable.TagId, :EntryNum AS NewEntry FROM TagTable WHERE Tag = :Tag" + + " AND NOT EXISTS (SELECT 1 FROM MsgTagTable AS SubTable WHERE SubTable.TagId = TagTable.TagId" + + " AND SubTable.EntryNum = :EntryNum)"; + case FIND_MSG_ID: - return "SELECT EntryNum From MsgTable WHERE MsgId = ?"; + return "SELECT EntryNum From MsgTable WHERE MsgId = :MsgId"; + case GET_MESSAGES: return "SELECT MsgTable.EntryNum, MsgTable.Msg FROM MsgTable"; + case GET_SIGNATURES: - return "SELECT Signature FROM SignatureTable WHERE EntryNum = ?"; + return "SELECT Signature FROM SignatureTable WHERE EntryNum = :EntryNum"; + case INSERT_MSG: - return "INSERT INTO MsgTable (MsgId, Msg) VALUES(?,?)"; + return "INSERT INTO MsgTable (MsgId, Msg) VALUES(:MsgId,:Msg)"; + case INSERT_NEW_TAG: - return "INSERT IGNORE INTO TagTable(Tag) VALUES (?)"; + return "INSERT INTO TagTable(Tag) SELECT DISTINCT :Tag AS NewTag FROM UtilityTable WHERE" + + " NOT EXISTS (SELECT 1 FROM TagTable AS SubTable WHERE SubTable.Tag = :Tag)"; + default: throw new IllegalArgumentException("Cannot serve a query of type " + queryType); } @@ -44,24 +66,26 @@ public class H2QueryProvider implements BulletinBoardSQLServer.SQLQueryProvider } @Override - public String getCondition(FilterType filterType) throws IllegalArgumentException { + public String getCondition(FilterType filterType, int serialNum) throws IllegalArgumentException { + + String serialString = Integer.toString(serialNum); switch(filterType) { case EXACT_ENTRY: - return "MsgTable.EntryNum = ?"; + return "MsgTable.EntryNum = :EntryNum" + serialString; case MAX_ENTRY: - return "MsgTable.EntryNum <= ?"; + return "MsgTable.EntryNum <= :EntryNum" + serialString; case MAX_MESSAGES: - return "LIMIT ?"; + return "LIMIT :Limit" + serialString; case MSG_ID: - return "MsgTable.MsgId = ?"; + return "MsgTable.MsgId = MsgId" + serialString; case SIGNER_ID: return "EXISTS (SELECT 1 FROM SignatureTable" - + " WHERE SignatureTable.SignerId = ? AND SignatureTable.EntryNum = MsgTable.EntryNum)"; + + " WHERE SignatureTable.SignerId = :SignerId" + serialString + " AND SignatureTable.EntryNum = MsgTable.EntryNum)"; case TAG: return "EXISTS (SELECT 1 FROM TagTable" + " INNER JOIN MsgTagTable ON TagTable.TagId = MsgTagTable.TagId" - + " WHERE TagTable.Tag = ? AND MsgTagTable.EntryNum = MsgTable.EntryNum)"; + + " WHERE TagTable.Tag = :Tag" + serialString + " AND MsgTagTable.EntryNum = MsgTable.EntryNum)"; default: throw new IllegalArgumentException("Cannot serve a filter of type " + filterType); } @@ -69,8 +93,43 @@ public class H2QueryProvider implements BulletinBoardSQLServer.SQLQueryProvider } @Override - public String getConnectionString() { - return dbConnectionString; + public String getConditionParamTypeName(FilterType filterType) throws IllegalArgumentException { + + switch(filterType) { + case EXACT_ENTRY: // Go through + case MAX_ENTRY: // Go through + case MAX_MESSAGES: + return "INT"; + + case MSG_ID: // Go through + case SIGNER_ID: + return "TINYBLOB"; + + case TAG: + return "VARCHAR"; + + default: + throw new IllegalArgumentException("Cannot serve a filter of type " + filterType); + } + + } + + @Override + public DataSource getDataSource() { + // TODO: Fix this + JdbcDataSource dataSource = new JdbcDataSource(); + dataSource.setURL("jdbc:h2:~/" + dbAddress + "/meerkat"); // TODO: make this generic + dataSource.setUser(username); + dataSource.setPassword(password); +// Context ctx = null; +// try { +// ctx = new InitialContext(); +// ctx.bind("jdbc/dsName", dataSource); +// } catch (NamingException e) { +// e.printStackTrace(); +// } + + return dataSource; } @@ -80,7 +139,7 @@ public class H2QueryProvider implements BulletinBoardSQLServer.SQLQueryProvider list.add("CREATE TABLE IF NOT EXISTS MsgTable (EntryNum INT NOT NULL AUTO_INCREMENT PRIMARY KEY, MsgId TINYBLOB UNIQUE, Msg BLOB)"); - list.add("CREATE TABLE IF NOT EXISTS TagTable (TagId INT NOT NULL AUTO_INCREMENT PRIMARY KEY, Tag varchar(50) UNIQUE)"); + list.add("CREATE TABLE IF NOT EXISTS TagTable (TagId INT NOT NULL AUTO_INCREMENT PRIMARY KEY, Tag VARCHAR(50) UNIQUE)"); list.add("CREATE TABLE IF NOT EXISTS MsgTagTable (EntryNum INT, TagId INT," + " FOREIGN KEY (EntryNum) REFERENCES MsgTable(EntryNum)," @@ -90,7 +149,13 @@ public class H2QueryProvider implements BulletinBoardSQLServer.SQLQueryProvider list.add("CREATE TABLE IF NOT EXISTS SignatureTable (EntryNum INT, SignerId TINYBLOB, Signature TINYBLOB UNIQUE," + " FOREIGN KEY (EntryNum) REFERENCES MsgTable(EntryNum))"); - list.add("CREATE UNIQUE INDEX IF NOT EXISTS SignerIdIndex ON SignatureTable(SignerId)"); + list.add("CREATE INDEX IF NOT EXISTS SignerIndex ON SignatureTable(SignerId)"); + list.add("CREATE UNIQUE INDEX IF NOT EXISTS SignerIndex ON SignatureTable(SignerId, EntryNum)"); + + // This is used to create a simple table with one entry. + // It is used for implementing a workaround for the missing INSERT IGNORE syntax + list.add("CREATE TABLE IF NOT EXISTS UtilityTable (Entry INT)"); + list.add("INSERT INTO UtilityTable (Entry) VALUES (1)"); return list; } @@ -99,6 +164,7 @@ public class H2QueryProvider implements BulletinBoardSQLServer.SQLQueryProvider public List getSchemaDeletionCommands() { List list = new LinkedList(); + list.add("DROP TABLE IF EXISTS UtilityTable"); list.add("DROP INDEX IF EXISTS SignerIdIndex"); list.add("DROP TABLE IF EXISTS MsgTagTable"); list.add("DROP TABLE IF EXISTS SignatureTable"); diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/MySQLQueryProvider.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/MySQLQueryProvider.java index 18d8189..9956646 100644 --- a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/MySQLQueryProvider.java +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/MySQLQueryProvider.java @@ -1,7 +1,10 @@ package meerkat.bulletinboard.sqlserver; +import com.mysql.jdbc.jdbc2.optional.MysqlDataSource; +import meerkat.bulletinboard.sqlserver.BulletinBoardSQLServer.SQLQueryProvider; import meerkat.protobuf.BulletinBoardAPI.FilterType; +import javax.sql.DataSource; import java.util.LinkedList; import java.util.List; @@ -9,12 +12,16 @@ import java.util.List; * Created by Arbel Deutsch Peled on 09-Dec-15. */ -public class MySQLQueryProvider implements BulletinBoardSQLServer.SQLQueryProvider { +public class MySQLQueryProvider implements SQLQueryProvider { - String dbConnectionString; + private String dbAddress; + private String username; + private String password; public MySQLQueryProvider(String dbAddress, String username, String password) { - dbConnectionString = "jdbc:mysql:" + dbAddress + "?user=" + username + "&password=" + password; + this.dbAddress = dbAddress; + this.username = username; + this.password = password; } @Override @@ -22,20 +29,20 @@ public class MySQLQueryProvider implements BulletinBoardSQLServer.SQLQueryProvid switch(queryType) { case ADD_SIGNATURE: - return "INSERT IGNORE INTO SignatureTable (EntryNum, SignerId, Signature) VALUES (?,?,?)"; + return "INSERT IGNORE INTO SignatureTable (EntryNum, SignerId, Signature) VALUES (:EntryNum, :SignerId, :Signature)"; case CONNECT_TAG: return "INSERT IGNORE INTO MsgTagTable (TagId, EntryNum)" - + " SELECT TagTable.TagId, ? AS EntryNum FROM TagTable WHERE Tag = ?"; + + " SELECT TagTable.TagId, :EntryNum AS EntryNum FROM TagTable WHERE Tag = :Tag"; case FIND_MSG_ID: - return "SELECT EntryNum From MsgTable WHERE MsgId = ?"; + return "SELECT EntryNum From MsgTable WHERE MsgId = :MsgId"; case GET_MESSAGES: return "SELECT MsgTable.EntryNum, MsgTable.Msg FROM MsgTable"; case GET_SIGNATURES: - return "SELECT Signature FROM SignatureTable WHERE EntryNum = ?"; + return "SELECT Signature FROM SignatureTable WHERE EntryNum = :EntryNum"; case INSERT_MSG: - return "INSERT INTO MsgTable (MsgId, Msg) VALUES(?,?)"; + return "INSERT INTO MsgTable (MsgId, Msg) VALUES(:MsgId, :Msg)"; case INSERT_NEW_TAG: - return "INSERT IGNORE INTO TagTable(Tag) VALUES (?)"; + return "INSERT IGNORE INTO TagTable(Tag) VALUES (:Tag)"; default: throw new IllegalArgumentException("Cannot serve a query of type " + queryType); } @@ -43,24 +50,26 @@ public class MySQLQueryProvider implements BulletinBoardSQLServer.SQLQueryProvid } @Override - public String getCondition(FilterType filterType) throws IllegalArgumentException { + public String getCondition(FilterType filterType, int serialNum) throws IllegalArgumentException { + + String serialString = Integer.toString(serialNum); switch(filterType) { case EXACT_ENTRY: - return "MsgTable.EntryNum = ?"; + return "MsgTable.EntryNum = :EntryNum" + serialString; case MAX_ENTRY: - return "MsgTable.EntryNum <= ?"; + return "MsgTable.EntryNum <= :EntryNum" + serialString; case MAX_MESSAGES: - return "LIMIT ?"; + return "LIMIT :Limit" + serialString; case MSG_ID: - return "MsgTable.MsgId = ?"; + return "MsgTable.MsgId = :MsgId" + serialString; case SIGNER_ID: return "EXISTS (SELECT 1 FROM SignatureTable" - + " WHERE SignatureTable.SignerId = ? AND SignatureTable.EntryNum = MsgTable.EntryNum)"; + + " WHERE SignatureTable.SignerId = :SignerId" + serialString + " AND SignatureTable.EntryNum = MsgTable.EntryNum)"; case TAG: return "EXISTS (SELECT 1 FROM TagTable" + " INNER JOIN MsgTagTable ON TagTable.TagId = MsgTagTable.TagId" - + " WHERE TagTable.Tag = ? AND MsgTagTable.EntryNum = MsgTable.EntryNum)"; + + " WHERE TagTable.Tag = :Tag" + serialString + " AND MsgTagTable.EntryNum = MsgTable.EntryNum)"; default: throw new IllegalArgumentException("Cannot serve a filter of type " + filterType); } @@ -68,10 +77,37 @@ public class MySQLQueryProvider implements BulletinBoardSQLServer.SQLQueryProvid } @Override - public String getConnectionString() { - return dbConnectionString; + public String getConditionParamTypeName(FilterType filterType) throws IllegalArgumentException { + + switch(filterType) { + case EXACT_ENTRY: // Go through + case MAX_ENTRY: // Go through + case MAX_MESSAGES: + return "INT"; + + case MSG_ID: // Go through + case SIGNER_ID: + return "TINYBLOB"; + + case TAG: + return "VARCHAR"; + + default: + throw new IllegalArgumentException("Cannot serve a filter of type " + filterType); + } + } + @Override + public DataSource getDataSource() { + MysqlDataSource dataSource = new MysqlDataSource(); + dataSource.setDatabaseName("meerkat"); //TODO: Make generic + dataSource.setUser(username); + dataSource.setPassword(password); + dataSource.setServerName(dbAddress); + + return dataSource; + } @Override public List getSchemaCreationCommands() { @@ -79,15 +115,15 @@ public class MySQLQueryProvider implements BulletinBoardSQLServer.SQLQueryProvid list.add("CREATE TABLE IF NOT EXISTS MsgTable (EntryNum INT NOT NULL AUTO_INCREMENT PRIMARY KEY, MsgId TINYBLOB, Msg BLOB, UNIQUE(MsgId(50)))"); - list.add("CREATE TABLE IF NOT EXISTS TagTable (TagId INT NOT NULL AUTO_INCREMENT PRIMARY KEY, Tag varchar(50), UNIQUE(Tag))"); + list.add("CREATE TABLE IF NOT EXISTS TagTable (TagId INT NOT NULL AUTO_INCREMENT PRIMARY KEY, Tag VARCHAR(50), UNIQUE(Tag))"); list.add("CREATE TABLE IF NOT EXISTS MsgTagTable (EntryNum INT, TagId INT," + " CONSTRAINT FOREIGN KEY (EntryNum) REFERENCES MsgTable(EntryNum)," + " CONSTRAINT FOREIGN KEY (TagId) REFERENCES TagTable(TagId)," + " CONSTRAINT UNIQUE (EntryNum, TagID))"); - list.add("CREATE TABLE IF NOT EXISTS SignatureTable (EntryNum INT, SignerId TINYBLOB, Signature TINYBLOB, UNIQUE(Signature(150))," - + " INDEX(SignerId(50)), CONSTRAINT FOREIGN KEY (EntryNum) REFERENCES MsgTable(EntryNum))"); + list.add("CREATE TABLE IF NOT EXISTS SignatureTable (EntryNum INT, SignerId TINYBLOB, Signature TINYBLOB," + + " INDEX(SignerId(32)), CONSTRAINT Uni UNIQUE(SignerId(32), EntryNum), CONSTRAINT FOREIGN KEY (EntryNum) REFERENCES MsgTable(EntryNum))"); return list; } diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/SQLiteQueryProvider.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/SQLiteQueryProvider.java index 5978c9d..945ae47 100644 --- a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/SQLiteQueryProvider.java +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/SQLiteQueryProvider.java @@ -1,22 +1,22 @@ package meerkat.bulletinboard.sqlserver; import meerkat.protobuf.BulletinBoardAPI.*; +import org.sqlite.SQLiteDataSource; +import javax.sql.DataSource; import java.util.LinkedList; import java.util.List; -import static meerkat.protobuf.BulletinBoardAPI.FilterType; - /** * Created by Arbel Deutsch Peled on 09-Dec-15. */ public class SQLiteQueryProvider implements BulletinBoardSQLServer.SQLQueryProvider { - String dbConnectionString; + String dbName; - public SQLiteQueryProvider(String dbAddress) { - dbConnectionString = "jdbc:sqlite:" + dbAddress; + public SQLiteQueryProvider(String dbName) { + this.dbName = dbName; } @Override @@ -24,20 +24,20 @@ public class SQLiteQueryProvider implements BulletinBoardSQLServer.SQLQueryProvi switch(queryType) { case ADD_SIGNATURE: - return "INSERT OR IGNORE INTO SignatureTable (EntryNum, SignerId, Signature) VALUES (?,?,?)"; + return "INSERT OR IGNORE INTO SignatureTable (EntryNum, SignerId, Signature) VALUES (:EntryNum,:SignerId,:Signature)"; case CONNECT_TAG: return "INSERT OR IGNORE INTO MsgTagTable (TagId, EntryNum)" - + " SELECT TagTable.TagId, ? AS EntryNum FROM TagTable WHERE Tag = ?"; + + " SELECT TagTable.TagId, :EntryNum AS EntryNum FROM TagTable WHERE Tag = :Tag"; case FIND_MSG_ID: - return "SELECT EntryNum From MsgTable WHERE MsgId = ?"; + return "SELECT EntryNum From MsgTable WHERE MsgId = :MsgId"; case GET_MESSAGES: return "SELECT MsgTable.EntryNum, MsgTable.Msg FROM MsgTable"; case GET_SIGNATURES: - return "SELECT Signature FROM SignatureTable WHERE EntryNum = ?"; + return "SELECT Signature FROM SignatureTable WHERE EntryNum = :EntryNum"; case INSERT_MSG: - return "INSERT INTO MsgTable (MsgId, Msg) VALUES(?,?)"; + return "INSERT INTO MsgTable (MsgId, Msg) VALUES(:MsgId,:Msg)"; case INSERT_NEW_TAG: - return "INSERT OR IGNORE INTO TagTable(Tag) VALUES (?)"; + return "INSERT OR IGNORE INTO TagTable(Tag) VALUES (:Tag)"; default: throw new IllegalArgumentException("Cannot serve a query of type " + queryType); } @@ -45,24 +45,26 @@ public class SQLiteQueryProvider implements BulletinBoardSQLServer.SQLQueryProvi } @Override - public String getCondition(FilterType filterType) throws IllegalArgumentException { + public String getCondition(FilterType filterType, int serialNum) throws IllegalArgumentException { + + String serialString = Integer.toString(serialNum); switch(filterType) { case EXACT_ENTRY: - return "MsgTable.EntryNum = ?"; + return "MsgTable.EntryNum = :EntryNum" + serialString; case MAX_ENTRY: - return "MsgTable.EntryNum <= ?"; + return "MsgTable.EntryNum <= :EntryNum" + serialString; case MAX_MESSAGES: - return "LIMIT = ?"; + return "LIMIT = :Limit" + serialString; case MSG_ID: - return "MsgTable.MsgId = ?"; + return "MsgTable.MsgId = :MsgId" + serialString; case SIGNER_ID: return "EXISTS (SELECT 1 FROM SignatureTable" - + " WHERE SignatureTable.SignerId = ? AND SignatureTable.EntryNum = MsgTable.EntryNum)"; + + " WHERE SignatureTable.SignerId = :SignerId" + serialString + " AND SignatureTable.EntryNum = MsgTable.EntryNum)"; case TAG: return "EXISTS (SELECT 1 FROM TagTable" + " INNER JOIN MsgTagTable ON TagTable.TagId = MsgTagTable.TagId" - + " WHERE TagTable.Tag = ? AND MsgTagTable.EntryNum = MsgTable.EntryNum)"; + + " WHERE TagTable.Tag = :Tag" + serialString + " AND MsgTagTable.EntryNum = MsgTable.EntryNum)"; default: throw new IllegalArgumentException("Cannot serve a filter of type " + filterType); } @@ -70,8 +72,18 @@ public class SQLiteQueryProvider implements BulletinBoardSQLServer.SQLQueryProvi } @Override - public String getConnectionString() { - return dbConnectionString; + public String getConditionParamTypeName(FilterType filterType) throws IllegalArgumentException { + return null; //TODO: write this. + } + + @Override + public DataSource getDataSource() { + // TODO: Fix this + SQLiteDataSource dataSource = new SQLiteDataSource(); + dataSource.setUrl("jdbc:sqlite:" + dbName); + dataSource.setDatabaseName("meerkat"); //TODO: Make generic + + return dataSource; } @@ -85,9 +97,11 @@ public class SQLiteQueryProvider implements BulletinBoardSQLServer.SQLQueryProvi list.add("CREATE TABLE IF NOT EXISTS MsgTagTable (EntryNum BLOB, TagId INTEGER, FOREIGN KEY (EntryNum)" + " REFERENCES MsgTable(EntryNum), FOREIGN KEY (TagId) REFERENCES TagTable(TagId), UNIQUE (EntryNum, TagID))"); - list.add("CREATE TABLE IF NOT EXISTS SignatureTable (EntryNum BLOB, SignerId BLOB, Signature BLOB UNIQUE," + list.add("CREATE TABLE IF NOT EXISTS SignatureTable (EntryNum INTEGER, SignerId BLOB, Signature BLOB," + " FOREIGN KEY (EntryNum) REFERENCES MsgTable(EntryNum))"); + list.add("CREATE INDEX IF NOT EXISTS SignerIndex ON SignatureTable(SignerId)"); + list.add("CREATE UNIQUE INDEX IF NOT EXISTS SignerIndex ON SignatureTable(SignerId, EntryNum)"); return list; } diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/EntryNumMapper.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/EntryNumMapper.java new file mode 100644 index 0000000..478c39e --- /dev/null +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/EntryNumMapper.java @@ -0,0 +1,18 @@ +package meerkat.bulletinboard.sqlserver.mappers; + +import meerkat.protobuf.BulletinBoardAPI.MessageID; +import org.springframework.jdbc.core.RowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Created by Arbel Deutsch Peled on 11-Dec-15. + */ +public class EntryNumMapper implements RowMapper { + + @Override + public Long mapRow(ResultSet rs, int rowNum) throws SQLException { + return rs.getLong(1); + } +} diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/MessageMapper.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/MessageMapper.java new file mode 100644 index 0000000..fdc1fa8 --- /dev/null +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/MessageMapper.java @@ -0,0 +1,32 @@ +package meerkat.bulletinboard.sqlserver.mappers; + +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.protobuf.BulletinBoardAPI.UnsignedBulletinBoardMessage; +import meerkat.protobuf.BulletinBoardAPI.BulletinBoardMessage; +import org.springframework.jdbc.core.RowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Created by Arbel Deutsch Peled on 11-Dec-15. + */ +public class MessageMapper implements RowMapper { + + @Override + public BulletinBoardMessage.Builder mapRow(ResultSet rs, int rowNum) throws SQLException { + + BulletinBoardMessage.Builder builder = BulletinBoardMessage.newBuilder(); + + try { + builder.setEntryNum(rs.getLong(1)) + .setMsg(UnsignedBulletinBoardMessage.parseFrom(rs.getBytes(2))); + + } catch (InvalidProtocolBufferException e) { + throw new SQLException(e.getMessage(), e); + } + + return builder; + } + +} diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/SignatureMapper.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/SignatureMapper.java new file mode 100644 index 0000000..60015c1 --- /dev/null +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/SignatureMapper.java @@ -0,0 +1,28 @@ +package meerkat.bulletinboard.sqlserver.mappers; + +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.protobuf.BulletinBoardAPI.BulletinBoardMessage; +import meerkat.protobuf.BulletinBoardAPI.UnsignedBulletinBoardMessage; +import meerkat.protobuf.Crypto.Signature; +import org.springframework.jdbc.core.RowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Created by Arbel Deutsch Peled on 11-Dec-15. + */ +public class SignatureMapper implements RowMapper { + + @Override + public Signature mapRow(ResultSet rs, int rowNum) throws SQLException { + + try { + return Signature.parseFrom(rs.getBytes(1)); + } catch (InvalidProtocolBufferException e) { + throw new SQLException(e.getMessage(), e); + } + + } + +} diff --git a/bulletin-board-server/src/test/java/meerkat/bulletinboard/GenericBulletinBoardServerTest.java b/bulletin-board-server/src/test/java/meerkat/bulletinboard/GenericBulletinBoardServerTest.java index 61354e7..4799e0d 100644 --- a/bulletin-board-server/src/test/java/meerkat/bulletinboard/GenericBulletinBoardServerTest.java +++ b/bulletin-board-server/src/test/java/meerkat/bulletinboard/GenericBulletinBoardServerTest.java @@ -356,8 +356,7 @@ public class GenericBulletinBoardServerTest { .addFilter(MessageFilter.newBuilder() .setType(FilterType.SIGNER_ID) .setId(signerIDs[1]) - .build() - ); + .build()); try { messages = bulletinBoardServer.readMessages(filterListBuilder.build()).getMessageList(); diff --git a/bulletin-board-server/src/test/java/meerkat/bulletinboard/H2BulletinBoardServerTest.java b/bulletin-board-server/src/test/java/meerkat/bulletinboard/H2BulletinBoardServerTest.java index 6210dd4..fc02b7b 100644 --- a/bulletin-board-server/src/test/java/meerkat/bulletinboard/H2BulletinBoardServerTest.java +++ b/bulletin-board-server/src/test/java/meerkat/bulletinboard/H2BulletinBoardServerTest.java @@ -7,13 +7,11 @@ import meerkat.comm.CommunicationException; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.junit.runner.Result; import java.lang.management.ManagementFactory; import java.lang.management.ThreadMXBean; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; -import java.sql.Statement; +import java.sql.*; import java.util.List; import static org.junit.Assert.fail; @@ -23,23 +21,25 @@ import static org.junit.Assert.fail; */ public class H2BulletinBoardServerTest { - private final String dbAddress = "~/meerkatTest"; + private final String dbAddress = "meerkatTest"; private GenericBulletinBoardServerTest serverTest; + private SQLQueryProvider queryProvider; + private final ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); // Used to time the tests -// @Before + @Before public void init(){ System.err.println("Starting to initialize H2BulletinBoardServerTest"); long start = threadBean.getCurrentThreadCpuTime(); - SQLQueryProvider queryProvider = new H2QueryProvider(dbAddress); + queryProvider = new H2QueryProvider(dbAddress, "", ""); try { - Connection conn = DriverManager.getConnection(queryProvider.getConnectionString()); + Connection conn = queryProvider.getDataSource().getConnection(); Statement stmt = conn.createStatement(); List deletionQueries = queryProvider.getSchemaDeletionCommands(); @@ -76,7 +76,7 @@ public class H2BulletinBoardServerTest { System.err.println("Time of operation: " + (end - start)); } -// @Test + @Test public void bulkTest() { System.err.println("Starting bulkTest of H2BulletinBoardServerTest"); long start = threadBean.getCurrentThreadCpuTime(); @@ -107,7 +107,7 @@ public class H2BulletinBoardServerTest { System.err.println("Time of operation: " + (end - start)); } -// @After + @After public void close() { System.err.println("Starting to close H2BulletinBoardServerTest"); long start = threadBean.getCurrentThreadCpuTime(); diff --git a/bulletin-board-server/src/test/java/meerkat/bulletinboard/MySQLBulletinBoardServerTest.java b/bulletin-board-server/src/test/java/meerkat/bulletinboard/MySQLBulletinBoardServerTest.java index af1cfb4..6f0b1a3 100644 --- a/bulletin-board-server/src/test/java/meerkat/bulletinboard/MySQLBulletinBoardServerTest.java +++ b/bulletin-board-server/src/test/java/meerkat/bulletinboard/MySQLBulletinBoardServerTest.java @@ -3,20 +3,17 @@ package meerkat.bulletinboard; import meerkat.bulletinboard.sqlserver.BulletinBoardSQLServer; import meerkat.bulletinboard.sqlserver.BulletinBoardSQLServer.SQLQueryProvider; import meerkat.bulletinboard.sqlserver.MySQLQueryProvider; -import meerkat.bulletinboard.sqlserver.SQLiteQueryProvider; import meerkat.comm.CommunicationException; import org.junit.After; import org.junit.Before; import org.junit.Test; -import java.io.File; import java.lang.management.ManagementFactory; import java.lang.management.ThreadMXBean; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; -import java.util.LinkedList; import java.util.List; import static org.junit.Assert.fail; @@ -26,7 +23,7 @@ import static org.junit.Assert.fail; */ public class MySQLBulletinBoardServerTest { - private final String dbAddress = "//localhost:3306/meerkat"; + private final String dbAddress = "localhost"; private final String username = "arbel"; private final String password = "mypass"; @@ -44,7 +41,7 @@ public class MySQLBulletinBoardServerTest { try { - Connection conn = DriverManager.getConnection(queryProvider.getConnectionString()); + Connection conn = queryProvider.getDataSource().getConnection(); Statement stmt = conn.createStatement(); List deletionQueries = queryProvider.getSchemaDeletionCommands(); diff --git a/meerkat-common/src/main/proto/meerkat/BulletinBoardAPI.proto b/meerkat-common/src/main/proto/meerkat/BulletinBoardAPI.proto index 4830afc..0fe35f8 100644 --- a/meerkat-common/src/main/proto/meerkat/BulletinBoardAPI.proto +++ b/meerkat-common/src/main/proto/meerkat/BulletinBoardAPI.proto @@ -52,6 +52,10 @@ enum FilterType { MAX_ENTRY = 2; // Find all entries in database up to specified entry number (chronological) SIGNER_ID = 3; // Find all entries in database that correspond to specific signature (signer) TAG = 4; // Find all entries in database that have a specific tag + + // NOTE: The MAX_MESSAGES filter must remain the last filter type + // This is because the condition it specifies in an SQL statement must come last in the statement + // Keeping it last here allows for easily sorting the filters and keeping the code general MAX_MESSAGES = 5; // Return at most some specified number of messages } From 975ad340be5af48896e128534787dbd46fa89b58 Mon Sep 17 00:00:00 2001 From: Arbel Deutsch Peled Date: Sat, 12 Dec 2015 12:36:00 +0200 Subject: [PATCH 09/15] Bulletin Board Server WebApp support for MySQL and H2 engines. --- .../sqlserver/H2QueryProvider.java | 23 ++------ .../sqlserver/MySQLQueryProvider.java | 12 +++-- .../webapp/BulletinBoardWebApp.java | 28 +++++++--- .../src/main/webapp/WEB-INF/web.xml | 20 +++++-- ...ulletinBoardSQLServerIntegrationTest.java} | 52 +------------------ .../H2BulletinBoardServerTest.java | 4 +- .../MySQLBulletinBoardServerTest.java | 4 +- 7 files changed, 59 insertions(+), 84 deletions(-) rename bulletin-board-server/src/test/java/meerkat/bulletinboard/{SQLiteServerIntegrationTest.java => BulletinBoardSQLServerIntegrationTest.java} (73%) diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/H2QueryProvider.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/H2QueryProvider.java index cf3fd68..fa2b146 100644 --- a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/H2QueryProvider.java +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/H2QueryProvider.java @@ -16,14 +16,10 @@ import java.util.List; public class H2QueryProvider implements BulletinBoardSQLServer.SQLQueryProvider { - private String dbAddress; - private String username; - private String password; + private String dbName; - public H2QueryProvider(String dbAddress, String username, String password) { - this.dbAddress = dbAddress; - this.username = username; - this.password = password; + public H2QueryProvider(String dbName) { + this.dbName = dbName; } @@ -116,18 +112,9 @@ public class H2QueryProvider implements BulletinBoardSQLServer.SQLQueryProvider @Override public DataSource getDataSource() { - // TODO: Fix this + JdbcDataSource dataSource = new JdbcDataSource(); - dataSource.setURL("jdbc:h2:~/" + dbAddress + "/meerkat"); // TODO: make this generic - dataSource.setUser(username); - dataSource.setPassword(password); -// Context ctx = null; -// try { -// ctx = new InitialContext(); -// ctx.bind("jdbc/dsName", dataSource); -// } catch (NamingException e) { -// e.printStackTrace(); -// } + dataSource.setURL("jdbc:h2:~/" + dbName); return dataSource; } diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/MySQLQueryProvider.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/MySQLQueryProvider.java index 9956646..c00c044 100644 --- a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/MySQLQueryProvider.java +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/MySQLQueryProvider.java @@ -15,11 +15,15 @@ import java.util.List; public class MySQLQueryProvider implements SQLQueryProvider { private String dbAddress; + private int dbPort; + private String dbName; private String username; private String password; - public MySQLQueryProvider(String dbAddress, String username, String password) { + public MySQLQueryProvider(String dbAddress, int dbPort, String dbName, String username, String password) { this.dbAddress = dbAddress; + this.dbPort = dbPort; + this.dbName = dbName; this.username = username; this.password = password; } @@ -101,10 +105,12 @@ public class MySQLQueryProvider implements SQLQueryProvider { @Override public DataSource getDataSource() { MysqlDataSource dataSource = new MysqlDataSource(); - dataSource.setDatabaseName("meerkat"); //TODO: Make generic + + dataSource.setServerName(dbAddress); + dataSource.setPort(dbPort); + dataSource.setDatabaseName(dbName); dataSource.setUser(username); dataSource.setPassword(password); - dataSource.setServerName(dbAddress); return dataSource; } diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/webapp/BulletinBoardWebApp.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/webapp/BulletinBoardWebApp.java index 1a8abaf..779982a 100644 --- a/bulletin-board-server/src/main/java/meerkat/bulletinboard/webapp/BulletinBoardWebApp.java +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/webapp/BulletinBoardWebApp.java @@ -13,6 +13,8 @@ import javax.ws.rs.core.MediaType; import meerkat.bulletinboard.BulletinBoardServer; import meerkat.bulletinboard.sqlserver.BulletinBoardSQLServer; +import meerkat.bulletinboard.sqlserver.H2QueryProvider; +import meerkat.bulletinboard.sqlserver.MySQLQueryProvider; import meerkat.bulletinboard.sqlserver.SQLiteQueryProvider; import meerkat.comm.CommunicationException; import meerkat.protobuf.BulletinBoardAPI.BoolMsg; @@ -48,15 +50,29 @@ public class BulletinBoardWebApp implements BulletinBoardServer, ServletContextL @Override public void contextInitialized(ServletContextEvent servletContextEvent) { ServletContext servletContext = servletContextEvent.getServletContext(); - String meerkatDB = servletContext.getInitParameter("meerkatdb"); - String dbType = servletContext.getInitParameter("dbtype"); - - if (dbType.compareTo("SQLite") == 0){ - bulletinBoard = new BulletinBoardSQLServer(new SQLiteQueryProvider(meerkatDB)); + String dbType = servletContext.getInitParameter("dbType"); + String dbName = servletContext.getInitParameter("dbName"); + + if ("SQLite".compareTo(dbType) == 0){ + + bulletinBoard = new BulletinBoardSQLServer(new SQLiteQueryProvider(dbName)); + + } else if ("H2".compareTo(dbType) == 0) { + + bulletinBoard = new BulletinBoardSQLServer(new H2QueryProvider(dbName)); + + } else if ("MySQL".compareTo(dbType) == 0) { + + String dbAddress = servletContext.getInitParameter("dbAddress"); + int dbPort = Integer.parseInt(servletContext.getInitParameter("dbPort")); + String username = servletContext.getInitParameter("username"); + String password = servletContext.getInitParameter("password"); + + bulletinBoard = new BulletinBoardSQLServer(new MySQLQueryProvider(dbAddress,dbPort,dbName,username,password)); } try { - init(meerkatDB); + init(dbName); servletContext.setAttribute(BULLETIN_BOARD_ATTRIBUTE_NAME, bulletinBoard); } catch (CommunicationException e) { System.err.println(e.getMessage()); diff --git a/bulletin-board-server/src/main/webapp/WEB-INF/web.xml b/bulletin-board-server/src/main/webapp/WEB-INF/web.xml index 5f513e9..226aa3b 100644 --- a/bulletin-board-server/src/main/webapp/WEB-INF/web.xml +++ b/bulletin-board-server/src/main/webapp/WEB-INF/web.xml @@ -15,11 +15,23 @@ /* - meerkatdb - meerkatdb + dbAddress + localhost - dbtype - SQLite + dbPort + 3306 + + dbName + meerkat + + username + arbel + + password + mypass + + dbType + H2 meerkat.bulletinboard.webapp.BulletinBoardWebApp diff --git a/bulletin-board-server/src/test/java/meerkat/bulletinboard/SQLiteServerIntegrationTest.java b/bulletin-board-server/src/test/java/meerkat/bulletinboard/BulletinBoardSQLServerIntegrationTest.java similarity index 73% rename from bulletin-board-server/src/test/java/meerkat/bulletinboard/SQLiteServerIntegrationTest.java rename to bulletin-board-server/src/test/java/meerkat/bulletinboard/BulletinBoardSQLServerIntegrationTest.java index 2a4d6d9..838adcc 100644 --- a/bulletin-board-server/src/test/java/meerkat/bulletinboard/SQLiteServerIntegrationTest.java +++ b/bulletin-board-server/src/test/java/meerkat/bulletinboard/BulletinBoardSQLServerIntegrationTest.java @@ -19,14 +19,13 @@ import javax.ws.rs.client.Entity; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.Response; -public class SQLiteServerIntegrationTest { +public class BulletinBoardSQLServerIntegrationTest { private static String PROP_GETTY_URL = "gretty.httpBaseURI"; private static String DEFAULT_BASE_URL = "http://localhost:8081"; private static String BASE_URL = System.getProperty(PROP_GETTY_URL, DEFAULT_BASE_URL); Client client; -// Connection connection; @Before public void setup() throws Exception { @@ -53,13 +52,6 @@ public class SQLiteServerIntegrationTest { MessageFilterList filterList; BulletinBoardMessageList msgList; -// try{ -// connection = DriverManager.getConnection("jdbc:sqlite:d:/arbel/projects/meerkat-java/bulletin-board-server/local-instances/meerkat.db"); -// } catch (SQLException e) { -// System.err.println(e.getMessage()); -// assert false; -// } - // Test writing mechanism System.err.println("******** Testing: " + Constants.POST_MESSAGE_PATH); @@ -119,47 +111,7 @@ public class SQLiteServerIntegrationTest { .build() ) .build(); - -// String sql = "SELECT MsgTable.EntryNum, MsgTable.Msg FROM MsgTable INNER JOIN SignatureTable ON SignatureTable.EntryNum = MsgTable.EntryNum WHERE SignatureTable.SignerId = ?"; -// PreparedStatement pstmt = connection.prepareStatement(sql); -// int i=1; -// for (MessageFilter filter : filterList.getFilterList()){ -// -// switch (filter.getType().getNumber()){ -// -// case FilterType.EXACT_ENTRY_VALUE: // Go through. -// case FilterType.MAX_ENTRY_VALUE: -// pstmt.setLong(i, filter.getEntry()); -// i++; -// break; -// -// case FilterType.MSG_ID_VALUE: // Go through. -// case FilterType.SIGNER_ID_VALUE: -// pstmt.setBytes(i, filter.getId().toByteArray()); -// i++; -// break; -// -// case FilterType.TAG_VALUE: -// pstmt.setString(i, filter.getTag()); -// break; -// -// // The max-messages condition is applied as a suffix. Therefore, it is treated differently. -// case FilterType.MAX_MESSAGES_VALUE: -// pstmt.setLong(filterList.getFilterList().size(), filter.getMaxMessages()); -// break; -// -// } -// } -// ResultSet rs = pstmt.executeQuery(); -// -// i = 0; -// while (rs.next()){ -// i++; -// assert rs.getBytes(2) -// } -// System.err.println("Local DB size = " + i); -// pstmt.close(); - + response = webTarget.request(Constants.MEDIATYPE_PROTOBUF).post(Entity.entity(filterList, Constants.MEDIATYPE_PROTOBUF)); System.err.println(response); msgList = response.readEntity(BulletinBoardMessageList.class); diff --git a/bulletin-board-server/src/test/java/meerkat/bulletinboard/H2BulletinBoardServerTest.java b/bulletin-board-server/src/test/java/meerkat/bulletinboard/H2BulletinBoardServerTest.java index fc02b7b..ef19310 100644 --- a/bulletin-board-server/src/test/java/meerkat/bulletinboard/H2BulletinBoardServerTest.java +++ b/bulletin-board-server/src/test/java/meerkat/bulletinboard/H2BulletinBoardServerTest.java @@ -21,7 +21,7 @@ import static org.junit.Assert.fail; */ public class H2BulletinBoardServerTest { - private final String dbAddress = "meerkatTest"; + private final String dbName = "meerkatTest"; private GenericBulletinBoardServerTest serverTest; @@ -35,7 +35,7 @@ public class H2BulletinBoardServerTest { System.err.println("Starting to initialize H2BulletinBoardServerTest"); long start = threadBean.getCurrentThreadCpuTime(); - queryProvider = new H2QueryProvider(dbAddress, "", ""); + queryProvider = new H2QueryProvider(dbName); try { diff --git a/bulletin-board-server/src/test/java/meerkat/bulletinboard/MySQLBulletinBoardServerTest.java b/bulletin-board-server/src/test/java/meerkat/bulletinboard/MySQLBulletinBoardServerTest.java index 6f0b1a3..e473931 100644 --- a/bulletin-board-server/src/test/java/meerkat/bulletinboard/MySQLBulletinBoardServerTest.java +++ b/bulletin-board-server/src/test/java/meerkat/bulletinboard/MySQLBulletinBoardServerTest.java @@ -24,6 +24,8 @@ import static org.junit.Assert.fail; public class MySQLBulletinBoardServerTest { private final String dbAddress = "localhost"; + private final int dbPort = 3306; + private final String dbName = "meerkat"; private final String username = "arbel"; private final String password = "mypass"; @@ -37,7 +39,7 @@ public class MySQLBulletinBoardServerTest { System.err.println("Starting to initialize MySQLBulletinBoardServerTest"); long start = threadBean.getCurrentThreadCpuTime(); - SQLQueryProvider queryProvider = new MySQLQueryProvider(dbAddress,username,password); + SQLQueryProvider queryProvider = new MySQLQueryProvider(dbAddress,dbPort,dbName,username,password); try { From 13733e66106f74978da2dd9b912e1dc8aa9a7f5d Mon Sep 17 00:00:00 2001 From: Arbel Deutsch Peled Date: Sat, 12 Dec 2015 13:12:35 +0200 Subject: [PATCH 10/15] Successful full-project build. Still untested Bulletin Board Client. --- .../bulletinboard/BulletinClientWorker.java | 22 ++-- .../BulletinBoardClientIntegrationTest.java | 120 +++++++++++++++--- 2 files changed, 111 insertions(+), 31 deletions(-) diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientWorker.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientWorker.java index eb67672..9ce5ef4 100644 --- a/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientWorker.java +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientWorker.java @@ -77,52 +77,52 @@ public class BulletinClientWorker implements Callable { WebTarget webTarget; Response response; - job.shuffleAddresses(); // This is done to randomize the order of access to servers primarily for READ operations - String requestPath; Message msg; + Message payload = job.getPayload(); + BulletinBoardMessageList msgList; int count = 0; // Used to count number of servers which contain the required message in a GET_REDUNDANCY request. + job.shuffleAddresses(); // This is done to randomize the order of access to servers primarily for READ operations + // Prepare the request. switch(job.getJobType()) { case POST_MESSAGE: // Make sure the payload is a BulletinBoardMessage - if (!(job.getPayload() instanceof BulletinBoardMessage)) { + if (!(payload instanceof BulletinBoardMessage)) { throw new IllegalArgumentException("Cannot post an object that is not an instance of BulletinBoardMessage"); } - msg = job.getPayload(); + msg = payload; requestPath = Constants.POST_MESSAGE_PATH; break; case READ_MESSAGES: // Make sure the payload is a MessageFilterList - if (!(job.getPayload() instanceof MessageFilterList)) { + if (!(payload instanceof MessageFilterList)) { throw new IllegalArgumentException("Read failed: an instance of MessageFilterList is required as payload for a READ_MESSAGES operation"); } - msg = job.getPayload(); + msg = payload; requestPath = Constants.READ_MESSAGES_PATH; break; case GET_REDUNDANCY: - // Make sure the payload is a BulletinBoardMessage - if (!(job.getPayload() instanceof BulletinBoardMessage)) { + // Make sure the payload is a MessageId + if (!(payload instanceof MessageID)) { throw new IllegalArgumentException("Cannot search for an object that is not an instance of BulletinBoardMessage"); } requestPath = Constants.READ_MESSAGES_PATH; - // Create a MsgID from the - digest.update((BulletinBoardMessage) job.getPayload()); msg = MessageFilterList.newBuilder() .addFilter(MessageFilter.newBuilder() .setType(FilterType.MSG_ID) - .setId(ByteString.copyFrom(digest.digest())) + .setId(payload.toByteString()) .build() ).build(); diff --git a/bulletin-board-client/src/test/java/BulletinBoardClientIntegrationTest.java b/bulletin-board-client/src/test/java/BulletinBoardClientIntegrationTest.java index f9699c1..b50d1e5 100644 --- a/bulletin-board-client/src/test/java/BulletinBoardClientIntegrationTest.java +++ b/bulletin-board-client/src/test/java/BulletinBoardClientIntegrationTest.java @@ -1,17 +1,24 @@ import com.google.protobuf.ByteString; +import com.google.protobuf.Message; import meerkat.bulletinboard.BulletinBoardClient; -import meerkat.bulletinboard.SimpleBulletinBoardClient; -import meerkat.comm.CommunicationException; +import meerkat.bulletinboard.BulletinBoardClient.ClientCallback; +import meerkat.bulletinboard.ThreadedBulletinBoardClient; import meerkat.protobuf.BulletinBoardAPI.*; import meerkat.protobuf.Crypto; +import meerkat.protobuf.Voting.*; import meerkat.util.BulletinBoardMessageComparator; + import org.junit.Before; import org.junit.Test; + +import static java.lang.Thread.sleep; import static org.junit.Assert.*; import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.number.OrderingComparison.*; import java.util.Comparator; +import java.util.Iterator; import java.util.LinkedList; import java.util.List; @@ -20,8 +27,74 @@ import java.util.List; */ public class BulletinBoardClientIntegrationTest { + private class PostCallback implements ClientCallback{ + + @Override + public void handleCallback(Object msg) {} + + @Override + public void handleFailure(Throwable t) { + System.err.println(t.getCause() + "\n" + t.getMessage()); + assert false; + } + } + + private class RedundancyCallback implements ClientCallback{ + + private float minRedundancy; + + public RedundancyCallback(float minRedundancy) { + this.minRedundancy = minRedundancy; + } + + @Override + public void handleCallback(Float redundancy) { + assertThat(redundancy, greaterThanOrEqualTo(minRedundancy)); + } + + @Override + public void handleFailure(Throwable t) { + System.err.println(t.getCause() + "\n" + t.getMessage()); + assert false; + } + } + + private class ReadCallback implements ClientCallback>{ + + private List expectedMsgList; + + public ReadCallback(List expectedMsgList) { + this.expectedMsgList = expectedMsgList; + } + + @Override + public void handleCallback(List messages) { + BulletinBoardMessageComparator msgComparator = new BulletinBoardMessageComparator(); + + assertThat(messages.size(), is(expectedMsgList.size())); + + Iterator expectedMessageIterator = expectedMsgList.iterator(); + Iterator receivedMessageIterator = messages.iterator(); + + while (expectedMessageIterator.hasNext()) { + assertThat(msgComparator.compare(expectedMessageIterator.next(), receivedMessageIterator.next()), is(0)); + } + + } + + @Override + public void handleFailure(Throwable t) { + System.err.println(t.getCause() + "\n" + t.getMessage()); + assert false; + } + } + private BulletinBoardClient bulletinBoardClient; + private PostCallback postCallback; + private RedundancyCallback redundancyCallback; + private ReadCallback readCallback; + private static String PROP_GETTY_URL = "gretty.httpBaseURI"; private static String DEFAULT_BASE_URL = "http://localhost:8081"; private static String BASE_URL = System.getProperty(PROP_GETTY_URL, DEFAULT_BASE_URL); @@ -29,17 +102,23 @@ public class BulletinBoardClientIntegrationTest { @Before public void init(){ - bulletinBoardClient = new SimpleBulletinBoardClient(); + bulletinBoardClient = new ThreadedBulletinBoardClient(); List testDB = new LinkedList(); testDB.add(BASE_URL); - bulletinBoardClient.init(testDB); + bulletinBoardClient.init(BulletinBoardClientParams.newBuilder() + .addBulletinBoardAddress("http://localhost:8081") + .setMinRedundancy((float) 1.0) + .build()); + + postCallback = new PostCallback(); + redundancyCallback = new RedundancyCallback((float) 1.0); } @Test - public void postTest(){ + public void postTest() { byte[] b1 = {(byte) 1, (byte) 2, (byte) 3, (byte) 4}; byte[] b2 = {(byte) 11, (byte) 12, (byte) 13, (byte) 14}; @@ -73,15 +152,15 @@ public class BulletinBoardClientIntegrationTest { .build()) .build(); + messageID = bulletinBoardClient.postMessage(msg,postCallback); + try { - messageID = bulletinBoardClient.postMessage(msg); - } catch (CommunicationException e) { - System.err.println("Error posting to BB Server: " + e.getMessage()); - assert false; - return; + sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); } - assertThat(bulletinBoardClient.getRedundancy(messageID), is((float) 1.00)); + bulletinBoardClient.getRedundancy(messageID,redundancyCallback); filterList = MessageFilterList.newBuilder() .addFilter( @@ -90,19 +169,20 @@ public class BulletinBoardClientIntegrationTest { .setTag("Signature") .build() ) -// .addFilter( -// MessageFilter.newBuilder() -// .setType(FilterType.TAG) -// .setTag("Trustee") -// .build() -// ) + .addFilter( + MessageFilter.newBuilder() + .setType(FilterType.TAG) + .setTag("Trustee") + .build() + ) .build(); - msgList = bulletinBoardClient.readMessages(filterList); + msgList = new LinkedList(); + msgList.add(msg); - assertThat(msgList.size(), is(1)); + readCallback = new ReadCallback(msgList); - assertThat(msgComparator.compare(msgList.iterator().next(), msg), is(0)); + bulletinBoardClient.readMessages(filterList, readCallback); } From 4f2d0e77388312a3761fa9bc4c93d3071e33c6cc Mon Sep 17 00:00:00 2001 From: Arbel Deutsch Peled Date: Sat, 12 Dec 2015 22:45:31 +0200 Subject: [PATCH 11/15] First working version of Threaded Bulletin Board Client. Tests do not report well. --- .../meerkat/bulletinboard/BulletinClientJob.java | 8 ++++---- .../bulletinboard/BulletinClientWorker.java | 11 ++++++++--- .../ThreadedBulletinBoardClient.java | 15 ++++++++++++++- .../java/BulletinBoardClientIntegrationTest.java | 6 +++++- .../bulletinboard/BulletinBoardClient.java | 6 ++++++ 5 files changed, 37 insertions(+), 9 deletions(-) diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientJob.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientJob.java index b63ca50..aca98d4 100644 --- a/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientJob.java +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientJob.java @@ -37,6 +37,10 @@ public class BulletinClientJob { this.maxRetry = maxRetry; } + public void updateServerAddresses(List newServerAdresses) { + this.serverAddresses = newServerAdresses; + } + public List getServerAddresses() { return serverAddresses; } @@ -57,10 +61,6 @@ public class BulletinClientJob { return maxRetry; } - public Iterator getAddressIterator() { - return serverAddresses.iterator(); - } - public void shuffleAddresses() { Collections.shuffle(serverAddresses); } diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientWorker.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientWorker.java index 9ce5ef4..3b9c781 100644 --- a/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientWorker.java +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientWorker.java @@ -17,6 +17,8 @@ import javax.ws.rs.client.Entity; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.Response; import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; import java.util.concurrent.Callable; /** @@ -80,6 +82,8 @@ public class BulletinClientWorker implements Callable { String requestPath; Message msg; + List serverAddresses = new LinkedList(job.getServerAddresses()); + Message payload = job.getPayload(); BulletinBoardMessageList msgList; @@ -114,7 +118,7 @@ public class BulletinClientWorker implements Callable { case GET_REDUNDANCY: // Make sure the payload is a MessageId if (!(payload instanceof MessageID)) { - throw new IllegalArgumentException("Cannot search for an object that is not an instance of BulletinBoardMessage"); + throw new IllegalArgumentException("Cannot search for an object that is not an instance of MessageID"); } requestPath = Constants.READ_MESSAGES_PATH; @@ -135,7 +139,7 @@ public class BulletinClientWorker implements Callable { // Iterate through servers - Iterator addressIterator = job.getAddressIterator(); + Iterator addressIterator = serverAddresses.iterator(); while (addressIterator.hasNext()) { @@ -192,7 +196,8 @@ public class BulletinClientWorker implements Callable { case POST_MESSAGE: // The job now contains the information required to ascertain whether enough server posts have succeeded - // It also contains the list of servers in which the post was not successful + // It will also contain the list of servers in which the post was not successful + job.updateServerAddresses(serverAddresses); return new BulletinClientJobResult(job, null); case GET_REDUNDANCY: diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/ThreadedBulletinBoardClient.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/ThreadedBulletinBoardClient.java index dd1ab0f..bb46c32 100644 --- a/bulletin-board-client/src/main/java/meerkat/bulletinboard/ThreadedBulletinBoardClient.java +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/ThreadedBulletinBoardClient.java @@ -13,6 +13,7 @@ import meerkat.protobuf.Voting; import java.util.List; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; /** * Created by Arbel Deutsch Peled on 05-Dec-15. @@ -107,7 +108,7 @@ public class ThreadedBulletinBoardClient implements BulletinBoardClient { public void readMessages(MessageFilterList filterList, ClientCallback> callback) { // Create job - BulletinClientJob job = new BulletinClientJob(meerkatDBs, minAbsoluteRedundancy, BulletinClientJob.JobType.GET_REDUNDANCY, + BulletinClientJob job = new BulletinClientJob(meerkatDBs, minAbsoluteRedundancy, BulletinClientJob.JobType.READ_MESSAGES, filterList, READ_MESSAGES_RETRY_NUM); // Submit job and create callback @@ -115,4 +116,16 @@ public class ThreadedBulletinBoardClient implements BulletinBoardClient { } + @Override + public void close() { + try { + listeningExecutor.shutdown(); + while (! listeningExecutor.isShutdown()) { + listeningExecutor.awaitTermination(10, TimeUnit.SECONDS); + } + } catch (InterruptedException e) { + System.err.println(e.getCause() + " " + e.getMessage()); + } + } + } diff --git a/bulletin-board-client/src/test/java/BulletinBoardClientIntegrationTest.java b/bulletin-board-client/src/test/java/BulletinBoardClientIntegrationTest.java index b50d1e5..3e33eea 100644 --- a/bulletin-board-client/src/test/java/BulletinBoardClientIntegrationTest.java +++ b/bulletin-board-client/src/test/java/BulletinBoardClientIntegrationTest.java @@ -30,7 +30,9 @@ public class BulletinBoardClientIntegrationTest { private class PostCallback implements ClientCallback{ @Override - public void handleCallback(Object msg) {} + public void handleCallback(Object msg) { + System.err.println("Post operation completed"); + } @Override public void handleFailure(Throwable t) { @@ -184,6 +186,8 @@ public class BulletinBoardClientIntegrationTest { bulletinBoardClient.readMessages(filterList, readCallback); + bulletinBoardClient.close(); + } } diff --git a/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardClient.java b/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardClient.java index 1577527..c51e561 100644 --- a/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardClient.java +++ b/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardClient.java @@ -45,4 +45,10 @@ public interface BulletinBoardClient { */ void readMessages(MessageFilterList filterList, ClientCallback> callback); + /** + * Closes all connections, if any. + * This is done in a synchronous (blocking) way. + */ + void close(); + } From bfc62cd77cd13fd1c5194e37cdcab35e89fb2244 Mon Sep 17 00:00:00 2001 From: Arbel Deutsch Peled Date: Sat, 12 Dec 2015 23:18:32 +0200 Subject: [PATCH 12/15] Slight enhancement to Server performance. --- .../sqlserver/BulletinBoardSQLServer.java | 23 ++++++++++--------- .../webapp/BulletinBoardWebApp.java | 6 ++--- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/BulletinBoardSQLServer.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/BulletinBoardSQLServer.java index ae2095a..52bf42b 100644 --- a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/BulletinBoardSQLServer.java +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/BulletinBoardSQLServer.java @@ -395,7 +395,9 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ BulletinBoardMessageList.Builder resultListBuilder = BulletinBoardMessageList.newBuilder(); - String sql; + // SQL length is roughly 50 characters per filter + 50 for the query itself + StringBuilder sqlBuilder = new StringBuilder(50 * (filterList.getFilterCount() + 1)); + MapSqlParameterSource namedParameters; int paramNum; @@ -410,14 +412,13 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ // Check if Tag/Signature tables are required for filtering purposes - sql = sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.GET_MESSAGES); - + sqlBuilder.append(sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.GET_MESSAGES)); // Add conditions namedParameters = new MapSqlParameterSource(); if (!filters.isEmpty()) { - sql += " WHERE "; + sqlBuilder.append(" WHERE "); for (paramNum = 0 ; paramNum < filters.size() ; paramNum++) { @@ -427,11 +428,11 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ if (isFirstFilter) { isFirstFilter = false; } else { - sql += " AND "; + sqlBuilder.append(" AND "); } } - sql += sqlQueryProvider.getCondition(filter.getType(), paramNum); + sqlBuilder.append(sqlQueryProvider.getCondition(filter.getType(), paramNum)); SQLQueryProvider.FilterTypeParam filterTypeParam = SQLQueryProvider.FilterTypeParam.getFilterTypeParamName(filter.getType()); @@ -447,13 +448,10 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ // Run query - List msgBuilders = jdbcTemplate.query(sql, namedParameters, messageMapper); - + List msgBuilders = jdbcTemplate.query(sqlBuilder.toString(), namedParameters, messageMapper); // Compile list of messages - sql = sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.GET_SIGNATURES); - for (BulletinBoardMessage.Builder msgBuilder : msgBuilders) { // Retrieve signatures @@ -461,7 +459,10 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ namedParameters = new MapSqlParameterSource(); namedParameters.addValue("EntryNum", msgBuilder.getEntryNum()); - List signatures = jdbcTemplate.query(sql, namedParameters, signatureMapper); + List signatures = jdbcTemplate.query( + sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.GET_SIGNATURES), + namedParameters, + signatureMapper); // Append signatures msgBuilder.addAllSig(signatures); diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/webapp/BulletinBoardWebApp.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/webapp/BulletinBoardWebApp.java index 779982a..b3fc03c 100644 --- a/bulletin-board-server/src/main/java/meerkat/bulletinboard/webapp/BulletinBoardWebApp.java +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/webapp/BulletinBoardWebApp.java @@ -53,15 +53,15 @@ public class BulletinBoardWebApp implements BulletinBoardServer, ServletContextL String dbType = servletContext.getInitParameter("dbType"); String dbName = servletContext.getInitParameter("dbName"); - if ("SQLite".compareTo(dbType) == 0){ + if ("SQLite".equals(dbType)){ bulletinBoard = new BulletinBoardSQLServer(new SQLiteQueryProvider(dbName)); - } else if ("H2".compareTo(dbType) == 0) { + } else if ("H2".equals(dbType)) { bulletinBoard = new BulletinBoardSQLServer(new H2QueryProvider(dbName)); - } else if ("MySQL".compareTo(dbType) == 0) { + } else if ("MySQL".equals(dbType)) { String dbAddress = servletContext.getInitParameter("dbAddress"); int dbPort = Integer.parseInt(servletContext.getInitParameter("dbPort")); From 79d29a05d353dc060c5a9d9c381dd2b81b5caed3 Mon Sep 17 00:00:00 2001 From: Arbel Deutsch Peled Date: Mon, 14 Dec 2015 09:45:40 +0200 Subject: [PATCH 13/15] Working version of ThreadedBulletinBoardClient. The integration test also passes with SQLite and MySQL engines. --- .../bulletinboard/BulletinClientWorker.java | 3 +- .../callbacks/PostMessageFutureCallback.java | 2 + .../BulletinBoardClientIntegrationTest.java | 49 +++++++++++++------ .../src/main/webapp/WEB-INF/web.xml | 2 +- 4 files changed, 39 insertions(+), 17 deletions(-) diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientWorker.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientWorker.java index 3b9c781..51599d5 100644 --- a/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientWorker.java +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientWorker.java @@ -1,6 +1,5 @@ package meerkat.bulletinboard; -import com.google.protobuf.ByteString; import com.google.protobuf.Message; import meerkat.comm.CommunicationException; import meerkat.crypto.Digest; @@ -126,7 +125,7 @@ public class BulletinClientWorker implements Callable { msg = MessageFilterList.newBuilder() .addFilter(MessageFilter.newBuilder() .setType(FilterType.MSG_ID) - .setId(payload.toByteString()) + .setId(((MessageID) payload).getID()) .build() ).build(); diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/PostMessageFutureCallback.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/PostMessageFutureCallback.java index 7e2a855..221ae1a 100644 --- a/bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/PostMessageFutureCallback.java +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/PostMessageFutureCallback.java @@ -35,6 +35,8 @@ public class PostMessageFutureCallback extends ClientFutureCallback { if (job.getMinServers() > 0 && job.isRetry()) { Futures.addCallback(listeningExecutor.submit(new BulletinClientWorker(job)), this); } + + callback.handleCallback(null); } @Override diff --git a/bulletin-board-client/src/test/java/BulletinBoardClientIntegrationTest.java b/bulletin-board-client/src/test/java/BulletinBoardClientIntegrationTest.java index 3e33eea..dda76c7 100644 --- a/bulletin-board-client/src/test/java/BulletinBoardClientIntegrationTest.java +++ b/bulletin-board-client/src/test/java/BulletinBoardClientIntegrationTest.java @@ -1,5 +1,4 @@ import com.google.protobuf.ByteString; -import com.google.protobuf.Message; import meerkat.bulletinboard.BulletinBoardClient; import meerkat.bulletinboard.BulletinBoardClient.ClientCallback; import meerkat.bulletinboard.ThreadedBulletinBoardClient; @@ -12,32 +11,33 @@ import meerkat.util.BulletinBoardMessageComparator; import org.junit.Before; import org.junit.Test; -import static java.lang.Thread.sleep; import static org.junit.Assert.*; import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.number.OrderingComparison.*; -import java.util.Comparator; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; +import java.util.*; +import java.util.concurrent.Semaphore; /** * Created by Arbel Deutsch Peled on 05-Dec-15. */ public class BulletinBoardClientIntegrationTest { + Semaphore jobSemaphore; + Vector thrown; + private class PostCallback implements ClientCallback{ @Override public void handleCallback(Object msg) { System.err.println("Post operation completed"); + jobSemaphore.release(); } @Override public void handleFailure(Throwable t) { - System.err.println(t.getCause() + "\n" + t.getMessage()); - assert false; + thrown.add(t); + jobSemaphore.release(); } } @@ -51,13 +51,15 @@ public class BulletinBoardClientIntegrationTest { @Override public void handleCallback(Float redundancy) { + System.err.println("Redundancy found is: " + redundancy); + jobSemaphore.release(); assertThat(redundancy, greaterThanOrEqualTo(minRedundancy)); } @Override public void handleFailure(Throwable t) { - System.err.println(t.getCause() + "\n" + t.getMessage()); - assert false; + thrown.add(t); + jobSemaphore.release(); } } @@ -71,6 +73,10 @@ public class BulletinBoardClientIntegrationTest { @Override public void handleCallback(List messages) { + + System.err.println(messages); + jobSemaphore.release(); + BulletinBoardMessageComparator msgComparator = new BulletinBoardMessageComparator(); assertThat(messages.size(), is(expectedMsgList.size())); @@ -86,8 +92,8 @@ public class BulletinBoardClientIntegrationTest { @Override public void handleFailure(Throwable t) { - System.err.println(t.getCause() + "\n" + t.getMessage()); - assert false; + thrown.add(t); + jobSemaphore.release(); } } @@ -117,6 +123,9 @@ public class BulletinBoardClientIntegrationTest { postCallback = new PostCallback(); redundancyCallback = new RedundancyCallback((float) 1.0); + thrown = new Vector<>(); + jobSemaphore = new Semaphore(0); + } @Test @@ -157,9 +166,9 @@ public class BulletinBoardClientIntegrationTest { messageID = bulletinBoardClient.postMessage(msg,postCallback); try { - sleep(2000); + jobSemaphore.acquire(); } catch (InterruptedException e) { - e.printStackTrace(); + System.err.println(e.getCause() + " " + e.getMessage()); } bulletinBoardClient.getRedundancy(messageID,redundancyCallback); @@ -185,9 +194,21 @@ public class BulletinBoardClientIntegrationTest { readCallback = new ReadCallback(msgList); bulletinBoardClient.readMessages(filterList, readCallback); + try { + jobSemaphore.acquire(2); + } catch (InterruptedException e) { + System.err.println(e.getCause() + " " + e.getMessage()); + } bulletinBoardClient.close(); + for (Throwable t : thrown) { + System.err.println(t.getMessage()); + } + if (thrown.size() > 0) { + assert false; + } + } } diff --git a/bulletin-board-server/src/main/webapp/WEB-INF/web.xml b/bulletin-board-server/src/main/webapp/WEB-INF/web.xml index 226aa3b..2198c07 100644 --- a/bulletin-board-server/src/main/webapp/WEB-INF/web.xml +++ b/bulletin-board-server/src/main/webapp/WEB-INF/web.xml @@ -31,7 +31,7 @@ mypass dbType - H2 + SQLite meerkat.bulletinboard.webapp.BulletinBoardWebApp From 9e4a20674223ac4a6e3ae60c81721428ff9e373d Mon Sep 17 00:00:00 2001 From: Tal Moran Date: Wed, 16 Dec 2015 18:15:53 +0200 Subject: [PATCH 14/15] re-adding required gradle files --- gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 52271 bytes gradle/wrapper/gradle-wrapper.properties | 7 +++++++ gradlew | 10 +++++++--- 3 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..30d399d8d2bf522ff5de94bf434a7cc43a9a74b5 GIT binary patch literal 52271 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 p/dev/null +cd "`dirname \"$PRG\"`/" >&- APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null +cd "$SAVED" >&- CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -109,7 +114,6 @@ fi if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` From e8e511d9ce636a127bb33d70ebfd9b2f230c6e1d Mon Sep 17 00:00:00 2001 From: Tal Moran Date: Wed, 16 Dec 2015 18:16:12 +0200 Subject: [PATCH 15/15] Move to public version of qilin --- meerkat-common/build.gradle | 3 +-- .../java/meerkat/crypto/concrete/ECElGamalEncryption.java | 8 ++++---- .../meerkat/crypto/concrete/ECElGamalEncryptionTest.java | 6 +++--- .../test/java/meerkat/crypto/concrete/ECElGamalUtils.java | 8 ++++---- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/meerkat-common/build.gradle b/meerkat-common/build.gradle index 6aa93cb..c510e0a 100644 --- a/meerkat-common/build.gradle +++ b/meerkat-common/build.gradle @@ -49,9 +49,8 @@ dependencies { compile 'com.google.guava:guava:11.0.+' // Crypto - compile 'org.factcenter.qilin:qilin:1.1+' + compile 'org.factcenter.qilin:qilin:1.2+' compile 'org.bouncycastle:bcprov-jdk15on:1.53' - compile 'org.bouncycastle:bcpkix-jdk15on:1.53' testCompile 'junit:junit:4.+' diff --git a/meerkat-common/src/main/java/meerkat/crypto/concrete/ECElGamalEncryption.java b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECElGamalEncryption.java index 1e075bf..f9936c7 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/concrete/ECElGamalEncryption.java +++ b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECElGamalEncryption.java @@ -17,10 +17,10 @@ import org.bouncycastle.math.ec.ECPoint; import org.bouncycastle.util.BigIntegers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import qilin.primitives.concrete.ECElGamal; -import qilin.primitives.concrete.ECGroup; -import qilin.util.PRGRandom; -import qilin.util.Pair; +import org.factcenter.qilin.primitives.concrete.ECElGamal; +import org.factcenter.qilin.primitives.concrete.ECGroup; +import org.factcenter.qilin.util.PRGRandom; +import org.factcenter.qilin.util.Pair; import java.io.ByteArrayOutputStream; import java.io.IOException; diff --git a/meerkat-common/src/test/java/meerkat/crypto/concrete/ECElGamalEncryptionTest.java b/meerkat-common/src/test/java/meerkat/crypto/concrete/ECElGamalEncryptionTest.java index bb52df9..81375e0 100644 --- a/meerkat-common/src/test/java/meerkat/crypto/concrete/ECElGamalEncryptionTest.java +++ b/meerkat-common/src/test/java/meerkat/crypto/concrete/ECElGamalEncryptionTest.java @@ -8,9 +8,9 @@ import org.junit.Before; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import qilin.primitives.concrete.ECElGamal; -import qilin.primitives.concrete.ECGroup; -import qilin.util.Pair; +import org.factcenter.qilin.primitives.concrete.ECElGamal; +import org.factcenter.qilin.primitives.concrete.ECGroup; +import org.factcenter.qilin.util.Pair; import java.math.BigInteger; import java.util.Random; diff --git a/meerkat-common/src/test/java/meerkat/crypto/concrete/ECElGamalUtils.java b/meerkat-common/src/test/java/meerkat/crypto/concrete/ECElGamalUtils.java index 44efe69..66e1647 100644 --- a/meerkat-common/src/test/java/meerkat/crypto/concrete/ECElGamalUtils.java +++ b/meerkat-common/src/test/java/meerkat/crypto/concrete/ECElGamalUtils.java @@ -11,10 +11,10 @@ import org.bouncycastle.jce.spec.ECPublicKeySpec; import org.bouncycastle.math.ec.ECPoint; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import qilin.primitives.concrete.ECElGamal; -import qilin.primitives.concrete.ECGroup; -import qilin.primitives.generic.ElGamal; -import qilin.util.Pair; +import org.factcenter.qilin.primitives.concrete.ECElGamal; +import org.factcenter.qilin.primitives.concrete.ECGroup; +import org.factcenter.qilin.primitives.generic.ElGamal; +import org.factcenter.qilin.util.Pair; import java.io.ByteArrayInputStream; import java.security.KeyFactory;