package org.fluxtream.connectors.bodymedia; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.TimeZone; import org.fluxtream.core.TimezoneMap; import org.fluxtream.core.aspects.FlxLogger; import org.fluxtream.core.connectors.Autonomous; import org.fluxtream.core.connectors.ObjectType; import org.fluxtream.core.connectors.SignpostOAuthHelper; import org.fluxtream.core.connectors.annotations.Updater; import org.fluxtream.core.connectors.updaters.AbstractUpdater; import org.fluxtream.core.connectors.updaters.UpdateFailedException; import org.fluxtream.core.connectors.updaters.UpdateInfo; import org.fluxtream.core.domain.AbstractFacet; import org.fluxtream.core.domain.ApiKey; import org.fluxtream.core.services.ApiDataService; import org.fluxtream.core.services.MetadataService; import org.fluxtream.core.utils.TimeUtils; import org.fluxtream.core.utils.Utils; import net.sf.json.JSONArray; import net.sf.json.JSONObject; import oauth.signpost.OAuthConsumer; import oauth.signpost.commonshttp.CommonsHttpOAuthConsumer; import oauth.signpost.exception.OAuthCommunicationException; import oauth.signpost.exception.OAuthExpectationFailedException; import oauth.signpost.exception.OAuthMessageSignerException; import oauth.signpost.exception.OAuthNotAuthorizedException; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.ResponseHandler; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.BasicResponseHandler; import org.joda.time.DateTime; import org.joda.time.DateTimeComparator; import org.joda.time.DateTimeConstants; import org.joda.time.DateTimeZone; import org.joda.time.LocalDate; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; @Component @Updater(prettyName = "BodyMedia", value = 88, objectTypes = {BodymediaBurnFacet.class, BodymediaSleepFacet.class, BodymediaStepsFacet.class}, hasFacets = true, defaultChannels = {"BodyMedia.mets", "BodyMedia.lying"}) public class BodymediaUpdater extends AbstractUpdater implements Autonomous { private static final long DEFAULT_RATE_DELAY = 500L; static FlxLogger logger = FlxLogger.getLogger(AbstractUpdater.class); @Autowired SignpostOAuthHelper signpostHelper; @Qualifier("metadataServiceImpl") @Autowired MetadataService metadataService; @Autowired BodymediaController bodymediaController; private final HashMap<ObjectType, String> url = new HashMap<ObjectType, String>(); private final HashMap<ObjectType, Integer> maxIncrement = new HashMap<ObjectType, Integer>(); private final DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyyMMdd"); DateTimeFormatter form = DateTimeFormat.forPattern("yyyyMMdd'T'HHmmssZ"); protected static DateTimeFormatter tzmapFormatter = DateTimeFormat.forPattern("yyyyMMdd'T'HHmmssZ"); private Long timeOfLastCall; public BodymediaUpdater() { super(); ObjectType burn = ObjectType.getObjectType(connector(), "burn"); ObjectType sleep = ObjectType.getObjectType(connector(), "sleep"); ObjectType steps = ObjectType.getObjectType(connector(), "steps"); url.put(burn, "burn/day/minute/intensity/"); url.put(sleep, "sleep/day/period/"); url.put(steps, "step/day/hour/"); maxIncrement.put(burn, 1); maxIncrement.put(sleep, 1); maxIncrement.put(steps, 31); } public void updateConnectorDataHistory(UpdateInfo updateInfo) throws Exception, UpdateFailedException { if (guestService.getApiKey(updateInfo.apiKey.getId())==null) { logger.info("Not updating BodyMedia connector instance with a deleted apiKeyId"); return; } // There's no difference between the initial history update and the incremental updates, so // just call updateConnectorData in either case updateConnectorData(updateInfo); } public void updateConnectorData(UpdateInfo updateInfo) throws Exception, UpdateFailedException { if (guestService.getApiKey(updateInfo.apiKey.getId())==null) { logger.info("Not updating BodyMedia connector instance with a deleted apiKeyId"); return; } // Check if the token has expired, and if so try to exchange it checkAndReplaceOauthToken(updateInfo); // Get timezone map for this user TimezoneMap tzMap = getTimezoneMap(updateInfo); // Insert tzMap into the updateInfo context so it's accessible to the extractors updateInfo.setContext("tzMap", tzMap); for (ObjectType ot : connector().objectTypes()) { // Get the start date either from user registration info or from the stored apiKeyAttributes. // Set the end date to be today + 1 to cover the case of people in timezones which are later // than the timezone of the server where it's already the next day DateTime start = getStartDate(updateInfo, ot); DateTime end = new DateTime().plusDays(1); retrieveHistory(updateInfo, ot, start, end); } } private void checkAndReplaceOauthToken(UpdateInfo updateInfo) throws OAuthExpectationFailedException, OAuthMessageSignerException, OAuthNotAuthorizedException, OAuthCommunicationException, UpdateFailedException { String time = guestService.getApiKeyAttribute(updateInfo.apiKey, "tokenExpiration"); if(time==null || Long.parseLong(time) < System.currentTimeMillis()/1000) bodymediaController.replaceToken(updateInfo); } /** * Retrieves that history for the given facet from the start date to the end date. It peforms the api calls in reverse order * starting from the end date. This is so that the most recent information is retrieved first. * @param updateInfo The api's info * @param ot The ObjectType that represents the facet to be updated * @param start The earliest date for which the burn history is retrieved. This date is included in the update. * @param end The latest date for which the burn history is retrieved. This date is also included in the update. * @throws Exception If either storing the data fails or if the rate limit is reached on Bodymedia's api */ private void retrieveHistory(UpdateInfo updateInfo, ObjectType ot, DateTime start, DateTime end) throws Exception { final String urlExtension = url.get(ot); final int increment = maxIncrement.get(ot); DateTimeComparator comparator = DateTimeComparator.getDateOnlyInstance(); DateTime current = start; // Setup the rate delay if we haven't already Long rateDelay = getRateDelay(updateInfo); try { // Loop from start to end, incrementing by the max number of days you can // specify for a given type of query. This is 1 for burn and sleep, and 31 for steps. //@ loop_invariant date.compareTo(userRegistrationDate) >= 0; while (comparator.compare(current, end) < 0) { if (guestService.getApiKey(updateInfo.apiKey.getId())==null) { logger.info("Not updating BodyMedia connector instance with a deleted apiKeyId"); return; } String startPeriod = current.toString(formatter); String endPeriod = current.plusDays(increment - 1).toString(formatter); String minutesUrl = "http://api.bodymedia.com/v2/json/" + urlExtension + startPeriod + "/" + endPeriod + "?api_key=" + guestService.getApiKeyAttribute(updateInfo.apiKey, "bodymediaConsumerKey"); //The following call may fail due to bodymedia's api. That is expected behavior enforceRateLimits(rateDelay); String json = signpostHelper.makeRestCall(updateInfo.apiKey, ot.value(), minutesUrl); guestService.setApiKeyAttribute(updateInfo.apiKey, "timeOfLastCall", String.valueOf(System.currentTimeMillis())); JSONObject bodymediaResponse = JSONObject.fromObject(json); JSONArray daysArray = bodymediaResponse.getJSONArray("days"); if(bodymediaResponse.has("lastSync")) { DateTime d = form.parseDateTime(bodymediaResponse.getJSONObject("lastSync").getString("dateTime")); // Get timezone map from UpdateInfo context TimezoneMap tzMap = (TimezoneMap)updateInfo.getContext("tzMap"); // Insert lastSync into the updateInfo context so it's accessible to the updater updateInfo.setContext("lastSync", d); List<AbstractFacet> newFacets = new ArrayList<AbstractFacet>(); for(Object o : daysArray) { if(o instanceof JSONObject) { if (ot==ObjectType.getObjectType(connector(), "steps")) newFacets.add(createOrUpdateStepsFacet((JSONObject)o, updateInfo, d, tzMap)); else if (ot==ObjectType.getObjectType(connector(), "burn")) newFacets.add(createOrUpdateBurnFacet((JSONObject)o, updateInfo, d, tzMap)); else newFacets.add(createOrUpdateSleepFacet((JSONObject)o, updateInfo, d, tzMap)); } } bodyTrackStorageService.storeApiData(updateInfo.apiKey, newFacets); } current = current.plusDays(increment); // Update the stored value that controls when we will start updating next time updateStartDate(updateInfo,ot,current); } } catch (Exception e) { StringBuilder sb = new StringBuilder("module=updateQueue component=updater action=BodymediaUpdater.retrieveHistory") .append(" message=\"exception while retrieving history\" connector=") .append(updateInfo.apiKey.getConnector().toString()) .append(" guestId=").append(updateInfo.apiKey.getGuestId()) .append(" updatingDate=").append(current); logger.info(sb.toString()); // Update the stored value that controls when we will start updating next time updateStartDate(updateInfo,ot,current); // Rethrow the error so that this task gets rescheduled throw e; } } /** * make sure that there is at least DEFAULT_RATE_DELAY ms between each API call. This works * because Updaters are singletons */ private void enforceRateLimits(Long rateDelay) { long waitTime = getWaitTime(rateDelay); if (waitTime>0) { try { Thread.currentThread().sleep(waitTime); } catch(Throwable e) { e.printStackTrace(); throw new RuntimeException("Unexpected error waiting to enforce rate limits."); } } setCallTime(); } private Long getRateDelay(UpdateInfo updateInfo) { Long rateDelay = (Long)updateInfo.getContext("rateDelay"); if (rateDelay != null) { return rateDelay; } // Rate delay not yet set, set to conservative default rateDelay = (Long)DEFAULT_RATE_DELAY; // Check if it's in attributes, and if so use that try { String rateDelayStr = guestService.getApiKeyAttribute(updateInfo.apiKey, "bodymediaRateDelayMs"); if(rateDelayStr==null) { // Not in key attributes, check oauth.properties. This is a hack because // this parameter didn't used to be stored with BodyMedia keys. This will cause // problems in the case where there are connector instances low-rate keys stored // without bodymediaRateDelayMs and bodymediaRateDelayMs is set lower in oauth.properties with // a new higher-rate key, but this should only be a temporary issue for systems that have // pre-0.9.0022 BodyMedia connector instances rateDelayStr = env.get("bodymediaRateDelayMs"); } if (rateDelayStr != null) { rateDelay = Long.valueOf(rateDelayStr); } } catch (Throwable e) { // If it didn't parse right, just use the default } // Update the updateInfo context so we can just return the precomputed value next time updateInfo.setContext("rateDelay", rateDelay); return rateDelay; } private long getWaitTime(Long rateDelay) { final long millisSinceLastCall = getMillisSinceLastCall(); if (millisSinceLastCall ==-1) return -1; // This assumes that setupRateDelay has already been called and // will die with a null pointer exception if that's not the case. // Unfortunately we can't call setupRateDelay here since we // don't have an apiKey else return rateDelay -millisSinceLastCall; } /** * Thread-safely return the time since we last made an API call * @return last time of API call */ private synchronized long getMillisSinceLastCall() { if (timeOfLastCall==null) return -1; return System.currentTimeMillis()-timeOfLastCall; } /** * Thread-safely save the time; this called each time we make an API call */ private synchronized void setCallTime() { timeOfLastCall = System.currentTimeMillis(); } private BodymediaSleepFacet createOrUpdateSleepFacet(final JSONObject day, final UpdateInfo updateInfo, final DateTime d, final TimezoneMap tzMap) throws Exception { final DateTime date = formatter.parseDateTime(day.getString("date")); final String dateString = TimeUtils.dateFormatter.print(date.getMillis()); final ApiDataService.FacetQuery facetQuery = new ApiDataService.FacetQuery("e.apiKeyId=? AND e.date=?", updateInfo.apiKey.getId(), dateString); final ApiDataService.FacetModifier<BodymediaSleepFacet> facetModifier = new ApiDataService.FacetModifier<BodymediaSleepFacet>() { @Override public BodymediaSleepFacet createOrModify(BodymediaSleepFacet facet, final Long apiKeyId) { if (facet == null) { facet = new BodymediaSleepFacet(updateInfo.apiKey.getId()); facet.guestId = updateInfo.apiKey.getGuestId(); facet.api = updateInfo.apiKey.getConnector().value(); facet.timeUpdated = System.currentTimeMillis(); } facet.efficiency = day.getDouble("efficiency"); facet.totalLying = day.getInt("totalLying"); facet.totalSleeping = day.getInt("totalSleep"); facet.json = day.getString("sleepPeriods"); facet.lastSync = d.getMillis(); facet.date = dateString; //https://developer.bodymedia.com/docs/read/api_reference_v2/Sleep_Service // sleep data is from noon the previous day to noon the current day, // so subtract MILLIS_IN_DAY/2 from midnight long MILLIS_IN_DAY = 86400000l; if(tzMap!=null) { // Create a LocalDate object which just captures the date without any // timezone assumptions LocalDate ld = new LocalDate(date.getYear(),date.getMonthOfYear(),date.getDayOfMonth()); // Use tzMap to convert date into a datetime with timezone information DateTime realDateStart = tzMap.getStartOfDate(ld); // Set the start and end times for the facet. The start time is the leading midnight // of burn.date according to BodyMedia's idea of what timezone you were in then. // End should, I think, be start + the number of minutes in the minutes array * // the number of milliseconds in a minute. facet.date = TimeUtils.dateFormatter.print(realDateStart.getMillis()); facet.start = realDateStart.getMillis() - DateTimeConstants.MILLIS_PER_DAY/2; facet.end = realDateStart.getMillis() + DateTimeConstants.MILLIS_PER_DAY/2; } else { facet.date = TimeUtils.dateFormatter.print(date.getMillis()); TimeZone timeZone = metadataService.getTimeZone(updateInfo.getGuestId(), date.getMillis()); long fromNoon = TimeUtils.fromMidnight(date.getMillis(), timeZone) - MILLIS_IN_DAY / 2; long toNoon = TimeUtils.toMidnight(date.getMillis(), timeZone) - MILLIS_IN_DAY / 2; facet.start = fromNoon; facet.end = toNoon; } return facet; } }; return apiDataService.createOrReadModifyWrite(BodymediaSleepFacet.class, facetQuery, facetModifier, updateInfo.apiKey.getId()); } private BodymediaBurnFacet createOrUpdateBurnFacet(final JSONObject day, final UpdateInfo updateInfo, final DateTime d, final TimezoneMap tzMap) throws Exception { final DateTime date = formatter.parseDateTime(day.getString("date")); final String dateString = TimeUtils.dateFormatter.print(date.getMillis()); final ApiDataService.FacetQuery facetQuery = new ApiDataService.FacetQuery("e.apiKeyId=? AND e.date=?", updateInfo.apiKey.getId(), dateString); final ApiDataService.FacetModifier<BodymediaBurnFacet> facetModifier = new ApiDataService.FacetModifier<BodymediaBurnFacet>() { @Override public BodymediaBurnFacet createOrModify(BodymediaBurnFacet facet, final Long apiKeyId) { if (facet == null) { facet = new BodymediaBurnFacet(updateInfo.apiKey.getId()); facet.guestId = updateInfo.apiKey.getGuestId(); facet.api = updateInfo.apiKey.getConnector().value(); facet.timeUpdated = System.currentTimeMillis(); } facet.setTotalCalories(day.getInt("totalCalories")); facet.setEstimatedCalories(day.getInt("estimatedCalories")); facet.setPredictedCalories(day.getInt("predictedCalories")); facet.json = day.getString("minutes"); facet.lastSync = d.getMillis(); facet.date = dateString; if(tzMap!=null) { // Create a LocalDate object which just captures the date without any // timezone assumptions LocalDate ld = new LocalDate(date.getYear(),date.getMonthOfYear(),date.getDayOfMonth()); // Use tzMap to convert date into a datetime with timezone information DateTime realDateStart = tzMap.getStartOfDate(ld); // Set the start and end times for the facet. The start time is the leading midnight // of burn.date according to BodyMedia's idea of what timezone you were in then. // End should, I think, be start + the number of minutes in the minutes array * // the number of milliseconds in a minute. facet.start = realDateStart.getMillis(); int minutesLength = 1440; facet.end = facet.start + DateTimeConstants.MILLIS_PER_MINUTE * minutesLength; } else { // This is the old code from Prasanth that uses metadataService, which isn't right TimeZone timeZone = metadataService.getTimeZone(updateInfo.getGuestId(), date.getMillis()); long fromMidnight = TimeUtils.fromMidnight(date.getMillis(), timeZone); long toMidnight = TimeUtils.toMidnight(date.getMillis(), timeZone); //Sets the start and end times for the facet so that it can be uniquely defined facet.start = fromMidnight; facet.end = toMidnight; } return facet; } }; return apiDataService.createOrReadModifyWrite(BodymediaBurnFacet.class, facetQuery, facetModifier, updateInfo.apiKey.getId()); } private BodymediaStepsFacet createOrUpdateStepsFacet(final JSONObject day, final UpdateInfo updateInfo, final DateTime d, final TimezoneMap tzMap) throws Exception { final DateTime date = formatter.parseDateTime(day.getString("date")); final String dateString = TimeUtils.dateFormatter.print(date.getMillis()); final ApiDataService.FacetQuery facetQuery = new ApiDataService.FacetQuery("e.apiKeyId=? AND e.date=?", updateInfo.apiKey.getId(), dateString); final ApiDataService.FacetModifier<BodymediaStepsFacet> facetModifier = new ApiDataService.FacetModifier<BodymediaStepsFacet>() { @Override public BodymediaStepsFacet createOrModify(BodymediaStepsFacet facet, final Long apiKeyId) { if (facet == null) { facet = new BodymediaStepsFacet(updateInfo.apiKey.getId()); facet.guestId = updateInfo.apiKey.getGuestId(); facet.api = updateInfo.apiKey.getConnector().value(); facet.timeUpdated = System.currentTimeMillis(); } facet.totalSteps = day.getInt("totalSteps"); facet.json = day.getString("hours"); facet.lastSync = d.getMillis(); facet.date = dateString; if(tzMap!=null) { // Create a LocalDate object which just captures the date without any // timezone assumptions LocalDate ld = new LocalDate(date.getYear(),date.getMonthOfYear(),date.getDayOfMonth()); // Use tzMap to convert date into a datetime with timezone information DateTime realDateStart = tzMap.getStartOfDate(ld); // Set the start and end times for the facet. The start time is the leading midnight // of date according to BodyMedia's idea of what timezone you were in then. // Need to figure out what end should be... facet.start = realDateStart.getMillis(); int minutesLength = 1440; facet.end = facet.start + DateTimeConstants.MILLIS_PER_MINUTE * minutesLength; } else { TimeZone timeZone = metadataService.getTimeZone(updateInfo.getGuestId(), date.getMillis()); long fromMidnight = TimeUtils.fromMidnight(date.getMillis(), timeZone); long toMidnight = TimeUtils.toMidnight(date.getMillis(), timeZone); facet.start = fromMidnight; facet.end = toMidnight; } return facet; } }; return apiDataService.createOrReadModifyWrite(BodymediaStepsFacet.class, facetQuery, facetModifier, updateInfo.apiKey.getId()); } // Update the start date for next time. The parameter the updateProgressDate is the date // of that retrieveHistory had gotten to when it completed or gave up. // If lastSync is set and is < updateProgressDate we will use that, and otherwise use updateProgressDate. void updateStartDate(UpdateInfo updateInfo, ObjectType ot, DateTime updateProgressTime) { DateTimeComparator comparator = DateTimeComparator.getDateOnlyInstance(); // Calculate the name of the key in the ApiAttributes table // where the next start of update for this object type is // stored and retrieve the stored value. This stored value // may potentially be null if something happened to the attributes table String updateKeyName = "BodyMedia." + ot.getName() + ".updateStartDate"; String storedUpdateStartDate = guestService.getApiKeyAttribute(updateInfo.apiKey, updateKeyName); // Retrieve the lastSync date if it has been added to the // updateInfo context by an extractor DateTime lastSync = (DateTime)updateInfo.getContext("lastSync"); // Check which is earlier: the lastSync time returned from Bodymedia or the // point we were in the update that just ended. Store the lesser of the two // in nextUpdateStartDate String nextUpdateStartDate = storedUpdateStartDate; if (lastSync != null) { if (comparator.compare(updateProgressTime, lastSync) > 0) { // lastSync from Bodymedia is less than the update progress nextUpdateStartDate = lastSync.toString(formatter); } else { // the update progress is less than lastSync from Bodymedia nextUpdateStartDate = updateProgressTime.toString(formatter); } } else { // Last sync is null, just leave the stored updateTime // alone since it's better to get some extra data next // time than to skip data from dates that potentially changed } // Store the new value if it's different than what's stored in ApiKeyAttributes if(storedUpdateStartDate==null || !nextUpdateStartDate.equals(storedUpdateStartDate)) { guestService.setApiKeyAttribute(updateInfo.apiKey, updateKeyName, nextUpdateStartDate); } } OAuthConsumer setupConsumer(ApiKey apiKey) { String api_key = guestService.getApiKeyAttribute(apiKey, "bodymediaConsumerKey"); String bodymediaConsumerSecret = guestService.getApiKeyAttribute(apiKey, "bodymediaConsumerSecret"); OAuthConsumer consumer = new CommonsHttpOAuthConsumer(api_key, bodymediaConsumerSecret); String accessToken = guestService.getApiKeyAttribute(apiKey, "accessToken"); String tokenSecret = guestService.getApiKeyAttribute(apiKey, "tokenSecret"); consumer.setTokenWithSecret(accessToken, tokenSecret); return consumer; } public DateTime getStartDate(UpdateInfo updateInfo, ObjectType ot) throws Exception { ApiKey apiKey = updateInfo.apiKey; // The updateStartDate for a given object type is stored in the apiKeyAttributes // as BodyMedia.<objectName>.updateStartDate. In the case of a failure the updater will store the date // that failed and start there next time. In the case of a successfully completed update it will store // the lastSync date returned from BodyMedia along with each API call. String updateKeyName = "BodyMedia." + ot.getName() + ".updateStartDate"; String updateStartDate = guestService.getApiKeyAttribute(apiKey, updateKeyName); // The first time we do this there won't be an apiKeyAttribute yet. In that case get the // registration date for the user and store that. if(updateStartDate == null) { OAuthConsumer consumer = setupConsumer(updateInfo.apiKey); String api_key = guestService.getApiKeyAttribute(updateInfo.apiKey, "bodymediaConsumerKey"); updateStartDate = getUserRegistrationDate(updateInfo, api_key, consumer); // Store in the apiKeyAttribute for next time guestService.setApiKeyAttribute(updateInfo.apiKey, updateKeyName, updateStartDate); } try{ return formatter.parseDateTime(updateStartDate); } catch (IllegalArgumentException e){ return TimeUtils.dateFormatter.parseDateTime(updateStartDate); } } public String getUserRegistrationDate(UpdateInfo updateInfo, String api_key, OAuthConsumer consumer) throws Exception { // dev only: artificially make history shorter //return "20130810"; long then = System.currentTimeMillis(); String requestUrl = "http://api.bodymedia.com/v2/json/user/info?api_key=" + api_key; HttpGet request = new HttpGet(requestUrl); consumer.sign(request); HttpClient client = env.getHttpClient(); enforceRateLimits(getRateDelay(updateInfo)); HttpResponse response = client.execute(request); int statusCode = response.getStatusLine().getStatusCode(); final String reasonPhrase = response.getStatusLine().getReasonPhrase(); if (statusCode == 200) { countSuccessfulApiCall(updateInfo.apiKey, updateInfo.objectTypes, then, requestUrl); ResponseHandler<String> responseHandler = new BasicResponseHandler(); String json = responseHandler.handleResponse(response); JSONObject userInfo = JSONObject.fromObject(json); return userInfo.getString("registrationDate"); } else { countFailedApiCall(updateInfo.apiKey, updateInfo.objectTypes, then, requestUrl, reasonPhrase, statusCode, reasonPhrase); throw new Exception("Error: " + statusCode + " Unexpected error trying to get BodyMedia user registration date for guestId="+updateInfo.getGuestId()); } } public TimezoneMap getTimezoneMap(UpdateInfo updateInfo) throws Exception { OAuthConsumer consumer = setupConsumer(updateInfo.apiKey); String api_key = guestService.getApiKeyAttribute(updateInfo.apiKey, "bodymediaConsumerKey"); JSONArray timezoneMapJson = getUserTimezoneHistory(updateInfo, api_key, consumer); TimezoneMap ret= new TimezoneMap(); try{ for(int i=0; i<timezoneMapJson.size(); i++) { JSONObject jsonRecord = timezoneMapJson.getJSONObject(i); final String tzName = jsonRecord.getString("value"); final String startDateStr = jsonRecord.getString("startDate"); final String endDateStr = jsonRecord.optString("endDate"); DateTime startDate; DateTime endDate; DateTimeZone tz; tz = DateTimeZone.forID(tzName); startDate = tzmapFormatter.parseDateTime(startDateStr); if(endDateStr.equals("")) { // Last entry in table has no endDate, set it to be one day in the future endDate=new DateTime(tz).plusDays(1); } else { endDate = tzmapFormatter.parseDateTime(endDateStr); } ret.add(startDate.getMillis(), endDate.getMillis(),tz); } } catch (Throwable e){ StringBuilder sb = new StringBuilder("module=updateQueue component=updater action=BodymediaUpdater.getTimezoneMap") .append(" message=\"exception while getting timezone map\" connector=") .append(updateInfo.apiKey.getConnector().toString()).append(" guestId=") .append(updateInfo.apiKey.getGuestId()) .append(" stackTrace=<![CDATA[").append(Utils.stackTrace(e)).append("]]>");; logger.info(sb.toString()); } return ret; } public JSONArray getUserTimezoneHistory(UpdateInfo updateInfo, String api_key, OAuthConsumer consumer) throws Exception { long then = System.currentTimeMillis(); String requestUrl = "http://api.bodymedia.com/v2/json/timezone?api_key=" + api_key; HttpGet request = new HttpGet(requestUrl); consumer.sign(request); HttpClient client = env.getHttpClient(); enforceRateLimits(getRateDelay(updateInfo)); HttpResponse response = client.execute(request); final int statusCode = response.getStatusLine().getStatusCode(); final String reasonPhrase = response.getStatusLine().getReasonPhrase(); if (statusCode == 200) { countSuccessfulApiCall(updateInfo.apiKey, updateInfo.objectTypes, then, requestUrl); ResponseHandler<String> responseHandler = new BasicResponseHandler(); String json = responseHandler.handleResponse(response); JSONObject userInfo = JSONObject.fromObject(json); return userInfo.getJSONArray("timezones"); } else { countFailedApiCall(updateInfo.apiKey, updateInfo.objectTypes, then, requestUrl, "", statusCode, reasonPhrase); throw new Exception("Error: " + statusCode + " Unexpected error trying to bodymedia timezone for user " + updateInfo.apiKey.getGuestId()); } } @Override public void setDefaultChannelStyles(ApiKey apiKey) {} }