<commit_before>package com.awsmithson.tcx2nikeplus.http;

import static com.awsmithson.tcx2nikeplus.Constants.*;
import com.awsmithson.tcx2nikeplus.util.Log;
import com.awsmithson.tcx2nikeplus.util.Util;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
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.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;


public class NikePlus
{

	private static final String URL_GENERATE_PIN = "https://secure-nikerunning.nike.com/nikeplus/v2/services/app/generate_pin.jsp";
	private static final String URL_CHECK_PIN_STATUS = "https://secure-nikerunning.nike.com/nikeplus/v2/services/device/get_pin_status.jsp";
	private static final String URL_DATA_SYNC_NON_GPS = "https://secure-nikerunning.nike.com/nikeplus/v2/services/device/sync.jsp";
	private static final String URL_DATA_SYNC_GPS = "https://secure-nikerunning.nike.com/gps/sync/plus/iphone";
	private static final String URL_DATA_SYNC_COMPLETE = "https://secure-nikerunning.nike.com/nikeplus/v2/services/device/sync_complete.jsp";

	private static final String USER_AGENT = "iTunes/9.0.3 (Macintosh; N; Intel)";

	private static final Log log = Log.getInstance();

	public NikePlus() {
	}


	/**
	 * Retrieves the pin from nike+ for the given login/password..
	 * @param login The login String (email address).
	 * @param password The login password.
	 * @return Nike+ pin.
	 * @throws IOException
	 * @throws MalformedURLException
	 * @throws ParserConfigurationException
	 * @throws SAXException
	 * @throws UnsupportedEncodingException
	 */
	public String generatePin(String login, String password) throws IOException, MalformedURLException, ParserConfigurationException, SAXException, UnsupportedEncodingException {

		// Send data
		URL url = new URL(String.format("%s?%s&%s", URL_GENERATE_PIN, Util.generateHttpParameter("login", login), Util.generateHttpParameter("password", password)));
		URLConnection conn = url.openConnection();
		conn.setRequestProperty("user-agent", USER_AGENT);

		// Get the response
		DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
		Document doc = db.parse(conn.getInputStream());
		doc.normalize();
		//log.out(Util.DocumentToString(doc));

		String pinStatus = Util.getSimpleNodeValue(doc, "status");

		if (!(pinStatus.equals("success")))
			throw new IllegalArgumentException("The email and password supplied are not valid");

		return Util.getSimpleNodeValue(doc, "pin");
	}

	/**
	 * Calls fullSync converting the File objects to Document.
	 * @param pin Nike+ pin.
	 * @param runXml Nike+ workout xml.
	 * @param gpxXml Nike+ gpx xml.
	 * @throws ParserConfigurationException
	 * @throws SAXException
	 * @throws IOException
	 * @throws MalformedURLException
	 * @throws NoSuchAlgorithmException
	 * @throws KeyManagementException
	 */
	private void fullSync(String pin, File runXml, File gpxXml) throws ParserConfigurationException, SAXException, IOException, MalformedURLException, NoSuchAlgorithmException, KeyManagementException {
		DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
		DocumentBuilder db = dbf.newDocumentBuilder();
		fullSync(pin, db.parse(runXml), ((gpxXml != null) ? db.parse(gpxXml) : null));
	}

