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.

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