package org.fluxtream.core.connectors.updaters; import org.fluxtream.core.aspects.FlxLogger; import org.fluxtream.core.connectors.ApiClientSupport; import org.fluxtream.core.connectors.Connector; import org.fluxtream.core.connectors.ObjectType; import org.fluxtream.core.connectors.dao.FacetDao; import org.fluxtream.core.domain.*; import org.fluxtream.core.services.*; import org.fluxtream.core.utils.Utils; import org.joda.time.format.ISODateTimeFormat; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import javax.persistence.Entity; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.Query; import java.util.Arrays; import java.util.List; import static org.fluxtream.core.utils.Utils.stackTrace; public abstract class AbstractUpdater extends ApiClientSupport { static FlxLogger logger = FlxLogger.getLogger(AbstractUpdater.class); @Autowired protected ApiDataService apiDataService; @Autowired protected ConnectorUpdateService connectorUpdateService; @Autowired protected GuestService guestService; @Autowired protected JPADaoService jpaDaoService; @Autowired protected FacetDao facetDao; @PersistenceContext protected EntityManager em; @Autowired protected NotificationsService notificationsService; @Autowired protected BodyTrackStorageService bodyTrackStorageService; @Autowired protected DataUpdateService dataUpdateService; private String connectorName; final protected Connector connector() { if (connectorName == null) connectorName = Connector.getConnectorName(this.getClass().getName()); return Connector.getConnector(connectorName); } public AbstractUpdater() { } public static void extractCommonFacetData(AbstractFacet facet, UpdateInfo updateInfo) { facet.apiKeyId = updateInfo.apiKey.getId(); facet.guestId = updateInfo.apiKey.getGuestId(); facet.api = updateInfo.apiKey.getConnector().value(); facet.timeUpdated = System.currentTimeMillis(); } @Autowired final protected void setConnectorUpdateService(@Qualifier("connectorUpdateServiceImpl") ConnectorUpdateService ads) { Connector connector = connector(); ads.addUpdater(connector, this); } public final UpdateResult updateDataHistory(UpdateInfo updateInfo) { final long updateStartTime = System.currentTimeMillis(); try { logger.info("module=updateQueue component=updater action=updateDataHistory" + " guestId=" + updateInfo.getGuestId() + " connector=" + updateInfo.apiKey.getConnector().getName()); updateConnectorDataHistory(updateInfo); // At this point, versions prior to 5/25/13 called // bodyTrackStorageService.storeInitialHistory(updateInfo.apiKey); // to flush all the facets created for this connector to the // datastore. However, incremental updates of connectors are // expected to flush their new facets to the datastore // as they go, which in practice means that the initial // history updates for almost all the connectors are // sent to the datastore at least twice. In fact, // it's worse than that because updateDataHistory is // potentially called by multiple different object types // for a given connector. So in the old version // a given facet was potentially sent to the datastore // 2*<number of object types> times (for example, // 6 times for the Bodymedia connector). // // It's much better to require that each connector flushes its // own facets to the datstore for both the initial history and // incremental updates. // // If the connector consistently uses the // apiDataService.cacheApiData // calls to store the data retrieved from API calls, then this // will happen automatically. See the Bodymedia connector for // an example. // // Otherwise, the updater needs to // manually make sure to flush the data to the datastore. // See the Mymee updater for an example. return UpdateResult.successResult(); } catch (RateLimitReachedException e) { StringBuilder sb = new StringBuilder("module=updateQueue component=updater action=updateDataHistory") .append(" message=\"rate limit was reached exception\" connector=") .append(updateInfo.apiKey.getConnector().toString()).append(" guestId=") .append(updateInfo.apiKey.getGuestId()); logger.warn(sb.toString()); return UpdateResult.rateLimitReachedResult(e); } catch (AuthRevokedException e) { StringBuilder sb = new StringBuilder("module=updateQueue component=updater action=updateDataHistory") .append(" message=\"auth revoked\" connector=") .append(updateInfo.apiKey.getConnector().toString()).append(" guestId=") .append(updateInfo.apiKey.getGuestId()); logger.warn(sb.toString()); return UpdateResult.authRevokedResult(e); } catch (AuthExpiredException e) { StringBuilder sb = new StringBuilder("module=updateQueue component=updater action=updateDataHistory") .append(" message=\"connector needs re-authorization\" connector=") .append(updateInfo.apiKey.getConnector().toString()).append(" guestId=") .append(updateInfo.apiKey.getGuestId()); logger.warn(sb.toString()); return UpdateResult.needsReauth(); } catch (UpdateFailedException e) { String stackTrace = stackTrace(e); StringBuilder sb = new StringBuilder("module=updateQueue component=updater action=updateDataHistory") .append(" message=\"update failed\" connector=") .append(updateInfo.apiKey.getConnector().toString()).append(" guestId=") .append(updateInfo.apiKey.getGuestId()) .append(" stackTrace=<![CDATA[" + stackTrace + "]]>"); logger.warn(sb.toString()); return UpdateResult.failedResult(e); } catch (Throwable t) { String stackTrace = stackTrace(t); StringBuilder sb = new StringBuilder("module=updateQueue component=updater action=updateDataHistory") .append(" message=\"Unexpected exception\" connector=") .append(updateInfo.apiKey.getConnector().toString()).append(" guestId=") .append(updateInfo.apiKey.getGuestId()) .append(" stackTrace=<![CDATA[" + stackTrace + "]]>"); logger.warn(sb.toString()); sb = new StringBuilder("We were unable to import your "); sb.append(updateInfo.apiKey.getConnector().prettyName()) .append(" data"); if (t.getMessage()!=null) { sb .append(", error message: \"") .append(t.getMessage()).toString(); } notificationsService.addNamedNotification(updateInfo.apiKey.getGuestId(), Notification.Type.WARNING, connector().statusNotificationName(), sb.toString()); return UpdateResult.failedResult(stackTrace, ApiKey.PermanentFailReason.UNKNOWN); } finally { try { // Update the time bounds no matter how we exit the updater. recordModifiedTimeBounds(updateStartTime,updateInfo); updateTimeBounds(updateInfo); } catch(Throwable t) { final String stackTrace = Utils.stackTrace(t); StringBuilder sb = new StringBuilder("module=updateQueue component=updater action=updateData") .append(" message=\"Couldn't update time bounds\" connector=") .append(updateInfo.apiKey.getConnector().toString()).append(" guestId=").append(updateInfo.apiKey.getGuestId()) .append(" stackTrace=<![CDATA[").append(stackTrace).append("]]>") .append(updateInfo.apiKey.getGuestId()); logger.warn(sb.toString()); } } } /** * Updates all connector information * @param updateInfo update information for the connector * @throws Exception If an api's limit has been reached or if an update fails for another reason */ protected abstract void updateConnectorDataHistory(UpdateInfo updateInfo) throws Exception; public final UpdateResult updateData(UpdateInfo updateInfo) { final long updateStartTime = System.currentTimeMillis(); UpdateResult updateResult; try { updateConnectorData(updateInfo); updateResult = UpdateResult.successResult(); } catch (AuthRevokedException e) { updateResult = UpdateResult.authRevokedResult(e); } catch (RateLimitReachedException e) { updateResult = UpdateResult.rateLimitReachedResult(e); } catch (AuthExpiredException e) { StringBuilder sb = new StringBuilder("module=updateQueue component=updater action=updateData").append(" message=\"connector needs re-authorization\" connector=").append(updateInfo.apiKey.getConnector().toString()).append(" guestId=").append(updateInfo.apiKey.getGuestId()); logger.warn(sb.toString()); return UpdateResult.needsReauth(); } catch (UpdateFailedException e) { final String stackTrace = Utils.stackTrace(e); StringBuilder sb = new StringBuilder("module=updateQueue component=updater action=updateData") .append(" message=\"Update failed exception\" connector=") .append(updateInfo.apiKey.getConnector().toString()).append(" guestId=").append(updateInfo.apiKey.getGuestId()) .append(" isPermanent=").append(e.isPermanent()) .append(" stackTrace=<![CDATA[").append(stackTrace).append("]]>") .append(updateInfo.apiKey.getGuestId()); logger.warn(sb.toString()); updateResult = UpdateResult.failedResult(e); } catch (Throwable e) { final String stackTrace = Utils.stackTrace(e); StringBuilder sb = new StringBuilder("module=updateQueue component=updater action=updateData") .append(" message=\"Unexpected exception\" connector=") .append(updateInfo.apiKey.getConnector().toString()).append(" guestId=").append(updateInfo.apiKey.getGuestId()) .append(" stackTrace=<![CDATA[").append(stackTrace).append("]]>") .append(updateInfo.apiKey.getGuestId()); logger.warn(sb.toString()); updateResult = UpdateResult.failedResult(stackTrace, ApiKey.PermanentFailReason.UNKNOWN); } finally { // Update the time bounds no matter how we exit the updater. try { recordModifiedTimeBounds(updateStartTime,updateInfo); updateTimeBounds(updateInfo); } catch(Throwable t) { final String stackTrace = Utils.stackTrace(t); StringBuilder sb = new StringBuilder("module=updateQueue component=updater action=updateData") .append(" message=\"Couldn't update time bounds\" connector=") .append(updateInfo.apiKey.getConnector().toString()).append(" guestId=").append(updateInfo.apiKey.getGuestId()) .append(" stackTrace=<![CDATA[").append(stackTrace).append("]]>") .append(updateInfo.apiKey.getGuestId()); logger.warn(sb.toString()); } } return updateResult; } private final void recordModifiedTimeBounds(final long startTime, final UpdateInfo updateInfo){ List<ObjectType> objectTypes = updateInfo.objectTypes(); if (objectTypes.size() == 0){ objectTypes = Arrays.asList(updateInfo.apiKey.getConnector().objectTypes()); } if (objectTypes.size() > 0){ for (ObjectType objectType : objectTypes){ try{ if (!objectType.isClientFacet()) continue; if (!updateInfo.apiKey.getConnector().hasFacets()) continue; Class<? extends AbstractFacet> facetClass = objectType.facetClass(); final String facetName = facetClass.getAnnotation(Entity.class).name(); String queryString = new StringBuilder("SELECT min(facet.start), max(facet.end) FROM ") .append(facetName) .append(" facet WHERE facet.apiKeyId=:apiKeyId AND facet.timeUpdated>=:since") .toString(); final Query query = em.createQuery(queryString); query.setParameter("apiKeyId", updateInfo.apiKey.getId()); query.setParameter("since", startTime); Object[] result = (Object[]) query.getResultList().get(0); if (result[0] == null) continue; dataUpdateService.logApiDataUpdate(updateInfo.apiKey.getGuestId(),updateInfo.apiKey.getId(),(long) objectType.value(),(Long) result[0],(Long) result[1]); } catch (Exception e){ e.printStackTrace(); } } } else{ //TODO: determine if this is ever reached try{ if (updateInfo.apiKey.getConnector().hasFacets()){ Class<? extends AbstractFacet> facetClass = updateInfo.apiKey.getConnector().facetClass(); final String facetName = facetClass.getAnnotation(Entity.class).name(); String queryString = new StringBuilder("SELECT min(facet.start), max(facet.end) FROM ") .append(facetName) .append(" facet WHERE facet.apiKeyId=:apiKeyId AND facet.timeUpdated>=:since") .toString(); final Query query = em.createQuery(queryString); query.setParameter("apiKeyId", updateInfo.apiKey.getId()); query.setParameter("since", startTime); Object[] result = (Object[]) query.getResultList().get(0); if (result[0] != null) dataUpdateService.logApiDataUpdate(updateInfo.apiKey.getGuestId(),updateInfo.apiKey.getId(),null,(Long) result[0],(Long) result[1]); } } catch (Exception e){ e.printStackTrace(); } } } private void updateTimeBounds(final UpdateInfo updateInfo) { final List<ObjectType> objectTypes = updateInfo.objectTypes(); if (objectTypes==null||objectTypes.size()==0) { saveTimeBoundaries(updateInfo.apiKey, null); } else { for (ObjectType objectType : objectTypes) { saveTimeBoundaries(updateInfo.apiKey, objectType); } } // Consider the last sync time to be whenever the updater // completes guestService.setApiKeyAttribute(updateInfo.apiKey, ApiKeyAttribute.LAST_SYNC_TIME_KEY, ISODateTimeFormat.dateHourMinuteSecondFraction(). withZoneUTC().print(System.currentTimeMillis())); } private void saveTimeBoundaries(final ApiKey apiKey, final ObjectType objectType) { final AbstractFacet oldestApiDataFacet = apiDataService.getOldestApiDataFacet(apiKey, objectType); if (oldestApiDataFacet!=null) guestService.setApiKeyAttribute(apiKey, objectType==null ? ApiKeyAttribute.MIN_TIME_KEY : objectType.getApiKeyAttributeName(ApiKeyAttribute.MIN_TIME_KEY), ISODateTimeFormat.dateHourMinuteSecondFraction().withZoneUTC().print(oldestApiDataFacet.start)); final AbstractFacet latestApiDataFacet = apiDataService.getLatestApiDataFacet(apiKey, objectType); if (latestApiDataFacet!=null) guestService.setApiKeyAttribute(apiKey, objectType==null ? ApiKeyAttribute.MAX_TIME_KEY : objectType.getApiKeyAttributeName(ApiKeyAttribute.MAX_TIME_KEY), ISODateTimeFormat.dateHourMinuteSecondFraction().withZoneUTC().print(Math.max(latestApiDataFacet.end, latestApiDataFacet.start))); } public void countSuccessfulApiCall(ApiKey apiKey, int objectTypes, long then, String query) { StringBuilder sb = new StringBuilder("module=updateQueue component=updater action=countSuccessfulApiCall") .append(" connector=" + connector().getName()) .append(" objectTypes=" + objectTypes) .append(" apiKeyId=").append(apiKey.getId()) .append(" guestId=").append(apiKey.getGuestId()) .append(" query=").append(query); logger.info(sb.toString()); connectorUpdateService.addApiUpdate(apiKey, objectTypes, then, System.currentTimeMillis() - then, query, true, 200, null); } public void countFailedApiCall(ApiKey apiKey, int objectTypes, long then, String query, String stackTrace, Integer httpResponseCode, String reason) { StringBuilder sb = new StringBuilder("module=updateQueue component=updater action=countFailedApiCall") .append(" connector=" + connector().getName()) .append(" objectTypes=" + objectTypes) .append(" apiKeyId=").append(apiKey.getId()) .append(" guestId=").append(apiKey.getGuestId()) .append(" query=").append(query) .append(" httpResponseCode=").append(httpResponseCode) .append(" reason=\"").append(reason).append("\"") .append(" stackTrace=<![CDATA[").append(stackTrace).append("]]>"); logger.info(sb.toString()); connectorUpdateService.addApiUpdate(apiKey, objectTypes, then, System.currentTimeMillis() - then, query, false, httpResponseCode, reason); } public void reportFailedApiCall(ApiKey apiKey, int objectTypes, long then, String query, String stackTrace, String reason) { StringBuilder sb = new StringBuilder("module=updateQueue component=updater action=countFailedApiCall") .append(" connector=" + connector().getName()) .append(" objectTypes=" + objectTypes) .append(" apiKeyId=").append(apiKey.getId()) .append(" guestId=").append(apiKey.getGuestId()) .append(" time=").append(ISODateTimeFormat.dateTimeNoMillis().print(then)) .append(" query=").append(query) .append(" reason=\"").append(reason).append("\"") .append(" stackTrace=<![CDATA[").append(stackTrace).append("]]>"); logger.info(sb.toString()); } /** * Performs and incremental update of the connector * @param updateInfo Update information * @throws Exception If update fails */ protected abstract void updateConnectorData(UpdateInfo updateInfo) throws Exception; public void afterConnectorUpdate(final UpdateInfo updateInfo) throws Exception {} public void afterHistoryUpdate(final UpdateInfo updateInfo) throws Exception {} public abstract void setDefaultChannelStyles(ApiKey apiKey); }