/*
* Copyright 2012-2013 Coronastreet Networks
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License
*/
package org.coronastreet.gpxconverter;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.swing.JTextArea;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.CookieStore;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.InputStreamBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.apache.xml.serialize.OutputFormat;
import org.apache.xml.serialize.XMLSerializer;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONException;
import org.jsoup.Jsoup;
import org.jsoup.select.Elements;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
@SuppressWarnings("deprecation")
public class StravaForm {
private String loginURL = "https://www.strava.com/login";
private String sessionURL = "https://www.strava.com/session";
private String uploadFormURL = "http://app.strava.com/upload/select";
private String uploadURL = "http://app.strava.com/upload/files";
private CloseableHttpClient httpClient;
private HttpContext localContext;
private CookieStore cookieStore;
private String email;
private String password;
private String tripName;
private String activityType;
private Document outDoc;
private String rideStartTime;
private String deviceType;
private boolean hasAltimeter = false;
private String outFile = "C:\\Temp\\temp.tcx";
private JTextArea statusTextArea;
private List<Trkpt> trackPoints;
public StravaForm() {
}
@SuppressWarnings("unused")
private String convertDoc() {
OutputFormat format = new OutputFormat(outDoc);
format.setIndenting(true);
StringWriter stringOut = new StringWriter ();
XMLSerializer serializer = new XMLSerializer(stringOut, format);
try {
serializer.serialize(outDoc);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return stringOut.toString();
}
public boolean processData() {
boolean success = false;
//load the output template
loadTCXTemplate();
setIdAndStartTime();
setDeviceType();
// Add the track data we imported to the output document
if (addTrackData()) {
success = true;
}
// Spit out the TCX file
// printOutFile();
success = true;
return success;
}
@SuppressWarnings("unused")
private void printOutFile(){
try {
OutputFormat format = new OutputFormat(outDoc);
format.setIndenting(true);
XMLSerializer serializer = new XMLSerializer(new FileOutputStream(new File(outFile)), format);
log("Writing out TCX file.");
serializer.serialize(outDoc);
} catch(IOException ie) {
ie.printStackTrace();
}
}
private void setIdAndStartTime() {
NodeList nl = outDoc.getElementsByTagName("Activity");
NodeList nl2 = ((Element) nl.item(0)).getElementsByTagName("Id");
if(nl2 != null && nl2.getLength() > 0) {
Element el = (Element)nl2.item(0);
el.appendChild(outDoc.createTextNode(rideStartTime));
}
NodeList nl3 = ((Element) nl.item(0)).getElementsByTagName("Lap");
if(nl3 != null && nl3.getLength() > 0) {
Element el = (Element)nl3.item(0);
el.setAttribute("StartTime", rideStartTime);
}
}
private Element createTrackPointElement(Trkpt tp){
Element eTrackpoint = outDoc.createElement("Trackpoint");
//create time element and time text node and attach it to the trackpoint
Element eTime = outDoc.createElement("Time");
eTime.appendChild(outDoc.createTextNode(tp.getTime()));
eTrackpoint.appendChild(eTime);
//create elevation element and elevation text node and attach it to the trackpoint
Element eElevation = outDoc.createElement("AltitudeMeters");
eElevation.appendChild(outDoc.createTextNode(tp.getElevation()));
eTrackpoint.appendChild(eElevation);
//create Speed Sensor element and attach it to the trackpoint
Element eSensorState = outDoc.createElement("SensorState");
eSensorState.appendChild(outDoc.createTextNode("Absent"));
eTrackpoint.appendChild(eSensorState);
// Create Lat/Long and add them to Position
Element ePosition = outDoc.createElement("Position");
Element eLatitudeDegrees = outDoc.createElement("LatitudeDegrees");
eLatitudeDegrees.appendChild(outDoc.createTextNode(tp.getLat()));
Element eLongitudeDegrees = outDoc.createElement("LongitudeDegrees");
eLongitudeDegrees.appendChild(outDoc.createTextNode(tp.getLon()));
ePosition.appendChild(eLongitudeDegrees);
ePosition.appendChild(eLatitudeDegrees);
eTrackpoint.appendChild(ePosition);
//create HeartRate and add it to the trackpoint
Element eHR = outDoc.createElement("HeartRateBpm");
eHR.setAttribute("xsi:type", "HeartRateInBeatsPerMinute_t");
Element eHRValue = outDoc.createElement("Value");
eHRValue.appendChild(outDoc.createTextNode(tp.getHr()));
eHR.appendChild(eHRValue);
eTrackpoint.appendChild(eHR);
//create Cadence element text node and add it to the trackpoint
Element eCad = outDoc.createElement("Cadence");
eCad.appendChild(outDoc.createTextNode(tp.getCad()));
eTrackpoint.appendChild(eCad);
//create Temperature element text node and add it to the trackpoint
Element eTemp = outDoc.createElement("Temperature");
eTemp.appendChild(outDoc.createTextNode(tp.getTemp()));
eTrackpoint.appendChild(eTemp);
return eTrackpoint;
}
private void setDeviceType() {
// Strava only recognizes Garmin devices for Altimeter stuff
// Everything else shows up as "Mobile"
if (hasAltimeter) {
deviceType = "Garmin Edge 800";
} else {
deviceType = "Garmin Edge 200";
}
NodeList nl = outDoc.getElementsByTagName("Activity");
NodeList nl1 = ((Element) nl.item(0)).getElementsByTagName("Creator");
NodeList nl2 = ((Element) nl1.item(0)).getElementsByTagName("Name");
if(nl2 != null && nl2.getLength() > 0) {
Element el = (Element)nl2.item(0);
el.appendChild(outDoc.createTextNode(deviceType));
}
}
private boolean addTrackData() {
boolean success = false;
// Get the Track element
Element track = null;
Element docEle = outDoc.getDocumentElement();
// GRabbing the track node. in theory, the node list should always return 1
NodeList nl = docEle.getElementsByTagName("Track");
if(nl != null && nl.getLength() > 0) {
track = (Element)nl.item(0);
}
int trkCounter = 0;
Iterator<Trkpt> it = trackPoints.iterator();
while(it.hasNext()) {
Trkpt t = (Trkpt)it.next();
Element tp = createTrackPointElement(t);
//dumpNode(tp);
track.appendChild(tp);
trkCounter++;
}
log("Added " + trkCounter + " trackpoints to the template.");
if (trkCounter >= 1) { success = true; }
return success;
}
public void upload() {
//httpClient = new DefaultHttpClient();
httpClient = HttpClientBuilder.create().build();
localContext = new BasicHttpContext();
cookieStore = new BasicCookieStore();
localContext.setAttribute(HttpClientContext.COOKIE_STORE, cookieStore);
//httpClient.getParams().setParameter(ClientPNames.COOKIE_POLICY, CookiePolicy.BROWSER_COMPATIBILITY);
if(doLogin()) {
//log("Ok....logged in...");
try {
// Have to fetch the form to get the CSRF Token
HttpGet get = new HttpGet(uploadFormURL);
HttpResponse formResponse = httpClient.execute(get, localContext);
//log("Fetched the upload form...: " + formResponse.getStatusLine());
org.jsoup.nodes.Document doc = Jsoup.parse(EntityUtils.toString(formResponse.getEntity()));
String csrftoken, csrfparam;
Elements metalinksParam = doc.select("meta[name=csrf-param]");
if (!metalinksParam.isEmpty()) {
csrfparam = metalinksParam.first().attr("content");
} else {
csrfparam = null;
log("Missing csrf-param?");
}
Elements metalinksToken = doc.select("meta[name=csrf-token]");
if (!metalinksToken.isEmpty()) {
csrftoken = metalinksToken.first().attr("content");
} else {
csrftoken = null;
log("Missing csrf-token?");
}
HttpPost request = new HttpPost(uploadURL);
request.setHeader("X-CSRF-Token", csrftoken);
MultipartEntity entity = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE);
entity.addPart("method", new StringBody("post"));
entity.addPart("new_uploader", new StringBody("1"));
entity.addPart(csrfparam, new StringBody(csrftoken));
entity.addPart("files[]", new InputStreamBody(document2InputStream(outDoc), "application/octet-stream", "temp.tcx"));
// Need to do this bit because without it you can't disable chunked encoding, and Strava doesn't support chunked.
ByteArrayOutputStream bArrOS = new ByteArrayOutputStream();
entity.writeTo(bArrOS);
bArrOS.flush();
ByteArrayEntity bArrEntity = new ByteArrayEntity(bArrOS.toByteArray());
bArrOS.close();
bArrEntity.setChunked(false);
bArrEntity.setContentEncoding(entity.getContentEncoding());
bArrEntity.setContentType(entity.getContentType());
request.setEntity(bArrEntity);
HttpResponse response = httpClient.execute(request, localContext);
if (response.getStatusLine().getStatusCode() != 200) {
log("Failed to Upload");
HttpEntity en = response.getEntity();
if (en != null) {
String output = EntityUtils.toString(en);
log(output);
}
} else {
HttpEntity ent = response.getEntity();
if (ent != null) {
String output = EntityUtils.toString(ent);
//log(output);
JSONObject userInfo = new JSONArray(output).getJSONObject(0);
//log("Object: " + userInfo.toString());
if (userInfo.get("workflow").equals("Error")) {
log("Upload Error: " + userInfo.get("error"));
} else {
log("Successful Uploaded. ID is " + userInfo.get("id"));
}
}
}
httpClient.close();
}catch (Exception ex) {
log("Exception? " + ex.getMessage());
ex.printStackTrace();
// handle exception here
}
} else {
log("Failed to upload!");
}
}
protected InputStream document2InputStream(Document document) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
OutputFormat outputFormat = new OutputFormat(document);
XMLSerializer serializer = new XMLSerializer(outputStream, outputFormat);
serializer.serialize(document);
return new ByteArrayInputStream(outputStream.toByteArray());
}
protected boolean doLogin() {
boolean ret = false;
log("Authenticating athlete...");
try {
HttpGet get = new HttpGet(loginURL);
HttpResponse response = httpClient.execute(get, localContext);
//log("Fetched the login form...: " + response.getStatusLine());
org.jsoup.nodes.Document doc = Jsoup.parse(EntityUtils.toString(response.getEntity()));
String csrftoken, csrfparam;
Elements metalinksParam = doc.select("meta[name=csrf-param]");
if (!metalinksParam.isEmpty()) {
csrfparam = metalinksParam.first().attr("content");
log("Setting csrf-param to " + csrfparam);
} else {
csrfparam = null;
log("Missing csrf-param?");
}
Elements metalinksToken = doc.select("meta[name=csrf-token]");
if (!metalinksToken.isEmpty()) {
csrftoken = metalinksToken.first().attr("content");
log("Setting csrf-token to " + csrftoken);
} else {
csrftoken = null;
log("Missing csrf-token?");
}
HttpPost post = new HttpPost(sessionURL);
post.setHeader("Referer", "https://www.strava.com/login");
List <NameValuePair> nvps = new ArrayList <NameValuePair>();
nvps.add(new BasicNameValuePair(csrfparam, csrftoken));
nvps.add(new BasicNameValuePair("plan", ""));
nvps.add(new BasicNameValuePair("email", email));
nvps.add(new BasicNameValuePair("password", password));
post.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));
HttpResponse sessionResponse = httpClient.execute(post, localContext);
if (sessionResponse.getStatusLine().getStatusCode() != 302) {
log("Failed to Login. " + sessionResponse.getStatusLine().getStatusCode());
String output = EntityUtils.toString(sessionResponse.getEntity());
log(output);
ret = false;
} else {
ret = true;
}
HttpEntity entity = sessionResponse.getEntity();
EntityUtils.consume(entity);
}catch (Exception ex) {
// handle exception here
ex.printStackTrace();
}
return ret;
}
private void loadTCXTemplate(){
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
try {
DocumentBuilder db = dbf.newDocumentBuilder();
log("Loading TCX template file.");
outDoc = db.parse(this.getClass().getResourceAsStream("/org/coronastreet/gpxconverter/tcxtemplate.xml"));
}catch(ParserConfigurationException pce) {
pce.printStackTrace();
}catch(SAXException se) {
se.printStackTrace();
}catch(IOException ioe) {
ioe.printStackTrace();
}
}
@SuppressWarnings("unused")
private void dumpNode(JSONObject o) throws JSONException {
log(o.toString(2));
}
private void log(String s) {
this.statusTextArea.append("STRAVA: " + s + "\n");
this.statusTextArea.repaint(1);
}
@SuppressWarnings("unused")
private void log(InputStream is) {
try {
BufferedReader rd = new BufferedReader(new InputStreamReader(is));
String line = "";
while ((line = rd.readLine()) != null) {
log(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public List<Trkpt> getTrackPoints() {
return trackPoints;
}
public void setTrackPoints(List<Trkpt> trackPoints) {
this.trackPoints = trackPoints;
}
public JTextArea getStatusTextArea() {
return statusTextArea;
}
public void setStatusTextArea(JTextArea statusTextArea) {
this.statusTextArea = statusTextArea;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getTripName() {
return tripName;
}
public void setTripName(String tripName) {
this.tripName = tripName;
}
public String getActivityType() {
return activityType;
}
public void setActivityType(String at) {
this.activityType = at;
}
public String getRideStartTime() {
return rideStartTime;
}
public void setRideStartTime(String rideStartTime) {
this.rideStartTime = rideStartTime;
}
public void setHasAltimeter(boolean hasAltimeter) {
this.hasAltimeter = hasAltimeter;
}
}