From 524139dac8f1596314ba34c129d5b1e4baa9350d Mon Sep 17 00:00:00 2001 From: Bianca Steffes Date: Wed, 6 Oct 2021 16:06:18 +0200 Subject: [PATCH] Code now expects refresh tokens and requests its own access tokens (not tested) --- export/.gitignore | 1 + export/src/export/Main.java | 399 +++++++++++++++++++++++++----------- 2 files changed, 276 insertions(+), 124 deletions(-) diff --git a/export/.gitignore b/export/.gitignore index 09e3bc9..905b9c3 100644 --- a/export/.gitignore +++ b/export/.gitignore @@ -1,2 +1,3 @@ /bin/ /target/ +/data.zip diff --git a/export/src/export/Main.java b/export/src/export/Main.java index 9953ef8..4280dfd 100644 --- a/export/src/export/Main.java +++ b/export/src/export/Main.java @@ -11,10 +11,12 @@ import java.io.InputStreamReader; import java.io.Reader; import java.net.HttpURLConnection; import java.net.URL; +import java.net.URLEncoder; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.zip.ZipEntry; @@ -30,19 +32,20 @@ import org.json.simple.parser.ParseException; public class Main { public static final String baseUrl = "https://www.strava.com/api/v3/"; + public static final String tokenUrl = "https://www.strava.com/api/v3/oauth/token"; private static int athleteId = 0; private static JSONParser parser = new JSONParser(); private static String testRequest; private static File errorFile; - + private static void writeError(String text) { - if(errorFile==null) + if (errorFile == null) { - errorFile = new File("error_"+System.currentTimeMillis()+".txt"); + errorFile = new File("error_" + System.currentTimeMillis() + ".txt"); } - - try(BufferedWriter bout = new BufferedWriter(new FileWriter(errorFile, true))) + + try (BufferedWriter bout = new BufferedWriter(new FileWriter(errorFile, true))) { bout.write(text); bout.newLine(); @@ -51,18 +54,20 @@ public class Main { } } - + /** * For testing: ensures the test requests are used + * * @param testrequest Request to be used */ static void setTest(String testrequest) { - testRequest=testrequest; + testRequest = testrequest; } - + /** * Saves the data of one athlete into a temp file and returns it. + * * @param token Identifier / authorization token of the athlete * @return created temp file */ @@ -74,22 +79,22 @@ public class Main // for each activity: save streams JSONArray allActivities = new JSONArray(); - int simpleActivityId=0; - for (String id: activities.keySet()) + int simpleActivityId = 0; + for (String id : activities.keySet()) { JSONObject data = addStreams(id, activities.get(id), token); data.put("activity_id", simpleActivityId); allActivities.add(data); simpleActivityId++; } - - // get general information + + // get general information JSONObject athleteInfo = saveGeneralInformation(token); athleteInfo.put("activities", allActivities); - + try { - File temp = File.createTempFile("Athlete_" +athleteId , ".json"); + File temp = File.createTempFile("Athlete_" + athleteId, ".json"); temp.deleteOnExit(); BufferedWriter bw = new BufferedWriter(new FileWriter(temp)); bw.write(athleteInfo.toString()); @@ -98,101 +103,106 @@ public class Main } catch (IOException e) { - writeError("Athlete "+athleteId+": Error writing temp file: "+e.toString()); + writeError("Athlete " + athleteId + ": Error writing temp file: " + e.toString()); } return null; } - + /** * Adds the streams to the given activity - * @param id Strava id of the activity - * @param data general information of the activity + * + * @param id Strava id of the activity + * @param data general information of the activity * @param token Identifier / authorization token of the athlete * @return The data with the added streams */ @SuppressWarnings("unchecked") static JSONObject addStreams(String id, JSONObject data, String token) { - //TODO: Array representation is not tested - String requestUrlExtension = "activities/"+id+"/streams?keys=[time, distance, latlng, altitude, " + // TODO: Array representation is not tested + String requestUrlExtension = "activities/" + id + "/streams?keys=[time, distance, latlng, altitude, " + "velocity_smooth, heartrate, cadence, watts, temp, moving, grade_smooth]&key_by_type="; - String json = makeOneRequest(requestUrlExtension, token); + String json = makeOneGetRequest(requestUrlExtension, token); String type = "type"; - + Object obj; try { obj = parser.parse(json); JSONArray listOfStreams = (JSONArray) obj; - for (int i=0; i getActivities(String token) { Map result = new HashMap<>(); /* - * Possible values = AlpineSki, BackcountrySki, Canoeing, Crossfit, EBikeRide, Elliptical, Golf, - * Handcycle, Hike, IceSkate, InlineSkate, Kayaking, Kitesurf, NordicSki, Ride, RockClimbing, - * RollerSki, Rowing, Run, Sail, Skateboard, Snowboard, Snowshoe, Soccer, StairStepper, - * StandUpPaddling, Surfing, Swim, Velomobile, VirtualRide, VirtualRun, Walk, WeightTraining, - * Wheelchair, Windsurf, Workout, Yoga + * Possible values = AlpineSki, BackcountrySki, Canoeing, Crossfit, EBikeRide, + * Elliptical, Golf, Handcycle, Hike, IceSkate, InlineSkate, Kayaking, Kitesurf, + * NordicSki, Ride, RockClimbing, RollerSki, Rowing, Run, Sail, Skateboard, + * Snowboard, Snowshoe, Soccer, StairStepper, StandUpPaddling, Surfing, Swim, + * Velomobile, VirtualRide, VirtualRun, Walk, WeightTraining, Wheelchair, + * Windsurf, Workout, Yoga */ - String type = "type"; + String type = "type"; String timezone = "timezone"; - String start = "start_date_local"; //Start date measured in users time zone + String start = "start_date_local"; // Start date measured in users time zone String id = "id"; - + int pageIndex = 1; - while(true) + while (true) { - String requestExtension = "athlete/activities?before=&after=&page="+pageIndex+"&per_page="; - String json = makeOneRequest(requestExtension, token); - if (json.isEmpty()||json.isBlank()||json.equals("")) //don't know where the last page is... + String requestExtension = "athlete/activities?before=&after=&page=" + pageIndex + "&per_page="; + String json = makeOneGetRequest(requestExtension, token); + if (json.isEmpty() || json.isBlank() || json.equals("")) // don't know where the last page is... { - break; + break; } - + Object obj; try { obj = parser.parse(json); JSONArray listOfActivites = (JSONArray) obj; - for (int i=0; i which should be zipped * @throws IOException If there was an error zipping */ - static void zipAllFiles(Map files) throws IOException + static void zipAllFiles(Map files) throws IOException { - FileOutputStream fos = new FileOutputStream("data.zip"); - ZipOutputStream zipOut = new ZipOutputStream(fos); - for (String key : files.keySet()) - { - File fileToZip = files.get(key); - FileInputStream fis = new FileInputStream(fileToZip); - ZipEntry zipEntry = new ZipEntry(key); - zipOut.putNextEntry(zipEntry); - - byte[] bytes = new byte[1024]; - int length; - while((length = fis.read(bytes)) >= 0) { - zipOut.write(bytes, 0, length); - } - fis.close(); - } - zipOut.close(); - fos.close(); + FileOutputStream fos = new FileOutputStream("data.zip"); + ZipOutputStream zipOut = new ZipOutputStream(fos); + for (String key : files.keySet()) + { + File fileToZip = files.get(key); + FileInputStream fis = new FileInputStream(fileToZip); + ZipEntry zipEntry = new ZipEntry(key); + zipOut.putNextEntry(zipEntry); + + byte[] bytes = new byte[1024]; + int length; + while ((length = fis.read(bytes)) >= 0) + { + zipOut.write(bytes, 0, length); + } + fis.close(); + } + zipOut.close(); + fos.close(); } - + /** - * Handles one request to the API + * Handles one GET request to the API + * * @param requestUrlExtension Extension for the baseUrl (without '/') - * @param token Identification / authorization token of the athlete + * @param token Identification / authorization token of the + * athlete * @return The response as a String, an empty String in case of error. */ - static String makeOneRequest(String requestUrlExtension, String token) + static String makeOneGetRequest(String requestUrlExtension, String token) { - //TODO: http requests are not tested - if(testRequest!= null) + // TODO: http requests are not tested + if (testRequest != null) { String varTestRequest = testRequest.replaceAll("\\:\\s*([0-9]{15,})\\,", ":\"$1\","); - testRequest=null; + testRequest = null; return varTestRequest; } - HttpURLConnection connection = null; - StringBuilder result = new StringBuilder(); - try + HttpsURLConnection connection = null; + try { - //Create connection - URL url = new URL(baseUrl+requestUrlExtension); - connection = (HttpsURLConnection) url.openConnection(); - connection.setRequestMethod("GET"); - connection.setRequestProperty("Authorization", "Bearer [["+token+"]]"); - - int responseCode = connection.getResponseCode(); - - if (responseCode != HttpURLConnection.HTTP_OK) - { - writeError("Wrong response code: "+responseCode); - return ""; - } - try (Reader reader = new BufferedReader( - new InputStreamReader(connection.getInputStream(), Charset.forName(StandardCharsets.UTF_8.name())))) - { - int c = reader.read(); - while (c != -1) - { - result.append((char) c); - c = reader.read(); - } - } + // Create connection + URL url = new URL(baseUrl + requestUrlExtension); + connection = (HttpsURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Authorization", "Bearer " + token); + + return getResponse(connection); } - catch(IOException e) + catch (IOException e) { - writeError("Error while handling request: "+e.toString()); + writeError("Error while handling GET request: " + e.toString()); + } + return ""; + } + + /** + * Reads the response from the connection and returns it as a String + * + * @param connection Connection to the site + * @return Response as a String + * @throws IOException in case of error + */ + static String getResponse(HttpsURLConnection connection) throws IOException + { + StringBuilder result = new StringBuilder(); + int responseCode = connection.getResponseCode(); + if (responseCode != HttpURLConnection.HTTP_OK) + { + writeError("Wrong response code: " + responseCode); return ""; } - //Numbers too long for int or long are turned into Strings - return result.toString().replaceAll("\\:\\s*([0-9]{15,})\\,", ":\"$1\","); + + try (Reader reader = new BufferedReader( + new InputStreamReader(connection.getInputStream(), Charset.forName(StandardCharsets.UTF_8.name())))) + { + int c = reader.read(); + while (c != -1) + { + result.append((char) c); + c = reader.read(); + } + } + + // Numbers too long for int or long are turned into Strings + return result.toString().replaceAll("\\:\\s*([0-9]{15,})\\,", ":\"$1\","); + } + + /** + * Used for generating the accessToken + * + * @param data Triplet containing: a the client_id, b the client_secret, c the + * refresh_token + * @return The response as a String, an empty String in case of error. + */ + static String makeOnePostRequest(Triplet data) + { + // TODO: http requests are not tested + HttpsURLConnection connection = null; + try + { + // Create connection + URL url = new URL(tokenUrl); + connection = (HttpsURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + + Map params = new LinkedHashMap<>(); + params.put("client_id", data.getA()); + params.put("client_secret", data.getB()); + params.put("grant_type", "refresh_token"); + params.put("refresh_token", data.getC()); + + StringBuilder postData = new StringBuilder(); + for (String key : params.keySet()) + { + if (postData.length() != 0) + { + postData.append('&'); + } + postData.append(URLEncoder.encode(key, "UTF-8")); + postData.append('='); + postData.append(URLEncoder.encode(params.get(key), "UTF-8")); + } + byte[] postDataBytes = postData.toString().getBytes("UTF-8"); + + connection.setRequestProperty("Content-Length", String.valueOf(postDataBytes.length)); + connection.setDoOutput(true); + connection.getOutputStream().write(postDataBytes); + + return getResponse(connection); + } + catch (IOException e) + { + writeError("Error while handling POST request: " + e.toString()); + } + return ""; + } + + /** + * Sends the old refresh token to strava and retrieves a new one and an access token + * @param refreshInfo Refresh data with a the cliend_id, b the client_secret and c the refresh_token + * @return A tuple with a the access_token and b the new refresh_token or {@code null} if there was an error + */ + static Tuple getAccessToken(Triplet refreshInfo) + { + String json = makeOnePostRequest(refreshInfo); + try + { + Object obj = parser.parse(json); + JSONObject data = (JSONObject) obj; + return new Tuple(data.get("access_token").toString(), data.get("refresh_token").toString()); + } + catch (ParseException e) + { + writeError("Athlete " + athleteId + ": Error parsing general information."); + } + return null; } public static void main(String[] args) { - //TODO: tokens need to be added by Enduco - List tokens = new ArrayList<>(); - - //TODO: Should we expect problems with memory?? - Map allFiles = new HashMap<>(); - for (String token : tokens) + // TODO: tokens need to be added by Enduco + + // Triplet a is client_id, b is client_secret, c is refresh_token + List refreshTokens = new ArrayList<>(); + // don't know if you need it but here you will find the new refresh tokens after + // the requests + List newRefreshTokens = new ArrayList<>(); + + // TODO: Should we expect problems with memory?? + Map allFiles = new HashMap<>(); + for (Triplet oneUser : refreshTokens) { - File athlete = oneAthlete(token); - if(athlete != null) + // a is access_token and b is new refresh_token + Tuple withAccessToken = getAccessToken(oneUser); + if (withAccessToken == null) { - allFiles.put("Athlete_"+athleteId+".json", athlete); + writeError("Coulnd't get new access token for client "+athleteId); + } + newRefreshTokens.add(new Triplet(oneUser.getA(), oneUser.getB(), withAccessToken.getB())); + + File athlete = oneAthlete(withAccessToken.getA()); + if (athlete != null) + { + allFiles.put("Athlete_" + athleteId + ".json", athlete); } athleteId++; } @@ -340,4 +453,42 @@ public class Main writeError("Files coulnd't be zipped"); } } +} + +class Tuple +{ + private String a; + private String b; + + public Tuple(String a, String b) + { + this.a = a; + this.b = b; + } + + public String getA() + { + return a; + } + + public String getB() + { + return b; + } +} + +class Triplet extends Tuple +{ + private String c; + + public Triplet(String a, String b, String c) + { + super(a, b); + this.c = c; + } + + public String getC() + { + return c; + } } \ No newline at end of file