Commit 32212756 authored by Ricco Førgaard's avatar Ricco Førgaard

Added support for ETags for cubes.

Fixes #1851
parents b85f4f9b b6243c5b
......@@ -77,7 +77,7 @@
<!-- use UTF-8 for everything -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<powermock.version>1.5.1</powermock.version>
<powermock.version>1.4.12</powermock.version>
</properties>
<build>
......
package com.nesstar.rest.common;
import com.nesstar.api.NesstarObject;
import com.nesstar.rest.Version;
public final class ETag {
public static final String HEADER_NAME = "ETag";
private String hash;
......@@ -32,13 +28,8 @@ public final class ETag {
return hash.hashCode();
}
public static ETag createEtag(String url, NesstarObject object) {
ETagGenerator generator = new ETagGenerator(Version.version());
return createETag(url, object, generator);
}
public static ETag createETag(String url, NesstarObject object, ETagGenerator generator) {
String checksum = generator.generateETagFromData(url, object);
public static ETag createETag(ETagGenerator generator) {
String checksum = generator.generateETagFromData();
return new ETag(checksum);
}
}
package com.nesstar.rest.common;
import java.io.IOException;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.nesstar.api.NesstarList;
import com.nesstar.api.NesstarObject;
import com.nesstar.api.NotAuthorizedException;
import com.nesstar.rest.request_objects.CubeRequest;
public class ETagData {
private final List<String> data;
protected static final Date zeroDate = new Date(0l);
private final static Logger LOG = LoggerFactory.getLogger(ETagData.class);
public ETagData() {
data = new LinkedList<String>();
}
public ETagData add(Object object) {
String objectAsString = convertObjectToString(object);
data.add(objectAsString);
return this;
}
public ETagData addNesstarObject(NesstarObject object) {
data.add(object.getId());
data.add(getTimestamp(object));
return this;
}
public ETagData addCubeRequest(CubeRequest request) {
addDimensionsFromCubeRequest(request);
addMeasuresFromCubeRequest(request);
return this;
}
public String getString() {
StringBuilder sb = new StringBuilder();
for (String str : data) {
sb.append(str);
}
return sb.toString();
}
private String convertObjectToString(Object object) {
String result = null;
if (object instanceof String) {
result = (String) object;
} else if (object instanceof Date) {
Date date = (Date) object;
result = Integer.toString(date.hashCode());
} else if (object instanceof NesstarObject) {
addNesstarObject((NesstarObject) object);
} else {
result = object.toString();
}
return result;
}
private String getTimestamp(NesstarObject object) {
String timestamp = null;
try {
Date date = getTimestampFromObject(object);
timestamp = Integer.toString(date.hashCode());
} catch (NotAuthorizedException e) {
LOG.error("Not authorized");
} catch (IOException e) {
LOG.warn("Unable to read timestamp: {}", e.getMessage());
}
return timestamp;
}
protected Date getTimestampFromObject(NesstarObject object) throws NotAuthorizedException, IOException {
Date timestamp;
if (object instanceof NesstarList) {
timestamp = getLatestTimestampFromList((NesstarList<NesstarObject>) object);
} else {
timestamp = object.getTimeStamp();
}
return timestamp;
}
protected Date getLatestTimestampFromList(NesstarList<NesstarObject> list) throws NotAuthorizedException, IOException {
Date latest = zeroDate;
for (NesstarObject object : list) {
if (object.getTimeStamp().after(latest)) {
latest = object.getTimeStamp();
}
}
return latest;
}
private void addDimensionsFromCubeRequest(CubeRequest request) {
String[] dimensions = getDimensionIDs(request);
for (String dimension : dimensions) {
data.add(dimension);
}
}
private void addMeasuresFromCubeRequest(CubeRequest request) {
for (String measure : request.getMeasures()) {
data.add(measure);
}
}
private String[] getDimensionIDs(CubeRequest request) {
String[] dimensionIDs = new String[request.getDimensions().size()];
for (int i = 0; i < dimensionIDs.length; i++) {
Map<String, Object> map = request.getDimensions().get(i);
dimensionIDs[i] = (String) map.get("id");
}
return dimensionIDs;
}
}
package com.nesstar.rest.common;
import java.io.IOException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import com.nesstar.api.NesstarList;
import com.nesstar.api.NesstarObject;
import com.nesstar.api.NotAuthorizedException;
import com.nesstar.rest.Version;
public class ETagGenerator {
private final String salt;
private final static String ALGORITHM = "MD5";
private static final Date zeroDate = new Date(0l);
protected final String salt;
protected final static String ALGORITHM = "MD5";
protected final ETagData data;
public ETagGenerator(String salt) {
public ETagGenerator(ETagData data, String salt) {
this.data = data;
this.salt = salt;
}
public String generateETagFromData(final String url, final NesstarObject object) {
String etag = null;
try {
String id = object.getId();
Date timestamp = getTimestampFromObject(object);
etag = generateChecksum(id, timestamp, url, salt);
} catch (NotAuthorizedException e) {
// Ignore silently
} catch (IOException e) {
}
return etag;
public ETagGenerator(ETagData data) {
this.data = data;
this.salt = Version.version();
}
private Date getTimestampFromObject(NesstarObject object) throws NotAuthorizedException, IOException {
Date timestamp;
if (object instanceof NesstarList) {
timestamp = getLatestTimestampFromList((NesstarList<NesstarObject>) object);
} else {
timestamp = object.getTimeStamp();
}
return timestamp;
public String generateETagFromData() {
String input = data.getString();
String etag = generateChecksum(input);
return etag;
}
private Date getLatestTimestampFromList(NesstarList<NesstarObject> list) throws NotAuthorizedException, IOException {
Date latest = zeroDate;
for (NesstarObject object : list) {
if (object.getTimeStamp().after(latest)) {
latest = object.getTimeStamp();
}
}
return latest;
protected String generateChecksum(final String input) {
String checksum = digest(input + salt);
return checksum;
}
private String generateChecksum(String id, Date timestamp, String url, String salt) {
protected String digest(String input) {
String checksum;
try {
MessageDigest md5 = MessageDigest.getInstance(ALGORITHM);
String dateAsText = Integer.toString(timestamp.hashCode());
String input = id + dateAsText + url + salt;
byte[] bytes = md5.digest(input.getBytes());
checksum = bytesToString(bytes);
} catch (NoSuchAlgorithmException e) {
checksum = "";
}
return checksum;
}
private String bytesToString(byte[] bytes) {
protected String bytesToString(byte[] bytes) {
return new BigInteger(1, bytes).toString(16);
}
}
\ No newline at end of file
......@@ -18,23 +18,20 @@ import com.nesstar.api.Server;
import com.nesstar.api.Study;
import com.nesstar.api.Variable;
import com.nesstar.api.VariableGroup;
import com.nesstar.rest.Version;
import com.nesstar.api.cube.Cube;
import com.nesstar.rest.common.ETag;
import com.nesstar.rest.common.ETagData;
import com.nesstar.rest.common.ETagGenerator;
import com.nesstar.rest.common.ServerHandler;
import com.nesstar.rest.request_objects.CubeRequest;
public final class EntityTagFilter implements Filter {
private static final String HEADER_NAME = "If-None-Match";
private ServerHandler serverHandler;
private ETagGenerator generator = new ETagGenerator(Version.version());
public EntityTagFilter(ServerHandler serverHandler) {
this.serverHandler = serverHandler;
}
public void setETagGenerator(ETagGenerator generator) {
this.generator = generator;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
......@@ -122,7 +119,7 @@ public final class EntityTagFilter implements Filter {
}
return null;
}
//child object
private NesstarObject getChildObjects(String parentType, String parentId, String childType) {
NesstarObject list = null;
try {
......@@ -170,8 +167,12 @@ public final class EntityTagFilter implements Filter {
throw new IOException();
}
Server server = getServer();
NesstarObject list = null;
Bank bank = getBankFromRequest(type, server);
return bank.getAll();
if (bank != null) {
list = bank.getAll();
}
return list;
}
private String translateListTypeToObjectType(String listType) {
......@@ -188,14 +189,20 @@ public final class EntityTagFilter implements Filter {
}
private Bank getBankFromRequest(String type, Server server) {
Class klass = null;
Bank bank = null;
if ("study".equalsIgnoreCase(type)) {
bank = server.getBank(Study.class);
klass = Study.class;
} else if ("variable".equalsIgnoreCase(type)) {
bank = server.getBank(Variable.class);
klass = Variable.class;
} else if ("variable-group".equalsIgnoreCase(type)) {
bank = server.getBank(VariableGroup.class);
klass = VariableGroup.class;
} else if ("cube".equalsIgnoreCase(type)) {
klass = Cube.class;
}
if (klass != null) {
bank = server.getBank(klass);
}
return bank;
}
......@@ -206,7 +213,30 @@ public final class EntityTagFilter implements Filter {
private ETag getEtagFromResource(HttpServletRequest request, NesstarObject object) {
String url = request.getRequestURL().toString();
return ETag.createETag(url, object, generator);
ETagData data = new ETagData();
data.add(url).add(object);
if (object instanceof Cube) {
addDataFromCubeRequest(data, request);
}
return ETag.createETag(new ETagGenerator(data));
}
private void addDataFromCubeRequest(ETagData data, HttpServletRequest request) {
CubeRequest cubeRequest = getCubeRequest(request);
if (cubeRequest != null) {
data.addCubeRequest(cubeRequest);
}
}
private CubeRequest getCubeRequest(HttpServletRequest request) {
String cubeRequestQuery = request.getParameter("q");
CubeRequest cubeRequest = null;
if (cubeRequestQuery != null) {
cubeRequest = CubeRequest.valueOf(cubeRequestQuery);
}
return cubeRequest;
}
private boolean etagsMatch(HttpServletRequest request, ETag etag) {
......
......@@ -5,6 +5,8 @@ import javax.servlet.http.HttpServletResponse;
import com.nesstar.api.NesstarObject;
import com.nesstar.rest.common.ETag;
import com.nesstar.rest.common.ETagData;
import com.nesstar.rest.common.ETagGenerator;
import com.nesstar.rest.common.ServerHandler;
class AbstractResource {
......@@ -14,12 +16,18 @@ class AbstractResource {
this.serverHandler = serverHandler;
}
protected void setEntityTagForResource(NesstarObject resource, final HttpServletRequest request, final HttpServletResponse response) {
String url = request.getRequestURL().toString();
ETagData data = new ETagData();
data.add(url).add(resource);
ETag etag = ETag.createETag(new ETagGenerator(data));
setETagHeader(etag, response);
}
// TODO: Due to a bug in Jetty (I think) the last character of the header value gets eaten.
// Here an extra character is added to circumvent that.
protected void setEntityTagForResource(NesstarObject resource, final HttpServletRequest request, final HttpServletResponse response) {
protected void setETagHeader(ETag etag, HttpServletResponse response) {
String stupidExtraPaddingToCircumventBugInJetty = "X";
String url = request.getRequestURL().toString();
ETag etag = ETag.createEtag(url, resource);
response.setHeader(ETag.HEADER_NAME, etag.getValue() + stupidExtraPaddingToCircumventBugInJetty);
}
}
......@@ -10,7 +10,12 @@ import java.util.Map;
import java.util.Stack;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.*;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
......@@ -25,6 +30,9 @@ import com.nesstar.api.cube.Member;
import com.nesstar.api.cube.Slice;
import com.nesstar.api.cube.SliceBuilder;
import com.nesstar.api.cube.SliceCoordinates;
import com.nesstar.rest.common.ETag;
import com.nesstar.rest.common.ETagData;
import com.nesstar.rest.common.ETagGenerator;
import com.nesstar.rest.common.Operation;
import com.nesstar.rest.common.OperationRunner;
import com.nesstar.rest.common.ResourceExtractor;
......@@ -58,7 +66,10 @@ public final class CubeResource extends AbstractResource{
@GET
@Path("/query")
@Timed
public Object cube(@QueryParam("q") final CubeRequest cubeRequest, @PathParam("cubeId") final String cubeId, @Context final HttpServletRequest request) {
public Object cube(@QueryParam("q") final CubeRequest cubeRequest,
@PathParam("cubeId") final String cubeId,
@Context final HttpServletRequest request,
@Context final HttpServletResponse response) {
final ClassVariables classVariables = new ClassVariables();
classVariables.values = new ArrayList<Map<String, Object>>();
classVariables.dimensionMemberList = new LinkedHashMap<Dimension, List<Member>>();
......@@ -79,6 +90,7 @@ public final class CubeResource extends AbstractResource{
classVariables.dimensions = classVariables.cube.getDimensions();
classVariables.measures = classVariables.cube.getMeasures();
performCube(cubeRequest, classVariables);
setEntityTagForResource(classVariables.cube, cubeRequest, request, response);
return getCubeObject(classVariables);
}
}, serverHandler);
......@@ -86,7 +98,7 @@ public final class CubeResource extends AbstractResource{
@GET
@Timed
public Object getCubeMetadata(@PathParam("cubeId") final String cubeId, @Context final HttpServletRequest request) {
public Object getCubeMetadata(@PathParam("cubeId") final String cubeId, @Context final HttpServletRequest request, @Context final HttpServletResponse response) {
final ClassVariables classVariables = new ClassVariables();
classVariables.values = null;
classVariables.dimensionMemberList = new LinkedHashMap<Dimension, List<Member>>();
......@@ -98,10 +110,19 @@ public final class CubeResource extends AbstractResource{
classVariables.cube = ResourceExtractor.getCube(cubeId, classVariables.server);
classVariables.slicedimensions = classVariables.cube.getDimensions();
classVariables.slicemeasures = classVariables.cube.getMeasures();
setEntityTagForResource(classVariables.cube, request, response);
return getCubeObject(classVariables);
}
}, serverHandler);
}
private void setEntityTagForResource(final Cube cube, final CubeRequest cubeRequest, final HttpServletRequest request, final HttpServletResponse response) {
ETagData data = new ETagData();
String url = request.getRequestURL().toString();
data.add(url).add(cube).add(cubeRequest);
ETag etag = ETag.createETag(new ETagGenerator(data));
setETagHeader(etag, response);
}
private boolean isInvalidRequest(CubeRequest cubeRequest) {
return cubeRequest.getDimensions() == null || cubeRequest.getDimensions().isEmpty();
......
......@@ -18,45 +18,57 @@ import com.nesstar.api.Variable;
public class ETagTest {
private Study study;
private String url = "http://www.schnapps.org";
private ETagGenerator generator = new ETagGenerator("0.1.2");
@Test
public void testGeneratingEtags() throws Exception {
String expectedTagValue = "148c8ef4be39449dc56acbc2e0e0b455";
ETag tag = ETag.createETag(url, study, generator);
String expectedTagValue = "8f204cb5ec866140852ea2af7f1647f5";
ETagData data = new ETagData();
data.add(url).add(study);
ETag tag = ETag.createETag(new ETagGenerator(data, "0.1.2"));
assertEquals("Tags don't match", expectedTagValue, tag.getValue());
}
@Test
public void testEmptyInput() {
ETag tag = ETag.createETag(url, study, generator);
ETagData data = new ETagData();
data.add(url).add(study);
ETag tag = ETag.createETag(new ETagGenerator(data, "0.1.2"));
assertFalse(tag.getValue().isEmpty());
}
@Test
public void testNullInput() {
ETag tag = ETag.createETag(url, study, generator);
ETagData data = new ETagData();
data.add(url).add(study);
ETag tag = ETag.createETag(new ETagGenerator(data, "0.1.2"));
assertFalse(tag.equals(null));
}
@Test
public void compareETags() throws Exception {
ETag first = ETag.createETag(url, study, generator);
ETag second = ETag.createETag(url, study, generator);
ETagData data = new ETagData();
data.add(url).add(study);
ETag first = ETag.createETag(new ETagGenerator(data, "0.1.2"));
ETag second = ETag.createETag(new ETagGenerator(data, "0.1.2"));
assertTrue(first.equals(second));
Variable variable = mock(Variable.class);
when(variable.getId()).thenReturn("variable1234");
when(variable.getTimeStamp()).thenReturn(createDate());
ETag variableEtag = ETag.createETag(url, variable, generator);
ETagData variableData = new ETagData();
variableData.add(url).add(variable);
ETag variableEtag = ETag.createETag(new ETagGenerator(data, "0.1.3"));
assertFalse(variableEtag.equals(first));
}
@Test
public void testETagInvalidatesWhenVersionIsUpdated() throws Exception {
ETag original = ETag.createETag(url, study, generator);
ETag afterVersionBump = ETag.createETag(url, study, new ETagGenerator("0.1.3"));
ETagData data = new ETagData();
data.add(url).add(study);
ETag original = ETag.createETag(new ETagGenerator(data, "0.1.2"));
ETag afterVersionBump = ETag.createETag(new ETagGenerator(data, "0.1.3"));
assertFalse("ETags should not be equal", original.equals(afterVersionBump));
}
......
......@@ -5,6 +5,7 @@ import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import java.util.ArrayList;
import java.util.Calendar;
......@@ -28,12 +29,13 @@ import com.nesstar.api.Server;
import com.nesstar.api.Study;
import com.nesstar.api.Variable;
import com.nesstar.api.VariableGroup;
import com.nesstar.api.cube.Cube;
import com.nesstar.rest.Version;
import com.nesstar.rest.common.ETag;
import com.nesstar.rest.common.ETagGenerator;
import com.nesstar.rest.common.ServerHandler;
@RunWith(PowerMockRunner.class)
@PrepareForTest({ServerHandler.class, ETag.class})
@PrepareForTest({ServerHandler.class, ETag.class, Version.class})
public class EntityTagFilterTest {
ServerHandler serverHandler;
HttpServletRequest request;
......@@ -50,7 +52,6 @@ public class EntityTagFilterTest {
createStudyList();
EntityTagFilter filter = new EntityTagFilter(serverHandler);
filter.setETagGenerator(new ETagGenerator("1.2.3"));
filter.doFilter(request, response, chain);
verify(bank).getAll();
verify(chain).doFilter(request, response);
......@@ -60,12 +61,11 @@ public class EntityTagFilterTest {
public void testFilteringWhereTagsMatch() throws Exception {
when(request.getRequestURL()).thenReturn(new StringBuffer("http://example.com/studies?foo=bar"));
when(request.getRequestURI()).thenReturn("/studies");
when(request.getHeader("If-None-Match")).thenReturn("5aeec6563dd660572d43f62a54f50f72");
when(request.getHeader("If-None-Match")).thenReturn("1ef1fc5c0ee256f7a20ef42f3c2c628f");
createStudyList();
EntityTagFilter filter = new EntityTagFilter(serverHandler);
filter.setETagGenerator(new ETagGenerator("1.2.3"));
filter.doFilter(request, response, chain);
verify(response).setStatus(304);
}
......@@ -74,10 +74,9 @@ public class EntityTagFilterTest {
public void testFilteringOnSingleElement() throws Exception {
when(request.getRequestURL()).thenReturn(new StringBuffer("http://example.com/variable/variable1234"));
when(request.getRequestURI()).thenReturn("/variable/variable1234");
when(request.getHeader("If-None-Match")).thenReturn("512d16fbde1830714ed91d115701ac84");
when(request.getHeader("If-None-Match")).thenReturn("d5e70bbf1ae70aba2272c7c0d607dffa");
EntityTagFilter filter = new EntityTagFilter(serverHandler);
filter.setETagGenerator(new ETagGenerator("1.2.3"));
filter.doFilter(request, response, chain);
verify(response).setStatus(304);
}
......@@ -86,13 +85,39 @@ public class EntityTagFilterTest {
public void testFilteringOnChildElements() throws Exception {
when(request.getRequestURL()).thenReturn(new StringBuffer("http://example.com/study/study1234/variables"));
when(request.getRequestURI()).thenReturn("/study/study1234/variables");
when(request.getHeader("If-None-Match")).thenReturn("167423607e417289f5e3952f5f823dd9");
when(request.getHeader("If-None-Match")).thenReturn("2658e53e86ddc592f8db1b9e2b15640");
EntityTagFilter filter = new EntityTagFilter(serverHandler);
filter.setETagGenerator(new ETagGenerator("1.2.3"));
filter.doFilter(request, response, chain);
verify(response).setStatus(304);
}
@Test
public void testFilteringOnCube() throws Exception {
when(request.getRequestURL()).thenReturn(new StringBuffer("http://example.com/cube/cube1234"));
when(request.getRequestURI()).thenReturn("/cube/cube1234");
when(request.getHeader("If-None-Match")).thenReturn("7fc6e13a37bb838767eead28c1581075");
EntityTagFilter filter = new EntityTagFilter(serverHandler);
filter.doFilter(request, response, chain);
verify(bank).get("cube1234");
verify(response).setStatus(304);
}
@Test
public void testFilteringOnCubeWithRequest() throws Exception {
when(request.getRequestURL()).thenReturn(new StringBuffer("http://example.com/cube/cube1234?q={\"dimensions\":[{\"id\":\"dimensionID1\",\"members\":[\"memberID1\",\"memberID2\"]}],\"measures\":[\"measureID1\",\"measureID2\"]}"));
when(request.getRequestURI()).thenReturn("/cube/cube1234");
when(request.getParameter("q")).thenReturn("{\"dimensions\":[{\"id\":\"dimensionID1\",\"members\":[\"memberID1\",\"memberID2\"]}],\"measures\":[\"measureID1\",\"measureID2\"]}");
when(request.getHeader("If-None-Match")).thenReturn("810397a21017716ed794183f92c6b8c2");
EntityTagFilter filter = new EntityTagFilter(serverHandler);
filter.doFilter(request, response, chain);
verify(bank).get("cube1234");
verify(response).setStatus(304);
}
@Before
public void setUp() throws Exception {
......@@ -107,6 +132,10 @@ public class EntityTagFilterTest {
Study study = mock(Study.class);
Variable variable = mock(Variable.class);
VariableGroup group = mock(VariableGroup.class);
Cube cube = mock(Cube.class);
mockStatic(Version.class);
when(Version.version()).thenReturn("0.1.2");
when(serverHandler.getServer()).thenReturn(server);
......@@ -119,6 +148,7 @@ public class EntityTagFilterTest {
when(bank.get("study1234")).thenReturn(study);
when(bank.get("variable1234")).thenReturn(variable);
when(bank.get("variablegroup1234")).thenReturn(group);
when(bank.get("cube1234")).thenReturn(cube);
when(study.getId()).thenReturn("study1234");
when(study.getTimeStamp()).thenReturn(createDate());
......@@ -130,6 +160,9 @@ public class EntityTagFilterTest {
when(group.getId()).thenReturn("variablegroup1234");
when(group.getTimeStamp()).thenReturn(createDate());
when(cube.getId()).thenReturn("cube1234");
when(cube.getTimeStamp()).thenReturn(createDate());
}
private void createStudyList() throws Exception {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment