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.

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