/*
* 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 java.util.regex.Matcher;
import java.util.regex.Pattern;
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.HttpHead;
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.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.nodes.Node;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
@SuppressWarnings("deprecation")
public class GarminForm {
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 String totalTimeInSeconds;
private String distanceMeters;
private String maximumSpeed;
private boolean hasAltimeter = false;
private String outFile = "C:\\Temp\\temp.tcx";
private JTextArea statusTextArea;
private List<Trkpt> trackPoints;
public GarminForm() {
}
@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();
setDistanceAndTime();
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;
}
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 void setDistanceAndTime() {
NodeList nl = outDoc.getElementsByTagName("Activity");
NodeList nl1 = ((Element) nl.item(0)).getElementsByTagName("Lap");
NodeList nl2 = ((Element) nl1.item(0)).getElementsByTagName("TotalTimeSeconds");
if(nl2 != null && nl2.getLength() > 0) {
Element el = (Element)nl2.item(0);
el.getFirstChild().setNodeValue(totalTimeInSeconds);
}
NodeList nl3 = ((Element) nl.item(0)).getElementsByTagName("DistanceMeters");
if(nl3 != null && nl3.getLength() > 0) {
Element el = (Element)nl3.item(0);
el.getFirstChild().setNodeValue(distanceMeters);
}
}
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;
}
private static String findFlowKey(Node node) {
String key = null;
for (int i = 0; i < node.childNodes().size();) {
Node child = node.childNode(i);
if (child.nodeName().equals("#comment")) {
//System.out.println(child.toString());
String flowKeyPattern = "\\<\\!-- flowExecutionKey\\: \\[(e1s1)\\] --\\>";
key = child.toString().replaceAll(flowKeyPattern, "$1").trim();
break;
} else {
findFlowKey(child);
i++;
}
}
return key;
}
public void upload() {
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()) {
try {
HttpGet get = new HttpGet("http://connect.garmin.com/transfer/upload#");
HttpResponse formResponse = httpClient.execute(get, localContext);
HttpEntity formEntity = formResponse.getEntity();
EntityUtils.consume(formEntity);
HttpPost request = new HttpPost("http://connect.garmin.com/proxy/upload-service-1.1/json/upload/.tcx");
request.setHeader("Referer", "http://connect.garmin.com/api/upload/widget/manualUpload.faces?uploadServiceVersion=1.1");
request.setHeader("Accept", "text/html, application/xhtml+xml, */*");
MultipartEntity entity = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE);
entity.addPart("data", new InputStreamBody(document2InputStream(outDoc), "application/octet-stream", "temp.tcx"));
// Need to do this bit because without it you can't disable chunked encoding
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);
output = "[" + output + "]"; //OMG Garmin Sucks at JSON.....
JSONObject uploadResponse = new JSONArray(output).getJSONObject(0);
JSONObject importResult = uploadResponse.getJSONObject("detailedImportResult");
try {
int uploadID = importResult.getInt("uploadId");
log("Success! UploadID is " + uploadID);
} catch (Exception e) {
JSONArray failures = (JSONArray)importResult.get("failures");
JSONObject failure = (JSONObject)failures.get(0);
JSONArray errorMessages = failure.getJSONArray("messages");
JSONObject errorMessage = errorMessages.getJSONObject(0);
String content = errorMessage.getString("content");
log("Upload Failed! Error: " + content);
}
}
}
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...");
String gauthURL = "https://sso.garmin.com/sso/login?service=http%3A%2F%2Fconnect.garmin.com%2Fpost-auth%2Flogin&webhost=olaxpw-connect07.garmin.com&source=http%3A%2F%2Fconnect.garmin.com%2Fde-DE%2Fsignin&redirectAfterAccountLoginUrl=http%3A%2F%2Fconnect.garmin.com%2Fpost-auth%2Flogin&redirectAfterAccountCreationUrl=http%3A%2F%2Fconnect.garmin.com%2Fpost-auth%2Flogin&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso&locale=de&id=gauth-widget&cssUrl=https%3A%2F%2Fstatic.garmincdn.com%2Fcom.garmin.connect%2Fui%2Fsrc-css%2Fgauth-custom.css&clientId=GarminConnect&rememberMeShown=true&rememberMeChecked=false&createAccountShown=true&openCreateAccount=false&usernameShown=true&displayNameShown=false&consumeServiceTicket=false&initialFocus=true&embedWidget=false";
try {
HttpGet get = new HttpGet(gauthURL);
HttpResponse formResponse = httpClient.execute(get, localContext);
//log("Fetched the gauth url...: " + formResponse.getStatusLine());
String out = EntityUtils.toString(formResponse.getEntity());
org.jsoup.nodes.Document doc = Jsoup.parse(out);
//System.out.println("RAW:\n" + out);
String flowKey = findFlowKey(doc);
//log("Looks like our Key is " + flowKey);
HttpPost post = new HttpPost(gauthURL);
post.setHeader("Referer", "https://sso.garmin.com/sso/login");
List <NameValuePair> nvps = new ArrayList <NameValuePair>();
nvps.add(new BasicNameValuePair("lt", flowKey));
nvps.add(new BasicNameValuePair("embed", "true"));
nvps.add(new BasicNameValuePair("username", GPXConverter.getPref("garmin_username")));
nvps.add(new BasicNameValuePair("password", AccountManager.decrypt(GPXConverter.getPref("garmin_password"))));
nvps.add(new BasicNameValuePair("_eventId", "submit"));
post.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));
HttpResponse sessionResponse = httpClient.execute(post, localContext);
String output = EntityUtils.toString(sessionResponse.getEntity());
Pattern ticketPattern = Pattern.compile("= '(http.*ticket=.*)';");
Matcher m = ticketPattern.matcher(output);
String ticketURL = null;
while (m.find()) { ticketURL = m.group(1); }
//log("Ticket? " + ticketURL);
HttpEntity entity = sessionResponse.getEntity();
EntityUtils.consume(entity);
HttpHead head = new HttpHead(ticketURL);
HttpResponse headResponse = httpClient.execute(head, localContext);
if (headResponse.getStatusLine().getStatusCode() == 200) {
ret = true;
}
HttpEntity ent = headResponse.getEntity();
EntityUtils.consume(ent);
} catch (Exception e) {
e.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("GARMIN: " + 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 String getTotalTimeInSeconds() {
return totalTimeInSeconds;
}
public void setTotalTimeInSeconds(String totalTimeInSeconds) {
this.totalTimeInSeconds = totalTimeInSeconds;
}
public String getDistanceMeters() {
return distanceMeters;
}
public void setDistanceMeters(String distanceMeters) {
this.distanceMeters = distanceMeters;
}
public String getMaximumSpeed() {
return maximumSpeed;
}
public void setMaximumSpeed(String maximumSpeed) {
this.maximumSpeed = maximumSpeed;
}
public void setHasAltimeter(boolean hasAltimeter) {
this.hasAltimeter = hasAltimeter;
}
}