package org.fluxtream.core.services.impl; import java.util.ArrayList; import java.util.Arrays; import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.ResourceBundle; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import org.fluxtream.core.Configuration; import org.fluxtream.core.connectors.Connector; import org.fluxtream.core.domain.ApiKey; import org.fluxtream.core.domain.ConnectorInfo; import org.fluxtream.core.domain.Gestalt; import org.fluxtream.core.services.ConnectorUpdateService; import org.fluxtream.core.services.GuestService; import org.fluxtream.core.services.JPADaoService; import org.fluxtream.core.services.SystemService; import org.fluxtream.core.updaters.quartz.Consumer; import org.fluxtream.core.updaters.quartz.Producer; import org.fluxtream.core.utils.JPAUtils; import net.sf.json.JSONArray; import org.apache.log4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Scope; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @Scope("singleton") @Transactional(readOnly=true) public class SystemServiceImpl implements SystemService, ApplicationListener<ContextRefreshedEvent> { static final Logger logger = Logger.getLogger(SystemServiceImpl.class); @Autowired ConnectorUpdateService connectorUpdateService; @Autowired Configuration env; @Autowired GuestService guestService; @PersistenceContext EntityManager em; @Autowired(required=false) Producer producer; @Autowired(required=false) Consumer consumer; @Autowired JPADaoService jpaDaoService; static Map<String, Connector> scopedApis = new Hashtable<String, Connector>(); static { if (Connector.getConnector("google_latitude")!=null) scopedApis.put("https://www.googleapis.com/auth/latitude.all.best", Connector.getConnector("google_latitude")); if (Connector.getConnector("google_calendar")!=null) scopedApis.put("https://www.googleapis.com/auth/calendar.readonly", Connector.getConnector("google_calendar")); if (Connector.getConnector("sms_backup")!=null) scopedApis.put("https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/gmail.readonly", Connector.getConnector("sms_backup")); if (Connector.getConnector("sleep_as_android")!=null) scopedApis.put("https://www.googleapis.com/auth/userinfo.email", Connector.getConnector("sleep_as_android")); } @Override public List<ConnectorInfo> getConnectors() throws Exception { List<ConnectorInfo> all = JPAUtils.find(em, ConnectorInfo.class, "connectors.all", (Object[])null); // Removed check for initializing the Connector table since this was causing // duplication of the entries in the Connector table. This means that we may // end up returning an incomplete list of connectors if this is // called during a thread other than the one in onApplicationEvent during startup //if (all.size() == 0) { // resetConnectorList(); // all = JPAUtils.find(em, ConnectorInfo.class, "connectors.all", // (Object[]) null); //} return all; } @Override public ConnectorInfo getConnectorInfo(final String connectorName) throws Exception { //List<ConnectorInfo> all = JPAUtils.find(em, ConnectorInfo.class, "connectors.all", (Object[])null); // Removed check for initializing the Connector table since this was causing // duplication of the entries in the Connector table. This means that we may // end up returning an incomplete list of connectors if this is // called during a thread other than the one in onApplicationEvent during startup //if (all.size() == 0) { // resetConnectorList(); //} final ConnectorInfo connectorInfo = JPAUtils.findUnique(em, ConnectorInfo.class, "connector.byName", connectorName); return connectorInfo; } @Transactional(readOnly = false) private void initializeConnectorList() { ResourceBundle res = ResourceBundle.getBundle("messages/connectors"); int order = 0; String release = env.get("release"); final String jawboneUp = "Jawbone UP"; String[] jawboneUpKeys = checkKeysExist(jawboneUp, Arrays.asList("jawboneUp.client.id", "jawboneUp.client.secret", "jawboneUp.validRedirectURL")); final ConnectorInfo jawboneUpConnectorInfo = new ConnectorInfo(jawboneUp, "/" + release + "/images/connectors/connector-up.png", res.getString("up"), "/up/token", Connector.getConnector("up"), order++, jawboneUpKeys!=null, false, true, jawboneUpKeys); jawboneUpConnectorInfo.supportsRenewTokens = true; jawboneUpConnectorInfo.renewTokensUrlTemplate = "up/token?apiKeyId=%s"; em.persist(jawboneUpConnectorInfo); final String evernote = "Evernote"; String[] evernoteKeys = checkKeysExist(evernote, Arrays.asList("evernoteConsumerKey", "evernoteConsumerSecret", "evernote.sandbox")); final ConnectorInfo evernoteConnectorInfo = new ConnectorInfo(evernote, "/" + release + "/images/connectors/connector-evernote.jpg", res.getString("evernote"), "/evernote/token", Connector.getConnector("evernote"), order++, evernoteKeys!=null, false, true, evernoteKeys); evernoteConnectorInfo.supportsRenewTokens = true; evernoteConnectorInfo.renewTokensUrlTemplate = "evernote/token?apiKeyId=%s"; em.persist(evernoteConnectorInfo); final String facebook = "Facebook"; String[] facebookKeys = checkKeysExist(facebook, Arrays.asList("facebook.appId", "facebook.appSecret")); final ConnectorInfo facebookConnectorInfo = new ConnectorInfo(facebook, "/" + release + "/images/connectors/connector-facebook.jpg", res.getString("facebook"), "/facebook/token", Connector.getConnector("facebook"), order++, facebookKeys!=null, false, false, facebookKeys); em.persist(facebookConnectorInfo); final String moves = "Moves"; String[] movesKeys = checkKeysExist(moves, Arrays.asList("moves.client.id", "moves.client.secret", "moves.validRedirectURL", "foursquare.client.id", "foursquare.client.secret")); final ConnectorInfo movesConnectorInfo = new ConnectorInfo(moves, "/" + release + "/images/connectors/connector-moves.jpg", res.getString("moves"), "/moves/oauth2/token", Connector.getConnector("moves"), order++, movesKeys!=null, false, true, movesKeys); movesConnectorInfo.supportsRenewTokens = true; movesConnectorInfo.renewTokensUrlTemplate = "moves/oauth2/token?apiKeyId=%s"; em.persist(movesConnectorInfo); final String latitude = "Google Latitude"; String[] latitudeKeys = checkKeysExist(latitude, Arrays.asList("google.client.id", "google.client.secret")); final ConnectorInfo latitudeConnectorInfo = new ConnectorInfo(latitude, "/" + release + "/images/connectors/connector-google_latitude.jpg", res.getString("google_latitude"), "upload:google_latitude", Connector.getConnector("google_latitude"), order++, latitudeKeys!=null, true, false, latitudeKeys); latitudeConnectorInfo.supportsRenewTokens = false; latitudeConnectorInfo.renewTokensUrlTemplate = "google/oauth2/%s/token?scope=https://www.googleapis.com/auth/latitude.all.best"; em.persist(latitudeConnectorInfo); final String fitbit = "Fitbit"; String[] fitbitKeys = checkKeysExist(fitbit, Arrays.asList("fitbitConsumerKey", "fitbitConsumerSecret")); final ConnectorInfo fitbitConnectorInfo = new ConnectorInfo(fitbit, "/images/connectors/connector-fitbit.jpg", res.getString("fitbit"), "/fitbit/token", Connector.getConnector("fitbit"), order++, fitbitKeys != null, false, true, fitbitKeys); fitbitConnectorInfo.supportsRenewTokens = true; fitbitConnectorInfo.renewTokensUrlTemplate = "fitbit/token?apiKeyId=%s"; em.persist(fitbitConnectorInfo); final String bodyMedia = "BodyMedia"; String[] bodymediaKeys = checkKeysExist(bodyMedia, Arrays.asList("bodymediaConsumerKey", "bodymediaConsumerSecret")); final ConnectorInfo bodymediaConnectorInfo = new ConnectorInfo(bodyMedia, "/" + release + "/images/connectors/connector-bodymedia.jpg", res.getString("bodymedia"), "/bodymedia/token", Connector.getConnector("bodymedia"), order++, bodymediaKeys!=null, false, true, bodymediaKeys); bodymediaConnectorInfo.supportsRenewTokens = true; bodymediaConnectorInfo.renewTokensUrlTemplate = "bodymedia/token?apiKeyId=%s"; em.persist(bodymediaConnectorInfo); final String withings = "Withings"; String[] withingsKeys = checkKeysExist(withings, Arrays.<String>asList("withingsConsumerKey", "withingsConsumerSecret")); final ConnectorInfo withingsConnectorInfo = new ConnectorInfo( withings, "/" + release + "/images/connectors/connector-withings.jpg", res.getString("withings"), "/withings/token", Connector.getConnector("withings"), order++, withingsKeys != null, false, true, withingsKeys); withingsConnectorInfo.supportsRenewTokens = true; withingsConnectorInfo.renewTokensUrlTemplate = "withings/token?apiKeyId=%s"; em.persist(withingsConnectorInfo); final String zeo = "Zeo"; String[] zeoKeys = checkKeysExist(zeo, new ArrayList<String>()); // Zeo no longer supports sync. The myzeo servers were disabled due to bankruptcy in May/June 2013 em.persist(new ConnectorInfo(zeo, "/" + release + "/images/connectors/connector-zeo.jpg", res.getString("zeo"), "ajax:/zeo/enterCredentials", Connector.getConnector("zeo"), order++, zeoKeys!=null, false, false, zeoKeys)); final String mymee = "Mymee"; em.persist(new ConnectorInfo(mymee, "/" + release + "/images/connectors/connector-mymee.jpg", res.getString("mymee"), "ajax:/mymee/enterAuthInfo", Connector.getConnector("mymee"), order++, true, false, true, null)); final String quantifiedMind = "QuantifiedMind"; String[] quantifiedMindKeys = checkKeysExist(quantifiedMind, new ArrayList<String>()); em.persist(new ConnectorInfo(quantifiedMind, "/" + release + "/images/connectors/connector-quantifiedmind.jpg", res.getString("quantifiedmind"), "ajax:/quantifiedmind/getTokenDialog", Connector.getConnector("quantifiedmind"), order++, quantifiedMindKeys!=null, false, true, quantifiedMindKeys)); final String flickr = "Flickr"; String[] flickrKeys = checkKeysExist(flickr, Arrays.asList("flickrConsumerKey", "flickrConsumerSecret", "flickr.validRedirectURL")); final ConnectorInfo flickrConnectorInfo = new ConnectorInfo(flickr, "/" + release + "/images/connectors/connector-flickr.jpg", res.getString("flickr"), "/flickr/token", Connector.getConnector("flickr"), order++, flickrKeys != null, false, true, flickrKeys); flickrConnectorInfo.supportsRenewTokens = true; flickrConnectorInfo.renewTokensUrlTemplate = "flickr/token?apiKeyId=%s"; em.persist(flickrConnectorInfo); final String googleCalendar = "Google Calendar"; String[] googleCalendarKeys = checkKeysExist(googleCalendar, Arrays.asList("google.client.id", "google.client.secret")); final ConnectorInfo googleCalendarConnectorInfo = new ConnectorInfo(googleCalendar, "/" + release + "/images/connectors/connector-google_calendar.jpg", res.getString("google_calendar"), "/google/oauth2/token?scope=https://www.googleapis.com/auth/calendar.readonly", Connector.getConnector("google_calendar"), order++, googleCalendarKeys != null, false, true, googleCalendarKeys); googleCalendarConnectorInfo.supportsRenewTokens = true; googleCalendarConnectorInfo.renewTokensUrlTemplate = "google/oauth2/%s/token?scope=https://www.googleapis.com/auth/calendar.readonly"; em.persist(googleCalendarConnectorInfo); final String lastFm = "Last fm"; String[] lastFmKeys = checkKeysExist(lastFm, Arrays.asList("lastfmConsumerKey", "lastfmConsumerSecret")); final ConnectorInfo lastfmConnectorInfo = new ConnectorInfo(lastFm, "/" + release + "/images/connectors/connector-lastfm.jpg", res.getString("lastfm"), "/lastfm/token", Connector.getConnector("lastfm"), order++, lastFmKeys != null, false, true, lastFmKeys); lastfmConnectorInfo.supportsRenewTokens = true; lastfmConnectorInfo.renewTokensUrlTemplate = "lastfm/token?apiKeyId=%s"; em.persist(lastfmConnectorInfo); final String twitter = "Twitter"; String[] twitterKeys = checkKeysExist(twitter, Arrays.asList("twitterConsumerKey", "twitterConsumerSecret")); ConnectorInfo twitterConnectorInfo = new ConnectorInfo(twitter, "/" + release + "/images/connectors/connector-twitter.jpg", res.getString("twitter"), "/twitter/token", Connector.getConnector("twitter"), order++, twitterKeys!=null, false, true, twitterKeys); twitterConnectorInfo.supportsRenewTokens = true; twitterConnectorInfo.renewTokensUrlTemplate = "twitter/token?apiKeyId=%s"; em.persist(twitterConnectorInfo); final String fluxtreamCapture = "Fluxtream Capture"; String[] fluxtreamCaptureKeys = checkKeysExist(fluxtreamCapture, new ArrayList<String>()); em.persist(new ConnectorInfo(fluxtreamCapture, "/" + release + "/images/connectors/connector-fluxtream_capture.png", res.getString("fluxtream_capture"), "ajax:/fluxtream_capture/about", Connector.getConnector("fluxtream_capture"), order++, fluxtreamCaptureKeys!=null, false, true, fluxtreamCaptureKeys)); String[] runkeeperKeys = checkKeysExist("Runkeeper", Arrays.asList("runkeeperConsumerKey", "runkeeperConsumerSecret")); final String runKeeper = "RunKeeper"; final ConnectorInfo runkeeperConnectorInfo = new ConnectorInfo(runKeeper, "/" + release + "/images/connectors/connector-runkeeper.jpg", res.getString("runkeeper"), "/runkeeper/token", Connector.getConnector("runkeeper"), order++, runkeeperKeys != null, false, true, runkeeperKeys); runkeeperConnectorInfo.supportsRenewTokens = true; runkeeperConnectorInfo.renewTokensUrlTemplate = "runkeeper/token?apiKeyId=%s"; em.persist(runkeeperConnectorInfo); String[] smsBackupKeys = checkKeysExist("SMS_Backup", Arrays.asList("google.client.id", "google.client.secret")); ConnectorInfo SMSBackupInfo = new ConnectorInfo("SMS_Backup", "/" + release + "/images/connectors/connector-sms_backup.jpg", res.getString("sms_backup"), "/google/oauth2/token?scope=https://www.googleapis.com/auth/userinfo.email%20https://www.googleapis.com/auth/gmail.readonly", Connector.getConnector("sms_backup"), order++, true, false,true,smsBackupKeys); SMSBackupInfo.supportsRenewTokens = true; SMSBackupInfo.renewTokensUrlTemplate = "google/oauth2/%s/token?scope=https://www.googleapis.com/auth/userinfo.email%%20https://www.googleapis.com/auth/gmail.readonly"; em.persist(SMSBackupInfo); final String beddit = "Beddit"; String[] bedditKeys = checkKeysExist(beddit, Arrays.asList("bedditConsumerKey", "bedditConsumerSecret")); em.persist(new ConnectorInfo(beddit, "/" + release + "/images/connectors/connector-beddit.jpg", res.getString("beddit"), "/beddit/token", Connector.getConnector("beddit"), order++, true, false, true, bedditKeys)); String[] sleepAsAndroidKeys = checkKeysExist("Sleep_As_Android", Arrays.asList("google.client.id", "google.client.secret")); ConnectorInfo SleepAsAndroidConnectorInfo = new ConnectorInfo("Sleep_As_Android", "/" + release + "/images/connectors/connector-sleep_as_android.jpg", res.getString("sleep_as_android"), "/google/oauth2/token?scope=https://www.googleapis.com/auth/userinfo.email", Connector.getConnector("sleep_as_android"), order++, true, false,true,sleepAsAndroidKeys); SleepAsAndroidConnectorInfo.supportsRenewTokens = true; SleepAsAndroidConnectorInfo.renewTokensUrlTemplate = "google/oauth2/%s/token?scope=https://www.googleapis.com/auth/userinfo.email"; em.persist(SleepAsAndroidConnectorInfo); String[] misfitKeys = checkKeysExist("Misfit", Arrays.asList("misfitConsumerKey", "misfitConsumerSecret")); final String misfit = "Misfit"; final ConnectorInfo misfitConnectorInfo = new ConnectorInfo(misfit, "/" + release + "/images/connectors/connector-misfit.jpg", res.getString("misfit"), "/misfit/token", Connector.getConnector("misfit"), order++, misfitKeys != null, false, true, misfitKeys); misfitConnectorInfo.supportsRenewTokens = true; misfitConnectorInfo.renewTokensUrlTemplate = "misfit/token?apiKeyId=%s"; em.persist(misfitConnectorInfo); } @Transactional(readOnly = false) private String[] checkKeysExist(String connectorName, List<String> keys) { String[] checkedKeys = new String[keys.size()]; int i=0; boolean fatalMissingKey=false; boolean nonFatalMissingKey=false; for (String key : keys) { String value = env.get(key); if (value==null) { fatalMissingKey=true; String msg = "Couldn't find key \"" + key + "\" while initializing the connector table. You need to add that key to your properties files.\n" + " See fluxtream-web/src/main/resources/samples/oauth.properties for details."; logger.info(msg); System.out.println(msg); } else if (value.equals("xxx")) { nonFatalMissingKey=true; String msg = "**** Found key \"" + key + "=xxx\" while populating the connector table. Disabling the " + connectorName + " connector"; logger.info(msg); System.out.println(msg); } else { checkedKeys[i++] = key; } } if(fatalMissingKey) { String msg = "***** Exiting execution due to missing configuration keys. See fluxtream-web/src/main/resources/samples/oauth.properties for details."; logger.info(msg); System.out.println(msg); System.exit(-1); } else if(nonFatalMissingKey) { return null; } return checkedKeys; } //private String singlyAuthorizeUrl(final String service) { // return (new StringBuilder("https://api.singly.com/oauth/authorize?client_id=") // .append(env.get("singly.client.id")) // .append("&redirect_uri=") // .append(env.get("homeBaseUrl")) // .append("singly/") // .append(service) // .append("/callback") // .append("&service=") // .append(service)).toString(); //} @Override public Connector getApiFromGoogleScope(String scope) { return scopedApis.get(scope); } boolean channelMappingsFixupWasExecuted() { final List<Gestalt> gestalts = jpaDaoService.findWithQuery("SELECT gestalt FROM Gestalt gestalt", Gestalt.class); if (gestalts.size()>1) throw new RuntimeException("Illegal State: multiple Gestalts have been found"); if (gestalts.size()==1) { Gestalt gestalt = gestalts.get(0); return gestalt.channelMappingsFixupWasExecuted; } else return false; } @Transactional(readOnly = false) public void resetConnectorList() throws Exception { System.out.println("Resetting connector table"); // Clear the existing data out of the Connector table JPAUtils.execute(em,"connector.deleteAll"); // The following call will initialize the Connector table by calling // the initializeConnectorList function and return the result initializeConnectorList(); } @Transactional(readOnly = false) public boolean checkConnectorInstanceKeys(List<ConnectorInfo> connectors) { // For each connector type in connectorInfos which is enabled, make sure that all of the existing connector // instances have stored apiKeyAttributeKeys. This is to support safe migration to version 0.9.0017. // Prior versions relied to continued coherence between the keys in the properties files // in fluxtream-web/src/main/resources and the existing connector instances. However, that behavior // conflicted with migrating a given machine to a different host name or migrating a given DB to a // different server without breaking sync capability for existing connector instances. // // The new behavior stores the apiKeyAttributeKeys from the properties file in the ApiKeyAttribute // table for each connector instance, which makes it more portable but also incurrs a migration // requirement. This function checks whether that migration needs to be performed for a given DB // instance JSONArray connectorsArray = new JSONArray(); boolean missingKeys=false; for (int i = 0; i < connectors.size(); i++) { final ConnectorInfo connectorInfo = connectors.get(i); final Connector api = connectorInfo.getApi(); if (api == null) { StringBuilder sb = new StringBuilder("module=SystemServiceImpl component=connectorStore action=checkConnectorInstanceKeys ") .append("message=\"null connector for " + connectorInfo.getName() + "\""); logger.warn(sb.toString()); continue; } if(connectorInfo.enabled==false) { StringBuilder sb = new StringBuilder("module=SystemServiceImpl component=connectorStore action=checkConnectorInstanceKeys ") .append("message=\"skipping connector instance keys check for disabled connector" + connectorInfo.getName() + "\""); logger.info(sb.toString()); continue; } String[] apiKeyAttributeKeys = connectorInfo.getApiKeyAttributesKeys(); if(apiKeyAttributeKeys==null) { StringBuilder sb = new StringBuilder("module=SystemServiceImpl component=connectorStore action=checkConnectorInstanceKeys ") .append("message=\"skipping connector instance keys check for connector" + connectorInfo.getName() + "; does not use keys\""); logger.info(sb.toString()); continue; } // This connector type is enabled, find all the instance keys for this connector type List<ApiKey> apiKeys = JPAUtils.find(em, ApiKey.class, "apiKeys.all.byApi", api.value()); for(ApiKey apiKey: apiKeys) { StringBuilder sb = new StringBuilder("module=SystemServiceImpl component=connectorStore action=checkConnectorInstanceKeys apiKeyId=" + apiKey.getId()) .append(" message=\"checking connector instance keys for connector" + connectorInfo.getName() + "\""); logger.info(sb.toString()); // Iterate over the apiKeyAttributeKeys to check if each is present for(String apiKeyAttributeKey: apiKeyAttributeKeys) { String apiKeyAttributeValue = guestService.getApiKeyAttribute(apiKey, apiKeyAttributeKey); if(apiKeyAttributeValue==null) { missingKeys=true; String msg = "**** Missing key \"" + apiKeyAttributeKey + "\" for apiKeyId=" + apiKey.getId() + " api=" + api.value() ; StringBuilder sb2 = new StringBuilder("module=SystemServiceImpl component=connectorStore action=checkConnectorInstanceKeys apiKeyId=" + apiKey.getId()) .append(" message=\"").append(msg).append("\""); logger.info(sb2.toString()); System.out.println(msg); } } } } return missingKeys; } @Override public void onApplicationEvent(final ContextRefreshedEvent event) { System.out.println("ApplicationContext started"); if (env.get("apiWebApp")!=null) { System.out.println("This is the API web app... Connector list isn't needed. Bye."); return; } if (event.getApplicationContext().getDisplayName().equals("Root WebApplicationContext")) { try { resetConnectorList(); resetSynchingApiKeys(); List<ConnectorInfo> connectors = getConnectors(); if (!channelMappingsFixupWasExecuted()) { String msg = "***** Exiting execution because the channel mappings fixup has not been executed yet.\n Check out fluxtream-admin-tools project, build, and execute 'java -jar target/flx-admin-tools.jar 8'"; logger.info(msg); System.out.println(msg); System.exit(-1); } boolean missingKeys=checkConnectorInstanceKeys(connectors); if(missingKeys) { String msg = "***** Exiting execution due to missing connector instance keys.\n Check out fluxtream-admin-tools project, build, and execute 'java -jar target/flx-admin-tools.jar 5'"; List<ConnectorInfo> connectors2 = getConnectors(); System.out.println("List of Connector table: before=" + connectors.size() + ", after=" + connectors2.size()); logger.info(msg); System.out.println(msg); System.exit(-1); } consumer.setContextStarted(); producer.setContextStarted(); } catch (Exception e) { e.printStackTrace(); } } } private void resetSynchingApiKeys() { jpaDaoService.execute("UPDATE ApiKey apiKey SET apiKey.synching=false"); } }