Mercurial > hg > release > thermostat-2.0
changeset 1573:872748861ab1
Make executing prepared statements more resilient for WebStorage.
Reviewed-by: vanaltj
Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2014-November/011796.html
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/storage/core/src/main/java/com/redhat/thermostat/storage/core/RetryableStatementExecutionException.java Tue Nov 11 16:02:59 2014 +0100 @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2014 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.storage.core; + +/** + * + * Exception thrown if execution of a {@link PreparedStatement} failed, but + * may succeed if the same statement gets executed a second time. + * + */ +@SuppressWarnings("serial") +public class RetryableStatementExecutionException extends + StatementExecutionException { + + public RetryableStatementExecutionException(Throwable cause) { + super(cause); + } + +}
--- a/web/client/src/main/java/com/redhat/thermostat/web/client/internal/WebStorage.java Thu Nov 27 04:08:51 2014 -0700 +++ b/web/client/src/main/java/com/redhat/thermostat/web/client/internal/WebStorage.java Tue Nov 11 16:02:59 2014 +0100 @@ -103,6 +103,7 @@ import com.redhat.thermostat.storage.core.IllegalDescriptorException; import com.redhat.thermostat.storage.core.IllegalPatchException; import com.redhat.thermostat.storage.core.PreparedStatement; +import com.redhat.thermostat.storage.core.RetryableStatementExecutionException; import com.redhat.thermostat.storage.core.SecureStorage; import com.redhat.thermostat.storage.core.StatementDescriptor; import com.redhat.thermostat.storage.core.StatementExecutionException; @@ -111,12 +112,14 @@ import com.redhat.thermostat.storage.core.StorageException; import com.redhat.thermostat.storage.model.Pojo; import com.redhat.thermostat.web.common.PreparedStatementResponseCode; +import com.redhat.thermostat.web.common.SharedStateId; import com.redhat.thermostat.web.common.WebPreparedStatement; import com.redhat.thermostat.web.common.WebPreparedStatementResponse; import com.redhat.thermostat.web.common.WebQueryResponse; import com.redhat.thermostat.web.common.typeadapters.PojoTypeAdapterFactory; import com.redhat.thermostat.web.common.typeadapters.PreparedParameterTypeAdapterFactory; import com.redhat.thermostat.web.common.typeadapters.PreparedParametersTypeAdapterFactory; +import com.redhat.thermostat.web.common.typeadapters.SharedStateIdTypeAdapterFactory; import com.redhat.thermostat.web.common.typeadapters.WebPreparedStatementResponseTypeAdapterFactory; import com.redhat.thermostat.web.common.typeadapters.WebPreparedStatementTypeAdapterFactory; import com.redhat.thermostat.web.common.typeadapters.WebQueryResponseTypeAdapterFactory; @@ -325,7 +328,7 @@ // statement execution private final transient Type parametrizedTypeToken; - public WebPreparedStatementImpl(Type parametrizedTypeToken, int numParams, int statementId) { + public WebPreparedStatementImpl(Type parametrizedTypeToken, int numParams, SharedStateId statementId) { super(numParams, statementId); this.parametrizedTypeToken = parametrizedTypeToken; } @@ -380,6 +383,7 @@ categoryIds = new HashMap<>(); gson = new GsonBuilder() .registerTypeAdapterFactory(new PojoTypeAdapterFactory()) + .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory()) .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory()) .registerTypeAdapterFactory(new WebQueryResponseTypeAdapterFactory()) .registerTypeAdapterFactory(new PreparedParameterTypeAdapterFactory()) @@ -575,24 +579,29 @@ } catch (Exception e) { throw new StatementExecutionException(e); } - if (qResp.getResponseCode() == PreparedStatementResponseCode.QUERY_SUCCESS) { - // Return an empty cursor + switch(qResp.getResponseCode()) { + case PreparedStatementResponseCode.QUERY_SUCCESS: return new WebCursor<T>(this, qResp.getResultList(), - qResp.hasMoreBatches(), - qResp.getCursorId(), parametrizedTypeToken, stmt); - } else if (qResp.getResponseCode() == PreparedStatementResponseCode.ILLEGAL_PATCH) { + qResp.hasMoreBatches(), + qResp.getCursorId(), parametrizedTypeToken, stmt); + case PreparedStatementResponseCode.ILLEGAL_PATCH: { String msg = "Illegal statement argument. See server logs for details."; IllegalArgumentException iae = new IllegalArgumentException(msg); IllegalPatchException e = new IllegalPatchException(iae); throw new StatementExecutionException(e); - } else { - // We only handle success responses and illegal patches, like - // we do for other storages. This is just a defensive measure in - // order to fail early in case something unexpected comes back. + } + case PreparedStatementResponseCode.PREP_STMT_BAD_STOKEN: { + String msg = "Query failed to execute. Server changed token. Clearing prepared stmt cache!"; + logger.log(Level.WARNING, msg); + clearPreparedStmtCache(); + throw new RetryableStatementExecutionException(new RuntimeException(msg)); + } + default: { String msg = "[query-execute] Unknown response from storage endpoint!"; IllegalStateException ise = new IllegalStateException(msg); throw new StatementExecutionException(ise); } + } } /** @@ -611,7 +620,8 @@ * @return */ <T extends Pojo> WebQueryResponse<T> getMore(int cursorId, Type parametrizedTypeToken, Integer batchSize, WebPreparedStatement<T> stmt) { - NameValuePair preparedStmtIdParam = new BasicNameValuePair("prepared-stmt-id", Integer.toString(stmt.getStatementId())); + String stmtId = gson.toJson(stmt.getStatementId()); + NameValuePair preparedStmtIdParam = new BasicNameValuePair("prepared-stmt-id", stmtId); NameValuePair cursorIdParam = new BasicNameValuePair("cursor-id", Integer.toString(cursorId)); NameValuePair batchSizeParam = new BasicNameValuePair("batch-size", batchSize.toString()); @@ -656,6 +666,11 @@ IllegalArgumentException iae = new IllegalArgumentException(msg); IllegalPatchException e = new IllegalPatchException(iae); throw new StatementExecutionException(e); + } else if (responseCode == PreparedStatementResponseCode.PREP_STMT_BAD_STOKEN) { + String msg = "Write failed to execute. Server changed token. Clearing prepared stmt cache!"; + logger.log(Level.WARNING, msg); + clearPreparedStmtCache(); + throw new RetryableStatementExecutionException(new RuntimeException(msg)); } return responseCode; } @@ -754,6 +769,12 @@ int getCategoryId(Category<?> category) { return categoryIds.get(category); } + + private void clearPreparedStmtCache() { + synchronized(this.stmtCache) { + stmtCache.clear(); + } + } @Override public <T extends Pojo> PreparedStatement<T> prepareStatement(StatementDescriptor<T> desc) @@ -799,13 +820,13 @@ Reader reader = getContentAsReader(entity); WebPreparedStatementResponse result = gson.fromJson(reader, WebPreparedStatementResponse.class); int numParams = result.getNumFreeVariables(); - int statementId = result.getStatementId(); - if (statementId == WebPreparedStatementResponse.ILLEGAL_STATEMENT) { + SharedStateId statementId = result.getStatementId(); + if (statementId.getId() == WebPreparedStatementResponse.ILLEGAL_STATEMENT) { // we've got a descriptor the endpoint doesn't know about or // refuses to accept for security reasons. String msg = "Unknown query descriptor which endpoint of " + WebStorage.class.getName() + " refused to accept!"; throw new IllegalDescriptorException(msg, desc.getDescriptor()); - } else if (statementId == WebPreparedStatementResponse.DESCRIPTOR_PARSE_FAILED) { + } else if (statementId.getId() == WebPreparedStatementResponse.DESCRIPTOR_PARSE_FAILED) { String msg = "Statement descriptor failed to parse. " + "Please check server logs for details!"; throw new DescriptorParsingException(msg); @@ -826,9 +847,9 @@ private final Type typeToken; private final int numParams; - private final int statementId; + private final SharedStateId statementId; - WebPreparedStatementHolder(Type typeToken, int numParams, int statementId) { + WebPreparedStatementHolder(Type typeToken, int numParams, SharedStateId statementId) { this.typeToken = typeToken; this.numParams = numParams; this.statementId = statementId;
--- a/web/client/src/test/java/com/redhat/thermostat/web/client/internal/WebStorageTest.java Thu Nov 27 04:08:51 2014 -0700 +++ b/web/client/src/test/java/com/redhat/thermostat/web/client/internal/WebStorageTest.java Tue Nov 11 16:02:59 2014 +0100 @@ -62,6 +62,7 @@ import java.util.Enumeration; import java.util.HashMap; import java.util.Map; +import java.util.UUID; import java.util.concurrent.CountDownLatch; import javax.servlet.ServletException; @@ -106,11 +107,13 @@ import com.redhat.thermostat.test.FreePortFinder; import com.redhat.thermostat.test.FreePortFinder.TryPort; import com.redhat.thermostat.web.common.PreparedStatementResponseCode; +import com.redhat.thermostat.web.common.SharedStateId; import com.redhat.thermostat.web.common.WebPreparedStatement; import com.redhat.thermostat.web.common.WebPreparedStatementResponse; import com.redhat.thermostat.web.common.WebQueryResponse; import com.redhat.thermostat.web.common.typeadapters.PojoTypeAdapterFactory; import com.redhat.thermostat.web.common.typeadapters.PreparedParameterTypeAdapterFactory; +import com.redhat.thermostat.web.common.typeadapters.SharedStateIdTypeAdapterFactory; import com.redhat.thermostat.web.common.typeadapters.WebPreparedStatementResponseTypeAdapterFactory; import com.redhat.thermostat.web.common.typeadapters.WebPreparedStatementTypeAdapterFactory; import com.redhat.thermostat.web.common.typeadapters.WebQueryResponseTypeAdapterFactory; @@ -264,6 +267,7 @@ public void preparingFaultyDescriptorThrowsException() throws UnsupportedEncodingException, IOException { Gson gson = new GsonBuilder() .registerTypeAdapterFactory(new WebPreparedStatementTypeAdapterFactory()) + .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory()) .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory()) .create(); @@ -272,7 +276,8 @@ StatementDescriptor<TestObj> desc = new StatementDescriptor<>(category, strDesc); WebPreparedStatementResponse fakeResponse = new WebPreparedStatementResponse(); - fakeResponse.setStatementId(WebPreparedStatementResponse.DESCRIPTOR_PARSE_FAILED); + SharedStateId id = new SharedStateId(WebPreparedStatementResponse.DESCRIPTOR_PARSE_FAILED, UUID.randomUUID()); + fakeResponse.setStatementId(id); prepareServer(gson.toJson(fakeResponse)); try { storage.prepareStatement(desc); @@ -289,6 +294,7 @@ public void preparingUnknownDescriptorThrowsException() throws UnsupportedEncodingException, IOException { Gson gson = new GsonBuilder() .registerTypeAdapterFactory(new WebPreparedStatementTypeAdapterFactory()) + .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory()) .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory()) .create(); @@ -296,7 +302,8 @@ StatementDescriptor<TestObj> desc = new StatementDescriptor<>(category, strDesc); WebPreparedStatementResponse fakeResponse = new WebPreparedStatementResponse(); - fakeResponse.setStatementId(WebPreparedStatementResponse.ILLEGAL_STATEMENT); + SharedStateId id = new SharedStateId(WebPreparedStatementResponse.ILLEGAL_STATEMENT, UUID.randomUUID()); + fakeResponse.setStatementId(id); prepareServer(gson.toJson(fakeResponse)); try { storage.prepareStatement(desc); @@ -314,6 +321,7 @@ public void forbiddenExecuteQueryThrowsConsumingExcptn() throws UnsupportedEncodingException, IOException { Gson gson = new GsonBuilder() .registerTypeAdapterFactory(new PojoTypeAdapterFactory()) + .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory()) .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory()) .registerTypeAdapterFactory(new WebQueryResponseTypeAdapterFactory()) .registerTypeAdapterFactory(new PreparedParameterTypeAdapterFactory()) @@ -325,9 +333,10 @@ PreparedStatement<TestObj> stmt = null; int fakePrepStmtId = 5; + SharedStateId id = new SharedStateId(fakePrepStmtId, UUID.randomUUID()); WebPreparedStatementResponse fakeResponse = new WebPreparedStatementResponse(); fakeResponse.setNumFreeVariables(1); - fakeResponse.setStatementId(fakePrepStmtId); + fakeResponse.setStatementId(id); prepareServer(gson.toJson(fakeResponse)); try { stmt = storage.prepareStatement(desc); @@ -337,7 +346,7 @@ } assertTrue(stmt instanceof WebPreparedStatement); WebPreparedStatement<TestObj> webStmt = (WebPreparedStatement<TestObj>)stmt; - assertEquals(fakePrepStmtId, webStmt.getStatementId()); + assertEquals(id, webStmt.getStatementId()); PreparedParameters params = webStmt.getParams(); assertEquals(1, params.getParams().length); assertNull(params.getParams()[0]); @@ -367,9 +376,9 @@ // should fill the cache WebPreparedStatement<TestObj> stmt = (WebPreparedStatement<TestObj>)testStorage.prepareStatement(desc); int numParams = stmt.getParams().getParams().length; - int stmtId = stmt.getStatementId(); + SharedStateId stmtId = stmt.getStatementId(); assertEquals(0, numParams); - assertEquals(1, stmtId); + assertEquals(1, stmtId.getId()); // this one should be cached, same stmtId and numParams as previous // one. stmt = (WebPreparedStatement<TestObj>)testStorage.prepareStatement(desc); @@ -379,7 +388,7 @@ " if it wasn't cached. Was it 2? Bad!", 0, numParams); assertEquals("PreparedStatementWebStorge increments a counter" + - " if it wasn't cached. Was it 3? Bad!", 1, stmtId); + " if it wasn't cached. Was it 3? Bad!", 1, stmtId.getId()); // preparing a different descriptor should not be cached. strDesc = "QUERY test WHERE 'foo' = ?l"; desc = new StatementDescriptor<>(category, strDesc); @@ -391,7 +400,7 @@ 2, numParams); assertEquals("PreparedStatementWebStorge increments a counter" + " if it wasn't cached. Triggers e.g. if it was erronously cached!", - 3, stmtId); + 3, stmtId.getId()); } /** @@ -423,11 +432,13 @@ * * By setting hasMoreBatches to true in WebQueryResponse we signal that * there are more batches available via getMore(). + * @throws IOException + * @throws UnsupportedEncodingException * * @see {@link #canPrepareAndExecuteQueryMultiBatchFailure()} */ @Test - public void canPrepareAndExecuteQueryMultiBatchSuccess() { + public void canPrepareAndExecuteQueryMultiBatchSuccess() throws UnsupportedEncodingException, IOException { WebQueryResponse<TestObj> fakeQueryResponse = new WebQueryResponse<>(); fakeQueryResponse.setResponseCode(PreparedStatementResponseCode.QUERY_SUCCESS); fakeQueryResponse.setResultList(getTwoTestObjects()); @@ -459,7 +470,10 @@ path = requestURI.substring(requestURI.lastIndexOf('/')); assertEquals("/get-more", path); // Verify correctly passed parameters - String[] requestParams = requestBody.split("&"); + StringReader reader = new StringReader(requestBody); + BufferedReader bufRead = new BufferedReader(reader); + String line = URLDecoder.decode(bufRead.readLine(), "UTF-8"); + String[] requestParams = line.split("&"); String prepStmtIdParam = requestParams[0]; String cursorIdParam = requestParams[1]; String batchSizeParam = requestParams[2]; @@ -467,7 +481,8 @@ String[] cursorIdArray = cursorIdParam.split("="); String[] batchSizeArray = batchSizeParam.split("="); assertEquals("prepared-stmt-id", prStmtArray[0]); - assertEquals("5", prStmtArray[1]); + SharedStateId prStmtId = gson.fromJson(prStmtArray[1], SharedStateId.class); + assertEquals(5, prStmtId.getId()); assertEquals("cursor-id", cursorIdArray[0]); assertEquals("444", cursorIdArray[1]); assertEquals("batch-size", batchSizeArray[0]); @@ -553,7 +568,8 @@ int fakePrepStmtId = 5; WebPreparedStatementResponse fakeResponse = new WebPreparedStatementResponse(); fakeResponse.setNumFreeVariables(1); - fakeResponse.setStatementId(fakePrepStmtId); + SharedStateId id = new SharedStateId(fakePrepStmtId, UUID.randomUUID()); + fakeResponse.setStatementId(id); prepareServer(gson.toJson(fakeResponse)); try { stmt = storage.prepareStatement(desc); @@ -563,7 +579,7 @@ } assertTrue(stmt instanceof WebPreparedStatement); WebPreparedStatement<TestObj> webStmt = (WebPreparedStatement<TestObj>)stmt; - assertEquals(fakePrepStmtId, webStmt.getStatementId()); + assertEquals(fakePrepStmtId, webStmt.getStatementId().getId()); PreparedParameters params = webStmt.getParams(); assertEquals(1, params.getParams().length); assertNull(params.getParams()[0]); @@ -595,6 +611,7 @@ private Gson getQueryGson() { return new GsonBuilder() .registerTypeAdapterFactory(new PojoTypeAdapterFactory()) + .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory()) .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory()) .registerTypeAdapterFactory(new WebQueryResponseTypeAdapterFactory()) .registerTypeAdapterFactory(new PreparedParameterTypeAdapterFactory()) @@ -610,6 +627,7 @@ obj2.setProperty1("fluffor2"); Gson gson = new GsonBuilder() .registerTypeAdapterFactory(new PojoTypeAdapterFactory()) + .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory()) .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory()) .registerTypeAdapterFactory(new WebQueryResponseTypeAdapterFactory()) .registerTypeAdapterFactory(new PreparedParameterTypeAdapterFactory()) @@ -623,7 +641,8 @@ int fakePrepStmtId = 3; WebPreparedStatementResponse fakeResponse = new WebPreparedStatementResponse(); fakeResponse.setNumFreeVariables(1); - fakeResponse.setStatementId(fakePrepStmtId); + SharedStateId id = new SharedStateId(fakePrepStmtId, UUID.randomUUID()); + fakeResponse.setStatementId(id); prepareServer(gson.toJson(fakeResponse)); try { stmt = storage.prepareStatement(desc); @@ -633,7 +652,7 @@ } assertTrue(stmt instanceof WebPreparedStatement); WebPreparedStatement<TestObj> webStmt = (WebPreparedStatement<TestObj>)stmt; - assertEquals(fakePrepStmtId, webStmt.getStatementId()); + assertEquals(fakePrepStmtId, webStmt.getStatementId().getId()); PreparedParameters params = webStmt.getParams(); assertEquals(1, params.getParams().length); assertNull(params.getParams()[0]); @@ -661,6 +680,7 @@ public void forbiddenExecuteWriteReturnsGenericWriteFailure() { Gson gson = new GsonBuilder() .registerTypeAdapterFactory(new PojoTypeAdapterFactory()) + .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory()) .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory()) .registerTypeAdapterFactory(new WebQueryResponseTypeAdapterFactory()) .registerTypeAdapterFactory(new PreparedParameterTypeAdapterFactory()) @@ -674,7 +694,8 @@ int fakePrepStmtId = 3; WebPreparedStatementResponse fakeResponse = new WebPreparedStatementResponse(); fakeResponse.setNumFreeVariables(1); - fakeResponse.setStatementId(fakePrepStmtId); + SharedStateId id = new SharedStateId(fakePrepStmtId, UUID.randomUUID()); + fakeResponse.setStatementId(id); prepareServer(gson.toJson(fakeResponse)); try { stmt = storage.prepareStatement(desc); @@ -684,7 +705,7 @@ } assertTrue(stmt instanceof WebPreparedStatement); WebPreparedStatement<TestObj> webStmt = (WebPreparedStatement<TestObj>)stmt; - assertEquals(fakePrepStmtId, webStmt.getStatementId()); + assertEquals(fakePrepStmtId, webStmt.getStatementId().getId()); PreparedParameters params = webStmt.getParams(); assertEquals(1, params.getParams().length); assertNull(params.getParams()[0]); @@ -980,7 +1001,10 @@ @Override <T extends Pojo> WebPreparedStatementHolder sendPrepareStmtRequest(StatementDescriptor<T> desc) throws DescriptorParsingException { - return new WebPreparedStatementHolder(TestObj.class, counter++, counter++); + int numParams = counter++; + int stmtId = counter++; + SharedStateId id = new SharedStateId(stmtId, UUID.randomUUID()); + return new WebPreparedStatementHolder(TestObj.class, numParams, id); } } }
--- a/web/common/src/main/java/com/redhat/thermostat/web/common/PreparedStatementResponseCode.java Thu Nov 27 04:08:51 2014 -0700 +++ b/web/common/src/main/java/com/redhat/thermostat/web/common/PreparedStatementResponseCode.java Tue Nov 11 16:02:59 2014 +0100 @@ -55,6 +55,7 @@ */ public static final int QUERY_FAILURE = -100; + /** * Failure code for expired cursors. Usually returned if * get-more requests failed because the underlying cursor @@ -74,6 +75,14 @@ public static final int ILLEGAL_PATCH = -1; /** + * Failure code for mismatching server tokens. This is usually happening if + * client and server get out of sync due to re-deployment or the like. + * Client should recover from this automatically by clearing the client + * cache and preparing statements again. + */ + public static final int PREP_STMT_BAD_STOKEN = -2; + + /** * Failure to execute a prepared write statement for some unknown reason. */ public static final int WRITE_GENERIC_FAILURE = -200;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/common/src/main/java/com/redhat/thermostat/web/common/SharedStateId.java Tue Nov 11 16:02:59 2014 +0100 @@ -0,0 +1,80 @@ +/* + * Copyright 2012-2014 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.web.common; + +import java.util.Objects; +import java.util.UUID; + +/** + * Simple data structure which uniquely identifies shared state between + * client (WebStorage) and server (WebStorageEndPoint). + * + */ +public class SharedStateId { + + // The id of the statement + private final int id; + // A unique token only used once per webapp deployment. + private final UUID serverToken; + + public SharedStateId(int id, UUID serverToken) { + this.id = id; + this.serverToken = serverToken; + } + + public int getId() { + return id; + } + + public UUID getServerToken() { + return serverToken; + } + + @Override + public boolean equals(Object other) { + if (other == null || other.getClass() != SharedStateId.class) { + return false; + } + SharedStateId o = (SharedStateId)other; + return id == o.id && serverToken.equals(o.serverToken); + } + + @Override + public int hashCode() { + return Objects.hash(id, serverToken); + } +}
--- a/web/common/src/main/java/com/redhat/thermostat/web/common/WebPreparedStatement.java Thu Nov 27 04:08:51 2014 -0700 +++ b/web/common/src/main/java/com/redhat/thermostat/web/common/WebPreparedStatement.java Tue Nov 11 16:02:59 2014 +0100 @@ -47,9 +47,9 @@ PreparedStatement<T> { private PreparedParameters params; - private int statementId; + private SharedStateId statementId; - public WebPreparedStatement(int numParams, int statementId) { + public WebPreparedStatement(int numParams, SharedStateId statementId) { this.params = new PreparedParameters(numParams); this.statementId = statementId; } @@ -58,11 +58,11 @@ // nothing. used for serialization } - public int getStatementId() { + public SharedStateId getStatementId() { return statementId; } - public void setStatementId(int statementId) { + public void setStatementId(SharedStateId statementId) { this.statementId = statementId; }
--- a/web/common/src/main/java/com/redhat/thermostat/web/common/WebPreparedStatementResponse.java Thu Nov 27 04:08:51 2014 -0700 +++ b/web/common/src/main/java/com/redhat/thermostat/web/common/WebPreparedStatementResponse.java Tue Nov 11 16:02:59 2014 +0100 @@ -60,13 +60,13 @@ } private int numFreeVariables; - private int statementId; + private SharedStateId statementId; - public int getStatementId() { + public SharedStateId getStatementId() { return statementId; } - public void setStatementId(int statementId) { + public void setStatementId(SharedStateId statementId) { this.statementId = statementId; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/common/src/main/java/com/redhat/thermostat/web/common/typeadapters/SharedStateIdTypeAdapter.java Tue Nov 11 16:02:59 2014 +0100 @@ -0,0 +1,113 @@ +/* + * Copyright 2012-2014 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.web.common.typeadapters; + +import java.io.IOException; +import java.util.UUID; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import com.redhat.thermostat.web.common.SharedStateId; + +class SharedStateIdTypeAdapter extends TypeAdapter<SharedStateId> { + + private static final String PROP_STMT_ID = "sid"; + private static final String PROP_SERVER_TOKEN = "stok"; + + @Override + public void write(JsonWriter out, SharedStateId value) throws IOException { + // handle null + if (value == null) { + out.nullValue(); + return; + } + + out.beginObject(); + + // statement id + out.name(PROP_STMT_ID); + out.value(value.getId()); + + // server token, may be null + if (value.getServerToken() != null) { + out.name(PROP_SERVER_TOKEN); + out.value(value.getServerToken().toString()); + } else { + out.name(PROP_SERVER_TOKEN); + out.nullValue(); + } + + out.endObject(); + + } + + @Override + public SharedStateId read(JsonReader in) throws IOException { + // handle null + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + + in.beginObject(); + + + // statement id + String name = in.nextName(); + if (!name.equals(PROP_STMT_ID)) { + throw new IllegalStateException("Expected name " + PROP_STMT_ID + " but was " + name); + } + int stmtId = in.nextInt(); + + UUID serverToken = null; + if (in.peek() == JsonToken.NAME) { + name = in.nextName(); + if (!name.equals(PROP_SERVER_TOKEN)) { + throw new IllegalStateException("Expected name " + PROP_SERVER_TOKEN + " but was " + name); + } + String sToken = in.nextString(); + serverToken = UUID.fromString(sToken); + } + in.endObject(); + + return new SharedStateId(stmtId, serverToken); + } + + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/common/src/main/java/com/redhat/thermostat/web/common/typeadapters/SharedStateIdTypeAdapterFactory.java Tue Nov 11 16:02:59 2014 +0100 @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2014 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.web.common.typeadapters; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.redhat.thermostat.web.common.SharedStateId; + +public class SharedStateIdTypeAdapterFactory implements TypeAdapterFactory { + + @Override + public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) { + Class<?> rawType = type.getRawType(); + if (rawType == SharedStateId.class) { + @SuppressWarnings("unchecked") + TypeAdapter<T> ta = (TypeAdapter<T>)new SharedStateIdTypeAdapter(); + return ta; + } + return null; + } + +}
--- a/web/common/src/main/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementResponseTypeAdapter.java Thu Nov 27 04:08:51 2014 -0700 +++ b/web/common/src/main/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementResponseTypeAdapter.java Tue Nov 11 16:02:59 2014 +0100 @@ -42,6 +42,7 @@ import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; +import com.redhat.thermostat.web.common.SharedStateId; import com.redhat.thermostat.web.common.WebPreparedStatementResponse; class WebPreparedStatementResponseTypeAdapter extends @@ -49,9 +50,10 @@ private static final String NUM_FREE_VARS_NAME = "numFreeVars"; private static final String STMT_ID_NAME = "stmtId"; - - WebPreparedStatementResponseTypeAdapter() { - // package-private no-arg constructor + private final TypeAdapter<SharedStateId> sharedStateTa; + + WebPreparedStatementResponseTypeAdapter(TypeAdapter<SharedStateId> sharedStateTa) { + this.sharedStateTa = sharedStateTa; } @Override @@ -71,7 +73,8 @@ } name = reader.nextName(); if (name.equals(STMT_ID_NAME)) { - response.setStatementId(reader.nextInt()); + SharedStateId id = sharedStateTa.read(reader); + response.setStatementId(id); } reader.endObject(); @@ -87,7 +90,7 @@ } int freeVars = value.getNumFreeVariables(); - int stmtId = value.getStatementId(); + SharedStateId stmtId = value.getStatementId(); writer.beginObject(); @@ -96,7 +99,7 @@ writer.value(freeVars); // stmt id writer.name(STMT_ID_NAME); - writer.value(stmtId); + sharedStateTa.write(writer, stmtId); writer.endObject(); }
--- a/web/common/src/main/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementResponseTypeAdapterFactory.java Thu Nov 27 04:08:51 2014 -0700 +++ b/web/common/src/main/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementResponseTypeAdapterFactory.java Tue Nov 11 16:02:59 2014 +0100 @@ -40,6 +40,7 @@ import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; import com.google.gson.reflect.TypeToken; +import com.redhat.thermostat.web.common.SharedStateId; import com.redhat.thermostat.web.common.WebPreparedStatementResponse; public class WebPreparedStatementResponseTypeAdapterFactory implements @@ -49,8 +50,9 @@ public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) { Class<?> rawType = type.getRawType(); if (rawType == WebPreparedStatementResponse.class) { + TypeAdapter<SharedStateId> sharedStateIdTa = gson.getAdapter(SharedStateId.class); @SuppressWarnings("unchecked") - TypeAdapter<T> ta = (TypeAdapter<T>)new WebPreparedStatementResponseTypeAdapter(); + TypeAdapter<T> ta = (TypeAdapter<T>)new WebPreparedStatementResponseTypeAdapter(sharedStateIdTa); return ta; } return null;
--- a/web/common/src/main/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementTypeAdapter.java Thu Nov 27 04:08:51 2014 -0700 +++ b/web/common/src/main/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementTypeAdapter.java Tue Nov 11 16:02:59 2014 +0100 @@ -44,6 +44,7 @@ import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; import com.redhat.thermostat.storage.core.PreparedParameters; +import com.redhat.thermostat.web.common.SharedStateId; import com.redhat.thermostat.web.common.WebPreparedStatement; @SuppressWarnings("rawtypes") @@ -52,10 +53,12 @@ private static final String PROP_PARAMS = "p"; private static final String PROP_STMT_ID = "sid"; - private final Gson gson; + private final TypeAdapter<SharedStateId> sharedStateTa; + private final TypeAdapter<PreparedParameters> prepParamsTa; WebPreparedStatementTypeAdapter(Gson gson) { - this.gson = gson; + this.sharedStateTa = gson.getAdapter(SharedStateId.class); + this.prepParamsTa = gson.getAdapter(PreparedParameters.class); } @Override @@ -71,12 +74,11 @@ // statement id out.name(PROP_STMT_ID); - out.value(value.getStatementId()); + sharedStateTa.write(out, value.getStatementId()); // prepared parameters out.name(PROP_PARAMS); - TypeAdapter<PreparedParameters> ta = gson.getAdapter(PreparedParameters.class); - ta.write(out, value.getParams()); + prepParamsTa.write(out, value.getParams()); out.endObject(); } @@ -97,8 +99,8 @@ if (!name.equals(PROP_STMT_ID)) { throw new IllegalStateException("Expected name " + PROP_STMT_ID + " but was " + name); } - int stmtId = in.nextInt(); - + SharedStateId id = sharedStateTa.read(in); + // params PreparedParameters params = null; // params value might be null and missing. @@ -107,15 +109,14 @@ if (!name.equals(PROP_PARAMS)) { throw new IllegalStateException("Expected name " + PROP_PARAMS + " but was " + name); } - TypeAdapter<PreparedParameters> ta = gson.getAdapter(PreparedParameters.class); - params = ta.read(in); + params = prepParamsTa.read(in); } in.endObject(); WebPreparedStatement<?> stmt = new WebPreparedStatement<>(); stmt.setParams(params); - stmt.setStatementId(stmtId); + stmt.setStatementId(id); return stmt; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/common/src/test/java/com/redhat/thermostat/web/common/SharedStateIdTest.java Tue Nov 11 16:02:59 2014 +0100 @@ -0,0 +1,87 @@ +/* + * Copyright 2012-2014 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.web.common; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.UUID; + +import org.junit.Test; + +public class SharedStateIdTest { + + @Test + public void testEquals() { + UUID uuid = UUID.randomUUID(); + SharedStateId id = new SharedStateId(3200, uuid); + SharedStateId id2 = new SharedStateId(3200, UUID.randomUUID()); + SharedStateId id3 = new SharedStateId(300, uuid); + SharedStateId equals = new SharedStateId(3200, uuid); + + assertFalse(id.equals(null)); + assertTrue(id.equals(equals)); + assertFalse("Different uuid", id.equals(id2)); + assertFalse("Different id val", id.equals(id3)); + assertFalse("UUID and id val different", id3.equals(id2)); + assertTrue(id.equals(id)); + } + + @Test + public void testHashCode() { + UUID uuid = UUID.randomUUID(); + SharedStateId id = new SharedStateId(3200, uuid); + SharedStateId id2 = new SharedStateId(3200, UUID.randomUUID()); + SharedStateId id3 = new SharedStateId(300, uuid); + SharedStateId equals = new SharedStateId(3200, uuid); + + assertTrue(id.hashCode() == equals.hashCode()); + assertTrue("Different uuid", id.hashCode() != id2.hashCode()); + assertTrue("Different id val", id.hashCode() != id3.hashCode()); + assertTrue("UUID and id val different", id3.hashCode() != id2.hashCode()); + assertTrue(id.hashCode() == id.hashCode()); + } + + @Test + public void testBasic() { + UUID uuid = UUID.randomUUID(); + SharedStateId id = new SharedStateId(3200, uuid); + assertEquals(uuid, id.getServerToken()); + assertEquals(3200, id.getId()); + } +}
--- a/web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/JsonPerformanceTest.java Thu Nov 27 04:08:51 2014 -0700 +++ b/web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/JsonPerformanceTest.java Tue Nov 11 16:02:59 2014 +0100 @@ -72,10 +72,14 @@ private final boolean debug; private final String testClass; + private final Gson oldGson; + private final Gson newGson; protected JsonPerformanceTest(boolean debug, String testClass) { this.debug = debug; this.testClass = testClass; + this.oldGson = getSlowGson(); + this.newGson = getFasterGson(); } /** @@ -237,9 +241,7 @@ } private double measureSerializationSpeed(final int iterations) { - final Gson oldGson = getSlowGson(); - final Gson newGson = getFasterGson(); - PerfTestResult result = runSerializationPerformanceTest(iterations, oldGson, newGson); + PerfTestResult result = runSerializationPerformanceTest(iterations); return result.getSpeedup(); } @@ -256,13 +258,11 @@ } private double measureDeserializationSpeed(final int iterations) { - final Gson oldGson = getSlowGson(); - final Gson newGson = getFasterGson(); - PerfTestResult result = runDeserializationPerformanceTest(iterations, oldGson, newGson); + PerfTestResult result = runDeserializationPerformanceTest(iterations); return result.getSpeedup(); } - private PerfTestResult runSerializationPerformanceTest(final int iterations, final Gson oldGson, final Gson newGson) { + private PerfTestResult runSerializationPerformanceTest(final int iterations) { List<String> list = new ArrayList<>(); double oldSum = 0; double newSum = 0; @@ -294,7 +294,7 @@ return res; } - private PerfTestResult runDeserializationPerformanceTest(final int iterations, final Gson oldGson, final Gson newGson) { + private PerfTestResult runDeserializationPerformanceTest(final int iterations) { List<T> list = new ArrayList<>(); double oldSum = 0;
--- a/web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/LegacyWebPreparedStatementSerializer.java Thu Nov 27 04:08:51 2014 -0700 +++ b/web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/LegacyWebPreparedStatementSerializer.java Tue Nov 11 16:02:59 2014 +0100 @@ -47,6 +47,7 @@ import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; import com.redhat.thermostat.storage.core.PreparedParameters; +import com.redhat.thermostat.web.common.SharedStateId; import com.redhat.thermostat.web.common.WebPreparedStatement; /** @@ -66,7 +67,7 @@ JsonObject result = new JsonObject(); JsonElement parameters = ctxt.serialize(stmt.getParams(), PreparedParameters.class); result.add(PROP_PARAMS, parameters); - JsonPrimitive stmtIdElem = new JsonPrimitive(stmt.getStatementId()); + JsonElement stmtIdElem = ctxt.serialize(stmt.getStatementId(), SharedStateId.class); result.add(PROP_STMT_ID, stmtIdElem); return result; } @@ -77,7 +78,7 @@ JsonElement paramsElem = jsonElem.getAsJsonObject().get(PROP_PARAMS); JsonElement stmtIdElem = jsonElem.getAsJsonObject().get(PROP_STMT_ID); PreparedParameters params = ctxt.deserialize(paramsElem, PreparedParameters.class); - int stmtId = ctxt.deserialize(stmtIdElem, int.class); + SharedStateId stmtId = ctxt.deserialize(stmtIdElem, SharedStateId.class); WebPreparedStatement<?> stmt = new WebPreparedStatement<>(); stmt.setStatementId(stmtId); stmt.setParams(params);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/SharedStateIdTypeAdapterTest.java Tue Nov 11 16:02:59 2014 +0100 @@ -0,0 +1,101 @@ +/* + * Copyright 2012-2014 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.web.common.typeadapters; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.util.UUID; + +import org.junit.Before; +import org.junit.Test; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.redhat.thermostat.web.common.SharedStateId; + +public class SharedStateIdTypeAdapterTest { + +private Gson gson; + + @Before + public void setup() { + gson = new GsonBuilder() + .registerTypeAdapter(SharedStateId.class, new SharedStateIdTypeAdapter()) + .create(); + } + + @Test + public void canSerializeBasic() { + UUID uuid = UUID.randomUUID(); + SharedStateId id = new SharedStateId(3, uuid); + String gsonStr = gson.toJson(id); + String expectedJSON = "{\"sid\":3,\"stok\":\"" + uuid.toString() + "\"}"; + assertEquals(expectedJSON, gsonStr); + } + + @Test + public void canSerializeNull() { + SharedStateId id = new SharedStateId(3000, null); + String gsonStr = gson.toJson(id); + String expectedJSON = "{\"sid\":3000}"; + assertEquals(expectedJSON, gsonStr); + } + + @Test + public void canDeserializeBasic() { + UUID uuid = UUID.randomUUID(); + SharedStateId id = new SharedStateId(3, uuid); + String json = "{\"sid\":3,\"stok\":\"" + uuid.toString() + "\"}"; + SharedStateId deserialized = gson.fromJson(json, SharedStateId.class); + assertEquals(3, deserialized.getId()); + assertEquals(uuid, deserialized.getServerToken()); + assertEquals(id, deserialized); + } + + /** + * UUID string value might be null. Make sure deserialization works for + * those. + */ + @Test + public void canDeserializeNullServerNonce() { + String json = "{\"sid\":3}"; + SharedStateId deserialized = gson.fromJson(json, SharedStateId.class); + assertEquals(3, deserialized.getId()); + assertNull(deserialized.getServerToken()); + } +}
--- a/web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementJSONPerformanceTest.java Thu Nov 27 04:08:51 2014 -0700 +++ b/web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementJSONPerformanceTest.java Tue Nov 11 16:02:59 2014 +0100 @@ -36,6 +36,8 @@ package com.redhat.thermostat.web.common.typeadapters; +import java.util.UUID; + import org.junit.experimental.categories.Category; import com.google.gson.Gson; @@ -43,6 +45,7 @@ import com.google.gson.reflect.TypeToken; import com.redhat.thermostat.storage.model.Pojo; import com.redhat.thermostat.testutils.PerformanceTest; +import com.redhat.thermostat.web.common.SharedStateId; import com.redhat.thermostat.web.common.WebPreparedStatement; import com.redhat.thermostat.web.common.typeadapters.PojoTypeAdapterFactory; import com.redhat.thermostat.web.common.typeadapters.WebPreparedStatementTypeAdapterFactory; @@ -66,6 +69,7 @@ protected Gson getSlowGson() { return new GsonBuilder() .registerTypeHierarchyAdapter(Pojo.class, new LegacyGSONConverter()) + .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory()) .registerTypeAdapter(WebPreparedStatement.class, new LegacyWebPreparedStatementSerializer()) .create(); } @@ -74,6 +78,7 @@ protected Gson getFasterGson() { return new GsonBuilder() .registerTypeAdapterFactory(new PojoTypeAdapterFactory()) + .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory()) .registerTypeAdapterFactory(new WebPreparedStatementTypeAdapterFactory()) .create(); } @@ -95,13 +100,13 @@ @Override protected String mutateJsonString(GsonContext ctx, int mutator) { - return String.format("{\"sid\":%d,\"p\":{\"params\":[]}}", mutator); + return String.format("{\"sid\":{\"sid\":%d,\"stok\":\"" + UUID.randomUUID() + "\"},\"p\":{\"params\":[]}}", mutator); } @Override protected WebPreparedStatement mutateToBeSerializedInstance(int mutator) { - WebPreparedStatement retval = new WebPreparedStatement<>(0, 555); - retval.setStatementId(mutator); + SharedStateId id = new SharedStateId(mutator, UUID.randomUUID()); + WebPreparedStatement retval = new WebPreparedStatement<>(0, id); return retval; }
--- a/web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementResponseJSONPerformanceTest.java Thu Nov 27 04:08:51 2014 -0700 +++ b/web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementResponseJSONPerformanceTest.java Tue Nov 11 16:02:59 2014 +0100 @@ -36,12 +36,15 @@ package com.redhat.thermostat.web.common.typeadapters; +import java.util.UUID; + import org.junit.experimental.categories.Category; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import com.redhat.thermostat.testutils.PerformanceTest; +import com.redhat.thermostat.web.common.SharedStateId; import com.redhat.thermostat.web.common.WebPreparedStatementResponse; import com.redhat.thermostat.web.common.typeadapters.WebPreparedStatementResponseTypeAdapterFactory; @@ -62,12 +65,15 @@ @Override protected Gson getSlowGson() { - return new GsonBuilder().create(); + return new GsonBuilder() + .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory()) + .create(); } @Override protected Gson getFasterGson() { return new GsonBuilder() + .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory()) .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory()) .create(); } @@ -95,14 +101,15 @@ protected String mutateJsonString(GsonContext ctx, int mutator) { int stmtId = (mutator + 1) * 20; int numFreeVars = mutator; - return String.format("{\"numFreeVars\":%d,\"stmtId\":%d}", numFreeVars, stmtId); + return String.format("{\"numFreeVars\":%d,\"stmtId\":{\"sid\":%d,\"stok\":\"%s\"}}", numFreeVars, stmtId, UUID.randomUUID().toString()); } @Override protected WebPreparedStatementResponse mutateToBeSerializedInstance(int mutator) { WebPreparedStatementResponse response = new WebPreparedStatementResponse(); response.setNumFreeVariables(3); - response.setStatementId(mutator); + SharedStateId id = new SharedStateId(mutator, UUID.randomUUID()); + response.setStatementId(id); return response; } @@ -128,7 +135,7 @@ @Override protected int getWarmDeserializationIterations() { - return 5000; + return 2000; } @Override @@ -138,7 +145,7 @@ @Override protected double getSelfDeserializationDelta() { - return 0.3; + return 0; } }
--- a/web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementResponseTypeAdapterTest.java Thu Nov 27 04:08:51 2014 -0700 +++ b/web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementResponseTypeAdapterTest.java Tue Nov 11 16:02:59 2014 +0100 @@ -39,12 +39,15 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import java.util.UUID; + import org.junit.Before; import org.junit.Test; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonSyntaxException; +import com.redhat.thermostat.web.common.SharedStateId; import com.redhat.thermostat.web.common.WebPreparedStatementResponse; public class WebPreparedStatementResponseTypeAdapterTest { @@ -54,6 +57,7 @@ @Before public void setup() { gson = new GsonBuilder() + .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory()) .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory()) .create(); } @@ -62,29 +66,35 @@ public void testSerializationDeserializationBasic() { WebPreparedStatementResponse response = new WebPreparedStatementResponse(); response.setNumFreeVariables(6); - response.setStatementId(WebPreparedStatementResponse.ILLEGAL_STATEMENT); + + UUID uuid = UUID.randomUUID(); + SharedStateId stmtId = new SharedStateId(WebPreparedStatementResponse.ILLEGAL_STATEMENT, uuid); + response.setStatementId(stmtId); String jsonStr = gson.toJson(response, WebPreparedStatementResponse.class); - String expectedString = "{\"numFreeVars\":6,\"stmtId\":-1}"; + String expectedString = "{\"numFreeVars\":6,\"stmtId\":{\"sid\":-1,\"stok\":\""+ uuid.toString() + "\"}}"; assertEquals(expectedString, jsonStr); WebPreparedStatementResponse actual = gson.fromJson(jsonStr, WebPreparedStatementResponse.class); assertEquals(6, actual.getNumFreeVariables()); - assertEquals(WebPreparedStatementResponse.ILLEGAL_STATEMENT, actual.getStatementId()); + assertEquals(stmtId, actual.getStatementId()); } - @Test(expected=JsonSyntaxException.class) - public void failDeserializePrimitiveSetNull() { - String jsonString = "{\"numFreeVars\":11,\"stmtId\":null}"; - gson.fromJson(jsonString, WebPreparedStatementResponse.class); + public void deserializeNull() { + String jsonString = "{\"numFreeVars\":11}"; + WebPreparedStatementResponse resp = gson.fromJson(jsonString, WebPreparedStatementResponse.class); + assertNull(resp.getStatementId()); + assertEquals(11, resp.getNumFreeVariables()); } @Test public void canDeserializeBasic() { - String jsonString = "{\"numFreeVars\":11,\"stmtId\":6}"; + UUID uuid = UUID.randomUUID(); + SharedStateId stmtId = new SharedStateId(6, uuid); + String jsonString = "{\"numFreeVars\":11,\"stmtId\":{\"sid\":6,\"stok\":\""+ uuid.toString() + "\"}}"; WebPreparedStatementResponse actual = gson.fromJson(jsonString, WebPreparedStatementResponse.class); - assertEquals(6, actual.getStatementId()); + assertEquals(stmtId, actual.getStatementId()); assertEquals(11, actual.getNumFreeVariables()); }
--- a/web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementTypeAdapterTest.java Thu Nov 27 04:08:51 2014 -0700 +++ b/web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementTypeAdapterTest.java Tue Nov 11 16:02:59 2014 +0100 @@ -40,6 +40,8 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import java.util.UUID; + import org.junit.Before; import org.junit.Test; @@ -48,6 +50,7 @@ import com.redhat.thermostat.storage.core.PreparedParameter; import com.redhat.thermostat.storage.core.PreparedParameters; import com.redhat.thermostat.storage.model.AgentInformation; +import com.redhat.thermostat.web.common.SharedStateId; import com.redhat.thermostat.web.common.WebPreparedStatement; import com.redhat.thermostat.web.common.WebPreparedStatementResponse; @@ -59,6 +62,7 @@ public void setup() { gson = new GsonBuilder() .registerTypeAdapterFactory(new WebPreparedStatementTypeAdapterFactory()) + .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory()) .registerTypeAdapterFactory(new PreparedParameterTypeAdapterFactory()) .registerTypeAdapterFactory(new PojoTypeAdapterFactory()) .create(); @@ -67,10 +71,11 @@ @Test public void canSerializeNullParams() { WebPreparedStatement<?> stmt = new WebPreparedStatement<>(); - stmt.setStatementId(500); + UUID uuid = UUID.randomUUID(); + stmt.setStatementId(new SharedStateId(500, uuid)); stmt.setParams(null); - String expected = "{\"sid\":500}"; + String expected = "{\"sid\":{\"sid\":500,\"stok\":\"" + uuid.toString() + "\"}}"; String actual = gson.toJson(stmt); assertEquals(expected, actual); @@ -78,28 +83,33 @@ @Test public void canDeserializeNullParams() { - String json = "{\"sid\": 500}"; + UUID uuid = UUID.randomUUID(); + String json = "{\"sid\":{\"sid\":500,\"stok\":\"" + uuid.toString() + "\"}}"; WebPreparedStatement<?> stmt = gson.fromJson(json, WebPreparedStatement.class); - assertEquals(500, stmt.getStatementId()); + assertEquals(new SharedStateId(500, uuid), stmt.getStatementId()); assertNull(stmt.getParams()); } @Test public void canSerializeEmptyParams() { - WebPreparedStatement<?> stmt = new WebPreparedStatement<>(0, 555); + UUID uuid = UUID.randomUUID(); + SharedStateId id = new SharedStateId(555, uuid); + WebPreparedStatement<?> stmt = new WebPreparedStatement<>(0, id); - String expected = "{\"sid\":555,\"p\":{\"params\":[]}}"; + String expected = "{\"sid\":{\"sid\":555,\"stok\":\"" + uuid.toString() + "\"},\"p\":{\"params\":[]}}"; String actual = gson.toJson(stmt); assertEquals(expected, actual); } @Test public void canDeserializeEmptyParams() { - String json = "{\"sid\":555,\"p\":{\"params\":[]}}"; + UUID uuid = UUID.randomUUID(); + SharedStateId id = new SharedStateId(555, uuid); + String json = "{\"sid\":{\"sid\":555,\"stok\":\"" + uuid.toString() + "\"},\"p\":{\"params\":[]}}"; WebPreparedStatement<?> stmt = gson.fromJson(json, WebPreparedStatement.class); - assertEquals(555, stmt.getStatementId()); + assertEquals(id, stmt.getStatementId()); assertNotNull(stmt.getParams()); assertEquals(0, stmt.getParams().getParams().length); } @@ -114,7 +124,9 @@ params.setBoolean(4, true); WebPreparedStatement<?> stmt = new WebPreparedStatement<>(); stmt.setParams(params); - stmt.setStatementId(WebPreparedStatementResponse.DESCRIPTOR_PARSE_FAILED); + UUID uuid = UUID.randomUUID(); + SharedStateId id = new SharedStateId(WebPreparedStatementResponse.DESCRIPTOR_PARSE_FAILED, uuid); + stmt.setStatementId(id); String jsonString = gson.toJson(stmt, WebPreparedStatement.class); WebPreparedStatement<?> newStmt = gson.fromJson(jsonString, WebPreparedStatement.class); assertNotNull(newStmt); @@ -134,7 +146,7 @@ assertEquals(String[].class, parameters[3].getType()); assertEquals(true, parameters[4].getValue()); assertEquals(boolean.class, parameters[4].getType()); - assertEquals(WebPreparedStatementResponse.DESCRIPTOR_PARSE_FAILED, newStmt.getStatementId()); + assertEquals(id, newStmt.getStatementId()); } /* @@ -153,13 +165,15 @@ WebPreparedStatement<?> stmt = new WebPreparedStatement<>(); stmt.setParams(params); - stmt.setStatementId(WebPreparedStatementResponse.DESCRIPTOR_PARSE_FAILED); + UUID uuid = UUID.randomUUID(); + SharedStateId id = new SharedStateId(WebPreparedStatementResponse.DESCRIPTOR_PARSE_FAILED, uuid); + stmt.setStatementId(id); String jsonString = gson.toJson(stmt, WebPreparedStatement.class); assertNotNull(jsonString); WebPreparedStatement<?> result = gson.fromJson(jsonString, WebPreparedStatement.class); - assertEquals(WebPreparedStatementResponse.DESCRIPTOR_PARSE_FAILED, result.getStatementId()); + assertEquals(id, result.getStatementId()); assertNotNull(result.getParams()); } }
--- a/web/server/src/main/java/com/redhat/thermostat/web/server/PreparedStatementHolder.java Thu Nov 27 04:08:51 2014 -0700 +++ b/web/server/src/main/java/com/redhat/thermostat/web/server/PreparedStatementHolder.java Tue Nov 11 16:02:59 2014 +0100 @@ -39,23 +39,24 @@ import com.redhat.thermostat.storage.core.PreparedStatement; import com.redhat.thermostat.storage.core.StatementDescriptor; import com.redhat.thermostat.storage.model.Pojo; +import com.redhat.thermostat.web.common.SharedStateId; class PreparedStatementHolder<T extends Pojo> { - private final int id; + private final SharedStateId stmtId; private final PreparedStatement<T> stmt; private final Class<T> dataClass; private final StatementDescriptor<T> desc; - PreparedStatementHolder(int id, PreparedStatement<T> stmt, Class<T> dataClass, StatementDescriptor<T> desc) { - this.id = id; + PreparedStatementHolder(SharedStateId stmtId, PreparedStatement<T> stmt, Class<T> dataClass, StatementDescriptor<T> desc) { + this.stmtId = stmtId; this.stmt = stmt; this.dataClass = dataClass; this.desc = desc; } - int getId() { - return id; + SharedStateId getId() { + return stmtId; } PreparedStatement<T> getStmt() {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/main/java/com/redhat/thermostat/web/server/PreparedStatementManager.java Tue Nov 11 16:02:59 2014 +0100 @@ -0,0 +1,132 @@ +/* + * Copyright 2012-2014 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.web.server; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +import com.redhat.thermostat.storage.core.PreparedStatement; +import com.redhat.thermostat.storage.core.StatementDescriptor; +import com.redhat.thermostat.storage.model.Pojo; +import com.redhat.thermostat.web.common.SharedStateId; + +/** + * Manager for {@link PreparedStatement}s which get prepared/executed via + * {@link WebStorage}. + * + */ +class PreparedStatementManager { + + // We have one map per server token. + private final Map<SharedStateId, PreparedStatementHolder<?>> preparedStatementIds; + private final Map<StatementDescriptor<?>, PreparedStatementHolder<?>> preparedStmts; + private int currentPreparedStmtId = 0; + + PreparedStatementManager() { + preparedStatementIds = new HashMap<>(); + preparedStmts = new HashMap<>(); + } + + // Test only constructor + PreparedStatementManager(int initialValue) { + this(); + currentPreparedStmtId = initialValue; + } + + @SuppressWarnings("unchecked") // we are the only ones adding them + synchronized <T extends Pojo> PreparedStatementHolder<T> getStatementHolder(SharedStateId id) { + return (PreparedStatementHolder<T>)preparedStatementIds.get(Objects.requireNonNull(id)); + } + + @SuppressWarnings("unchecked") // we are the only ones adding them + synchronized <T extends Pojo> PreparedStatementHolder<T> getStatementHolder(StatementDescriptor<T> desc) { + return (PreparedStatementHolder<T>)preparedStmts.get(Objects.requireNonNull(desc)); + } + + /** + * Adds a new {@link PreparedStatementHolder} into this + * {@link PreparedStatementManager}. Adding an equal {@code targetDesc} + * statement twice will yield the same returned id. + * + * @param serverToken + * A server token used for creating a new {@link SharedStateId} + * if the target statement is not already tracked. + * @param targetStmt + * The target statement to keep track of. + * @param dataClass + * The data class of the target statement. + * @param targetDesc + * The {@link StatementDescriptor} which was used for creating + * the target statement {@code targetStmt} + * @return A unique ID identifying this statement. It's suitable to be + * shared between server and client. + */ + synchronized <T extends Pojo> SharedStateId createAndPutHolder( + UUID serverToken, + PreparedStatement<T> targetStmt, + Class<T> dataClass, + StatementDescriptor<T> targetDesc) { + // check if we have this descriptor already added + @SuppressWarnings("unchecked") + PreparedStatementHolder<T> holder = (PreparedStatementHolder<T>)preparedStmts.get(Objects.requireNonNull(targetDesc)); + if (holder != null) { + // nothing to do + assert( preparedStatementIds.get(holder.getId()) != null ); + return holder.getId(); + } + // OK, must be a new statement we don't yet track + SharedStateId id = new SharedStateId(currentPreparedStmtId, Objects.requireNonNull(serverToken)); + currentPreparedStmtId++; + // There is nothing we can do other than using a long rather than an int + // for the ID. That being said, having more than 2 billion *different* queries + // seems more than unlikely. It may very well be a bug. Either way it + // seems like a good idea to fail hard in order to be in the know about + // this situation. + if (currentPreparedStmtId == Integer.MAX_VALUE) { + throw new IllegalStateException("Too many different statements!"); + } + holder = new PreparedStatementHolder<>(id, + Objects.requireNonNull(targetStmt), + Objects.requireNonNull(dataClass), + targetDesc); + preparedStmts.put(targetDesc, holder); + preparedStatementIds.put(id, holder); + return id; + } +}
--- a/web/server/src/main/java/com/redhat/thermostat/web/server/WebStorageEndPoint.java Thu Nov 27 04:08:51 2014 -0700 +++ b/web/server/src/main/java/com/redhat/thermostat/web/server/WebStorageEndPoint.java Tue Nov 11 16:02:59 2014 +0100 @@ -43,6 +43,7 @@ import java.io.PrintWriter; import java.io.Writer; import java.lang.reflect.Array; +import java.net.URLDecoder; import java.security.Principal; import java.util.ArrayList; import java.util.Collections; @@ -51,6 +52,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; @@ -100,12 +102,14 @@ import com.redhat.thermostat.storage.query.BinaryLogicalOperator; import com.redhat.thermostat.storage.query.Expression; import com.redhat.thermostat.web.common.PreparedStatementResponseCode; +import com.redhat.thermostat.web.common.SharedStateId; import com.redhat.thermostat.web.common.WebPreparedStatement; import com.redhat.thermostat.web.common.WebPreparedStatementResponse; import com.redhat.thermostat.web.common.WebQueryResponse; import com.redhat.thermostat.web.common.typeadapters.PojoTypeAdapterFactory; import com.redhat.thermostat.web.common.typeadapters.PreparedParameterTypeAdapterFactory; import com.redhat.thermostat.web.common.typeadapters.PreparedParametersTypeAdapterFactory; +import com.redhat.thermostat.web.common.typeadapters.SharedStateIdTypeAdapterFactory; import com.redhat.thermostat.web.common.typeadapters.WebPreparedStatementResponseTypeAdapterFactory; import com.redhat.thermostat.web.common.typeadapters.WebPreparedStatementTypeAdapterFactory; import com.redhat.thermostat.web.common.typeadapters.WebQueryResponseTypeAdapterFactory; @@ -131,6 +135,7 @@ private static final String TOKEN_MANAGER_KEY = "token-manager"; private static final String USER_PRINCIPAL_CALLBACK_KEY = "user-principal-callback"; private static final String CURSOR_MANAGER_KEY = "cursor-manager"; + private static final String PREPARED_STMT_MANAGER_KEY = "prepared-stmt-manager"; private static final int UNKNOWN_CURSOR_ID = -0xdeadbeef; private static final String CATEGORY_KEY_FORMAT = "%s|%s"; @@ -153,12 +158,9 @@ private Map<String, Integer> categoryIds; private Map<Integer, Category<?>> categories; - - private Map<StatementDescriptor<?>, PreparedStatementHolder<?>> preparedStmts; - private Map<Integer, PreparedStatementHolder<?>> preparedStatementIds; - // Lock to be held for setting/getting prepared queries in the above maps - private Object preparedStmtLock = new Object(); - private int currentPreparedStmtId; + // A unique server token, which gets reset on every Servlet.init() call. + // I.e. every reload/redeployment. + private UUID serverToken; // read-only set of all known statement descriptors we trust and allow private Set<String> knownStatementDescriptors; @@ -176,6 +178,7 @@ gson = new GsonBuilder() .registerTypeAdapterFactory(new PojoTypeAdapterFactory()) + .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory()) .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory()) .registerTypeAdapterFactory(new WebQueryResponseTypeAdapterFactory()) .registerTypeAdapterFactory(new PreparedParameterTypeAdapterFactory()) @@ -184,8 +187,6 @@ .create(); categoryIds = new HashMap<>(); categories = new HashMap<>(); - preparedStatementIds = new HashMap<>(); - preparedStmts = new HashMap<>(); TokenManager tokenManager = new TokenManager(); String timeoutParam = getInitParameter(TOKEN_MANAGER_TIMEOUT_PARAM); if (timeoutParam != null) { @@ -208,6 +209,10 @@ PrincipalCallbackFactory cbFactory = new PrincipalCallbackFactory(info); PrincipalCallback callback = Objects.requireNonNull(cbFactory.getCallback()); servletContext.setAttribute(USER_PRINCIPAL_CALLBACK_KEY, callback); + synchronized(servletContext) { + servletContext.setAttribute(PREPARED_STMT_MANAGER_KEY, new PreparedStatementManager()); + } + serverToken = UUID.randomUUID(); } @Override @@ -350,7 +355,8 @@ if (cat == null) { // bad category? we refuse to accept this logger.log(Level.WARNING, "Attepted to prepare a statement with an illegal category id"); - response.setStatementId(WebPreparedStatementResponse.ILLEGAL_STATEMENT); + SharedStateId id = new SharedStateId(WebPreparedStatementResponse.ILLEGAL_STATEMENT, serverToken); + response.setStatementId(id); writeResponse(resp, response, WebPreparedStatementResponse.class); return; } @@ -360,50 +366,46 @@ String msg = "Attempted to prepare a statement descriptor which we " + "don't trust! Descriptor was: ->" + desc.getDescriptor() + "<-"; logger.log(Level.WARNING, msg); - response.setStatementId(WebPreparedStatementResponse.ILLEGAL_STATEMENT); + SharedStateId id = new SharedStateId(WebPreparedStatementResponse.ILLEGAL_STATEMENT, serverToken); + response.setStatementId(id); writeResponse(resp, response, WebPreparedStatementResponse.class); return; } - synchronized (preparedStmtLock) { - // see if we've prepared this query already - if (preparedStmts.containsKey(desc)) { - PreparedStatementHolder<T> holder = (PreparedStatementHolder<T>) preparedStmts - .get(desc); - ParsedStatement<T> parsed = holder.getStmt() - .getParsedStatement(); - int freeVars = parsed.getNumParams(); - response.setNumFreeVariables(freeVars); - response.setStatementId(holder.getId()); - writeResponse(resp, response, - WebPreparedStatementResponse.class); - return; - } - - // Prepare the target statement and put it into our prepared statement - // maps. + PreparedStatementManager prepStmtManager = getPreparedStmtManager(); + // see if we've prepared this query already + PreparedStatementHolder<T> holder = prepStmtManager.getStatementHolder(desc); + if (holder != null) { + ParsedStatement<T> parsed = holder.getStmt().getParsedStatement(); + int freeVars = parsed.getNumParams(); + response.setNumFreeVariables(freeVars); + SharedStateId id = new SharedStateId(holder.getId().getId(), serverToken); + response.setStatementId(id); + writeResponse(resp, response, WebPreparedStatementResponse.class); + return; + } else { + // Prepare the target statement and track it via + // PreparedStatementManager PreparedStatement<T> targetPreparedStatement; try { targetPreparedStatement = (PreparedStatement<T>) storage .prepareStatement(desc); } catch (DescriptorParsingException e) { logger.log(Level.WARNING, "Descriptor parse error!", e); - response.setStatementId(WebPreparedStatementResponse.DESCRIPTOR_PARSE_FAILED); + SharedStateId id = new SharedStateId(WebPreparedStatementResponse.DESCRIPTOR_PARSE_FAILED, serverToken); + response.setStatementId(id); writeResponse(resp, response, WebPreparedStatementResponse.class); return; } - PreparedStatementHolder<T> holder = new PreparedStatementHolder<T>( - currentPreparedStmtId, targetPreparedStatement, - (Class<T>) cat.getDataClass(), desc); - preparedStmts.put(desc, holder); - preparedStatementIds.put(currentPreparedStmtId, holder); + SharedStateId stmtId = prepStmtManager.createAndPutHolder( + serverToken, targetPreparedStatement, cat.getDataClass(), + desc); ParsedStatement<?> parsed = targetPreparedStatement .getParsedStatement(); response.setNumFreeVariables(parsed.getNumParams()); - response.setStatementId(currentPreparedStmtId); + response.setStatementId(stmtId); writeResponse(resp, response, WebPreparedStatementResponse.class); - currentPreparedStmtId++; } } @@ -607,9 +609,22 @@ @SuppressWarnings("unchecked") WebPreparedStatement<T> stmt = gson.fromJson(queryParam, WebPreparedStatement.class); + // Check if the server token the client knows about still matches. + // Bail out early otherwise. + SharedStateId stmtId = stmt.getStatementId(); + if (!serverToken.equals(stmtId.getServerToken())) { + logger.log(Level.INFO, "Server token: '" + serverToken + + "' and client token '" + stmtId.getServerToken() + + "' out of sync."); + WebQueryResponse<T> response = new WebQueryResponse<>(); + response.setResponseCode(PreparedStatementResponseCode.PREP_STMT_BAD_STOKEN); + writeResponse(resp, response, WebQueryResponse.class); + return; + } PreparedParameters p = stmt.getParams(); PreparedParameter[] params = p.getParams(); - PreparedStatementHolder<T> targetStmtHolder = getStatementHolderFromId(stmt.getStatementId()); + PreparedStatementManager prepStmtManager = getPreparedStmtManager(); + PreparedStatementHolder<T> targetStmtHolder = prepStmtManager.getStatementHolder(stmtId); PreparedStatement<T> targetStmt = targetStmtHolder.getStmt(); ParsedStatement<T> parsed = targetStmt.getParsedStatement(); Query<T> targetQuery = null; @@ -665,6 +680,16 @@ } writeQueryResponse(resp, response, resultsList, targetStmtHolder); } + + private PreparedStatementManager getPreparedStmtManager() { + ServletContext servletContext = getServletContext(); + PreparedStatementManager prepStmtManager = null; + synchronized(servletContext) { + prepStmtManager = (PreparedStatementManager)servletContext.getAttribute(PREPARED_STMT_MANAGER_KEY); + } + // If this throws a NPE this is certainly a bug. + return Objects.requireNonNull(prepStmtManager); + } private <T extends Pojo> void writeQueryResponse(HttpServletResponse resp, WebQueryResponse<T> response, List<T> resultsList, PreparedStatementHolder<T> targetStmtHolder) throws IOException { @SuppressWarnings("unchecked") @@ -694,7 +719,8 @@ String cursorIdParam = req.getParameter("cursor-id"); String batchSizeParam = req.getParameter("batch-size"); - int stmtId = Integer.parseInt(stmtIdParam); + // Statement Id is JSON encoded. + SharedStateId id = gson.fromJson(URLDecoder.decode(stmtIdParam, "UTF-8"), SharedStateId.class); int cursorId = Integer.parseInt(cursorIdParam); int batchSize = Integer.parseInt(batchSizeParam); @@ -712,7 +738,8 @@ @SuppressWarnings("unchecked") BatchCursor<T> batchCursor = (BatchCursor<T>)cursorManager.get(cursorId); - PreparedStatementHolder<T> targetStmtHolder = getStatementHolderFromId(stmtId); + PreparedStatementManager prepStmtManager = getPreparedStmtManager(); + PreparedStatementHolder<T> targetStmtHolder = prepStmtManager.getStatementHolder(id); if (batchCursor == null) { // This either means: // 1. The underlying (backing-storage) cursor didn't have @@ -790,9 +817,20 @@ String queryParam = req.getParameter("prepared-stmt"); WebPreparedStatement<T> stmt = gson.fromJson(queryParam, WebPreparedStatement.class); + // Check if the server token the client knows about still matches. + // Bail out early otherwise. + SharedStateId stmtId = stmt.getStatementId(); + if (!serverToken.equals(stmtId.getServerToken())) { + logger.log(Level.INFO, "Server token: '" + serverToken + + "' and client token '" + stmtId.getServerToken() + + "' out of sync."); + writeResponse(resp, PreparedStatementResponseCode.PREP_STMT_BAD_STOKEN, int.class); + return; + } PreparedParameters p = stmt.getParams(); PreparedParameter[] params = p.getParams(); - PreparedStatementHolder<T> targetStmtHolder = getStatementHolderFromId(stmt.getStatementId()); + PreparedStatementManager prepStmtManager = getPreparedStmtManager(); + PreparedStatementHolder<T> targetStmtHolder = prepStmtManager.getStatementHolder(stmt.getStatementId()); PreparedStatement<T> targetStmt = targetStmtHolder.getStmt(); ParsedStatement<T> parsed = targetStmt.getParsedStatement(); @@ -860,12 +898,6 @@ return patchedQuery; } - private <T extends Pojo> PreparedStatementHolder<T> getStatementHolderFromId(int statementId) { - @SuppressWarnings("unchecked") // we are the only ones adding them - PreparedStatementHolder<T> holder = (PreparedStatementHolder<T>)preparedStatementIds.get(statementId); - return holder; - } - private Category<?> getCategoryFromId(int categoryId) { Category<?> category = categories.get(categoryId); return category;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/test/java/com/redhat/thermostat/web/server/PreparedStatementManagerTest.java Tue Nov 11 16:02:59 2014 +0100 @@ -0,0 +1,221 @@ +/* + * Copyright 2012-2014 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.web.server; + +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; + +import org.junit.Test; + +import com.redhat.thermostat.storage.core.Category; +import com.redhat.thermostat.storage.core.PreparedStatement; +import com.redhat.thermostat.storage.core.StatementDescriptor; +import com.redhat.thermostat.storage.model.Pojo; +import com.redhat.thermostat.web.common.SharedStateId; + +public class PreparedStatementManagerTest { + + /** + * Verify that every call to put creates a new ID entry only if + * statements (id'ed by descriptor) are different. + */ + @Test + public void canCreateAndPutStmt() { + PreparedStatementManager manager = new PreparedStatementManager(); + UUID serverToken = UUID.randomUUID(); + @SuppressWarnings("unchecked") + PreparedStatement<TestPojo> targetStmt = mock(PreparedStatement.class); + @SuppressWarnings({ "unchecked" }) + StatementDescriptor<TestPojo> testDesc = new StatementDescriptor<>(mock(Category.class), "unused"); + Class<TestPojo> testClass = TestPojo.class; + SharedStateId id = manager.createAndPutHolder(serverToken, targetStmt, testClass, testDesc); + assertNotNull(id); + assertEquals(serverToken, id.getServerToken()); + assertEquals("counter starts with 0", 0, id.getId()); + // do it again with a different statement + @SuppressWarnings("unchecked") + PreparedStatement<TestPojo> otherTargetStmt = mock(PreparedStatement.class); + @SuppressWarnings({ "unchecked" }) + StatementDescriptor<TestPojo> otherTestDesc = new StatementDescriptor<>(mock(Category.class), "other"); + SharedStateId otherId = manager.createAndPutHolder(serverToken, otherTargetStmt, testClass, otherTestDesc); + assertNotNull(otherId); + assertFalse(id.equals(otherId)); + assertEquals(1, otherId.getId()); + assertEquals(serverToken, otherId.getServerToken()); + // do it once more with same token, which should yield the same id than + // we used to get for the first createAndPutHolder() call. + SharedStateId thirdId = manager.createAndPutHolder(serverToken, targetStmt, testClass, testDesc); + assertEquals(id, thirdId); + assertSame(id, thirdId); + } + + /** + * Verify that adding a new value when the int ID would overflow throws an + * exception. + */ + @Test + public void intIdOverflowThrowsException() { + PreparedStatementManager manager = new PreparedStatementManager(Integer.MAX_VALUE - 1); + UUID serverNonce = UUID.randomUUID(); + @SuppressWarnings("unchecked") + PreparedStatement<TestPojo> targetStmt = mock(PreparedStatement.class); + @SuppressWarnings({ "unchecked" }) + StatementDescriptor<TestPojo> testDesc = new StatementDescriptor<>(mock(Category.class), "unused"); + Class<TestPojo> testClass = TestPojo.class; + try { + manager.createAndPutHolder(serverNonce, targetStmt, testClass, testDesc); + fail("Should have thrown ISE due to int id overflow"); + } catch (IllegalStateException e) { + assertEquals("Too many different statements!", e.getMessage()); + } + } + + /** + * Basic parameters must not be null. + */ + @Test + public void rejectsNullValuesAsParameters() { + PreparedStatementManager manager = new PreparedStatementManager(); + @SuppressWarnings("unchecked") + PreparedStatement<TestPojo> targetStmt = mock(PreparedStatement.class); + @SuppressWarnings({ "unchecked" }) + StatementDescriptor<TestPojo> testDesc = new StatementDescriptor<>(mock(Category.class), "unused"); + Class<TestPojo> testClass = TestPojo.class; + try { + manager.createAndPutHolder(null, targetStmt, testClass, testDesc); + fail("Expected NPE due to null server token"); + } catch (NullPointerException e) { + // pass + } + UUID serverNonce = UUID.randomUUID(); + try { + manager.createAndPutHolder(serverNonce, null, testClass, testDesc); + fail("Expected NPE due to null target stmt"); + } catch (NullPointerException e) { + // pass + } + try { + manager.createAndPutHolder(serverNonce, targetStmt, null, testDesc); + fail("Expected NPE due to null data class"); + } catch (NullPointerException e) { + // pass + } + try { + manager.createAndPutHolder(serverNonce, targetStmt, testClass, null); + fail("Expected NPE due to null stmt descriptor"); + } catch (NullPointerException e) { + // pass + } + } + + @Test + public void canGetAddedHolderItemViaID() { + PreparedStatementManager manager = new PreparedStatementManager(); + UUID serverNonce = UUID.randomUUID(); + @SuppressWarnings("unchecked") + PreparedStatement<TestPojo> targetStmt = mock(PreparedStatement.class); + @SuppressWarnings({ "unchecked" }) + StatementDescriptor<TestPojo> testDesc = new StatementDescriptor<>(mock(Category.class), "unused"); + Class<TestPojo> testClass = TestPojo.class; + SharedStateId id = manager.createAndPutHolder(serverNonce, targetStmt, testClass, testDesc); + PreparedStatementHolder<TestPojo> holder = manager.getStatementHolder(id); + assertNotNull(holder); + assertSame(serverNonce, holder.getId().getServerToken()); + assertSame(id, holder.getId()); + assertSame(targetStmt, holder.getStmt()); + assertSame(testClass, holder.getDataClass()); + assertSame(testDesc, holder.getStatementDescriptor()); + // do it again with an equal ID + SharedStateId retrievalId = new SharedStateId(0, serverNonce); + assertTrue(retrievalId.equals(id)); + holder = manager.getStatementHolder(retrievalId); + assertEquals(id, holder.getId()); + assertNotSame(retrievalId, holder.getId()); + assertSame(id, holder.getId()); + assertSame(targetStmt, holder.getStmt()); + assertSame(testClass, holder.getDataClass()); + assertSame(testDesc, holder.getStatementDescriptor()); + + // unknown server token should return null + assertNull(manager.getStatementHolder(new SharedStateId(0, UUID.randomUUID()))); + } + + @Test + public void canGetAddedHolderItemViaDescriptor() { + PreparedStatementManager manager = new PreparedStatementManager(); + UUID serverNonce = UUID.randomUUID(); + @SuppressWarnings("unchecked") + PreparedStatement<TestPojo> targetStmt = mock(PreparedStatement.class); + @SuppressWarnings({ "unchecked" }) + Category<TestPojo> cat = mock(Category.class); + StatementDescriptor<TestPojo> testDesc = new StatementDescriptor<>(cat, "no-matter"); + Class<TestPojo> testClass = TestPojo.class; + SharedStateId id = manager.createAndPutHolder(serverNonce, targetStmt, testClass, testDesc); + PreparedStatementHolder<TestPojo> holder = manager.getStatementHolder(testDesc); + assertNotNull(holder); + assertSame(serverNonce, holder.getId().getServerToken()); + assertSame(id, holder.getId()); + assertSame(targetStmt, holder.getStmt()); + assertSame(testClass, holder.getDataClass()); + assertSame(testDesc, holder.getStatementDescriptor()); + + // do it again with an equal descriptor + StatementDescriptor<TestPojo> equalDesc = new StatementDescriptor<>(cat, "no-matter"); + assertNotSame(testDesc, equalDesc); + assertEquals(testDesc, equalDesc); + holder = manager.getStatementHolder(equalDesc); + assertNotNull(holder); + assertSame(serverNonce, holder.getId().getServerToken()); + assertSame(id, holder.getId()); + assertSame(targetStmt, holder.getStmt()); + assertSame(testClass, holder.getDataClass()); + assertSame(testDesc, holder.getStatementDescriptor()); + } + + private static class TestPojo implements Pojo { + // nothing + } +}
--- a/web/server/src/test/java/com/redhat/thermostat/web/server/WebStorageEndpointTest.java Thu Nov 27 04:08:51 2014 -0700 +++ b/web/server/src/test/java/com/redhat/thermostat/web/server/WebStorageEndpointTest.java Tue Nov 11 16:02:59 2014 +0100 @@ -132,12 +132,14 @@ import com.redhat.thermostat.test.FreePortFinder; import com.redhat.thermostat.test.FreePortFinder.TryPort; import com.redhat.thermostat.web.common.PreparedStatementResponseCode; +import com.redhat.thermostat.web.common.SharedStateId; import com.redhat.thermostat.web.common.WebPreparedStatement; import com.redhat.thermostat.web.common.WebPreparedStatementResponse; import com.redhat.thermostat.web.common.WebQueryResponse; import com.redhat.thermostat.web.common.typeadapters.PojoTypeAdapterFactory; import com.redhat.thermostat.web.common.typeadapters.PreparedParameterTypeAdapterFactory; import com.redhat.thermostat.web.common.typeadapters.PreparedParametersTypeAdapterFactory; +import com.redhat.thermostat.web.common.typeadapters.SharedStateIdTypeAdapterFactory; import com.redhat.thermostat.web.common.typeadapters.WebPreparedStatementResponseTypeAdapterFactory; import com.redhat.thermostat.web.common.typeadapters.WebPreparedStatementTypeAdapterFactory; import com.redhat.thermostat.web.common.typeadapters.WebQueryResponseTypeAdapterFactory; @@ -335,6 +337,7 @@ conn.setDoOutput(true); Gson gson = new GsonBuilder() .registerTypeAdapterFactory(new PojoTypeAdapterFactory()) + .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory()) .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory()) .registerTypeAdapterFactory(new WebQueryResponseTypeAdapterFactory()) .registerTypeAdapterFactory(new PreparedParameterTypeAdapterFactory()) @@ -348,7 +351,7 @@ Reader in = new InputStreamReader(conn.getInputStream()); WebPreparedStatementResponse response = gson.fromJson(in, WebPreparedStatementResponse.class); assertEquals("descriptor not trusted, so expected number should be negative!", -1, response.getNumFreeVariables()); - assertEquals(WebPreparedStatementResponse.ILLEGAL_STATEMENT, response.getStatementId()); + assertEquals(WebPreparedStatementResponse.ILLEGAL_STATEMENT, response.getStatementId().getId()); assertEquals("application/json; charset=UTF-8", conn.getContentType()); } @@ -371,8 +374,8 @@ TrustedPreparedQueryTestResult prepareQueryResult = prepareQuery(strDescriptor, moreBatches); Type typeToken = new TypeToken<WebQueryResponse<TestClass>>(){}.getType(); - // now execute the query we've just prepared - WebPreparedStatement<TestClass> stmt = new WebPreparedStatement<>(1, 0); + + WebPreparedStatement<TestClass> stmt = new WebPreparedStatement<>(1, prepareQueryResult.stmtId); stmt.setString(0, "fluff"); // Execute the query, preserver the cookie @@ -407,7 +410,7 @@ getMoreConn.setDoOutput(true); OutputStreamWriter out = new OutputStreamWriter(getMoreConn.getOutputStream()); - String body = "prepared-stmt-id=" + stmt.getStatementId() + "&"; + String body = "prepared-stmt-id=" + URLEncoder.encode(prepareQueryResult.gson.toJson(stmt.getStatementId()), "UTF-8") + "&"; body += "cursor-id=" + cursorId + "&"; body += "batch-size=" + batchSize; out.write(body); @@ -442,8 +445,8 @@ TrustedPreparedQueryTestResult prepareQueryResult = prepareQuery(strDescriptor, moreBatches); Type typeToken = new TypeToken<WebQueryResponse<TestClass>>(){}.getType(); - // now execute the query we've just prepared - WebPreparedStatement<TestClass> stmt = new WebPreparedStatement<>(1, 0); + + WebPreparedStatement<TestClass> stmt = new WebPreparedStatement<>(1, prepareQueryResult.stmtId); stmt.setString(0, "fluff"); // Execute the query, preserver the cookie @@ -478,7 +481,7 @@ getMoreConn.setDoOutput(true); OutputStreamWriter out = new OutputStreamWriter(getMoreConn.getOutputStream()); - String body = "prepared-stmt-id=" + stmt.getStatementId() + "&"; + String body = "prepared-stmt-id=" + URLEncoder.encode(prepareQueryResult.gson.toJson(stmt.getStatementId()), "UTF-8") + "&"; body += "cursor-id=" + cursorId + "&"; body += "batch-size=" + batchSize; out.write(body); @@ -533,11 +536,13 @@ private final Gson gson; private final Query<TestClass> mockMongoQuery; private final BatchCursor<TestClass> cursor; + private final SharedStateId stmtId; - private TrustedPreparedQueryTestResult(Gson gson, Query<TestClass> mockMongoQuery, BatchCursor<TestClass> cursor) { + private TrustedPreparedQueryTestResult(Gson gson, Query<TestClass> mockMongoQuery, BatchCursor<TestClass> cursor, SharedStateId stmtId) { this.cursor = cursor; this.gson = gson; this.mockMongoQuery = mockMongoQuery; + this.stmtId = stmtId; } } @@ -585,6 +590,7 @@ conn.setDoOutput(true); Gson gson = new GsonBuilder() .registerTypeAdapterFactory(new PojoTypeAdapterFactory()) + .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory()) .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory()) .registerTypeAdapterFactory(new WebQueryResponseTypeAdapterFactory()) .registerTypeAdapterFactory(new PreparedParameterTypeAdapterFactory()) @@ -599,10 +605,10 @@ Reader in = new InputStreamReader(conn.getInputStream()); WebPreparedStatementResponse response = gson.fromJson(in, WebPreparedStatementResponse.class); assertEquals(1, response.getNumFreeVariables()); - assertEquals(0, response.getStatementId()); + assertEquals(0, response.getStatementId().getId()); assertEquals("application/json; charset=UTF-8", conn.getContentType()); - return new TrustedPreparedQueryTestResult(gson, mockMongoQuery, cursor); + return new TrustedPreparedQueryTestResult(gson, mockMongoQuery, cursor, response.getStatementId()); } private String setupPreparedQueryWithTrustedDescriptor() throws Exception { @@ -715,6 +721,7 @@ conn.setDoOutput(true); Gson gson = new GsonBuilder() .registerTypeAdapterFactory(new PojoTypeAdapterFactory()) + .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory()) .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory()) .registerTypeAdapterFactory(new WebQueryResponseTypeAdapterFactory()) .registerTypeAdapterFactory(new PreparedParameterTypeAdapterFactory()) @@ -729,13 +736,13 @@ Reader in = new InputStreamReader(conn.getInputStream()); WebPreparedStatementResponse response = gson.fromJson(in, WebPreparedStatementResponse.class); assertEquals(1, response.getNumFreeVariables()); - assertEquals(0, response.getStatementId()); + assertEquals(0, response.getStatementId().getId()); assertEquals("application/json; charset=UTF-8", conn.getContentType()); // now execute the query we've just prepared - WebPreparedStatement<TestClass> stmt = new WebPreparedStatement<>(1, 0); + WebPreparedStatement<TestClass> stmt = new WebPreparedStatement<>(1, response.getStatementId()); stmt.setString(0, "fluff"); url = new URL(endpoint + "/query-execute"); @@ -837,6 +844,7 @@ conn.setDoOutput(true); Gson gson = new GsonBuilder() .registerTypeAdapterFactory(new PojoTypeAdapterFactory()) + .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory()) .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory()) .registerTypeAdapterFactory(new WebQueryResponseTypeAdapterFactory()) .registerTypeAdapterFactory(new PreparedParameterTypeAdapterFactory()) @@ -851,13 +859,13 @@ Reader in = new InputStreamReader(conn.getInputStream()); WebPreparedStatementResponse response = gson.fromJson(in, WebPreparedStatementResponse.class); assertEquals(0, response.getNumFreeVariables()); - assertEquals(0, response.getStatementId()); + assertEquals(0, response.getStatementId().getId()); assertEquals("application/json; charset=UTF-8", conn.getContentType()); // now execute the query we've just prepared - WebPreparedStatement<AggregateCount> stmt = new WebPreparedStatement<>(0, 0); + WebPreparedStatement<AggregateCount> stmt = new WebPreparedStatement<>(0, response.getStatementId()); url = new URL(endpoint + "/query-execute"); HttpURLConnection conn2 = (HttpURLConnection) url.openConnection(); @@ -968,6 +976,7 @@ conn.setDoOutput(true); Gson gson = new GsonBuilder() .registerTypeAdapterFactory(new PojoTypeAdapterFactory()) + .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory()) .registerTypeAdapterFactory(new PreparedParameterTypeAdapterFactory()) .registerTypeAdapterFactory(new WebPreparedStatementTypeAdapterFactory()) .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory()) @@ -981,13 +990,13 @@ Reader in = new InputStreamReader(conn.getInputStream()); WebPreparedStatementResponse response = gson.fromJson(in, WebPreparedStatementResponse.class); assertEquals(2, response.getNumFreeVariables()); - assertEquals(0, response.getStatementId()); + assertEquals(0, response.getStatementId().getId()); assertEquals("application/json; charset=UTF-8", conn.getContentType()); // now execute the ADD we've just prepared - WebPreparedStatement<TestClass> stmt = new WebPreparedStatement<>(2, 0); + WebPreparedStatement<TestClass> stmt = new WebPreparedStatement<>(2, response.getStatementId()); stmt.setString(0, "fluff"); stmt.setString(1, "test2");