You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

515 lines
14 KiB

  1. package export;
  2. import java.io.BufferedReader;
  3. import java.io.BufferedWriter;
  4. import java.io.File;
  5. import java.io.FileInputStream;
  6. import java.io.FileOutputStream;
  7. import java.io.FileWriter;
  8. import java.io.IOException;
  9. import java.io.InputStreamReader;
  10. import java.io.Reader;
  11. import java.net.HttpURLConnection;
  12. import java.net.URL;
  13. import java.net.URLEncoder;
  14. import java.nio.charset.Charset;
  15. import java.nio.charset.StandardCharsets;
  16. import java.util.ArrayList;
  17. import java.util.HashMap;
  18. import java.util.LinkedHashMap;
  19. import java.util.List;
  20. import java.util.Map;
  21. import java.util.concurrent.TimeUnit;
  22. import java.util.zip.ZipEntry;
  23. import java.util.zip.ZipOutputStream;
  24. import javax.net.ssl.HttpsURLConnection;
  25. import org.json.simple.JSONArray;
  26. import org.json.simple.JSONObject;
  27. import org.json.simple.parser.JSONParser;
  28. import org.json.simple.parser.ParseException;
  29. public class Main
  30. {
  31. public static final String baseUrl = "https://www.strava.com/api/v3/";
  32. public static final String tokenUrl = "https://www.strava.com/api/v3/oauth/token";
  33. private static int athleteId = 0;
  34. private static JSONParser parser = new JSONParser();
  35. private static String testRequest;
  36. private static File errorFile;
  37. //TODO: enduco needs to insert the correct request limit for 15 Minutes here
  38. private static final int requestLimit15Minutes = 100;
  39. private static int waitTimeMil = 60000*3*15/requestLimit15Minutes;
  40. private static long lastRequest=0;
  41. private static void writeError(String text)
  42. {
  43. if (errorFile == null)
  44. {
  45. errorFile = new File("error_" + System.currentTimeMillis() + ".txt");
  46. }
  47. try (BufferedWriter bout = new BufferedWriter(new FileWriter(errorFile, true)))
  48. {
  49. bout.write(text);
  50. bout.newLine();
  51. }
  52. catch (IOException e)
  53. {
  54. }
  55. }
  56. /**
  57. * For testing: ensures the test requests are used
  58. *
  59. * @param testrequest Request to be used
  60. */
  61. static void setTest(String testrequest)
  62. {
  63. testRequest = testrequest;
  64. }
  65. /**
  66. * Saves the data of one athlete into a temp file and returns it.
  67. *
  68. * @param token Identifier / authorization token of the athlete
  69. * @return created temp file
  70. */
  71. @SuppressWarnings("unchecked")
  72. static File oneAthlete(String token)
  73. {
  74. // get Activities
  75. Map<String, JSONObject> activities = getActivities(token);
  76. // for each activity: save streams
  77. JSONArray allActivities = new JSONArray();
  78. int simpleActivityId = 0;
  79. for (String id : activities.keySet())
  80. {
  81. JSONObject data = addStreams(id, activities.get(id), token);
  82. data.put("activity_id", simpleActivityId);
  83. allActivities.add(data);
  84. simpleActivityId++;
  85. }
  86. // get general information
  87. JSONObject athleteInfo = saveGeneralInformation(token);
  88. athleteInfo.put("activities", allActivities);
  89. try
  90. {
  91. File temp = File.createTempFile("Athlete_" + athleteId, ".json");
  92. temp.deleteOnExit();
  93. BufferedWriter bw = new BufferedWriter(new FileWriter(temp));
  94. bw.write(athleteInfo.toString());
  95. bw.close();
  96. return temp;
  97. }
  98. catch (IOException e)
  99. {
  100. writeError("Athlete " + athleteId + ": Error writing temp file: " + e.toString());
  101. }
  102. return null;
  103. }
  104. /**
  105. * Adds the streams to the given activity
  106. *
  107. * @param id Strava id of the activity
  108. * @param data general information of the activity
  109. * @param token Identifier / authorization token of the athlete
  110. * @return The data with the added streams
  111. */
  112. @SuppressWarnings("unchecked")
  113. static JSONObject addStreams(String id, JSONObject data, String token)
  114. {
  115. String requestUrlExtension = "activities/" + id + "/streams?"
  116. + "keys=[time,distance,latlng,altitude,velocity_smooth,heartrate,"
  117. + "cadence,watts,temp,moving,grade_smooth]&key_by_type=true";
  118. String json = makeOneGetRequest(requestUrlExtension, token);
  119. if (json.isEmpty()||json.isBlank()||json.equals(""))
  120. {
  121. return data;
  122. }
  123. Object obj;
  124. try
  125. {
  126. obj = parser.parse(json);
  127. JSONObject listOfStreams = (JSONObject) obj;
  128. for (Object key : listOfStreams.keySet())
  129. {
  130. JSONObject oneStream = (JSONObject) listOfStreams.get(key);
  131. data.put("stream_" + key.toString(), oneStream);
  132. }
  133. }
  134. catch (ParseException | NumberFormatException e)
  135. {
  136. writeError("Athlete " + athleteId + ": Error parsing json (Streams): " + e.toString());
  137. }
  138. return data;
  139. }
  140. /**
  141. * Gathers all activities of a user, extracts the general information and the
  142. * ids.
  143. *
  144. * @param token Identifier / authorization token of the athlete
  145. * @return A Map with key = Strava Id of an activity and value = JSONObject with
  146. * the general information of the activity
  147. */
  148. @SuppressWarnings("unchecked")
  149. static Map<String, JSONObject> getActivities(String token)
  150. {
  151. Map<String, JSONObject> result = new HashMap<>();
  152. /*
  153. * Possible values = AlpineSki, BackcountrySki, Canoeing, Crossfit, EBikeRide,
  154. * Elliptical, Golf, Handcycle, Hike, IceSkate, InlineSkate, Kayaking, Kitesurf,
  155. * NordicSki, Ride, RockClimbing, RollerSki, Rowing, Run, Sail, Skateboard,
  156. * Snowboard, Snowshoe, Soccer, StairStepper, StandUpPaddling, Surfing, Swim,
  157. * Velomobile, VirtualRide, VirtualRun, Walk, WeightTraining, Wheelchair,
  158. * Windsurf, Workout, Yoga
  159. */
  160. String type = "type";
  161. String timezone = "timezone";
  162. String start = "start_date_local"; // Start date measured in users time zone
  163. String id = "id";
  164. int pageIndex = 1;
  165. while (true)
  166. {
  167. String requestExtension = "athlete/activities?page=" + pageIndex;
  168. String json = makeOneGetRequest(requestExtension, token);
  169. if (json.isEmpty() || json.isBlank() || json.equals("") || json.equals("[]")) // don't know where the last page is...
  170. {
  171. break;
  172. }
  173. Object obj;
  174. try
  175. {
  176. obj = parser.parse(json);
  177. JSONArray listOfActivites = (JSONArray) obj;
  178. for (int i = 0; i < listOfActivites.size(); i++)
  179. {
  180. JSONObject oneActivity = (JSONObject) listOfActivites.get(i);
  181. JSONObject toSave = new JSONObject();
  182. toSave.put(type, oneActivity.get(type));
  183. toSave.put(timezone, oneActivity.get(timezone));
  184. toSave.put(start, oneActivity.get(start));
  185. toSave.put("athlete_id", athleteId);
  186. Object idObj = oneActivity.get(id);
  187. if (idObj != null)
  188. {
  189. result.put(oneActivity.get(id).toString(), toSave);
  190. }
  191. }
  192. }
  193. catch (ParseException | NumberFormatException e)
  194. {
  195. writeError("Athlete " + athleteId + ": Error parsing json (Activities): " + e.toString());
  196. }
  197. pageIndex++;
  198. }
  199. return result;
  200. }
  201. /**
  202. * Extracts an athletes general information.
  203. *
  204. * @param token Identifier / authorization token of the athlete
  205. * @return extracted data or null if there was an error
  206. */
  207. @SuppressWarnings("unchecked")
  208. static JSONObject saveGeneralInformation(String token)
  209. {
  210. String sex = "sex"; // Possible values = M, F
  211. String country = "country";
  212. String date_pref = "date_preference";
  213. String meas_pref = "measurement_preference"; // Possible values = feet, meters
  214. String weight = "weight";
  215. String json = makeOneGetRequest("athlete", token);
  216. JSONObject toSave = new JSONObject();
  217. try
  218. {
  219. Object obj = parser.parse(json);
  220. JSONObject data = (JSONObject) obj;
  221. toSave.put(sex, data.get(sex));
  222. toSave.put(country, data.get(country));
  223. toSave.put(date_pref, data.get(date_pref));
  224. toSave.put(meas_pref, data.get(meas_pref));
  225. toSave.put(weight, data.get(weight));
  226. toSave.put("id", athleteId);
  227. return toSave;
  228. }
  229. catch (ParseException e)
  230. {
  231. writeError("Athlete " + athleteId + ": Error parsing general information.");
  232. return null;
  233. }
  234. }
  235. /**
  236. * Zip a list of files in one .zip file.
  237. *
  238. * @param files HasMap of <intended Filename, File> which should be zipped
  239. * @throws IOException If there was an error zipping
  240. */
  241. static void zipAllFiles(Map<String, File> files) throws IOException
  242. {
  243. FileOutputStream fos = new FileOutputStream("data.zip");
  244. ZipOutputStream zipOut = new ZipOutputStream(fos);
  245. for (String key : files.keySet())
  246. {
  247. File fileToZip = files.get(key);
  248. FileInputStream fis = new FileInputStream(fileToZip);
  249. ZipEntry zipEntry = new ZipEntry(key);
  250. zipOut.putNextEntry(zipEntry);
  251. byte[] bytes = new byte[1024];
  252. int length;
  253. while ((length = fis.read(bytes)) >= 0)
  254. {
  255. zipOut.write(bytes, 0, length);
  256. }
  257. fis.close();
  258. }
  259. zipOut.close();
  260. fos.close();
  261. }
  262. /**
  263. * Handles one GET request to the API
  264. *
  265. * @param requestUrlExtension Extension for the baseUrl (without '/')
  266. * @param token Identification / authorization token of the
  267. * athlete
  268. * @return The response as a String, an empty String in case of error.
  269. */
  270. static String makeOneGetRequest(String requestUrlExtension, String token)
  271. {
  272. if (testRequest != null)
  273. {
  274. String varTestRequest = testRequest.replaceAll("\\:\\s*([0-9]{15,})\\,", ":\"$1\",");
  275. testRequest = null;
  276. return varTestRequest;
  277. }
  278. HttpsURLConnection connection = null;
  279. try
  280. {
  281. long timeSinceLastRequest =System.currentTimeMillis()-lastRequest;
  282. if( timeSinceLastRequest<waitTimeMil)
  283. {
  284. try
  285. {
  286. TimeUnit.MILLISECONDS.sleep(waitTimeMil-timeSinceLastRequest);
  287. }
  288. catch (InterruptedException e)
  289. {
  290. }
  291. };
  292. lastRequest=System.currentTimeMillis();
  293. // Create connection
  294. URL url = new URL(baseUrl + requestUrlExtension);
  295. connection = (HttpsURLConnection) url.openConnection();
  296. connection.setRequestMethod("GET");
  297. connection.setRequestProperty("Authorization", "Bearer " + token);
  298. return getResponse(connection);
  299. }
  300. catch (IOException e)
  301. {
  302. writeError("Error while handling GET request: " + e.toString());
  303. }
  304. return "";
  305. }
  306. /**
  307. * Reads the response from the connection and returns it as a String
  308. *
  309. * @param connection Connection to the site
  310. * @return Response as a String
  311. * @throws IOException in case of error
  312. */
  313. static String getResponse(HttpsURLConnection connection) throws IOException
  314. {
  315. StringBuilder result = new StringBuilder();
  316. int responseCode = connection.getResponseCode();
  317. if (responseCode != HttpURLConnection.HTTP_OK )
  318. {
  319. //excluded error messages appearing on missing streams
  320. if(responseCode != HttpURLConnection.HTTP_NOT_FOUND)
  321. {
  322. writeError("Wrong response code: " + responseCode);
  323. }
  324. return "";
  325. }
  326. try (Reader reader = new BufferedReader(
  327. new InputStreamReader(connection.getInputStream(), Charset.forName(StandardCharsets.UTF_8.name()))))
  328. {
  329. int c = reader.read();
  330. while (c != -1)
  331. {
  332. result.append((char) c);
  333. c = reader.read();
  334. }
  335. }
  336. // Numbers too long for int or long are turned into Strings
  337. return result.toString().replaceAll("\\:\\s*([0-9]{15,})\\,", ":\"$1\",");
  338. }
  339. /**
  340. * Used for generating the accessToken
  341. *
  342. * @param data Triplet containing: a the client_id, b the client_secret, c the
  343. * refresh_token
  344. * @return The response as a String, an empty String in case of error.
  345. */
  346. static String makeOnePostRequest(Triplet data)
  347. {
  348. HttpsURLConnection connection = null;
  349. try
  350. {
  351. // Create connection
  352. URL url = new URL(tokenUrl);
  353. connection = (HttpsURLConnection) url.openConnection();
  354. connection.setRequestMethod("POST");
  355. connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
  356. Map<String, String> params = new LinkedHashMap<>();
  357. params.put("client_id", data.getA());
  358. params.put("client_secret", data.getB());
  359. params.put("grant_type", "refresh_token");
  360. params.put("refresh_token", data.getC());
  361. StringBuilder postData = new StringBuilder();
  362. for (String key : params.keySet())
  363. {
  364. if (postData.length() != 0)
  365. {
  366. postData.append('&');
  367. }
  368. postData.append(URLEncoder.encode(key, "UTF-8"));
  369. postData.append('=');
  370. postData.append(URLEncoder.encode(params.get(key), "UTF-8"));
  371. }
  372. byte[] postDataBytes = postData.toString().getBytes("UTF-8");
  373. connection.setRequestProperty("Content-Length", String.valueOf(postDataBytes.length));
  374. connection.setDoOutput(true);
  375. connection.getOutputStream().write(postDataBytes);
  376. return getResponse(connection);
  377. }
  378. catch (IOException e)
  379. {
  380. writeError("Error while handling POST request: " + e.toString());
  381. }
  382. return "";
  383. }
  384. /**
  385. * Sends the old refresh token to strava and retrieves a new one and an access token
  386. * @param refreshInfo Refresh data with a the cliend_id, b the client_secret and c the refresh_token
  387. * @return A tuple with a the access_token and b the new refresh_token or {@code null} if there was an error
  388. */
  389. static Tuple getAccessToken(Triplet refreshInfo)
  390. {
  391. String json = makeOnePostRequest(refreshInfo);
  392. try
  393. {
  394. Object obj = parser.parse(json);
  395. JSONObject data = (JSONObject) obj;
  396. return new Tuple(data.get("access_token").toString(), data.get("refresh_token").toString());
  397. }
  398. catch (ParseException e)
  399. {
  400. writeError("Athlete " + athleteId + ": Error parsing general information.");
  401. }
  402. return null;
  403. }
  404. public static void main(String[] args)
  405. {
  406. // TODO: tokens need to be added by Enduco
  407. // Triplet a is client_id, b is client_secret, c is refresh_token
  408. List<Triplet> refreshTokens = new ArrayList<>();
  409. // don't know if you need it but here you will find the new refresh tokens after
  410. // the requests
  411. List<Triplet> newRefreshTokens = new ArrayList<>();
  412. Map<String, File> allFiles = new HashMap<>();
  413. for (Triplet oneUser : refreshTokens)
  414. {
  415. // a is access_token and b is new refresh_token
  416. Tuple withAccessToken = getAccessToken(oneUser);
  417. if (withAccessToken == null)
  418. {
  419. writeError("Coulnd't get new access token for client "+athleteId);
  420. continue;
  421. }
  422. newRefreshTokens.add(new Triplet(oneUser.getA(), oneUser.getB(), withAccessToken.getB()));
  423. File athlete = oneAthlete(withAccessToken.getA());
  424. if (athlete != null)
  425. {
  426. allFiles.put("Athlete_" + athleteId + ".json", athlete);
  427. }
  428. athleteId++;
  429. }
  430. try
  431. {
  432. zipAllFiles(allFiles);
  433. }
  434. catch (IOException e)
  435. {
  436. writeError("Files coulnd't be zipped");
  437. }
  438. }
  439. }
  440. class Tuple
  441. {
  442. private String a;
  443. private String b;
  444. public Tuple(String a, String b)
  445. {
  446. this.a = a;
  447. this.b = b;
  448. }
  449. public String getA()
  450. {
  451. return a;
  452. }
  453. public String getB()
  454. {
  455. return b;
  456. }
  457. }
  458. class Triplet extends Tuple
  459. {
  460. private String c;
  461. public Triplet(String a, String b, String c)
  462. {
  463. super(a, b);
  464. this.c = c;
  465. }
  466. public String getC()
  467. {
  468. return c;
  469. }
  470. }