	/**
	 * Does a full synchronisation cycle (check-pin-status, sync, end-sync) with nike+ for the given pin and xml document(s).
	 * @param pin Nike+ pin.
	 * @param runXml Nike+ workout xml.
	 * @param gpxXml Nike+ gpx xml.
	 * @throws IOException
	 * @throws MalformedURLException
	 * @throws ParserConfigurationException
	 * @throws SAXException
	 * @throws NoSuchAlgorithmException
	 * @throws KeyManagementException
	 */
	public void fullSync(String pin, Document runXml, Document gpxXml) throws IOException, MalformedURLException, ParserConfigurationException, SAXException, NoSuchAlgorithmException, KeyManagementException {
		
		boolean haveGpx = (gpxXml != null);
		runXml.normalizeDocument();
		if (haveGpx) gpxXml.normalizeDocument();

		log.out("Uploading to Nike+...");
		
		try {
			log.out(" - Checking pin status...");
			checkPinStatus(pin);

			log.out(" - Syncing data...");
			Document nikeResponse = (haveGpx)
				? syncDataGps(pin, runXml, gpxXml)
				: syncDataNonGps(pin, runXml)
			;

			//<?xml version="1.0" encoding="UTF-8" standalone="no"?><plusService><status>success</status></plusService>
			if (Util.getSimpleNodeValue(nikeResponse, "status").equals(NIKE_SUCCESS)) {
				log.out(" - Sync successful.");
				return;
			}

			//<?xml version="1.0" encoding="UTF-8" standalone="no"?><plusService><status>failure</status><serviceException errorCode="InvalidRunError">snapshot duration greater than run (threshold 30000 ms): 82980</serviceException></plusService>
			NodeList nikeServiceExceptionL = nikeResponse.getElementsByTagName("serviceException");
			if ((nikeServiceExceptionL != null) && (nikeServiceExceptionL.getLength() > 0)) {
				Node nikeServiceException = nikeServiceExceptionL.item(0);
				throw new RuntimeException(String.format("%s: %s", nikeServiceException.getAttributes().item(0).getNodeValue(), Util.getSimpleNodeValue(nikeServiceException)));
			}
			else {
				log.out(Util.DocumentToString(nikeResponse));

				String nikeError = Util.getSimpleNodeValue(nikeResponse, "error");
				log.out(nikeError);
				if (nikeError.indexOf("<?xml ") == -1)
					throw new RuntimeException(String.format("Nike+ Error: %s", nikeError));

				else {
					/*
					2011-02-19 - Oh dear nikeplus...  What is this xml nonsense you are coming out with?!?  Representing an xml document within an xml node?  Tasty.
					 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
					 <response>
					 <status>failure</status>
					 <error>

					 <![CDATA[Failed to sync runXml: problem syncing runXML response from device sync service:
					 <?xml version="1.0" encoding="UTF-8"?>
					 <plusService><commit_msg>Improved (slightly) error message for nike+ server errors.<commit_after>package com.awsmithson.tcx2nikeplus.http;

import static com.awsmithson.tcx2nikeplus.Constants.*;
import com.awsmithson.tcx2nikeplus.util.Log;
import com.awsmithson.tcx2nikeplus.util.Util;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
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.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;


public class NikePlus
{

	private static final String URL_GENERATE_PIN = "https://secure-nikerunning.nike.com/nikeplus/v2/services/app/generate_pin.jsp";
	private static final String URL_CHECK_PIN_STATUS = "https://secure-nikerunning.nike.com/nikeplus/v2/services/device/get_pin_status.jsp";
	private static final String URL_DATA_SYNC_NON_GPS = "https://secure-nikerunning.nike.com/nikeplus/v2/services/device/sync.jsp";
	private static final String URL_DATA_SYNC_GPS = "https://secure-nikerunning.nike.com/gps/sync/plus/iphone";
	private static final String URL_DATA_SYNC_COMPLETE = "https://secure-nikerunning.nike.com/nikeplus/v2/services/device/sync_complete.jsp";

	private static final String USER_AGENT = "iTunes/9.0.3 (Macintosh; N; Intel)";

	private static final Log log = Log.getInstance();

	public NikePlus() {
	}


	/**
	 * Retrieves the pin from nike+ for the given login/password..
	 * @param login The login String (email address).
	 * @param password The login password.
	 * @return Nike+ pin.
	 * @throws IOException
	 * @throws MalformedURLException
	 * @throws ParserConfigurationException
	 * @throws SAXException
	 * @throws UnsupportedEncodingException
	 */
	public String generatePin(String login, String password) throws IOException, MalformedURLException, ParserConfigurationException, SAXException, UnsupportedEncodingException {

		// Send data
		URL url = new URL(String.format("%s?%s&%s", URL_GENERATE_PIN, Util.generateHttpParameter("login", login), Util.generateHttpParameter("password", password)));
		URLConnection conn = url.openConnection();
		conn.setRequestProperty("user-agent", USER_AGENT);

		// Get the response
		DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
		Document doc = db.parse(conn.getInputStream());
		doc.normalize();
		//log.out(Util.DocumentToString(doc));

		String pinStatus = Util.getSimpleNodeValue(doc, "status");

		if (!(pinStatus.equals("success")))
			throw new IllegalArgumentException("The email and password supplied are not valid");

		return Util.getSimpleNodeValue(doc, "pin");
	}

	/**
	 * Calls fullSync converting the File objects to Document.
	 * @param pin Nike+ pin.
	 * @param runXml Nike+ workout xml.
	 * @param gpxXml Nike+ gpx xml.
	 * @throws ParserConfigurationException
	 * @throws SAXException
	 * @throws IOException
	 * @throws MalformedURLException
	 * @throws NoSuchAlgorithmException
	 * @throws KeyManagementException
	 */
	private void fullSync(String pin, File runXml, File gpxXml) throws ParserConfigurationException, SAXException, IOException, MalformedURLException, NoSuchAlgorithmException, KeyManagementException {
		DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
		DocumentBuilder db = dbf.newDocumentBuilder();
		fullSync(pin, db.parse(runXml), ((gpxXml != null) ? db.parse(gpxXml) : null));
	}

	/**
	 * Does a full synchronisation cycle (check-pin-status, sync, end-sync) with nike+ for the given pin and xml document(s).
	 * @param pin Nike+ pin.
	 * @param runXml Nike+ workout xml.
	 * @param gpxXml Nike+ gpx xml.
	 * @throws IOException
	 * @throws MalformedURLException
	 * @throws ParserConfigurationException
	 * @throws SAXException
	 * @throws NoSuchAlgorithmException
	 * @throws KeyManagementException
	 */
	public void fullSync(String pin, Document runXml, Document gpxXml) throws IOException, MalformedURLException, ParserConfigurationException, SAXException, NoSuchAlgorithmException, KeyManagementException {
		
		boolean haveGpx = (gpxXml != null);
		runXml.normalizeDocument();
		if (haveGpx) gpxXml.normalizeDocument();

		log.out("Uploading to Nike+...");
		
		try {
			log.out(" - Checking pin status...");
			checkPinStatus(pin);

			log.out(" - Syncing data...");
			Document nikeResponse = (haveGpx)
				? syncDataGps(pin, runXml, gpxXml)
				: syncDataNonGps(pin, runXml)
			;

			//<?xml version="1.0" encoding="UTF-8" standalone="no"?><plusService><status>success</status></plusService>
			if (Util.getSimpleNodeValue(nikeResponse, "status").equals(NIKE_SUCCESS)) {
				log.out(" - Sync successful.");
				return;
			}

			//<?xml version="1.0" encoding="UTF-8" standalone="no"?><plusService><status>failure</status><serviceException errorCode="InvalidRunError">snapshot duration greater than run (threshold 30000 ms): 82980</serviceException></plusService>
			NodeList nikeServiceExceptionL = nikeResponse.getElementsByTagName("serviceException");
			if ((nikeServiceExceptionL != null) && (nikeServiceExceptionL.getLength() > 0)) {
				Node nikeServiceException = nikeServiceExceptionL.item(0);
				throw new RuntimeException(String.format("%s: %s", nikeServiceException.getAttributes().item(0).getNodeValue(), Util.getSimpleNodeValue(nikeServiceException)));
			}
			else {
				log.out(Util.DocumentToString(nikeResponse));

				String nikeError = Util.getSimpleNodeValue(nikeResponse, "error");
				log.out(nikeError);
				if (nikeError.indexOf("<?xml ") == -1)
					throw new RuntimeException(String.format("Nike+ Error: %s.\nThis is likely to be a problem at Nike+'s end.\nPlease try again later, contact me if the problem persists.", nikeError));

				else {
					/*
					2011-02-19 - Oh dear nikeplus...  What is this xml nonsense you are coming out with?!?  Representing an xml document within an xml node?  Tasty.
					 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
					 <response>
					 <status>failure</status>
					 <error>

					 <![CDATA[Failed to sync runXml: problem syncing runXML response from device sync service:
					 <?xml version="1.0" encoding="UTF-8"?>
					 <plusService>
{'fraction_non_alphanumeric': 0.11879866518353727, 'fraction_numerical': 0.007119021134593994, 'mean_word_length': 5.080252479711452, 'pattern_counts': {'":': 0, '<': 45, '<?xml version=': 8, '>': 43, 'https://': 10, 'lorem ipsum': 0, 'www.': 0, 'xml': 34}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6809924', 'n_tokens_mistral': 4386, 'n_tokens_neox': 3966, 'n_words': 928}