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.

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