Commit 78777df2 authored by Ricco Førgaard's avatar Ricco Førgaard

Version number included in ETag hash.

The REST API now includes the REST server's version number in the string
that gets hashed and used as ETag.

The version comes from the `<version>` tag in the pom.xml. It can also
be accessed via HTTP at `/version`.

The filename of the jar will also reflect the version number.

This means we should start doing more proper releases of the REST API
like we do with the Java API.

Fixes #1853
parent d2bf8930
......@@ -4,9 +4,19 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<name>Nesstar REST API</name>
<groupId>com.nesstar</groupId>
<artifactId>nesstar_rest_api</artifactId>
<version>0.1</version>
<version>0.1.0</version>
<organization>
<name>NSD</name>
<url>http://nsd.no</url>
</organization>
<scm>
<connection>scm:git:https://prosjekt.nsd.uib.no/gitlab/nesstar/nesstar-rest-api.git</connection>
</scm>
<repositories>
<repository>
......@@ -71,7 +81,20 @@
</properties>
<build>
<finalName>${project.artifactId}-${project.version}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<archive>
<manifest>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
......
......@@ -18,6 +18,7 @@ import com.nesstar.rest.resources.TabulateResource;
import com.nesstar.rest.resources.TranslationResource;
import com.nesstar.rest.resources.VariableGroupResource;
import com.nesstar.rest.resources.VariableResource;
import com.nesstar.rest.resources.VersionResource;
import com.yammer.dropwizard.Service;
import com.yammer.dropwizard.assets.AssetsBundle;
import com.yammer.dropwizard.config.Bootstrap;
......@@ -25,7 +26,7 @@ import com.yammer.dropwizard.config.Environment;
import com.yammer.dropwizard.config.FilterBuilder;
public class NesstarDropService extends Service<NesstarDropConfiguration> {
public static void main(String[] args) throws Exception {
new NesstarDropService().run(args);
}
......@@ -56,6 +57,7 @@ public class NesstarDropService extends Service<NesstarDropConfiguration> {
environment.addResource(new CubeResource(serverHandler));
environment.addResource(new QueryResource(serverHandler));
environment.addResource(new DownloadResource(serverHandler));
environment.addResource(new VersionResource());
environment.addHealthCheck(new NesstarHealthCheck(serverHandler));
......
package com.nesstar.rest;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Enumeration;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Version {
private static String versionNumber;
private static final Logger LOG = LoggerFactory.getLogger(Version.class);
private Version() {
LOG.debug("Initializing version");
versionNumber = readVersionNumber();
}
public static String version() {
return getInstance().getVersion();
}
private static Version getInstance() {
return VersionHolder.INSTANCE;
}
private String getVersion() {
return versionNumber;
}
private String readVersionNumber() {
LOG.debug("Reading version number");
String version;
try {
Manifest manifest = getManifest();
version = getVersionFromManifest(manifest);
} catch (IOException e) {
LOG.warn("Exception occurred while reading manifest: {}, {}", e.getMessage(), e.getCause());
version = "";
}
return version;
}
private Manifest getManifest() throws IOException {
LOG.debug("Getting manifest");
Enumeration manifestSources = Thread.currentThread().getContextClassLoader().getResources(JarFile.MANIFEST_NAME);
while (manifestSources.hasMoreElements()) {
URL url = (URL) manifestSources.nextElement();
InputStream stream = url.openStream();
if (stream != null) {
Manifest manifest = new Manifest(stream);
Attributes attributes = manifest.getMainAttributes();
String mainClassName = attributes.getValue("Main-Class");
if ("com.nesstar.rest.NesstarDropService".equals(mainClassName)) {
return manifest;
}
}
}
return null;
}
private String getVersionFromManifest(Manifest mf) throws IOException {
String version = null;
if (mf != null) {
Attributes attributes = mf.getMainAttributes();
version = attributes.getValue("Implementation-Version");
}
return version;
}
private static class VersionHolder {
private static final Version INSTANCE = new Version();
}
}
package com.nesstar.rest.common;
import java.io.IOException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Calendar;
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 ETag {
public static final String HEADER_NAME = "ETag";
......@@ -40,72 +33,14 @@ public class ETag {
}
return false;
}
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) {
String checksum = Generator.generateETagFromData(url, object);
public static ETag createETag(String url, NesstarObject object, ETagGenerator generator) {
String checksum = generator.generateETagFromData(url, object);
return new ETag(checksum);
}
private static class Generator {
private static String generateETagFromData(String url, NesstarObject object) {
String etag = null;
try {
String id = object.getId();
Date timestamp = getTimestampFromObject(object);
etag = generateChecksum(id, timestamp, url);
} catch (NotAuthorizedException e) {
// Ignore silently
} catch (IOException e) {
}
return etag;
}
private static Date getTimestampFromObject(NesstarObject object) throws NotAuthorizedException, IOException {
Date timestamp;
if (object instanceof NesstarList) {
timestamp = getLatestTimestampFromList((NesstarList<NesstarObject>) object);
} else {
timestamp = object.getTimeStamp();
}
return timestamp;
}
private static Date getLatestTimestampFromList(NesstarList<NesstarObject> list) throws NotAuthorizedException, IOException {
Date latest = createDefaultDate();
for (NesstarObject object : list) {
if (object.getTimeStamp().after(latest)) {
latest = object.getTimeStamp();
}
}
return latest;
}
private static Date createDefaultDate() {
Calendar calendar = Calendar.getInstance();
calendar.set(1970, 0, 0, 0, 0);
return calendar.getTime();
}
private static String generateChecksum(String id, Date timestamp, String url) {
String checksum;
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
String dateAsText = Integer.toString(timestamp.hashCode());
String input = id + dateAsText + url;
byte[] bytes = md5.digest(input.getBytes());
checksum = bytesToString(bytes);
} catch (NoSuchAlgorithmException e) {
checksum = "";
}
return checksum;
}
private static String bytesToString(byte[] bytes) {
return new BigInteger(1, bytes).toString(16);
}
}
}
package com.nesstar.rest.common;
import java.io.IOException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Calendar;
import java.util.Date;
import com.nesstar.api.NesstarList;
import com.nesstar.api.NesstarObject;
import com.nesstar.api.NotAuthorizedException;
public class ETagGenerator {
private final String salt;
private final static String ALGORITHM = "MD5";
public ETagGenerator(String salt) {
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;
}
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;
}
private Date getLatestTimestampFromList(NesstarList<NesstarObject> list) throws NotAuthorizedException, IOException {
Date latest = createDefaultDate();
for (NesstarObject object : list) {
if (object.getTimeStamp().after(latest)) {
latest = object.getTimeStamp();
}
}
return latest;
}
private Date createDefaultDate() {
Calendar calendar = Calendar.getInstance();
calendar.set(1970, 0, 0, 0, 0);
return calendar.getTime();
}
private String generateChecksum(String id, Date timestamp, String url, String salt) {
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) {
return new BigInteger(1, bytes).toString(16);
}
}
\ No newline at end of file
......@@ -18,16 +18,23 @@ 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.rest.common.ETag;
import com.nesstar.rest.common.ETagGenerator;
import com.nesstar.rest.common.ServerHandler;
public 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 {
......@@ -199,7 +206,7 @@ public class EntityTagFilter implements Filter {
private ETag getEtagFromResource(HttpServletRequest request, NesstarObject object) {
String url = request.getRequestURL().toString();
return ETag.createETag(url, object);
return ETag.createETag(url, object, generator);
}
private boolean etagsMatch(HttpServletRequest request, ETag etag) {
......
......@@ -19,7 +19,7 @@ public class AbstractResource {
protected void setEntityTagForResource(NesstarObject resource, final HttpServletRequest request, final HttpServletResponse response) {
String stupidExtraPaddingToCircumventBugInJetty = "X";
String url = request.getRequestURL().toString();
ETag etag = ETag.createETag(url, resource);
ETag etag = ETag.createEtag(url, resource);
response.setHeader(ETag.HEADER_NAME, etag.getValue() + stupidExtraPaddingToCircumventBugInJetty);
}
}
package com.nesstar.rest.resources;
import java.util.HashMap;
import java.util.Map;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
@Path("version")
@Produces(MediaType.APPLICATION_JSON)
public class VersionResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
public Object getVersion() {
Map<String, Object> result = new HashMap<String, Object>();
String version = com.nesstar.rest.Version.version();
result.put("version", version);
return result;
}
}
......@@ -18,40 +18,48 @@ 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 = "9e69ad13b2b2f36d0b62bba6c38f72a6";
ETag tag = ETag.createETag(url, study);
String expectedTagValue = "148c8ef4be39449dc56acbc2e0e0b455";
ETag tag = ETag.createETag(url, study, generator);
assertEquals("Tags don't match", expectedTagValue, tag.getValue());
assertTrue("Tags aren't equal", tag.equals(expectedTagValue));
}
@Test
public void testEmptyInput() {
ETag tag = ETag.createETag(url, study);
ETag tag = ETag.createETag(url, study, generator);
assertFalse(tag.equals(""));
}
@Test
public void testNullInput() {
ETag tag = ETag.createETag(url, study);
ETag tag = ETag.createETag(url, study, generator);
assertFalse(tag.equals(null));
}
@Test
public void compareETags() throws Exception {
ETag first = ETag.createETag(url, study);
ETag second = ETag.createETag(url, study);
ETag first = ETag.createETag(url, study, generator);
ETag second = ETag.createETag(url, study, generator);
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);
ETag variableEtag = ETag.createETag(url, variable, generator);
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"));
assertFalse("ETags should not be equal", original.equals(afterVersionBump));
}
@Before
public void setUp() throws Exception {
......@@ -67,5 +75,4 @@ public class ETagTest {
calendar.set(Calendar.MILLISECOND, 0);
return calendar.getTime();
}
}
......@@ -29,6 +29,7 @@ import com.nesstar.api.Study;
import com.nesstar.api.Variable;
import com.nesstar.api.VariableGroup;
import com.nesstar.rest.common.ETag;
import com.nesstar.rest.common.ETagGenerator;
import com.nesstar.rest.common.ServerHandler;
@RunWith(PowerMockRunner.class)
......@@ -49,6 +50,7 @@ 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);
......@@ -58,11 +60,12 @@ 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("c6cb92f5b6a2d0d740941938bc4b5b1");
when(request.getHeader("If-None-Match")).thenReturn("5aeec6563dd660572d43f62a54f50f72");
createStudyList();
EntityTagFilter filter = new EntityTagFilter(serverHandler);
filter.setETagGenerator(new ETagGenerator("1.2.3"));
filter.doFilter(request, response, chain);
verify(response).setStatus(304);
}
......@@ -71,9 +74,10 @@ 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("91ec3486a18d53dc3cac90f7b86b9fc3");
when(request.getHeader("If-None-Match")).thenReturn("512d16fbde1830714ed91d115701ac84");
EntityTagFilter filter = new EntityTagFilter(serverHandler);
filter.setETagGenerator(new ETagGenerator("1.2.3"));
filter.doFilter(request, response, chain);
verify(response).setStatus(304);
}
......@@ -82,9 +86,10 @@ 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("aa51af7d0dbd47ad20b8ccd270b5a6a9");
when(request.getHeader("If-None-Match")).thenReturn("167423607e417289f5e3952f5f823dd9");
EntityTagFilter filter = new EntityTagFilter(serverHandler);
filter.setETagGenerator(new ETagGenerator("1.2.3"));
filter.doFilter(request, response, chain);
verify(response).setStatus(304);
}
......
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