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.

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