diff --git a/pom.xml b/pom.xml
index 612c5174..60c681cb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -198,5 +198,15 @@
1.10.1
test
+
+ org.apache.httpcomponents
+ fluent-hc
+ 4.5.3
+
+
+ org.apache.httpcomponents
+ httpmime
+ 4.5.3
+
diff --git a/src/main/java/ninja/eivind/hotsreplayuploader/providers/hotsapi/HotsApiProvider.java b/src/main/java/ninja/eivind/hotsreplayuploader/providers/hotsapi/HotsApiProvider.java
new file mode 100644
index 00000000..46435ad9
--- /dev/null
+++ b/src/main/java/ninja/eivind/hotsreplayuploader/providers/hotsapi/HotsApiProvider.java
@@ -0,0 +1,166 @@
+// Copyright 2015-2016 Eivind Vegsundvåg
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package ninja.eivind.hotsreplayuploader.providers.hotsapi;
+
+import ninja.eivind.hotsreplayuploader.models.ReplayFile;
+import ninja.eivind.hotsreplayuploader.models.Status;
+import ninja.eivind.hotsreplayuploader.providers.Provider;
+import ninja.eivind.stormparser.models.Replay;
+import ninja.eivind.hotsreplayuploader.utils.ReplayUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import org.apache.http.entity.mime.MultipartEntityBuilder;
+import org.apache.http.entity.ContentType;
+import org.apache.http.HttpEntity;
+import org.apache.http.client.fluent.Request;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.io.File;
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.util.UUID;
+
+/**
+ * JSON response from hotsapi.net call to check if a replay exists
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+class DuplicateResponse {
+ @JsonProperty()
+ private boolean exists;
+
+ public boolean isDuplicate() {
+ return exists;
+ }
+}
+
+/**
+ * JSON response from hotsapi.net upload call
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+class UploadResponse {
+ @JsonProperty()
+ private boolean success;
+
+ @JsonProperty()
+ private String status;
+
+ public boolean isSuccess() {
+ return success;
+ }
+
+ /**
+ * Upload result (Success, Duplicate, AiDetected, CustomGame, PtrRegion, TooOld, Incomplete)
+ */
+ public String getStatus() {
+ return status;
+ }
+}
+
+
+/**
+ * Implements a {@link Provider} to upload replays to hotsapi.net.
+ */
+@Component
+public class HotsApiProvider extends Provider {
+
+ private static final Logger LOG = LoggerFactory.getLogger(HotsApiProvider.class);
+
+ private static final String UPLOAD_URL = "http://hotsapi.net/api/v1/replays/";
+ private static final String DUPLICATE_URL = "http://hotsapi.net/api/v1/replays/fingerprints/v3/";
+
+ public HotsApiProvider() {
+ super("HotsApi");
+ }
+
+ @Override
+ public Status upload(final ReplayFile replayFile) {
+ final File file = replayFile.getFile();
+ if (!(file.exists() && file.canRead())) {
+ return Status.EXCEPTION;
+ }
+
+ final String fileName = UUID.randomUUID() + ".StormReplay";
+ LOG.info("Assigning remote file name " + fileName + " to " + replayFile);
+
+ return uploadFile(file, fileName);
+ }
+
+ @Override
+ public Status getPreStatus(final Replay replay) {
+
+ // Temporary fix for computer players found until the parser supports this
+ if (ReplayUtils.replayHasComputerPlayers(replay)) {
+ LOG.info("Computer players found for replay, tagging as uploaded.");
+ return Status.UNSUPPORTED_GAME_MODE;
+ }
+ try {
+ final String matchId = ReplayUtils.getMatchId(replay);
+ LOG.info("Calculated matchId to be" + matchId);
+ final String uri = DUPLICATE_URL + matchId;
+ final String response = Request.Get(uri)
+ .execute()
+ .returnContent()
+ .asString();
+ final DuplicateResponse result = new ObjectMapper().readValue(response, DuplicateResponse.class);
+ if (result.isDuplicate()) {
+ return Status.UPLOADED;
+ }
+ } catch (NoSuchAlgorithmException e) {
+ LOG.warn("Platform does not support MD5; cannot proceed with parsing", e);
+ } catch (IOException e) {
+ return Status.EXCEPTION;
+ }
+ return Status.NEW;
+ }
+
+ private Status uploadFile(File file, String fileName) {
+ try {
+ final HttpEntity entity = MultipartEntityBuilder.create()
+ .addBinaryBody("file", file, ContentType.APPLICATION_OCTET_STREAM, fileName)
+ .build();
+ final String response = Request.Post(UPLOAD_URL)
+ .body(entity)
+ .execute()
+ .returnContent()
+ .asString();
+ final UploadResponse result = new ObjectMapper().readValue(response, UploadResponse.class);
+ LOG.info("File " + fileName + " uploaded to remote storage.");
+ switch (result.getStatus()) {
+ case "Duplicate":
+ case "Success":
+ LOG.info("File registered with hotsapi.net");
+ return Status.UPLOADED;
+ case "AiDetected":
+ case "CustomGame":
+ case "PtrRegion":
+ case "TooOld":
+ case "Incomplete":
+ LOG.warn("File not supported by hotsapi.net");
+ return Status.UNSUPPORTED_GAME_MODE;
+ default:
+ LOG.error("Could not upload file. Unknown status \"" + result + "\" received.");
+ return Status.EXCEPTION;
+ }
+
+ } catch (Exception e) {
+ LOG.error("Could not upload file.", e);
+ return Status.EXCEPTION;
+ }
+ }
+
+}
diff --git a/src/main/java/ninja/eivind/hotsreplayuploader/providers/hotslogs/HotsLogsProvider.java b/src/main/java/ninja/eivind/hotsreplayuploader/providers/hotslogs/HotsLogsProvider.java
index b394b01b..310616e0 100644
--- a/src/main/java/ninja/eivind/hotsreplayuploader/providers/hotslogs/HotsLogsProvider.java
+++ b/src/main/java/ninja/eivind/hotsreplayuploader/providers/hotslogs/HotsLogsProvider.java
@@ -19,8 +19,7 @@
import ninja.eivind.hotsreplayuploader.models.Status;
import ninja.eivind.hotsreplayuploader.providers.Provider;
import ninja.eivind.hotsreplayuploader.utils.SimpleHttpClient;
-import ninja.eivind.stormparser.models.Player;
-import ninja.eivind.stormparser.models.PlayerType;
+import ninja.eivind.hotsreplayuploader.utils.ReplayUtils;
import ninja.eivind.stormparser.models.Replay;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -30,11 +29,8 @@
import java.io.File;
import java.io.IOException;
-import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
-import java.util.List;
import java.util.UUID;
-import java.util.stream.Collectors;
/**
* Implements a {@link Provider} to upload replays to hotslogs.com.
@@ -65,48 +61,6 @@ public static boolean isMaintenance() {
return maintenance + 600000L > System.currentTimeMillis();
}
- private static UUID getUUIDForString(String concatenatedString) throws NoSuchAlgorithmException {
- final byte[] hashed = MessageDigest.getInstance("MD5").digest(concatenatedString.getBytes());
- final byte[] reArranged = reArrangeForUUID(hashed);
- return getUUID(reArranged);
- }
-
- private static byte[] reArrangeForUUID(byte[] hashed) {
- return new byte[]{
- hashed[3],
- hashed[2],
- hashed[1],
- hashed[0],
-
- hashed[5],
- hashed[4],
- hashed[7],
- hashed[6],
- hashed[8],
- hashed[9],
- hashed[10],
- hashed[11],
- hashed[12],
- hashed[13],
- hashed[14],
- hashed[15],
- };
- }
-
- private static UUID getUUID(byte[] bytes) {
- long msb = 0;
- long lsb = 0;
- assert bytes.length == 16 : "data must be 16 bytes in length";
- for (int i = 0; i < 8; i++) {
- msb = (msb << 8) | (bytes[i] & 0xff);
- }
- for (int i = 8; i < 16; i++) {
- lsb = (lsb << 8) | (bytes[i] & 0xff);
- }
-
- return new UUID(msb, lsb);
- }
-
@Override
public Status upload(final ReplayFile replayFile) {
if (isMaintenance()) {
@@ -130,12 +84,12 @@ public Status upload(final ReplayFile replayFile) {
public Status getPreStatus(final Replay replay) {
// Temporary fix for computer players found until the parser supports this
- if (replayHasComputerPlayers(replay)) {
+ if (ReplayUtils.replayHasComputerPlayers(replay)) {
LOG.info("Computer players found for replay, tagging as uploaded.");
return Status.UNSUPPORTED_GAME_MODE;
}
try {
- final String matchId = getMatchId(replay);
+ final String matchId = ReplayUtils.getMatchId(replay);
LOG.info("Calculated matchId to be" + matchId);
final String uri = BASE_URL + "&ReplayHash=" + matchId;
final String result = getHttpClient().simpleRequest(uri).toLowerCase();
@@ -180,37 +134,6 @@ private Status uploadFileToHotSLogs(File file, String fileName, String uri) {
}
}
- private boolean replayHasComputerPlayers(Replay replay) {
- return replay.getReplayDetails()
- .getPlayers()
- .stream()
- .map(Player::getPlayerType)
- .anyMatch(playerType -> playerType == PlayerType.COMPUTER);
- }
-
- protected String getMatchId(Replay replay) throws NoSuchAlgorithmException {
- final String concatenatedString = getConcatenatedString(replay);
-
- return getUUIDForString(concatenatedString).toString();
-
- }
-
- private String getConcatenatedString(Replay replay) {
- final String randomValue = String.valueOf(replay.getInitData().getRandomValue());
- final List battleNetIdsSorted = replay.getReplayDetails()
- .getPlayers()
- .stream()
- .map(Player::getBNetId)
- .map(Long::parseLong)
- .sorted()
- .map(String::valueOf)
- .collect(Collectors.toList());
- final StringBuilder builder = new StringBuilder();
- battleNetIdsSorted.forEach(builder::append);
- builder.append(randomValue);
- return builder.toString();
- }
-
public SimpleHttpClient getHttpClient() {
return httpClient;
}
diff --git a/src/main/java/ninja/eivind/hotsreplayuploader/utils/ReplayUtils.java b/src/main/java/ninja/eivind/hotsreplayuploader/utils/ReplayUtils.java
new file mode 100644
index 00000000..a32322fb
--- /dev/null
+++ b/src/main/java/ninja/eivind/hotsreplayuploader/utils/ReplayUtils.java
@@ -0,0 +1,100 @@
+// Copyright 2015-2016 Eivind Vegsundvåg
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package ninja.eivind.hotsreplayuploader.utils;
+
+import ninja.eivind.stormparser.models.Replay;
+import ninja.eivind.stormparser.models.Player;
+import ninja.eivind.stormparser.models.PlayerType;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.MessageDigest;
+import java.util.UUID;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class ReplayUtils {
+ private static UUID getUUIDForString(String concatenatedString) throws NoSuchAlgorithmException {
+ final byte[] hashed = MessageDigest.getInstance("MD5").digest(concatenatedString.getBytes());
+ final byte[] reArranged = reArrangeForUUID(hashed);
+ return getUUID(reArranged);
+ }
+
+ private static byte[] reArrangeForUUID(byte[] hashed) {
+ return new byte[]{
+ hashed[3],
+ hashed[2],
+ hashed[1],
+ hashed[0],
+
+ hashed[5],
+ hashed[4],
+ hashed[7],
+ hashed[6],
+ hashed[8],
+ hashed[9],
+ hashed[10],
+ hashed[11],
+ hashed[12],
+ hashed[13],
+ hashed[14],
+ hashed[15],
+ };
+ }
+
+ private static UUID getUUID(byte[] bytes) {
+ long msb = 0;
+ long lsb = 0;
+ assert bytes.length == 16 : "data must be 16 bytes in length";
+ for (int i = 0; i < 8; i++) {
+ msb = (msb << 8) | (bytes[i] & 0xff);
+ }
+ for (int i = 8; i < 16; i++) {
+ lsb = (lsb << 8) | (bytes[i] & 0xff);
+ }
+
+ return new UUID(msb, lsb);
+ }
+
+ private static String getConcatenatedString(Replay replay) {
+ final String randomValue = String.valueOf(replay.getInitData().getRandomValue());
+ final List battleNetIdsSorted = replay.getReplayDetails()
+ .getPlayers()
+ .stream()
+ .map(Player::getBNetId)
+ .map(Long::parseLong)
+ .sorted()
+ .map(String::valueOf)
+ .collect(Collectors.toList());
+ final StringBuilder builder = new StringBuilder();
+ battleNetIdsSorted.forEach(builder::append);
+ builder.append(randomValue);
+ return builder.toString();
+ }
+
+ /**
+ * Compute the replay ID, in the format shared by hotslogs and hotsapi
+ */
+ public static String getMatchId(Replay replay) throws NoSuchAlgorithmException {
+ return getUUIDForString(getConcatenatedString(replay)).toString();
+ }
+
+ public static boolean replayHasComputerPlayers(Replay replay) {
+ return replay.getReplayDetails()
+ .getPlayers()
+ .stream()
+ .map(Player::getPlayerType)
+ .anyMatch(playerType -> playerType == PlayerType.COMPUTER);
+ }
+}
diff --git a/src/test/java/ninja/eivind/hotsreplayuploader/providers/hotslogs/HotSLogsProviderTest.java b/src/test/java/ninja/eivind/hotsreplayuploader/providers/hotslogs/HotSLogsProviderTest.java
index c4994a54..93e0f33b 100644
--- a/src/test/java/ninja/eivind/hotsreplayuploader/providers/hotslogs/HotSLogsProviderTest.java
+++ b/src/test/java/ninja/eivind/hotsreplayuploader/providers/hotslogs/HotSLogsProviderTest.java
@@ -19,6 +19,7 @@
import ninja.eivind.hotsreplayuploader.concurrent.tasks.UploadTask;
import ninja.eivind.hotsreplayuploader.models.ReplayFile;
import ninja.eivind.hotsreplayuploader.utils.SimpleHttpClient;
+import ninja.eivind.hotsreplayuploader.utils.ReplayUtils;
import ninja.eivind.stormparser.StormParser;
import ninja.eivind.stormparser.models.Replay;
import org.junit.Before;
@@ -67,7 +68,7 @@ public void setUp() throws IOException {
@Test
public void testGetMatchId() throws NoSuchAlgorithmException {
String expected = "5543abb9-af35-3ce6-a026-e9d5517f2964";
- String actual = provider.getMatchId(parsedReplay);
+ String actual = ReplayUtils.getMatchId(parsedReplay);
assertEquals("Match ID is calculated as expected", expected, actual);
}