package org.fluxtream.core.api;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.wordnik.swagger.annotations.*;
import net.sf.json.JSONObject;
import org.apache.commons.lang.StringUtils;
import org.codehaus.jackson.annotate.JsonAutoDetect;
import org.codehaus.jackson.annotate.JsonMethod;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig;
import org.fluxtream.core.Configuration;
import org.fluxtream.core.api.gson.UpdateInfoSerializer;
import org.fluxtream.core.aspects.FlxLogger;
import org.fluxtream.core.auth.AuthHelper;
import org.fluxtream.core.auth.CoachRevokedException;
import org.fluxtream.core.connectors.Connector;
import org.fluxtream.core.connectors.ObjectType;
import org.fluxtream.core.connectors.bodytrackResponders.AbstractBodytrackResponder;
import org.fluxtream.core.connectors.updaters.UpdateFailedException;
import org.fluxtream.core.connectors.updaters.UpdateInfo;
import org.fluxtream.core.domain.*;
import org.fluxtream.core.mvc.models.ConnectorModelFull;
import org.fluxtream.core.services.*;
import org.fluxtream.core.utils.Utils;
import org.joda.time.DateTime;
import org.joda.time.format.ISODateTimeFormat;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;
/**
*
*/
@Path("/v1/connectors")
@Component("RESTConnectorStore")
@Api(value = "/connectors", description = "Connector and connector settings management operations (list, add, remove, etc.)")
@Scope("request")
public class ConnectorStore {
FlxLogger logger = FlxLogger.getLogger(ConnectorStore.class);
@Autowired
GuestService guestService;
@Autowired
SystemService sysService;
@Autowired
SettingsService settingsService;
@Autowired
ConnectorUpdateService connectorUpdateService;
@Autowired
private ApiDataService apiDataService;
@Autowired
Configuration env;
@Autowired
BeanFactory beanFactory;
Gson gson;
ObjectMapper mapper = new ObjectMapper();
@Autowired
private BuddiesService buddiesService;
public ConnectorStore() {
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(UpdateInfo.class, new UpdateInfoSerializer());
gson = gsonBuilder.create();
mapper.setVisibility(JsonMethod.FIELD, JsonAutoDetect.Visibility.ANY);
mapper.configure(SerializationConfig.Feature.FAIL_ON_EMPTY_BEANS, false);
}
@POST
@Produces({ MediaType.APPLICATION_JSON })
@ApiOperation(value = "Reset connector settings to their default values", response = String.class,
notes="A set of default values are stored alongside user modified values for all connector settings")
@Path("/settings/reset/{apiKeyId}")
public Response resetConnectorSettings(@ApiParam(value="The connector's ApiKey ID", required=true) @PathParam("apiKeyId") long apiKeyId) {
settingsService.resetConnectorSettings(apiKeyId);
return Response.ok("connector settings reset!").build();
}
@GET
@Path("/settings/{apiKeyId}")
@ApiOperation(value = "Retrieve connector settings", response = Object.class,
notes = "The structure of the returned object is connector dependent")
@Produces({MediaType.APPLICATION_JSON})
@ApiResponses({
@ApiResponse(code = 403, message = "If the supplied ApiKey doesn't belong to the logged-in user")
})
public String getConnectorSettings(@ApiParam(value="The connector's ApiKey ID", required=true) @PathParam("apiKeyId") long apiKeyId) throws UpdateFailedException, IOException {
final ApiKey apiKey = guestService.getApiKey(apiKeyId);
final long guestId = AuthHelper.getGuestId();
if (apiKey.getGuestId()!=guestId)
Response.status(Response.Status.FORBIDDEN).build();
final Object settings = settingsService.getConnectorSettings(apiKey.getId());
String json = mapper.writeValueAsString(settings);
return json;
}
@POST
@Path("/settings/{apiKeyId}")
@ApiOperation(value = "Save user-modified connector settings", response = String.class,
notes = "The structure of the returned object is connector dependent")
@Produces({MediaType.TEXT_PLAIN})
@ApiResponses({
@ApiResponse(code = 403, message = "If the supplied ApiKey doesn't belong to the logged-in user")
})
public Response saveConnectorSettings(@ApiParam(value="The connector's ApiKey ID", required=true) @PathParam("apiKeyId") long apiKeyId,
@ApiParam(value="JSON-serialized connector settings object", required=true) @FormParam("json") String json) {
final ApiKey apiKey = guestService.getApiKey(apiKeyId);
final long guestId = AuthHelper.getGuestId();
try {
if (apiKey.getGuestId()!=guestId)
Response.status(Response.Status.FORBIDDEN).build();
settingsService.saveConnectorSettings(apiKey.getId(), json);
} catch (Throwable e) {
return Response.serverError().entity(e.getMessage()).build();
}
return Response.ok("Saved connector settings").build();
}
@POST
@Path("/renew/{apiKeyId}")
@Produces({MediaType.APPLICATION_JSON})
public String renewConnectorTokens(@PathParam("apiKeyId") long apiKeyId) throws Exception {
final ApiKey apiKey = guestService.getApiKey(apiKeyId);
ConnectorInfo connectorInfo = sysService.getConnectorInfo(apiKey.getConnector().getName());
JSONObject renewInfo = new JSONObject();
final String renewTokensUrlTemplate = connectorInfo.renewTokensUrlTemplate;
final String renewTokensUrl = String.format(renewTokensUrlTemplate, apiKey.getId());
renewInfo.accumulate("redirectTo", env.get("homeBaseUrl") + renewTokensUrl);
return renewInfo.toString();
}
@GET
@Path("/installed")
@ApiOperation(value = "Retrieve the list of installed (/added) connectors for the current user",
responseContainer = "Array", response = ConnectorModelFull.class)
@Produces({MediaType.APPLICATION_JSON})
@ApiResponses({
@ApiResponse(code = 401, message = "You are no longer logged in"),
@ApiResponse(code = 403, message = "Buddy-to-access authorization has been revoked")
})
public Response getInstalledConnectors(@ApiParam(value="Buddy to access username parameter (" + BuddiesService.BUDDY_TO_ACCESS_PARAM + ")", required=false) @QueryParam(BuddiesService.BUDDY_TO_ACCESS_PARAM) String buddyToAccessParameter){
CoachingBuddy coachee;
try { coachee = AuthHelper.getCoachee(buddyToAccessParameter, buddiesService);
} catch (CoachRevokedException e) {return Response.status(403).entity("Sorry, permission to access this data has been revoked. Please reload your browser window").build();}
Guest guest = ApiHelper.getBuddyToAccess(guestService, coachee);
if (guest==null)
return Response.status(401).entity("You are no longer logged in").build();
ResourceBundle res = ResourceBundle.getBundle("messages/connectors");
try {
List<ConnectorInfo> connectors = sysService.getConnectors();
List<ConnectorModelFull> connectorsArray = new ArrayList<ConnectorModelFull>();
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=API component=connectorStore action=getInstalledConnectors ");
sb.append("message=\"null connector for " + connectorInfo.getName() + "\"");
logger.warn(sb);
continue;
}
if (!guestService.hasApiKey(guest.getId(), api)||api.getName().equals("facebook")/*HACK*/) {
connectors.remove(i--);
} else {
ConnectorInfo connector = connectorInfo;
ConnectorModelFull connectorModel = new ConnectorModelFull();
Connector conn = Connector.fromValue(connector.api);
ApiKey apiKey = guestService.getApiKey(guest.getId(), conn);
connectorModel.prettyName = conn.prettyName();
List<String> facetTypes = new ArrayList<String>();
ObjectType[] objTypes = conn.objectTypes();
if (objTypes != null) {
for (ObjectType obj : objTypes) {
facetTypes.add(connector.connectorName + "-" + obj.getName());
}
}
connectorModel.facetTypes = facetTypes;
connectorModel.status = apiKey.status!=null?apiKey.status.toString():"NA";
connectorModel.name = connector.name;
connectorModel.connectUrl = connector.connectUrl;
connectorModel.image = env.get("homeBaseUrl") + connector.image.substring(1);
connectorModel.connectorName = connector.connectorName;
connectorModel.enabled = connector.enabled;
connectorModel.manageable = connector.manageable;
connectorModel.text = connector.text;
connectorModel.api = connector.api;
connectorModel.apiKeyId = apiKey.getId();
connectorModel.lastSync = connector.supportsSync?getLastSync(apiKey):Long.MAX_VALUE;
connectorModel.latestData = getLatestData(apiKey);
final String auditTrail = checkForErrors(apiKey);
connectorModel.errors = auditTrail!=null;
connectorModel.auditTrail = auditTrail!=null?auditTrail:"";
connectorModel.syncing = checkIfSyncInProgress(guest.getId(), conn);
connectorModel.channels = settingsService.getChannelsForConnector(guest.getId(), conn);
connectorModel.sticky = connector.connectorName.equals("fluxtream_capture");
connectorModel.supportsRenewToken = connector.supportsRenewTokens;
connectorModel.supportsSync = connector.supportsSync;
connectorModel.supportsFileUpload = connector.supportsFileUpload;
connectorModel.prettyName = conn.prettyName();
final String uploadMessageKey = conn.getName() + ".upload";
if (res.containsKey(uploadMessageKey)) {
final String uploadMessage = res.getString(uploadMessageKey);
connectorModel.uploadMessage = uploadMessage;
}
connectorsArray.add(connectorModel);
}
}
if (buddyToAccessParameter!=null) {
final List<SharedConnector> sharedConnectors = buddiesService.getSharedConnectors(AuthHelper.getGuestId(), coachee.guestId);
List<ConnectorModelFull> unshared = new ArrayList<ConnectorModelFull>();
eachTrustingBuddyConnector:for (int i=0; i<connectorsArray.size(); i++) {
ConnectorModelFull fullModel = connectorsArray.get(i);
for (SharedConnector sharedConnector : sharedConnectors) {
if (sharedConnector.connectorName.equals(fullModel.connectorName))
continue eachTrustingBuddyConnector;
}
unshared.add(fullModel);
}
for (ConnectorModelFull toRemove : unshared) {
for (int i=0; i<connectorsArray.size(); i++) {
ConnectorModelFull fullModel = connectorsArray.get(i);
if (fullModel.apiKeyId==toRemove.apiKeyId)
connectorsArray.remove(fullModel);
}
}
}
StringBuilder sb = new StringBuilder("module=API component=connectorStore action=getInstalledConnectors")
.append(" guestId=").append(guest.getId());
logger.info(sb.toString());
return Response.ok(connectorsArray).build();
}
catch (Exception e) {
StringBuilder sb = new StringBuilder("module=API component=connectorStore action=getInstalledConnectors")
.append(" guestId=").append(guest.getId())
.append(" stackTrace=<![CDATA[").append(Utils.stackTrace(e)).append("]]>");
System.out.println(sb.toString());
logger.warn(sb.toString());
return Response.serverError().entity("Failed to get installed connectors: " + e.getMessage()).build();
}
}
@GET
@Path("/uninstalled")
@ApiOperation(value = "Retrieve the list of available (/not-yet-added) connectors for the current user",
responseContainer="Array", response = ConnectorInfo.class,
notes = "The structure of the returned object is connector dependent")
@ApiResponses({
@ApiResponse(code = 401, message = "If the guest is no longer logged in")
})
@Produces({MediaType.APPLICATION_JSON})
public Response getUninstalledConnectors(){
Guest guest = AuthHelper.getGuest();
// If no guest is logged in, return empty array
if(guest==null)
return Response.status(401).entity("You are no longer logged in").build();
try {
List<ConnectorInfo> allConnectors = sysService.getConnectors();
List<ConnectorInfo> connectors = new ArrayList<ConnectorInfo>();
for (ConnectorInfo connector : allConnectors) {
if (connector.enabled&&!connector.connectorName.equals("facebook")) {
connector.image = env.get("homeBaseUrl") + connector.image.substring(1);
connectors.add(connector);
}
}
for (int i = 0; i < connectors.size(); i++){
if (guestService.hasApiKey(guest.getId(), connectors.get(i).getApi()))
connectors.remove(i--);
}
StringBuilder sb = new StringBuilder("module=API component=connectorStore action=getUninstalledConnectors")
.append(" guestId=").append(guest.getId());
logger.info(sb.toString());
return Response.ok(gson.toJson(connectors)).build();
}
catch (Exception e) {
StringBuilder sb = new StringBuilder("module=API component=connectorStore action=getUninstalledConnectors")
.append(" guestId=").append(guest.getId())
.append(" stackTrace=<![CDATA[").append(Utils.stackTrace(e)).append("]]>");
logger.warn(sb.toString());
return Response.serverError().entity("Failed to get uninstalled connectors: " + e.getMessage()).build();
}
}
private boolean checkIfSyncInProgress(long guestId, Connector connector){
final ApiKey apiKey = guestService.getApiKey(guestId, connector);
return (apiKey.synching);
}
private String checkForErrors(ApiKey apiKey) {
return apiKey.stackTrace;
}
private long getLastSync(ApiKey apiKey) {
if (!apiKey.getConnector().hasFacets())
return Long.MAX_VALUE;
final String lastSyncTimeAtt = guestService.getApiKeyAttribute(apiKey, ApiKeyAttribute.LAST_SYNC_TIME_KEY);
// only return the ApiKey's lastSyncTime if we have it cached as an attribute
if (lastSyncTimeAtt !=null && StringUtils.isNotEmpty(lastSyncTimeAtt)) {
final DateTime dateTime = ISODateTimeFormat.dateHourMinuteSecondFraction().withZoneUTC().parseDateTime(lastSyncTimeAtt);
return dateTime.getMillis();
}
// fall back to old method of querying the ApiUpdates table
ApiUpdate update = null; // connectorUpdateService.getLastSuccessfulUpdate(apiKey);
return update != null ? update.ts : Long.MAX_VALUE;
}
private long getLatestData(ApiKey apiKey) {
if (!apiKey.getConnector().hasFacets())
return Long.MAX_VALUE;
final ObjectType[] objectTypes = apiKey.getConnector().objectTypes();
if (objectTypes==null||objectTypes.length==0) {
final String maxTimeAtt = guestService.getApiKeyAttribute(apiKey, ApiKeyAttribute.MAX_TIME_KEY);
// only return the ApiKey's maxTime if we have it cached as an attribute
if (maxTimeAtt !=null && StringUtils.isNotEmpty(maxTimeAtt)) {
final DateTime dateTime = ISODateTimeFormat.dateHourMinuteSecondFraction().withZoneUTC().parseDateTime(maxTimeAtt);
return dateTime.getMillis();
}
} else {
long maxTime = Long.MIN_VALUE;
for (ObjectType objectType : objectTypes) {
final String maxTimeAtt = guestService.getApiKeyAttribute(apiKey, objectType.getApiKeyAttributeName(ApiKeyAttribute.MAX_TIME_KEY));
if (maxTimeAtt !=null && StringUtils.isNotEmpty(maxTimeAtt)) {
final DateTime dateTime = ISODateTimeFormat.dateHourMinuteSecondFraction().withZoneUTC().parseDateTime(maxTimeAtt);
final long maxObjectTypeTime = dateTime.getMillis();
if (maxObjectTypeTime>maxTime)
maxTime = maxObjectTypeTime;
}
}
// only return the ApiKey's maxTime if we have it cached as an attribute for any its connector's objectTypes
if (maxTime>Long.MIN_VALUE)
return maxTime;
}
// fall back to old method of querying the facets table
AbstractFacet facet = null; // apiDataService.getLatestApiDataFacet(apiKey, null);
return facet == null ? Long.MAX_VALUE : facet.end;
}
@DELETE
@Path("/{connector}")
@Produces({MediaType.TEXT_PLAIN})
@ApiResponses({
@ApiResponse(code = 401, message = "If the guest is no longer logged in")
})
public Response deleteConnector(@PathParam("connector") String connector){
Response response = null;
Guest guest = AuthHelper.getGuest();
// If no guest is logged in, return empty array
if(guest==null)
return Response.status(401).entity("You are no longer logged in").build();
try{
Connector apiToRemove = Connector.fromString(connector);
guestService.removeApiKeys(guest.getId(), apiToRemove);
response = Response.ok("Successfully removed " + connector + ".").build();
StringBuilder sb = new StringBuilder("module=API component=connectorStore action=deleteConnector")
.append(" connector=").append(connector)
.append(" guestId=").append(guest.getId());
logger.info(sb.toString());
}
catch (Exception e){
StringBuilder sb = new StringBuilder("module=API component=connectorStore action=deleteConnector")
.append(" connector=").append(connector)
.append(" guestId=").append(guest.getId())
.append(" stackTrace=<![CDATA[").append(Utils.stackTrace(e)).append("]]>");
logger.warn(sb.toString());
response = Response.serverError().entity("Failed to remove " + connector + ".").build();
}
return response;
}
@POST
@Path("/{connector}/channels")
@Produces({MediaType.TEXT_PLAIN})
@ApiResponses({
@ApiResponse(code = 401, message = "If the guest is no longer logged in")
})
public Response setConnectorChannels(@PathParam("connector") String connectorName, @FormParam("channels") String channels){
Response response = null;
Guest guest = AuthHelper.getGuest();
// If no guest is logged in, return empty array
if(guest==null)
return Response.status(401).entity("You are no longer logged in").build();
try {
ApiKey apiKey = guestService.getApiKey(guest.getId(), Connector.getConnector(connectorName));
settingsService.setChannelsForConnector(guest.getId(),apiKey.getConnector(),channels.split(","));
response = Response.ok("Successfully updated channels for " + connectorName + ".").build();
StringBuilder sb = new StringBuilder("module=API component=connectorStore action=setConnectorChannels")
.append(" connector=").append(connectorName)
.append(" channels=").append(channels)
.append(" guestId=").append(guest.getId());
logger.info(sb.toString());
}
catch (Exception e) {
StringBuilder sb = new StringBuilder("module=API component=connectorStore action=setConnectorChannels")
.append(" connector=").append(connectorName)
.append(" guestId=").append(guest.getId())
.append(" stackTrace=<![CDATA[").append(Utils.stackTrace(e)).append("]]>");
logger.warn(sb.toString());
response = Response.serverError().entity("Failed to set channels for " + connectorName + ".").build();
}
return response;
}
@GET
@Path("/filters")
@Produces({MediaType.APPLICATION_JSON})
public Response getConnectorFilterState(){
long vieweeId = AuthHelper.getGuestId();
try {
StringBuilder sb = new StringBuilder("module=API component=connectorStore action=getConnectorFilterState")
.append(" guestId=").append(vieweeId);
logger.info(sb.toString());
return Response.ok(settingsService.getConnectorFilterState(vieweeId)).build();
}
catch (Exception e) {
StringBuilder sb = new StringBuilder("module=API component=connectorStore action=getConnectorFilterState")
.append(" guestId=").append(vieweeId)
.append(" stackTrace=<![CDATA[").append(Utils.stackTrace(e)).append("]]>");
logger.warn(sb.toString());
return Response.serverError().entity("Failed to get filters: " + e.getMessage()).build();
}
}
@POST
@Path("/filters")
@Produces({MediaType.TEXT_PLAIN})
@ApiResponses({
@ApiResponse(code = 401, message = "If the guest is no longer logged in")
})
public Response setConnectorFilterState(@FormParam("filterState") String stateJSON){
Response response = null;
Guest guest = AuthHelper.getGuest();
// If no guest is logged in, return empty array
if(guest==null)
return Response.status(401).entity("You are no longer logged in").build();
try {
settingsService.setConnectorFilterState(guest.getId(), stateJSON);
StringBuilder sb = new StringBuilder("module=API component=connectorStore action=setConnectorFilterState")
.append(" filterState=").append(stateJSON)
.append(" guestId=").append(guest.getId());
logger.info(sb.toString());
response = Response.ok("Successfully updated filters state!").build();
}
catch (Exception e) {
StringBuilder sb = new StringBuilder("module=API component=connectorStore action=setConnectorFilterState")
.append(" guestId=").append(guest.getId())
.append(" filterState=").append(stateJSON)
.append(" stackTrace=<![CDATA[").append(Utils.stackTrace(e)).append("]]>");
logger.warn(sb.toString());
response = Response.serverError().entity("Failed to udpate filters state!").build();
}
return response;
}
@GET
@Path("/{objectTypeName}/data")
@Produces({MediaType.APPLICATION_JSON})
@ApiResponses({
@ApiResponse(code = 401, message = "If the guest is no longer logged in"),
@ApiResponse(code = 403, message = "If this call is made by a coach and the coachee revoked access to this object type")
})
public Response getData(@PathParam("objectTypeName") String objectTypeName,
@QueryParam("start") long start,
@QueryParam("end") long end,
@QueryParam("value") String value,
@ApiParam(value="Buddy to access username parameter (" + BuddiesService.BUDDY_TO_ACCESS_PARAM + ")", required=false) @QueryParam(BuddiesService.BUDDY_TO_ACCESS_PARAM) String buddyToAccessParameter){
Guest guest = AuthHelper.getGuest();
if(guest==null)
return Response.status(401).entity("You are no longer logged in").build();
CoachingBuddy coachee;
try {
coachee = AuthHelper.getCoachee(buddyToAccessParameter, buddiesService);
} catch (CoachRevokedException e) {
return Response.status(403).entity("Sorry, permission to access this data has been revoked. Please reload your browser window").build();
}
if (coachee!=null) {
guest = guestService.getGuestById(coachee.guestId);
}
String [] objectTypeNameParts = objectTypeName.split("-");
ApiKey apiKey = guestService.getApiKeys(guest.getId(),Connector.getConnector(objectTypeNameParts[0])).get(0);
Connector connector = apiKey.getConnector();
final AbstractBodytrackResponder bodytrackResponder = connector.getBodytrackResponder(beanFactory);
return Response.ok(bodytrackResponder.getFacetVOs(settingsService.getSettings(guest.getId()), apiKey, objectTypeName, start, end, value)).build();
}
}