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.

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