package org.fluxtream.core.services.impl;
import com.google.gson.*;
import com.wordnik.swagger.annotations.ApiModel;
import com.wordnik.swagger.annotations.ApiModelProperty;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.fluxtream.core.Configuration;
import org.fluxtream.core.TimeInterval;
import org.fluxtream.core.aspects.FlxLogger;
import org.fluxtream.core.auth.AuthHelper;
import org.fluxtream.core.connectors.Connector;
import org.fluxtream.core.connectors.ObjectType;
import org.fluxtream.core.domain.*;
import org.fluxtream.core.services.*;
import org.fluxtream.core.utils.JPAUtils;
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.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import java.io.*;
import java.lang.reflect.Type;
import java.util.*;
//import java.nio.file.Path;
//import java.nio.file.Paths;
/**
*
* @author Candide Kemmler (candide@fluxtream.com)
*/
@Component
@Transactional(readOnly = true)
public class BodyTrackHelper {
public interface BodyTrackUploadResult {
/**
* Status code for the upload operation.
* @see #isSuccess()
*/
int getStatusCode();
/** Text output from the upload, usually (always?) JSON. */
String getResponse();
/** Returns <code>true</code> if the upload was successful, <code>false</code> otherwise. */
boolean isSuccess();
}
protected long getMinTimeForApiKey(long apiKeyId, Integer objectTypeId){
ApiKey apiKey = guestService.getApiKey(apiKeyId);
ObjectType[] objectTypes;
if (objectTypeId != null)
objectTypes = apiKey.getConnector().getObjectTypesForValue(objectTypeId);
else
objectTypes = apiKey.getConnector().objectTypes();
if (objectTypes == null || objectTypes.length == 0){
final String minTimeAtt = guestService.getApiKeyAttribute(apiKey, ApiKeyAttribute.MIN_TIME_KEY);
if (minTimeAtt !=null && StringUtils.isNotEmpty(minTimeAtt)) {
final DateTime dateTime = ISODateTimeFormat.dateHourMinuteSecondFraction().withZoneUTC().parseDateTime(minTimeAtt);
return dateTime.getMillis();
}
}
else{
long minTime = Long.MAX_VALUE;
for (ObjectType objectType : objectTypes){
final String minTimeAtt = guestService.getApiKeyAttribute(apiKey, objectType.getApiKeyAttributeName(ApiKeyAttribute.MIN_TIME_KEY));
if (minTimeAtt !=null && StringUtils.isNotEmpty(minTimeAtt)) {
final DateTime dateTime = ISODateTimeFormat.dateHourMinuteSecondFraction().withZoneUTC().parseDateTime(minTimeAtt);
minTime = Math.min(minTime, dateTime.getMillis());
}
}
if (minTime < Long.MAX_VALUE)
return minTime;
}
//if we couldn't get the minTime from ApiKey Attributes fallback to oldest facet
AbstractFacet facet;
if (objectTypes == null || objectTypes.length == 0){
facet = apiDataService.getOldestApiDataFacet(apiKey,null);
}
else{
facet = null;
for (ObjectType objectType : objectTypes){
AbstractFacet potentialFacet = apiDataService.getOldestApiDataFacet(apiKey,objectType);
if (potentialFacet!=null) {
if (facet == null || facet.start > potentialFacet.start)
facet = potentialFacet;
}
}
}
if (facet != null)
return facet.start;
else
return Long.MAX_VALUE;
}
protected long getMaxTimeForApiKey(long apiKeyId, Integer objectTypesMask){
ApiKey apiKey = guestService.getApiKey(apiKeyId);
ObjectType[] objectTypes;
if (objectTypesMask != null)
objectTypes = apiKey.getConnector().getObjectTypesForValue(objectTypesMask);
else
objectTypes = apiKey.getConnector().objectTypes();
if (objectTypes == null || objectTypes.length == 0){
final String maxTimeAtt = guestService.getApiKeyAttribute(apiKey, ApiKeyAttribute.MAX_TIME_KEY);
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);
maxTime = Math.max(maxTime,dateTime.getMillis());
}
}
if (maxTime > Long.MIN_VALUE)
return maxTime;
}
//if we couldn't get the minTime from ApiKey Attributes fallback to oldest facet
AbstractFacet facet;
if (objectTypes == null || objectTypes.length == 0){
facet = apiDataService.getLatestApiDataFacet(apiKey,null);
}
else{
facet = null;
for (ObjectType objectType : objectTypes){
AbstractFacet potentialFacet = apiDataService.getLatestApiDataFacet(apiKey,objectType);
if (potentialFacet != null && (facet == null || facet.end < potentialFacet.end))
facet = potentialFacet;
}
}
if (facet != null)
return facet.end;
else
return Long.MIN_VALUE;
}
public void setChannelBounds(final ChannelMapping mapping, final Channel channel, ChannelInfoResponse infoResponse) {
Set<String> channelNames = infoResponse.channel_specs.keySet();
boolean datastoreChannelBoundsSet = false;
switch (mapping.getChannelType()){
case photo:
channel.min = 0.6;
channel.max = 1;
break;
case data:
Connector connector = Connector.fromDeviceNickname(mapping.getDeviceName());
if (connector!=null) {
// historically, some channels have used a device nickname, others a connector name
String deviceChannelName = new StringBuilder(connector.getName()).append(".").append(mapping.getChannelName()).toString();
String deviceNicknameChannelName = new StringBuilder(mapping.getDeviceName()).append(".").append(mapping.getChannelName()).toString();
String internalDeviceNicknameChannelName = new StringBuilder(mapping.getInternalDeviceName()).append(".").append(mapping.getChannelName()).toString();
String deviceInternalChannelName = new StringBuilder(mapping.getDeviceName()).append(".").append(mapping.getInternalChannelName()).toString();
for (String channelName : channelNames) {
if (channelName.toLowerCase().equals(deviceChannelName.toLowerCase()) || channelName.toLowerCase().equals(deviceNicknameChannelName.toLowerCase())||
channelName.toLowerCase().equals(internalDeviceNicknameChannelName.toLowerCase())||deviceInternalChannelName.equals(channelName)) {
ChannelSpecs channelSpecs = infoResponse.channel_specs.get(channelName);
channel.min = channelSpecs.channel_bounds.min_value;
channel.max = channelSpecs.channel_bounds.max_value;
channel.min_time = channelSpecs.channel_bounds.min_time;
channel.max_time = channelSpecs.channel_bounds.max_time;
datastoreChannelBoundsSet = true;
break;
}
}
} else {
channel.min = 0;
channel.max = 1;
}
break;
default:
channel.min = 0;
channel.max = 1;
break;
}
if (!datastoreChannelBoundsSet) {
long maxTime = getMaxTimeForApiKey(mapping.getApiKeyId(), mapping.getObjectTypes());
long minTime = getMinTimeForApiKey(mapping.getApiKeyId(), mapping.getObjectTypes());
if (maxTime < minTime) {
channel.max_time = 0d;
channel.min_time = 0d;
} else {
channel.max_time = maxTime / 1000.0;
channel.min_time = minTime / 1000.0;
}
}
}
@PersistenceContext
EntityManager em;
static final boolean verboseOutput = false;
static final boolean showOutput = false;
@Autowired
DataUpdateService dataUpdateService;
@Autowired
Configuration env;
@Autowired
PhotoService photoService;
@Autowired
GuestService guestService;
@Autowired
ApiDataService apiDataService;
@Autowired
BuddiesService buddiesService;
@Autowired
BeanFactory beanFactory;
// Create a Gson parser which handles ChannelBounds specially to avoid problems with +/- infinity
Gson gson = new GsonBuilder().registerTypeAdapter(ChannelBounds.class, new ChannelBoundsDeserializer()).create();
static FlxLogger logger = FlxLogger.getLogger(BodyTrackHelper.class);
private int executeDataStore(String commandName, Object[] parameters,OutputStream out){
try{
Runtime rt = Runtime.getRuntime();
String launchCommand = env.targetEnvironmentProps.getString("btdatastore.exec.location") + "/" + commandName + " " +
env.targetEnvironmentProps.getString("btdatastore.db.location");
// Path commandPath= Paths.get(env.targetEnvironmentProps.getString("btdatastore.exec.location"));
// Path launchExecutable = commandPath.resolve(commandName);
// String launchCommand = launchExecutable.toString()+ " " + env.targetEnvironmentProps.getString("btdatastore.db.location");
for (Object param : parameters){
launchCommand += ' ';
String part = param.toString();
if (part.indexOf(' ') == -1){
launchCommand += part;
}
else{
launchCommand += "\"" + part + "\"";
}
}
if (showOutput)
System.out.println("BTDataStore: running with command: " + launchCommand);
//create process for operation
final Process pr = rt.exec(launchCommand);
new Thread(){//outputs the errorstream
public void run(){
BufferedReader error = new BufferedReader(new InputStreamReader(pr.getErrorStream()));
String line=null;
try{
if (verboseOutput && showOutput){
while((line=error.readLine()) != null) { //output all console output from the execution
System.out.println("BTDataStore-error: " + line);
}
}
else
while (error.readLine() != null) {}
} catch(Exception ignored){}
}
}.start();
BufferedReader input = new BufferedReader(new InputStreamReader(pr.getInputStream()));
String line;
boolean first = true;
while((line=input.readLine()) != null) { //output all console output from the execution
if (showOutput)
System.out.println("BTDataStore: " + line);
if (first){
first = false;
}
else{
out.write("\n".getBytes());
}
out.write(line.getBytes());
}
int exitValue = pr.waitFor();
if (showOutput)
System.out.println("BTDataStore: exited with code " + exitValue);
return exitValue;
}
catch (Exception e){
if (showOutput)
System.out.println("BTDataStore: datastore execution failed!");
throw new RuntimeException("Datastore execution failed");
}
}
private DataStoreExecutionResult executeDataStore(String commandName, Object[] parameters){
final StringBuilder responseBuilder = new StringBuilder();
int result = executeDataStore(commandName,parameters,new OutputStream(){
@Override
public void write(final int b) throws IOException {
responseBuilder.append((char) b);
}
});
return new DataStoreExecutionResult(result,responseBuilder.toString());
}
//start and end are optional
public int exportToCSV(final Long guestId, final Collection<String> channelNames, final Long start, final Long end, final OutputStream out){
try{
if (guestId == null)
throw new IllegalArgumentException();
if (channelNames == null || channelNames.size() == 0)
throw new IllegalArgumentException();
ArrayList<String> params = new ArrayList<String>();
params.add("--csv");
params.add("" + guestId);
params.addAll(channelNames);
if (start != null){
params.add("--start");
params.add("" + start);
}
if (end != null){
params.add("--end");
params.add("" + end);
}
final DataStoreExecutionResult dataStoreExecutionResult = executeDataStore("export",params.toArray(new String[]{}));
out.write(dataStoreExecutionResult.getResponse().getBytes());
return dataStoreExecutionResult.getStatusCode();
}
catch (Exception e){
return -1;
}
}
public BodyTrackUploadResult uploadToBodyTrack(final ApiKey apiKey,
final String deviceName,
final Collection<String> channelNames,
final List<List<Object>> data) {
try{
if (apiKey == null)
throw new IllegalArgumentException();
final File tempFile = File.createTempFile("input",".json");
Map<String,Object> tempFileMapping = new HashMap<String,Object>();
tempFileMapping.put("data", data);
tempFileMapping.put("channel_names", channelNames);
FileOutputStream fos = new FileOutputStream(tempFile);
final String bodyTrackJSONData = gson.toJson(tempFileMapping);
fos.write(bodyTrackJSONData.getBytes());
fos.close();
final DataStoreExecutionResult dataStoreExecutionResult = executeDataStore("import", new Object[]{apiKey.getGuestId(), deviceName, tempFile.getAbsolutePath()});
ParsedBodyTrackUploadResult parsedResult = new ParsedBodyTrackUploadResult(dataStoreExecutionResult, deviceName, gson);
if (!dataStoreExecutionResult.isSuccess()) {
logger.warn("Datastore: There was an error persisting data to the datastore, guestId: " + apiKey.getGuestId() + ", deviceName: " + deviceName + ", tempFile: " + tempFile.getCanonicalPath());
dataUpdateService.logBodyTrackDataUpdate(apiKey.getGuestId(),
apiKey.getId(), null, deviceName, channelNames.toArray(new String[channelNames.size()]), dataStoreExecutionResult.getResponse());
} else {
try {
long startTime = 0, endTime = 0;
if (parsedResult.getParsedResponse().min_time!=null)
startTime = (long) (parsedResult.getParsedResponse().min_time * 1000);
if (parsedResult.getParsedResponse().max_time!=null)
endTime = (long) (parsedResult.getParsedResponse().max_time * 1000);
dataUpdateService.logBodyTrackDataUpdate(apiKey.getGuestId(),
apiKey.getId(), null, deviceName, channelNames.toArray(new String[channelNames.size()]), startTime, endTime);
} catch (Throwable t) {
logger.warn("Datastore: couldn't log successful api data update");
logger.warn(ExceptionUtils.getStackTrace(t));
}
}
tempFile.delete();
return parsedResult;
} catch (Exception e) {
System.err.println("Could not persist to datastore");
System.err.println(Utils.stackTrace(e));
throw new RuntimeException("Could not persist to datastore");
}
}
public BodyTrackUploadResult uploadJsonToBodyTrack(final Long guestId,
final String deviceName,
final String json) {
try{
if (guestId == null)
throw new IllegalArgumentException();
final File tempFile = File.createTempFile("input",".json");
FileOutputStream fos = new FileOutputStream(tempFile);
fos.write(json.getBytes());
fos.close();
final ParsedBodyTrackUploadResult dataStoreExecutionResult = new ParsedBodyTrackUploadResult(executeDataStore("import", new Object[]{guestId, deviceName, tempFile.getAbsolutePath()}), deviceName, gson);
tempFile.delete();
if (dataStoreExecutionResult.isSuccess()){//log to DataUpdate table //TODO: confirm this works
List<ApiKey> keys = guestService.getApiKeys(guestId,Connector.getConnector("fluxtream_capture"));
long apiKeyId = -1;
if (keys.size() > 0){
apiKeyId = keys.get(0).getId();
}
dataUpdateService.logBodyTrackDataUpdate(guestId,apiKeyId,null,dataStoreExecutionResult);
}
return dataStoreExecutionResult;
} catch (Exception e) {
System.out.println("Could not persist to datastore");
System.out.println(Utils.stackTrace(e));
throw new RuntimeException("Could not persist to datastore");
}
}
public GetTileResponse fetchTileObject(Long guestId, String deviceNickname, String channelName, int level, long offset){
try{
if (guestId == null)
throw new IllegalArgumentException();
ChannelMapping mapping = getChannelMapping(guestId, deviceNickname, channelName);
String internalDeviceName = mapping != null ? mapping.getInternalDeviceName() : deviceNickname;
String internalChannelName = mapping != null ? mapping.getInternalChannelName() : channelName;
internalDeviceName = checkDatastoreDir(guestId, internalDeviceName);
final DataStoreExecutionResult dataStoreExecutionResult = executeDataStore("gettile", new Object[]{guestId, internalDeviceName + "." + internalChannelName, level, offset});
String result = dataStoreExecutionResult.getResponse();
// TODO: check statusCode in DataStoreExecutionResult
GetTileResponse tileResponse = gson.fromJson(result,GetTileResponse.class);
if (tileResponse.data == null){
tileResponse = GetTileResponse.getEmptyTile(level,offset);
}//TODO:several fields are missing still and should be implemented
return tileResponse;
}
catch(Exception e){
return GetTileResponse.getEmptyTile(level,offset);
}
}
private String checkDatastoreDir(Long guestId, String internalDeviceName) throws IOException {
File dir = new File(env.targetEnvironmentProps.getString("btdatastore.db.location")+File.separator+guestId+File.separator+ internalDeviceName);
if (dir.exists() && dir.getCanonicalPath().endsWith(internalDeviceName))
return internalDeviceName;
String connectorName = Connector.fromDeviceNickname(internalDeviceName).getName();
dir = new File(env.targetEnvironmentProps.getString("btdatastore.db.location")+File.separator+guestId+File.separator+connectorName);
if (dir.exists() && dir.getCanonicalPath().endsWith(connectorName))
return connectorName;
return internalDeviceName;
}
public String fetchTile(Long guestId, String deviceNickname, String channelName, int level, long offset){
return gson.toJson(fetchTileObject(guestId,deviceNickname,channelName,level,offset));
}
public String getSourcesResponse(Long guestId, TrustedBuddy trustedBuddy) {
final SourcesResponse response = new SourcesResponse();
final DataStoreExecutionResult dataStoreExecutionResult = executeDataStore("info",new Object[]{"-r",guestId});
String result = dataStoreExecutionResult.getResponse();
// Iterate over the various (photo) connectors (if any), manually inserting each into the ChannelSpecs
final Map<String, TimeInterval> photoChannelTimeRanges = photoService.getPhotoChannelTimeRanges(guestId, null);
// TODO: check statusCode in DataStoreExecutionResult
ChannelInfoResponse infoResponse = gson.fromJson(result, ChannelInfoResponse.class);
// create the 'All' photos block
final Source allPhotosSource = getAllPhotosSource(infoResponse, photoChannelTimeRanges);
// retrieve channel mappings directly if trustedBuddy is null or through the SharedChannels otherwise
final List<ChannelMapping> channelMappings = getChannelMappings(guestId, trustedBuddy);
// populateResponseWithChannelMappings is meant to be backward compatible with the LegacyBodytrackController
// and so it includes a trustedBuddy parameter because it has another (deprecated) way of figuring out
// access permissions to a buddy's info - here it has to be null since we have already filtered out
// Channels to which the loggedIn guest doesn't have access
try {
populateResponseWithChannelMappings(guestId, null /*IMPORTANT: trustedBuddy needs to be null here*/,
response, channelMappings, infoResponse);
} catch (Throwable e) {
e.printStackTrace();
throw new RuntimeException("Unexpected error trying to populate response with channel mappings: " + e.getMessage());
}
// if trustedBuddy is null, add the All photos block to the response
if (trustedBuddy==null&&!photoChannelTimeRanges.isEmpty()) {
response.sources.add(allPhotosSource);
}
final String jsonResponse = gson.toJson(response);
return jsonResponse;
}
public String listSources(Long guestId, TrustedBuddy trustedBuddy){
SourcesResponse response = null;
try{
if (guestId == null) {
throw new IllegalArgumentException();
}
final DataStoreExecutionResult dataStoreExecutionResult = executeDataStore("info",new Object[]{"-r",guestId});
String result = dataStoreExecutionResult.getResponse();
// TODO: check statusCode in DataStoreExecutionResult
ChannelInfoResponse infoResponse = gson.fromJson(result,ChannelInfoResponse.class);
// Iterate over the various (photo) connectors (if any), manually inserting each into the ChannelSpecs
final Map<String, TimeInterval> photoChannelTimeRanges = photoService.getPhotoChannelTimeRanges(guestId, trustedBuddy);
// create the 'All' photos block
final Source allPhotosSource = getAllPhotosSource(infoResponse, photoChannelTimeRanges);
// create the respone
response = new SourcesResponse(infoResponse, guestId, trustedBuddy);
// filter out photo connectors that aren't shared with this user
if (trustedBuddy !=null) {
List<String> sourcesToRemove = new ArrayList<String>();
for (Source source : response.sources) {
final Connector photoConnectorForSource = Connector.fromDeviceNickname(source.name);
if (photoConnectorForSource!=null) {
final List<ApiKey> apiKeys = guestService.getApiKeys(trustedBuddy.guestId, photoConnectorForSource);
for (ApiKey apiKey : apiKeys) {
if (buddiesService.getSharedConnector(apiKey.getId(), AuthHelper.getGuestId())==null) {
sourcesToRemove.add(source.name);
break;
}
}
// 09/15/2014 on Anne's request: until we have thoroughly fixed the management
// of channel mappings, sources that don't map to connectors need to continue being
// shared with buddies
// } else {
// // let's be conservative: if we don't know this connector, let's assume
// // it wasn't shared
// sourcesToRemove.add(source.name);
}
}
for (String sourceName : sourcesToRemove) {
response.deleteSource(sourceName);
}
}
//TODO: this is a hack to prevent double flickr photo channel showing up
response.deleteSource("Flickr");
response.deleteSource("SMS_Backup");
final List<ChannelMapping> channelMappings = getChannelMappings(guestId, trustedBuddy);
populateResponseWithChannelMappings(guestId, trustedBuddy, response, channelMappings, infoResponse);
// add the All photos block to the response
if (!photoChannelTimeRanges.isEmpty()) {
response.sources.add(allPhotosSource);
}
for (Source source : response.sources){
if (source.max_time < source.min_time)
source.min_time = source.max_time = 0.0;
}
final String jsonResponse = gson.toJson(response);
return jsonResponse;
}
catch(Exception e){
e.printStackTrace();
StringBuilder sb = new StringBuilder("module=bodytrackHelper component=listSources action=listSources")
.append(" guestId=")
.append(guestId)
.append(" message=").append(e.getMessage());
if(response!=null) {
// In case the exception was caused by speical floating point values such as
// Infinity, create a gson builder that will potentially let us debug even though
// the javascript would choke on the result if we returned it
Gson errorGson = new GsonBuilder().serializeSpecialFloatingPointValues().create();
sb.append(" response=").append(errorGson.toJson(response));
}
logger.error(sb.toString());
return gson.toJson(new SourcesResponse(null, guestId, trustedBuddy));
}
}
private Source getAllPhotosSource(ChannelInfoResponse infoResponse, Map<String, TimeInterval> photoChannelTimeRanges) {
final Source allPhotosSource = new Source();
if (!photoChannelTimeRanges.isEmpty()) {
allPhotosSource.name = PhotoService.ALL_DEVICES_NAME;
allPhotosSource.channels = new ArrayList<Channel>();
final Channel allPhotosChannel = new Channel();
allPhotosSource.channels.add(allPhotosChannel);
allPhotosChannel.name = PhotoService.DEFAULT_PHOTOS_CHANNEL_NAME; // photo channels are always named the same
allPhotosChannel.objectTypeName = PhotoService.DEFAULT_PHOTOS_CHANNEL_NAME;
allPhotosChannel.type = PhotoService.DEFAULT_PHOTOS_CHANNEL_NAME;
allPhotosChannel.builtin_default_style = new ChannelStyle();
allPhotosChannel.style = allPhotosChannel.builtin_default_style;
allPhotosChannel.min = .6;
allPhotosChannel.max = 1;
allPhotosChannel.min_time = Double.MAX_VALUE;
allPhotosChannel.max_time = Double.MIN_VALUE;
final double defaultTimeForNullTimeIntervals = System.currentTimeMillis() / 1000;
for (final String channelName : photoChannelTimeRanges.keySet()) {
final ChannelSpecs channelSpecs = new ChannelSpecs();
final TimeInterval timeInterval = photoChannelTimeRanges.get(channelName);
// mark this channel as a photo channel so that the grapher can properly render it as a photo channel
channelSpecs.channelType = PhotoService.DEFAULT_PHOTOS_CHANNEL_NAME;
final String[] connectorNameAndObjectTypeName = channelName.split("\\.");
if (connectorNameAndObjectTypeName.length > 1) {
channelSpecs.objectTypeName = connectorNameAndObjectTypeName[1];
}
channelSpecs.channel_bounds = new ChannelBounds();
if (timeInterval == null) {
channelSpecs.channel_bounds.min_time = defaultTimeForNullTimeIntervals;
channelSpecs.channel_bounds.max_time = defaultTimeForNullTimeIntervals;
}
else {
channelSpecs.channel_bounds.min_time = timeInterval.getStart() / 1000;
channelSpecs.channel_bounds.max_time = timeInterval.getEnd() / 1000;
}
channelSpecs.channel_bounds.min_value = .6;
channelSpecs.channel_bounds.max_value = 1;
infoResponse.channel_specs.put(channelName, channelSpecs);
if (timeInterval != null) {
// update the min/max times in ChannelInfoResponse and in the All photos channel
infoResponse.min_time = Math.min(infoResponse.min_time, channelSpecs.channel_bounds.min_time);
infoResponse.max_time = Math.max(infoResponse.max_time, channelSpecs.channel_bounds.max_time);
allPhotosChannel.min_time = Math.min(allPhotosChannel.min_time, channelSpecs.channel_bounds.min_time);
allPhotosChannel.max_time = Math.max(allPhotosChannel.max_time, channelSpecs.channel_bounds.max_time);
}
}
}
return allPhotosSource;
}
private void populateResponseWithChannelMappings(Long guestId,
TrustedBuddy trustedBuddy,
SourcesResponse response,
List<ChannelMapping> channelMappings,
ChannelInfoResponse infoResponse) {
for (ChannelMapping mapping : channelMappings){
ApiKey api = guestService.getApiKey(mapping.getApiKeyId());
// This is to prevent a rare condition when working, under development, on a branch that
// doesn't yet support a connector that is supported on another branch and resulted
// in data being populated in the database which is going to cause a crash here
if (api==null||api.getConnector()==null)
continue;
// filter out not shared connectors
if (trustedBuddy !=null&& buddiesService.getSharedConnector(api.getId(), AuthHelper.getGuestId())==null)
continue;
Source source;
String deviceName;
if (mapping.getInternalDeviceName()!=null&&!mapping.getInternalDeviceName().equals(mapping.getDeviceName())) {
source = response.hasSource(mapping.getInternalDeviceName());
deviceName = mapping.getInternalDeviceName();
} else {
source = response.hasSource(mapping.getDeviceName());
deviceName = mapping.getDeviceName();
}
if (source == null){
source = new Source();
response.sources.add(source);
source.name = Utils.sanitize(deviceName);
source.channels = new ArrayList<Channel>();
source.min_time = Double.MAX_VALUE;
source.max_time = Double.MIN_VALUE;
}
Channel channel = new Channel();
channel.name = Utils.sanitize(mapping.getChannelName());
channel.type = mapping.getChannelType().name();
channel.time_type = mapping.getTimeType().name();
source.channels.add(channel);
// Set builtin default style and style to a line by default
channel.builtin_default_style = ChannelStyle.getDefaultChannelStyle(channel.name);
channel.style = channel.builtin_default_style;
// getDefaultStyle checks for user-generated overrides in the database.
// If it returns non-null we set style to the user-generated value, otherwise we leave
// it as the builtin default
ChannelStyle userStyle = getDefaultStyle(guestId,api.getConnector().getDeviceNickname(),channel.name);
if (userStyle != null) {
channel.style = userStyle;
} else {
userStyle = getDefaultStyle(guestId,api.getConnector().getName(),channel.name);
if (userStyle != null)
channel.style = userStyle;
}
setChannelBounds(mapping, channel, infoResponse);
source.min_time = Math.min(source.min_time,channel.min_time);
source.max_time = Math.max(source.max_time,channel.max_time);
}
long now = System.currentTimeMillis();
}
public SourceInfo getSourceInfoObject(final Long guestId, final String deviceName){
try{
if (guestId == null)
throw new IllegalArgumentException();
long then = System.currentTimeMillis();
final DataStoreExecutionResult dataStoreExecutionResult = executeDataStore("info",new Object[]{"-r",guestId});
String result = dataStoreExecutionResult.getResponse();
long now = System.currentTimeMillis();
System.out.println("datastore execution time = " + (now-then)); then = now;
// TODO: check statusCode in DataStoreExecutionResult
ChannelInfoResponse infoResponse = gson.fromJson(result,ChannelInfoResponse.class);
final Map<String, TimeInterval> photoChannelTimeRanges = photoService.getPhotoChannelTimeRanges(guestId, null);
if (!photoChannelTimeRanges.isEmpty()) {
final double defaultTimeForNullTimeIntervals = System.currentTimeMillis() / 1000;
for (final String channelName : photoChannelTimeRanges.keySet()) {
final ChannelSpecs channelSpecs = new ChannelSpecs();
final TimeInterval timeInterval = photoChannelTimeRanges.get(channelName);
// mark this channel as a photo channel so that the grapher can properly render it as a photo channel
channelSpecs.channelType = PhotoService.DEFAULT_PHOTOS_CHANNEL_NAME;
final String[] connectorNameAndObjectTypeName = channelName.split("\\.");
if (connectorNameAndObjectTypeName.length > 1) {
channelSpecs.objectTypeName = connectorNameAndObjectTypeName[1];
}
channelSpecs.channel_bounds = new ChannelBounds();
if (timeInterval == null) {
channelSpecs.channel_bounds.min_time = defaultTimeForNullTimeIntervals;
channelSpecs.channel_bounds.max_time = defaultTimeForNullTimeIntervals;
}
else {
channelSpecs.channel_bounds.min_time = timeInterval.getStart() / 1000;
channelSpecs.channel_bounds.max_time = timeInterval.getEnd() / 1000;
}
channelSpecs.channel_bounds.min_value = .6;
channelSpecs.channel_bounds.max_value = 1;
infoResponse.channel_specs.put(channelName, channelSpecs);
if (timeInterval != null) {
// update the min/max times in ChannelInfoResponse and in the All photos channel
infoResponse.min_time = Math.min(infoResponse.min_time, channelSpecs.channel_bounds.min_time);
infoResponse.max_time = Math.max(infoResponse.max_time, channelSpecs.channel_bounds.max_time);
}
}
}
SourceInfo response = new SourceInfo(infoResponse,deviceName);
return response;
}
catch(Exception e){
return new SourceInfo(null, null);
}
}
public String getSourceInfo(final Long guestId, final String deviceName) {
return gson.toJson(getSourceInfoObject(guestId,deviceName));
}
public void setDefaultStyle(final Long guestId, final String deviceName, final String channelName, final ChannelStyle style) {
setDefaultStyle(guestId,deviceName,channelName, gson.toJson(style));
}
@Deprecated
@Transactional(readOnly = false)
public void deleteStyle(final Long guestId, final String deviceName) {
try {
JPAUtils.execute(em, "channelStyle.delete.byGuestAndDeviceName", guestId, deviceName);
} catch(Exception e) {logger.warn("Couldn't delete Channel Style for connector "
+ deviceName
+ ", guest: " + guestId
+ "\n" + ExceptionUtils.getStackTrace(e));}
}
@Transactional(readOnly = false)
public void setDefaultStyle(final Long guestId, final String deviceName, final String channelName, final String style) {
try{
if (guestId == null)
throw new IllegalArgumentException();
org.fluxtream.core.domain.ChannelStyle savedStyle = JPAUtils.findUnique(em, org.fluxtream.core.domain.ChannelStyle.class,
"channelStyle.byDeviceNameAndChannelName",
guestId, deviceName, channelName);
if (savedStyle==null) {
savedStyle = new org.fluxtream.core.domain.ChannelStyle();
savedStyle.guestId = guestId;
savedStyle.channelName = channelName;
savedStyle.deviceName = deviceName;
savedStyle.json = style;
em.persist(savedStyle);
} else {
savedStyle.json = style;
em.merge(savedStyle);
}
List<ApiKey> keys = guestService.getApiKeys(guestId,Connector.getConnector("fluxtream_capture"));
long apiKeyId = -1;
if (keys.size() > 0){
apiKeyId = keys.get(0).getId();
}
dataUpdateService.logBodyTrackStyleUpdate(guestId,apiKeyId,null,deviceName,new String[]{channelName});
}
catch (Exception e){
}
}
public String getDeviceName(long apiKeyId) {
ChannelMapping channelMapping = JPAUtils.findUnique(em, ChannelMapping.class, "channelMapping.byApiKeyId", apiKeyId);
if (channelMapping!=null)
return channelMapping.getDeviceName();
return null;
}
public String getInternalDeviceName(long apiKeyId) {
ApiKey apiKey = guestService.getApiKey(apiKeyId);
if (apiKey.getConnector().getName().equals("fluxtream_capture"))
return getDeviceName(apiKeyId);
ChannelMapping channelMapping = JPAUtils.findUnique(em, ChannelMapping.class, "channelMapping.byApiKeyId", apiKeyId);
if (channelMapping!=null)
return channelMapping.getInternalDeviceName();
return null;
}
public ChannelMapping getChannelMapping(long guestId, String displayDeviceName, String displayChannelName){
ChannelMapping channelMapping = JPAUtils.findUnique(em, ChannelMapping.class, "channelMapping.byDisplayName", guestId, displayDeviceName, displayChannelName);
return channelMapping;
}
public List<ChannelMapping> getChannelMappings(long guestId, TrustedBuddy trustedBuddy){
if (trustedBuddy ==null)
return JPAUtils.find(em, ChannelMapping.class, "channelMapping.all",guestId);
else {
List<SharedChannel> sharedChannels = buddiesService.getSharedChannels(trustedBuddy.buddyId, trustedBuddy.guestId);
List<ChannelMapping> channelMappings = new ArrayList<ChannelMapping>();
for (SharedChannel sharedChannel : sharedChannels) {
channelMappings.add(sharedChannel.channelMapping);
}
return channelMappings;
}
}
public void deleteChannelMappings(ApiKey apiKey){
Query query = em.createNamedQuery("channelMapping.delete");
query.setParameter(1,apiKey.getGuestId());
query.setParameter(2,apiKey.getId());
query.executeUpdate();
}
public void setBuiltinDefaultStyle(final Long guestId, final String deviceName, final String channelName, final ChannelStyle style){
setBuiltinDefaultStyle(guestId, deviceName, channelName, gson.toJson(style));
}
@Transactional(readOnly = false)
public void setBuiltinDefaultStyle(final Long guestId, final String deviceName, final String channelName, final String style) {
try{
if (guestId == null)
throw new IllegalArgumentException();
org.fluxtream.core.domain.ChannelStyle savedStyle = JPAUtils.findUnique(em, org.fluxtream.core.domain.ChannelStyle.class, "channelStyle.byDeviceNameAndChannelName", guestId, deviceName, channelName);
if (savedStyle==null) {
savedStyle = new org.fluxtream.core.domain.ChannelStyle();
savedStyle.guestId = guestId;
savedStyle.channelName = channelName;
savedStyle.deviceName = deviceName;
savedStyle.json = style;
em.persist(savedStyle);
}
}
catch (Exception e){
}
}
private ChannelStyle getDefaultStyle(long guestId, String deviceName, String channelName){
org.fluxtream.core.domain.ChannelStyle savedStyle = JPAUtils.findUnique(em, org.fluxtream.core.domain.ChannelStyle.class,
"channelStyle.byDeviceNameAndChannelName",
guestId, deviceName, channelName);
if(savedStyle == null)
return null;
return gson.fromJson(savedStyle.json,ChannelStyle.class);
}
@Transactional(readOnly = false)
public String saveView(long guestId, String viewName, String viewJSON){
GrapherView view = JPAUtils.findUnique(em, GrapherView.class,"grapherView.byName",guestId,viewName);
if (view == null){
view = new GrapherView();
view.guestId = guestId;
view.name = viewName;
view.json = viewJSON;
view.lastUsed = System.currentTimeMillis();
em.persist(view);
}
else{
view.json = viewJSON;
em.merge(view);
}
AddViewResult result = new AddViewResult();
result.saved_view_id = view.getId();
result.populateViews(em, gson, guestId);
return gson.toJson(result);
}
public String listViews(long guestId){
ViewsList list = new ViewsList();
list.populateViews(em, gson, guestId);
return gson.toJson(list);
}
@Transactional(readOnly = false)
public String getView(Long guestId, long viewId){
GrapherView view = JPAUtils.findUnique(em, GrapherView.class,"grapherView.byId",guestId,viewId);
if (view != null){
view.lastUsed = System.currentTimeMillis();
em.merge(view);
}
return view == null ? "{\"error\",\"No matching view found for user " + guestId + "\"}" : view.json;
}
@Transactional(readOnly = false)
public void deleteView(Long guestId, long viewId){
GrapherView view = JPAUtils.findUnique(em, GrapherView.class,"grapherView.byId",guestId,viewId);
em.remove(view);
}
public String getAllTagsForUser(final Long guestId) {
final List<Tag> tagList = JPAUtils.find(em, Tag.class, "tags.all", guestId);
final TagsJson tagsJson = new TagsJson();
if ((tagList != null) && (!tagList.isEmpty())) {
for (final Tag tag : tagList) {
if (tag != null && tag.name != null && tag.name.length() > 0) {
tagsJson.tags.add(tag.name);
}
}
}
return gson.toJson(tagsJson);
}
private static class TagsJson {
private final SortedSet<String> tags = new TreeSet<String>();
}
@ApiModel
public static class ViewsList{
@ApiModelProperty
public LinkedList<ViewStub> views = new LinkedList<ViewStub>();
void populateViews(EntityManager em, Gson gson, long guestId){
List<GrapherView> viewList = JPAUtils.find(em, GrapherView.class,"grapherView",guestId);
for (GrapherView view : viewList){
views.add(0,new ViewStub(view,gson));
}
}
}
public static class AddViewResult extends ViewsList{
public long saved_view_id;
}
@ApiModel
public static class ViewStub{
@ApiModelProperty
public long id;
@ApiModelProperty
public long last_used;
@ApiModelProperty
public String name;
@ApiModelProperty
public AxisRange time_range;
public ViewStub(GrapherView view,Gson gson){
id = view.getId();
last_used = view.lastUsed;
name = view.name;
ViewJSON json = gson.fromJson(view.json,ViewJSON.class);
time_range = new AxisRange();
time_range.min = json.v2.x_axis.min * 1000;
time_range.max = json.v2.x_axis.max * 1000;
}
}
public static class ViewJSON{
public String name;
public ViewData v2;
}
public static class ViewData{
public AxisRange x_axis;
public boolean show_add_pane;
public ArrayList<ViewChannelData> y_axes;
}
public static class AxisRange{
public double min;
public double max;
}
public static class GetTileResponse{
public Object[][] data;
public String[] fields;
public int level;
public long offset;
public int sample_width;
public String type = "value";
public static GetTileResponse getEmptyTile(int level, long offset){
GetTileResponse tileResponse = new GetTileResponse();
tileResponse.data = new Object[0][];
tileResponse.level = level;
tileResponse.offset = offset;
tileResponse.fields = new String[]{"time", "mean", "stddev", "count"};
return tileResponse;
}
}
private static class ChannelInfoResponse {
Map<String,ChannelSpecs> channel_specs;
double max_time;
double min_time;
}
private static class ChannelSpecs{
String channelType;
String objectTypeName;
String time_type;
ChannelBounds channel_bounds;
public ChannelSpecs(){
// time_type defaults to gmt. It can be overridden to "local" for channels that only know local time
time_type = "gmt";
}
}
private static class ChannelBounds{
double max_time;
double max_value;
double min_time;
double min_value;
}
class ChannelBoundsDeserializer implements JsonDeserializer<ChannelBounds>{
// Create a custom deserializer for ChannelBounds to deal with the possibility
// of the values being interpreted as +/- Infinity and causing json creation errors
// later on. The min/max time fields are required. The min/max value fields are
// optional and default to 0.
@Override
public ChannelBounds deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
ChannelBounds cb=new ChannelBounds();
JsonObject jo = (JsonObject)json;
cb.max_time=Math.max(Math.min(jo.get("max_time").getAsDouble(), Double.MAX_VALUE),-Double.MAX_VALUE);
cb.min_time=Math.max(Math.min(jo.get("min_time").getAsDouble(), Double.MAX_VALUE), -Double.MAX_VALUE);
try {
cb.max_value=Math.max(Math.min(jo.get("max_value").getAsDouble(), Double.MAX_VALUE),-Double.MAX_VALUE);
cb.min_value=Math.max(Math.min(jo.get("min_value").getAsDouble(), Double.MAX_VALUE), -Double.MAX_VALUE);
} catch(Throwable e) {
cb.min_value=cb.max_value=0;
}
return cb;
}
}
@ApiModel
public class SourcesResponse {
@ApiModelProperty
public List<Source> sources = new ArrayList<Source>();
public SourcesResponse() {}
public SourcesResponse(ChannelInfoResponse infoResponse, Long guestId, TrustedBuddy trustedBuddy){
sources = new ArrayList<Source>();
if (infoResponse == null)
return;
for (Map.Entry<String,ChannelSpecs> entry : infoResponse.channel_specs.entrySet()){
String fullName = entry.getKey();
ChannelSpecs specs = entry.getValue();
String[] split = fullName.split("\\.");
// device.objectTypeName._comment should not generate an entry
if (split.length>2)
continue;
String deviceName = split[0];
String objectTypeName = split[1];
Source source = null;
if (trustedBuddy ==null || trustedBuddy.hasAccessToDevice(deviceName, env)) {
for (Source src : sources)
if (src.name.equals(deviceName)){
source = src;
break;
}
if (source == null){
source = new Source();
source.name = deviceName;
source.channels = new ArrayList<Channel>();
sources.add(source);
}
Channel newChannel = new Channel(objectTypeName,specs);
// Setup style settings. The Channel constructor sets builtin_default_style
// and style to default line settings. getDefaultStyle checks for user-generated overrides
// in the database. If it returns non-null we set style to the user-generated value.
ChannelStyle userStyle = getDefaultStyle(guestId,source.name,newChannel.name);
if (userStyle != null)
newChannel.style = userStyle;
// Temporary hack: Until generic support is available for time_type, special case
// devices named 'Zeo', 'Fitbit', or 'Flickr' to use time_type="local"
if(source.name.equals("Zeo") || source.name.equals("Fitbit") || source.name.equals("Flickr")) {
newChannel.time_type="local";
}
// Add channel to source's channel list
source.channels.add(newChannel);
}
}
}
public Source hasSource(String deviceName){
for (Source s : sources){
if (s.name.equals(deviceName))
return s;
}
return null;
}
public void deleteSource(String deviceName){
for (Iterator<Source> i = sources.iterator(); i.hasNext();){
Source s = i.next();
if (s.name.equals(deviceName))
i.remove();
}
}
}
@ApiModel
public static class SourceInfo{
@ApiModelProperty
public Source info;
public SourceInfo(ChannelInfoResponse infoResponse, String deviceName){
info = new Source();
info.name = deviceName;
info.channels = new ArrayList<Channel>();
info.max_time = System.currentTimeMillis() / 1000.0;
info.min_time = info.max_time - 1;
if (infoResponse == null)
return;
for (Map.Entry<String,ChannelSpecs> entry : infoResponse.channel_specs.entrySet()){
String fullName = entry.getKey();
ChannelSpecs specs = entry.getValue();
String[] split = fullName.split("\\.");
// device.objectTypeName._comment should not generate an entry
if (split.length>2)
continue;
String devName = split[0];
String objectTypeName = split[1];
Source source = null;
if (devName.equals(deviceName)){
Channel channel = new Channel(objectTypeName,specs);
info.channels.add(channel);
if (channel.min_time < info.min_time)
info.min_time = channel.min_time;
if (channel.max_time > info.max_time)
info.max_time = channel.max_time;
}
}
}
}
@ApiModel
public static class Source{
@ApiModelProperty
public String name;
@ApiModelProperty
public List<Channel> channels;
@ApiModelProperty
public Double min_time = 0.0;
@ApiModelProperty
public Double max_time = 0.0;
}
@ApiModel
public static class Channel{
@ApiModelProperty
public String type;
@ApiModelProperty
public ChannelStyle builtin_default_style;
@ApiModelProperty
public ChannelStyle style;
@ApiModelProperty
public double max;
@ApiModelProperty
public double min;
@ApiModelProperty
public Double min_time;
@ApiModelProperty
public Double max_time;
@ApiModelProperty
public String name;
@ApiModelProperty
public String objectTypeName;
@ApiModelProperty
public String time_type;
public Channel(){
// time_type defaults to gmt. It can be overridden to "local" for channels that only know local time
time_type = "gmt";
}
public Channel(String name, ChannelSpecs specs){
this.name = name;
max = specs.channel_bounds.max_value;
min = specs.channel_bounds.min_value;
min_time = specs.channel_bounds.min_time;
max_time = specs.channel_bounds.max_time;
if (specs.channelType != null) {
this.name = PhotoService.DEFAULT_PHOTOS_CHANNEL_NAME; // photo channels are always named the same
type = specs.channelType;
}
if (specs.objectTypeName != null) {
objectTypeName = specs.objectTypeName;
}
style = builtin_default_style = ChannelStyle.getDefaultChannelStyle(name);
// time_type defaults to gmt. It can be overridden to "local" for channels that only know local time
time_type = specs.time_type;
}
}
public static class ViewChannelData extends Channel{
public Integer channel_height;
public String channel_name;
public String device_name;
}
@ApiModel
public static class ChannelStyle{
@ApiModelProperty
public HighlightStyling highlight;
@ApiModelProperty
public CommentStyling comments;
@ApiModelProperty
public List<Style> styles;
@ApiModelProperty
public MainTimespanStyle timespanStyles;
public static ChannelStyle getDefaultChannelStyle(String name){
ChannelStyle style = new ChannelStyle();
style.styles = new ArrayList<Style>();
if (name.equals("Sleep_Graph")){
Style subStyle = new Style();
subStyle.type = "zeo";
subStyle.show = true;
style.styles.add(subStyle);
}
else{
Style subStyle = new Style();
subStyle.type = "line";
subStyle.lineWidth = 1;
subStyle.show = true;
style.styles.add(subStyle);
}
return style;
}
}
public static class TimespanStyle{
public Integer borderWidth;
public String borderColor;
public String fillColor;
public Double top;
public Double bottom;
public String iconURL;
}
public static class MainTimespanStyle{
public TimespanStyle defaultStyle;
public Map<String, TimespanStyle> values;
}
public static class CommentStyling{
public Boolean show;
public Integer verticalMargin;
public ArrayList<Style> styles;
}
public static class HighlightStyling{
public Integer lineWidth;
public ArrayList<Style> styles;
}
public static class Style{
public String type;
public Integer lineWidth;
public String color;
public String fillColor;
public Integer marginWidth;
public String numberFormat;
public Integer verticalOffset;
public Integer radius;
public Boolean fill;
public Boolean show;
}
public static final class UploadResponseChannelSpecs{
ChannelBounds imported_bounds;
ChannelBounds channel_bounds;
}
public static final class UploadResponse{
Map<String,UploadResponseChannelSpecs> channel_specs;
int failed_records;
int successful_records;
Double max_time; //seconds
Double min_time; //seconds
}
public static final class ParsedBodyTrackUploadResult implements BodyTrackUploadResult {
private int statusCode;
private String responseText;
private UploadResponse parsedResponse;
private String deviceName;
private ParsedBodyTrackUploadResult(BodyTrackUploadResult result, String deviceName,Gson gson){
this.statusCode = result.getStatusCode();
this.responseText = result.getResponse();
this.deviceName = deviceName;
if (result.isSuccess())
this.parsedResponse = gson.fromJson(responseText,UploadResponse.class);
else {
this.parsedResponse = new UploadResponse();
logger.warn("Couldn't upload to bodytrack, (response text is \"" + responseText + "\", statusCode: " + statusCode + ", deviceName: " + deviceName + ")");
}
}
public UploadResponse getParsedResponse(){
return parsedResponse;
}
public String getDeviceName(){
return deviceName;
}
@Override
public int getStatusCode() {
return statusCode;
}
@Override
public String getResponse() {
return responseText;
}
@Override
public boolean isSuccess() {
return statusCode == 0;
}
}
public static final class DataStoreExecutionResult implements BodyTrackUploadResult {
private final int statusCode;
private final String response;
private DataStoreExecutionResult(final int statusCode, final String response) {
this.statusCode = statusCode;
this.response = response;
}
@Override
public int getStatusCode() {
return statusCode;
}
@Override
public String getResponse() {
return response;
}
@Override
public boolean isSuccess() {
return statusCode == 0;
}
}
}