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.

674 lines
19 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.Calendar;
  18. import java.util.GregorianCalendar;
  19. import java.util.HashMap;
  20. import java.util.LinkedHashMap;
  21. import java.util.List;
  22. import java.util.Map;
  23. import java.util.concurrent.TimeUnit;
  24. import java.util.zip.ZipEntry;
  25. import java.util.zip.ZipOutputStream;
  26. import javax.net.ssl.HttpsURLConnection;
  27. import org.json.simple.JSONArray;
  28. import org.json.simple.JSONObject;
  29. import org.json.simple.parser.JSONParser;
  30. import org.json.simple.parser.ParseException;
  31. public class Main
  32. {
  33. public static final String baseUrl = "https://www.strava.com/api/v3/";
  34. public static final String tokenUrl = "https://www.strava.com/api/v3/oauth/token";
  35. private static final int retryTimes = 100;
  36. private static final int httpCodeLimitReached = 429;
  37. private static int athleteId = -1;
  38. private static JSONParser parser = new JSONParser();
  39. private static String testRequest;
  40. private static File errorFile;
  41. // TODO: enduco needs to insert the correct request limits here
  42. private static final int requestLimit15Minutes = 100;
  43. private static final int requestLimitDay = 1000 / 3;
  44. private static int dailyRequestCount = 0;
  45. private static int waitTimeMil = 60000 * 3 * 15 / requestLimit15Minutes;
  46. private static long lastRequestTimeInMillis = 0;
  47. private static String accessToken = "";
  48. /**
  49. * a is client_id, b is client_secret, c is refresh_token
  50. */
  51. private static Triplet refreshInfo;
  52. private static void writeError(String text)
  53. {
  54. if (errorFile == null)
  55. {
  56. errorFile = new File("error_" + System.currentTimeMillis() + ".txt");
  57. }
  58. try (BufferedWriter bout = new BufferedWriter(new FileWriter(errorFile, true)))
  59. {
  60. bout.write(text);
  61. bout.newLine();
  62. }
  63. catch (IOException e)
  64. {
  65. }
  66. }
  67. /**
  68. * For testing: ensures the test requests are used
  69. *
  70. * @param testrequest Request to be used
  71. */
  72. static void setTest(String testrequest)
  73. {
  74. testRequest = testrequest;
  75. }
  76. /**
  77. * Saves the data of one athlete into a temp file and returns it.
  78. *
  79. * @return created temp file
  80. */
  81. @SuppressWarnings("unchecked")
  82. static File oneAthlete()
  83. {
  84. // get Activities and general information
  85. Map<String, JSONObject> activities;
  86. JSONObject athleteInfo;
  87. try
  88. {
  89. activities = getActivities();
  90. athleteInfo = saveGeneralInformation();
  91. }
  92. catch (NoAccessException e1)
  93. {
  94. writeError("Athlete " + athleteId + ": Access expired and no new token possible");
  95. return null; // no data at all. Stop right away
  96. }
  97. if (athleteInfo == null) // error getting General Information
  98. {
  99. athleteInfo = new JSONObject();
  100. }
  101. // for each activity: save streams
  102. JSONArray allActivities = new JSONArray();
  103. int simpleActivityId = 0;
  104. for (String id : activities.keySet())
  105. {
  106. JSONObject data;
  107. try
  108. {
  109. data = addStreams(id, activities.get(id));
  110. }
  111. catch (NoAccessException e)
  112. {
  113. writeError("Athlete " + athleteId + ": Access expired and no new token possible");
  114. break; //stop the loop and save what you got up to there
  115. }
  116. data.put("activity_id", simpleActivityId);
  117. allActivities.add(data);
  118. simpleActivityId++;
  119. }
  120. athleteInfo.put("activities", allActivities);
  121. try
  122. {
  123. File temp = File.createTempFile("Athlete_" + athleteId, ".json");
  124. temp.deleteOnExit();
  125. BufferedWriter bw = new BufferedWriter(new FileWriter(temp));
  126. bw.write(athleteInfo.toString());
  127. bw.close();
  128. return temp;
  129. }
  130. catch (IOException e)
  131. {
  132. writeError("Athlete " + athleteId + ": Error writing temp file: " + e.toString());
  133. }
  134. return null;
  135. }
  136. /**
  137. * Adds the streams to the given activity
  138. *
  139. * @param id Strava id of the activity
  140. * @param data general information of the activity
  141. * @return The data with the added streams
  142. * @throws NoAccessException If the access token expired and no new one could be acquired.
  143. */
  144. @SuppressWarnings("unchecked")
  145. static JSONObject addStreams(String id, JSONObject data) throws NoAccessException
  146. {
  147. String requestUrlExtension = "activities/" + id + "/streams?"
  148. + "keys=[time,distance,latlng,altitude,velocity_smooth,heartrate,"
  149. + "cadence,watts,temp,moving,grade_smooth]&key_by_type=true";
  150. String json = makeGetRequestWithRetry(requestUrlExtension);
  151. if (json == null || json.isEmpty() || json.isBlank() || json.equals(""))
  152. {
  153. return data;
  154. }
  155. Object obj;
  156. try
  157. {
  158. obj = parser.parse(json);
  159. JSONObject listOfStreams = (JSONObject) obj;
  160. for (Object key : listOfStreams.keySet())
  161. {
  162. JSONObject oneStream = (JSONObject) listOfStreams.get(key);
  163. data.put("stream_" + key.toString(), oneStream);
  164. }
  165. }
  166. catch (ParseException | NumberFormatException e)
  167. {
  168. writeError("Athlete " + athleteId + ": Error parsing json (Streams): " + e.toString());
  169. }
  170. return data;
  171. }
  172. /**
  173. * Gathers all activities of a user, extracts the general information and the
  174. * ids.
  175. *
  176. * @return A Map with key = Strava Id of an activity and value = JSONObject with
  177. * the general information of the activity
  178. * @throws NoAccessException If the access token expired and no new one could be acquired.
  179. */
  180. @SuppressWarnings("unchecked")
  181. static Map<String, JSONObject> getActivities() throws NoAccessException
  182. {
  183. Map<String, JSONObject> result = new HashMap<>();
  184. /*
  185. * Possible values = AlpineSki, BackcountrySki, Canoeing, Crossfit, EBikeRide,
  186. * Elliptical, Golf, Handcycle, Hike, IceSkate, InlineSkate, Kayaking, Kitesurf,
  187. * NordicSki, Ride, RockClimbing, RollerSki, Rowing, Run, Sail, Skateboard,
  188. * Snowboard, Snowshoe, Soccer, StairStepper, StandUpPaddling, Surfing, Swim,
  189. * Velomobile, VirtualRide, VirtualRun, Walk, WeightTraining, Wheelchair,
  190. * Windsurf, Workout, Yoga
  191. */
  192. String type = "type";
  193. String timezone = "timezone";
  194. String start = "start_date_local"; // Start date measured in users time zone
  195. String id = "id";
  196. int pageIndex = 1;
  197. while (true)
  198. {
  199. String requestExtension = "athlete/activities?per_page=100&page=" + pageIndex;
  200. String json = makeGetRequestWithRetry(requestExtension);
  201. if (json == null || json.isEmpty() || json.isBlank() || json.equals("") || json.equals("[]")) // don't know
  202. // where the
  203. // last page
  204. // is...
  205. {
  206. break;
  207. }
  208. Object obj;
  209. try
  210. {
  211. obj = parser.parse(json);
  212. JSONArray listOfActivites = (JSONArray) obj;
  213. for (int i = 0; i < listOfActivites.size(); i++)
  214. {
  215. JSONObject oneActivity = (JSONObject) listOfActivites.get(i);
  216. JSONObject toSave = new JSONObject();
  217. toSave.put(type, oneActivity.get(type));
  218. toSave.put(timezone, oneActivity.get(timezone));
  219. toSave.put(start, oneActivity.get(start));
  220. toSave.put("athlete_id", athleteId);
  221. Object idObj = oneActivity.get(id);
  222. if (idObj != null)
  223. {
  224. result.put(oneActivity.get(id).toString(), toSave);
  225. }
  226. }
  227. }
  228. catch (ParseException | NumberFormatException e)
  229. {
  230. writeError("Athlete " + athleteId + ": Error parsing json (Activities): " + e.toString());
  231. }
  232. pageIndex++;
  233. }
  234. return result;
  235. }
  236. /**
  237. * In the case that the daily request limit has been reached, this method waits
  238. * for the next day
  239. */
  240. static void checkRequestLimit()
  241. {
  242. if (dailyRequestCount > requestLimitDay) // daily request limit reached, wait until the next day
  243. {
  244. Calendar tomorrow = new GregorianCalendar();
  245. tomorrow.setTimeInMillis(System.currentTimeMillis());
  246. tomorrow.set(Calendar.DAY_OF_YEAR, tomorrow.get(Calendar.DAY_OF_YEAR) + 1);
  247. if (tomorrow.get(Calendar.DAY_OF_YEAR) == 1) // reached a new year ...
  248. {
  249. tomorrow.set(Calendar.YEAR, 2022);
  250. }
  251. tomorrow.set(Calendar.HOUR_OF_DAY, 0);
  252. tomorrow.set(Calendar.MINUTE, 0);
  253. try
  254. {
  255. TimeUnit.MILLISECONDS.sleep(tomorrow.getTimeInMillis() - System.currentTimeMillis());
  256. }
  257. catch (InterruptedException e1)
  258. {
  259. }
  260. }
  261. }
  262. /**
  263. * Method is used to find the next request window: It tries the same request
  264. * again after 5 minutes. After a set number of times (retryTimes) it stops if
  265. * it still wasn't successful.
  266. *
  267. * @param urlExtension UrlExtension for the request
  268. * @return Data as a String or {@code null} if there is no data
  269. * @throws NoAccessException If the access token expired and no new one could be acquired.
  270. */
  271. static String makeGetRequestWithRetry(String urlExtension) throws NoAccessException
  272. {
  273. checkRequestLimit();
  274. String json = null;
  275. int count = 0;
  276. do
  277. {
  278. try
  279. {
  280. json = makeOneGetRequest(urlExtension);
  281. dailyRequestCount++;
  282. }
  283. catch (ResponseCodeWrongException e)
  284. {
  285. // tried enough times, so stop now
  286. if (count >= retryTimes)
  287. {
  288. writeError(
  289. "Athlete: " + athleteId + " Retry limit reached. Last error code: " + e.getResponseCode());
  290. return null;
  291. }
  292. if (e.getResponseCode()==HttpURLConnection.HTTP_UNAUTHORIZED)
  293. { //token might have expired
  294. if(!getAccessToken()) //token doesn't work anymore and we can't get a new one
  295. {
  296. throw new NoAccessException();
  297. }
  298. }
  299. else if (e.getResponseCode() == httpCodeLimitReached)
  300. {// request limit is reached, try again later
  301. count++;
  302. }
  303. else // some other error: try only one other time!
  304. {
  305. count = retryTimes;
  306. }
  307. // Sleep for 5 minutes and try to get the next 15 min request window
  308. try
  309. {
  310. TimeUnit.MINUTES.sleep(5);
  311. }
  312. catch (InterruptedException e1)
  313. {
  314. }
  315. }
  316. } while (json == null);
  317. return json;
  318. }
  319. /**
  320. * Extracts an athletes general information.
  321. *
  322. * @return extracted data or null if there was an error
  323. * @throws NoAccessException If the access token expired and no new one could be acquired.
  324. */
  325. @SuppressWarnings("unchecked")
  326. static JSONObject saveGeneralInformation() throws NoAccessException
  327. {
  328. String sex = "sex"; // Possible values = M, F
  329. String country = "country";
  330. String date_pref = "date_preference";
  331. String meas_pref = "measurement_preference"; // Possible values = feet, meters
  332. String weight = "weight";
  333. String json = makeGetRequestWithRetry("athlete");
  334. JSONObject toSave = new JSONObject();
  335. try
  336. {
  337. Object obj = parser.parse(json);
  338. JSONObject data = (JSONObject) obj;
  339. toSave.put(sex, data.get(sex));
  340. toSave.put(country, data.get(country));
  341. toSave.put(date_pref, data.get(date_pref));
  342. toSave.put(meas_pref, data.get(meas_pref));
  343. toSave.put(weight, data.get(weight));
  344. toSave.put("id", athleteId);
  345. return toSave;
  346. }
  347. catch (ParseException e)
  348. {
  349. writeError("Athlete " + athleteId + ": Error parsing general information.");
  350. return null;
  351. }
  352. catch (NullPointerException e)
  353. {
  354. writeError("Athlete " + athleteId + ": No general information found.");
  355. return null;
  356. }
  357. }
  358. /**
  359. * Zip a list of files in one .zip file.
  360. *
  361. * @param files HasMap of <intended Filename, File> which should be zipped
  362. * @param count COunt or id to create distinct files each time
  363. * @throws IOException If there was an error zipping
  364. */
  365. static void zipFiles(Map<String, File> files, int count) throws IOException
  366. {
  367. FileOutputStream fos = new FileOutputStream("data(" + count + ").zip");
  368. ZipOutputStream zipOut = new ZipOutputStream(fos);
  369. for (String key : files.keySet())
  370. {
  371. File fileToZip = files.get(key);
  372. FileInputStream fis = new FileInputStream(fileToZip);
  373. ZipEntry zipEntry = new ZipEntry(key);
  374. zipOut.putNextEntry(zipEntry);
  375. byte[] bytes = new byte[1024];
  376. int length;
  377. while ((length = fis.read(bytes)) >= 0)
  378. {
  379. zipOut.write(bytes, 0, length);
  380. }
  381. fis.close();
  382. }
  383. zipOut.close();
  384. fos.close();
  385. }
  386. /**
  387. * Handles one GET request to the API
  388. *
  389. * @param requestUrlExtension Extension for the baseUrl (without '/')
  390. * @return The response as a String, an empty String in case of error.
  391. * @throws ResponseCodeWrongException If there was an http error
  392. */
  393. static String makeOneGetRequest(String requestUrlExtension) throws ResponseCodeWrongException
  394. {
  395. if (testRequest != null)
  396. {
  397. String varTestRequest = testRequest.replaceAll("\\:\\s*([0-9]{15,})\\,", ":\"$1\",");
  398. testRequest = null;
  399. return varTestRequest;
  400. }
  401. HttpsURLConnection connection = null;
  402. try
  403. {
  404. long timeSinceLastRequest = System.currentTimeMillis() - lastRequestTimeInMillis;
  405. if (timeSinceLastRequest < waitTimeMil)
  406. {
  407. try
  408. {
  409. TimeUnit.MILLISECONDS.sleep(waitTimeMil - timeSinceLastRequest);
  410. }
  411. catch (InterruptedException e)
  412. {
  413. }
  414. }
  415. ;
  416. lastRequestTimeInMillis = System.currentTimeMillis();
  417. // Create connection
  418. URL url = new URL(baseUrl + requestUrlExtension);
  419. connection = (HttpsURLConnection) url.openConnection();
  420. connection.setRequestMethod("GET");
  421. connection.setRequestProperty("Authorization", "Bearer " + accessToken);
  422. return getResponse(connection);
  423. }
  424. catch (IOException e)
  425. {
  426. writeError("Athlete: " + athleteId + " Error while handling GET request: " + e.toString());
  427. }
  428. return "";
  429. }
  430. /**
  431. * Reads the response from the connection and returns it as a String
  432. *
  433. * @param connection Connection to the site
  434. * @return Response as a String
  435. * @throws IOException in case of error with the stream
  436. * @throws ResponseCodeWrongException if no data was read because of http
  437. * problems
  438. */
  439. static String getResponse(HttpsURLConnection connection) throws IOException, ResponseCodeWrongException
  440. {
  441. StringBuilder result = new StringBuilder();
  442. int responseCode = connection.getResponseCode();
  443. if (responseCode != HttpURLConnection.HTTP_OK)
  444. {
  445. // excluded error messages appearing on missing streams and reached rate limit
  446. if (responseCode != HttpURLConnection.HTTP_NOT_FOUND && responseCode != httpCodeLimitReached)
  447. {
  448. writeError("Athlete: " + athleteId + " Wrong response code: " + responseCode);
  449. }
  450. throw new ResponseCodeWrongException(responseCode);
  451. }
  452. try (Reader reader = new BufferedReader(
  453. new InputStreamReader(connection.getInputStream(), Charset.forName(StandardCharsets.UTF_8.name()))))
  454. {
  455. int c = reader.read();
  456. while (c != -1)
  457. {
  458. result.append((char) c);
  459. c = reader.read();
  460. }
  461. }
  462. // Numbers too long for int or long are turned into Strings
  463. return result.toString().replaceAll("\\:\\s*([0-9]{15,})\\,", ":\"$1\",");
  464. }
  465. /**
  466. * Used for generating the accessToken
  467. *
  468. * @return The response as a String, an empty String in case of error.
  469. * @throws ResponseCodeWrongException If there was an http error
  470. */
  471. static String makeOnePostRequest() throws ResponseCodeWrongException
  472. {
  473. HttpsURLConnection connection = null;
  474. try
  475. {
  476. // Create connection
  477. URL url = new URL(tokenUrl);
  478. connection = (HttpsURLConnection) url.openConnection();
  479. connection.setRequestMethod("POST");
  480. connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
  481. Map<String, String> params = new LinkedHashMap<>();
  482. params.put("client_id", refreshInfo.getA());
  483. params.put("client_secret", refreshInfo.getB());
  484. params.put("grant_type", "refresh_token");
  485. params.put("refresh_token", refreshInfo.getC());
  486. StringBuilder postData = new StringBuilder();
  487. for (String key : params.keySet())
  488. {
  489. if (postData.length() != 0)
  490. {
  491. postData.append('&');
  492. }
  493. postData.append(URLEncoder.encode(key, "UTF-8"));
  494. postData.append('=');
  495. postData.append(URLEncoder.encode(params.get(key), "UTF-8"));
  496. }
  497. byte[] postDataBytes = postData.toString().getBytes("UTF-8");
  498. connection.setRequestProperty("Content-Length", String.valueOf(postDataBytes.length));
  499. connection.setDoOutput(true);
  500. connection.getOutputStream().write(postDataBytes);
  501. return getResponse(connection);
  502. }
  503. catch (IOException e)
  504. {
  505. writeError("Athlete: " + athleteId + "Error while handling POST request: " + e.toString());
  506. }
  507. return "";
  508. }
  509. /**
  510. * Sends the old refresh token to strava and retrieves a new one and an access
  511. * token
  512. *
  513. * @param refreshInfo Refresh data with a the cliend_id, b the client_secret and
  514. * c the refresh_token
  515. * @return {@code true} if everything went right or {@code false} if there was
  516. * an error
  517. */
  518. static boolean getAccessToken()
  519. {
  520. checkRequestLimit();
  521. String json = null;
  522. int count = 0;
  523. do
  524. {
  525. try
  526. {
  527. json = makeOnePostRequest();
  528. dailyRequestCount++;
  529. }
  530. catch (ResponseCodeWrongException e)
  531. {
  532. // tried enough times, so stop now
  533. if (count >= retryTimes)
  534. {
  535. return false;
  536. }
  537. if (e.getResponseCode()==HttpURLConnection.HTTP_UNAUTHORIZED)
  538. { //token might have expired
  539. if(!getAccessToken()) //token doesn't work anymore and we can't get a new one
  540. {
  541. return false;
  542. }
  543. }
  544. else if (e.getResponseCode() == httpCodeLimitReached)
  545. {
  546. // request limit is reached, try again later
  547. count++;
  548. }
  549. else // some other error: try only one other time!
  550. {
  551. count = retryTimes;
  552. }
  553. // Sleep for 5 minutes and try to get the next 15 min request window
  554. try
  555. {
  556. TimeUnit.MINUTES.sleep(5);
  557. }
  558. catch (InterruptedException e1)
  559. {
  560. }
  561. }
  562. } while (json == null);
  563. try
  564. {
  565. Object obj = parser.parse(json);
  566. JSONObject data = (JSONObject) obj;
  567. accessToken = data.get("access_token").toString();
  568. refreshInfo.setC(data.get("refresh_token").toString());
  569. return true;
  570. }
  571. catch (ParseException e)
  572. {
  573. writeError("Athlete " + athleteId + ": Error parsing refresh info.");
  574. }
  575. return false;
  576. }
  577. public static void main(String[] args)
  578. {
  579. // TODO: tokens need to be added by Enduco
  580. // Triplet a is client_id, b is client_secret, c is refresh_token
  581. List<Triplet> refreshTokens = new ArrayList<>();
  582. // don't know if you need it but here you will find the new refresh tokens after
  583. // the requests
  584. List<Triplet> newRefreshTokens = new ArrayList<>();
  585. Map<String, File> allFiles = new HashMap<>();
  586. int zipcount = 1;
  587. for (Triplet oneUser : refreshTokens)
  588. {
  589. refreshInfo = oneUser;
  590. athleteId++;
  591. if (!getAccessToken())
  592. {
  593. writeError("Couldn't get new access token for client " + athleteId);
  594. continue;
  595. }
  596. File athlete = oneAthlete();
  597. if (athlete != null)
  598. {
  599. allFiles.put("Athlete_" + athleteId + ".json", athlete);
  600. }
  601. newRefreshTokens.add(refreshInfo);
  602. // pack zip-files of 10 athletes
  603. if (allFiles.size() >= 10)
  604. {
  605. try
  606. {
  607. zipFiles(allFiles, zipcount);
  608. zipcount++;
  609. allFiles = new HashMap<>();
  610. }
  611. catch (IOException e)
  612. {
  613. writeError("Files coulnd't be zipped");
  614. }
  615. }
  616. }
  617. // zip the rest
  618. try
  619. {
  620. zipFiles(allFiles, zipcount);
  621. }
  622. catch (IOException e)
  623. {
  624. writeError("Files coulnd't be zipped");
  625. }
  626. }
  627. }