Package org.fluxtream.core.api

Source Code of org.fluxtream.core.api.BodyTrackController

package org.fluxtream.core.api;

import com.google.gson.*;
import com.google.gson.annotations.Expose;
import com.google.gson.reflect.TypeToken;
import com.sun.jersey.core.header.ContentDisposition;
import com.sun.jersey.multipart.BodyPart;
import com.sun.jersey.multipart.BodyPartEntity;
import com.sun.jersey.multipart.MultiPart;
import com.wordnik.swagger.annotations.*;
import org.apache.commons.io.IOUtils;
import org.fluxtream.core.Configuration;
import org.fluxtream.core.SimpleTimeInterval;
import org.fluxtream.core.TimeInterval;
import org.fluxtream.core.TimeUnit;
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.connectors.bodytrackResponders.AbstractBodytrackResponder;
import org.fluxtream.core.connectors.dao.FacetDao;
import org.fluxtream.core.connectors.fluxtream_capture.FluxtreamCapturePhoto;
import org.fluxtream.core.connectors.fluxtream_capture.FluxtreamCapturePhotoFacet;
import org.fluxtream.core.connectors.fluxtream_capture.FluxtreamCapturePhotoStore;
import org.fluxtream.core.connectors.vos.AbstractPhotoFacetVO;
import org.fluxtream.core.domain.*;
import org.fluxtream.core.images.ImageOrientation;
import org.fluxtream.core.mvc.models.DimensionModel;
import org.fluxtream.core.mvc.models.TimespanModel;
import org.fluxtream.core.services.*;
import org.fluxtream.core.services.impl.BodyTrackHelper;
import org.fluxtream.core.utils.ConnectorUtils;
import org.fluxtream.core.utils.HashUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormatter;
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.security.access.annotation.Secured;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import java.lang.reflect.Type;
import java.security.NoSuchAlgorithmException;
import java.util.*;

@Path("/v1/bodytrack")
@Component("RESTBodytrackController")
@Api(value = "/bodytrack", description = "CSV export and import, timeline-related operations")
@Scope("request")
public class BodyTrackController {

    private static final FlxLogger LOG = FlxLogger.getLogger(BodyTrackController.class);
    private static final FlxLogger LOG_DEBUG = FlxLogger.getLogger("Fluxtream");
    private static final int ONE_WEEK_IN_SECONDS = 604800;

    @Autowired
  GuestService guestService;

    @Autowired
    DataUpdateService dataUpdateService;

  @Autowired
  BodyTrackStorageService bodytrackStorageService;

    @Autowired
    BodyTrackHelper bodyTrackHelper;

    @Autowired
    BuddiesService buddiesService;

    @Autowired
    private FluxtreamCapturePhotoStore fluxtreamCapturePhotoStore;

    @Autowired
    PhotoService photoService;

  Gson gson = new Gson();

  @Autowired
  Configuration env;

    @Autowired
  protected ApiDataService apiDataService;

    @Autowired
    FacetDao facetDao;

    @Autowired JsonResponseHelper jsonResponseHelper;

    @Autowired
    BeanFactory beanFactory;

    @GET
    @Path("/exportCSV/{UID}/fluxtream-export-from-{start}-to-{end}.csv")
    @ApiOperation(value = "CSV export of data from a given time range")
    @ApiResponses({
        @ApiResponse(code=200, message="CSV data")
    })
    public void exportCSV(@ApiParam(value="Channels", required=true) @QueryParam("channels") String channels,
                          @ApiParam(value="Start time (epoch seconds)", required=true) @PathParam("start") Long start,
                          @ApiParam(value="End time (epoch seconds)", required=true) @PathParam("end") Long end,
                          @ApiParam(value="User ID (must be ID of loggedIn user)", required=true) @PathParam("UID") Long uid,
                          @Context HttpServletResponse response){
        try{
            long loggedInUserId = AuthHelper.getGuestId();
            boolean accessAllowed = isOwnerOrAdmin(uid);
            CoachingBuddy coachee = buddiesService.getTrustingBuddy(loggedInUserId, uid);

            if (!accessAllowed && coachee==null) {
                uid = null;
            }

            if (uid == null) {
                throw new Exception();
            }

            response.setContentType("text/csv");

            String[] channelArray = gson.fromJson(channels,String[].class);

            bodyTrackHelper.exportToCSV(uid,Arrays.asList(channelArray),start,end,response.getOutputStream());
            response.flushBuffer();

        }
        catch (Exception e){
            try{
                response.sendError(500);
            } catch(Exception e2){
                System.err.print("failed to send error response");
                e2.printStackTrace();
            }
        }
    }

    @GET
    @Path("/exportCSV/{UID}/fluxtream-export-from-{start}.csv")
    @ApiOperation(value = "CSV export of data from a given start time onwards")
    @ApiResponses({
            @ApiResponse(code=200, message="CSV data")
    })
    public void exportCSVStartOnly(@ApiParam(value="Channels", required=true) @QueryParam("channels") String channels,
                                   @ApiParam(value="Start time (epoch seconds)", required=true) @PathParam("start") Long start,
                                   @ApiParam(value="User ID (must be ID of loggedIn user)", required=true) @PathParam("UID") Long uid,
                                   @Context HttpServletResponse response){
        exportCSV(channels,start,null,uid,response);
    }

    @GET
    @Path("/exportCSV/{UID}/fluxtream-export-to-{end}.csv")
    @ApiOperation(value = "CSV export of data from a given end time and all before")
    @ApiResponses({
            @ApiResponse(code=200, message="CSV data")
    })
    public void exportCSVEndOnly(@ApiParam(value="Channels", required=true) @QueryParam("channels") String channels,
                                 @ApiParam(value="End time (epoch seconds)", required=true) @PathParam("end") Long end,
                                 @ApiParam(value="User ID (must be ID of loggedIn user)", required=true) @PathParam("UID") Long uid,
                                 @Context HttpServletResponse response){
        exportCSV(channels,null,end,uid,response);
    }

    @GET
    @Path("/exportCSV/{UID}/fluxtream-export.csv")
    @ApiOperation(value = "CSV export all data for given user id")
    @ApiResponses({
            @ApiResponse(code=200, message="CSV data")
    })
    public void exportCSVNoParams(@ApiParam(value="Channels", required=true) @QueryParam("channels") String channels,
                                  @ApiParam(value="User ID (must be ID of loggedIn user)", required=true) @PathParam("UID") Long uid,
                                  @Context HttpServletResponse response){
        exportCSV(channels,null,null,uid,response);
    }


    @POST
  @Path("/uploadHistory")
    @Secured("ROLE_ADMIN")
  @Produces("text/plain")
  public Response loadHistory(@QueryParam("username") String username,
      @QueryParam("connectorName") String connectorName) throws InstantiationException,
      IllegalAccessException, ClassNotFoundException {
        Response response;
        try{
            Guest guest = guestService.getGuest(username);

            if (!isOwnerOrAdmin(guest.getId())){
                response = Response.ok("Failure!").build();
            }
            else {
                final ApiKey apiKey = guestService.getApiKey(guest.getId(), Connector.getConnector(connectorName));
                bodytrackStorageService.storeInitialHistory(apiKey);
                response = Response.ok("Success!").build();
            }
        }
        catch (Exception e){
            response = Response.serverError().entity("Failure!").build();
        }
        return response;
    }

    @DELETE
    @Path("/users/{UID}/views/{id}")
    @ApiOperation(value = "Delete a view")
    @Produces("text/plain")
    @ApiResponses({
            @ApiResponse(code=200, message="Successfully deleted view {viewId}")
    })
    public Response deleteBodytrackView(@ApiParam(value="User ID (must be ID of loggedIn user)", required=true) @PathParam("UID") Long uid,
                                      @ApiParam(value="View ID", required=true) @PathParam("id") long viewId){
        Response response;
        try{
            if (!isOwnerOrAdmin(uid)){
                uid = null;
            }
            bodyTrackHelper.deleteView(uid, viewId);
            response = Response.ok("successfully deleted view " + viewId).build();
        }
        catch (Exception e){
            response = Response.serverError().entity("failed to delete view " + viewId).build();
        }
        return response;
    }

    @POST
    @Path("/upload")
    @ApiOperation(value = "Upload binary data via multipart encoding for the current logged in user", response = BodyTrackUploadResponse.class)
    @Consumes({MediaType.MULTIPART_FORM_DATA,MediaType.APPLICATION_FORM_URLENCODED})
    @Produces({MediaType.APPLICATION_JSON})
    public Response uploadToBodytrack(@ApiParam(value="The device to upload the data for", required=true) @FormParam("dev_nickname") String deviceNickname,
                                      @ApiParam(value="JSON encoded array of channels being uploaded for", required=true) @FormParam("channel_names") String channels,
                                      @ApiParam(value="Multipart form data to be uploaded", required=true@FormParam("data") String data){
        Response response;
        try{
            long guestId = AuthHelper.getGuestId();
            Type channelsType =  new TypeToken<Collection<String>>(){}.getType();

            List<List<Object>> parsedData = new ArrayList<List<Object>>();

            //Gson doesn't seem to be able to handle arrays with mixed types nicely
            //This will parse through the array, we don't need much error checking because we want this to fail if the data is malformed
            JsonElement element = new JsonParser().parse(data);

            for (JsonElement e : element.getAsJsonArray()){
                List<Object> currentList = new ArrayList<Object>();
                parsedData.add(currentList);
                for (JsonElement dataPoint : e.getAsJsonArray()){
                    if (dataPoint instanceof JsonNull){
                        currentList.add(null);
                    }
                    else{
                        JsonPrimitive primitive = dataPoint.getAsJsonPrimitive();
                        if (primitive.isBoolean()){
                            currentList.add(primitive.getAsBoolean());
                        }
                        else if (primitive.isString()){
                            currentList.add(primitive.getAsString());
                        }
                        else{
                            currentList.add(primitive.getAsDouble());
                        }
                    }
                }
            }

            final BodyTrackHelper.BodyTrackUploadResult uploadResult = bodyTrackHelper.uploadToBodyTrack(guestId, deviceNickname, (Collection<String>)gson.fromJson(channels, channelsType), parsedData);
            if (uploadResult instanceof BodyTrackHelper.ParsedBodyTrackUploadResult){
                BodyTrackHelper.ParsedBodyTrackUploadResult parsedResult = (BodyTrackHelper.ParsedBodyTrackUploadResult) uploadResult;
                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,parsedResult);
            }
            response = createResponseFromBodyTrackUploadResult(uploadResult);
        }
        catch (Exception e){
            response = Response.serverError().entity("Upload failed!").build();
        }
        return response;
    }

    @POST
    @Path("/jupload")
    @ApiOperation(value = "Upload JSON data for current logged in user", response = BodyTrackUploadResponse.class)
    @Produces({MediaType.APPLICATION_JSON})
    public Response uploadJsonToBodytrack(@ApiParam(value="The device to upload the data for", required=true) @QueryParam("dev_nickname"String deviceNickname,
                                          @ApiParam(value="The data to upload", required=true) String body){
        Response response;
        try{
            long uid = AuthHelper.getGuestId();
            response = createResponseFromBodyTrackUploadResult(bodyTrackHelper.uploadJsonToBodyTrack(uid, deviceNickname, body));
        }
        catch (Exception e){
            response = Response.serverError().entity("Upload failed!").build();
        }
        return response;
    }

    private Response createResponseFromBodyTrackUploadResult(final BodyTrackHelper.BodyTrackUploadResult uploadResult) {

        // check the uploadResult for success, and create a new Response accordingly
        Response response;
        if (uploadResult.isSuccess()) {
            response = Response.ok("Upload successful!").build();
        }
        else {
            response = Response.serverError().entity("Upload failed!").build();
        }

        // Now try to parse the response in the uploadResult as JSON, inflating it into a BodyTrackUploadResponse
        BodyTrackUploadResponse bodyTrackUploadResponse = null;
        try {
            bodyTrackUploadResponse = gson.fromJson(uploadResult.getResponse(), BodyTrackUploadResponse.class);
        }
        catch (JsonSyntaxException e) {
            LOG.error("JsonSyntaxException while trying to convert the BodyTrackUploadResult response into a BodyTrackUploadResponse.  Response was [" + uploadResult.getResponse() + "]", e);
        }

        // add the response to the payload if non-null
        if (bodyTrackUploadResponse != null) {
            response = Response.ok(bodyTrackUploadResponse).build();
        }
        return response;
    }

    // Based on code from http://aruld.info/handling-multiparts-in-restful-applications-using-jersey/ and http://stackoverflow.com/a/4687942
    @POST
    @Path("/photoUpload")
    @ApiOperation(value = "Upload a photo for the current logged in user", response = PhotoUploadResponsePayload.class)
    @Consumes({MediaType.MULTIPART_FORM_DATA})
    @Produces({MediaType.APPLICATION_JSON})
    public Response handlePhotoUpload(@ApiParam(value="Connector to upload the photo for", required=true) @QueryParam("connector_name") final String connectorName,
                                      final MultiPart multiPart) {
        Response response;

        final Connector connector = Connector.getConnector(connectorName);
        if (connector != null) {
            // We currently only support photo uploads for the Fluxtream Capture connector
            if ("fluxtream_capture".equals(connector.getName())) {
                try {
                    if (multiPart != null) {
                        byte[] photoBytes = null;
                        String jsonMetadata = null;

                        // iterate over the body parts and pick out the "photo" and "metadata" parts
                        for (final BodyPart bodyPart : multiPart.getBodyParts()) {

                            final ContentDisposition contentDisposition = bodyPart.getContentDisposition();
                            if (contentDisposition != null) {
                                final Map<String, String> parameters = contentDisposition.getParameters();
                                if (parameters != null) {
                                    final String name = parameters.get("name");
                                    if ("photo".equals(name)) {
                                        // found the photo part
                                        final BodyPartEntity bodyPartEntity = (BodyPartEntity)bodyPart.getEntity();
                                        photoBytes = IOUtils.toByteArray(bodyPartEntity.getInputStream());
                                    }
                                    else if ("metadata".equals(name)) {
                                        // found the metadata part
                                        jsonMetadata = bodyPart.getEntityAs(String.class);
                                    }
                                }
                            }
                        }

                        // if we found both "photo" and "metadata"
                        if (photoBytes != null && jsonMetadata != null) {
                            final long guestId = AuthHelper.getGuestId();

                            // Record the upload request time.  In reality we don't really care about the upload
                            // time, but rather we want to ensure that the Fluxtream Capture connector is
                            // auto-added to the user's set of connectors, and this is the way to do it.  Note that the
                            // the field recorded here is merely the time of the last upload request *request* and says
                            // nothing about whether that request was actually successful
                            final ApiKey apiKey;
                            List<ApiKey> apiKeys = guestService.getApiKeys(guestId, connector);
                            if (apiKeys != null && !apiKeys.isEmpty()) {
                                apiKey = apiKeys.get(0);
                            }
                            else {
                                apiKey = guestService.createApiKey(guestId, connector);
                            }
                            guestService.setApiKeyAttribute(apiKey, "last_upload_request_time", String.valueOf(System.currentTimeMillis()));

                            // We have a photo and the metadata, so pass control to the FluxtreamCapturePhotoStore to save the photo
                            LOG_DEBUG.debug("BodyTrackController.savePhoto(" + guestId + ", " + photoBytes.length + ", " + jsonMetadata + ")");
                            try {
                                final FluxtreamCapturePhotoStore.OperationResult<FluxtreamCapturePhoto> result = fluxtreamCapturePhotoStore.saveOrUpdatePhoto(guestId, photoBytes, jsonMetadata, apiKey.getId());
                                final String photoStoreKey = result.getData().getPhotoStoreKey();
                                final Long databaseRecordId = result.getDatabaseRecordId();
                                LOG.info("BodyTrackController.handlePhotoUpload(): photo [" + photoStoreKey + "] " + result.getOperation() + " sucessfully!");
                                response = jsonResponseHelper.ok(new PhotoUploadResponsePayload(result.getOperation(), databaseRecordId, photoStoreKey));
                            }
                            catch (FluxtreamCapturePhotoStore.UnsupportedImageFormatException e) {
                                final String message = "UnsupportedImageFormatException while trying to save the photo";
                                LOG.error("BodyTrackController.handlePhotoUpload(): " + message);
                                response = jsonResponseHelper.unsupportedMediaType(message);
                            }
                            catch (FluxtreamCapturePhotoStore.InvalidDataException e) {
                                final String message = "InvalidDataException while trying to save the photo";
                                LOG.error("BodyTrackController.handlePhotoUpload(): " + message, e);
                                response = jsonResponseHelper.badRequest(message);
                            }
                            catch (FluxtreamCapturePhotoStore.StorageException e) {
                                final String message = "StorageException while trying to save the photo";
                                LOG.error("BodyTrackController.handlePhotoUpload(): " + message, e);
                                response = jsonResponseHelper.internalServerError(message);
                            }
                        }
                        else {
                            final String message = "Upload failed because both the 'photo' and 'metadata' parts are required and must be non-null";
                            LOG.error("BodyTrackController.handlePhotoUpload(): " + message);
                            response = jsonResponseHelper.badRequest(message);
                        }
                    }
                    else {
                        final String message = "Upload failed because the Multipart was null";
                        LOG.error("BodyTrackController.handlePhotoUpload(): " + message);
                        response = jsonResponseHelper.badRequest(message);
                    }
                }
                catch (Exception e) {
                    final String message = "Upload failed due to an unexpected exception";
                    LOG.error("BodyTrackController.handlePhotoUpload(): " + message, e);
                    response = jsonResponseHelper.internalServerError(message);
                }
            }
            else {
                final String message = "Upload failed because photo uploads are currently only allowed for the Fluxtream Capture connector";
                LOG.error("BodyTrackController.handlePhotoUpload(): " + message);
                response = jsonResponseHelper.badRequest(message);
            }
        }
        else {
            final String message = "Upload failed because the connector [" + connectorName + "] is unknown";
            LOG.error("BodyTrackController.handlePhotoUpload(): " + message);
            response = jsonResponseHelper.badRequest(message);
        }

        return response;
    }

    @GET
    @Path("/photo/{UID}.{PhotoStoreKeySuffix}")
    @ApiResponses({
            @ApiResponse(code=200, message="Photo Image data (png/jpg)"),
            @ApiResponse(code=403, message="In case of unauthorized access")
    })
    @ApiOperation(value="Retrieve a specific photo by photo store key suffix")
    public Response getFluxtreamCapturePhoto(@ApiParam(value="User ID", required=true) @PathParam("UID") final Long uid,
                                             @ApiParam(value="Photo Store Key Suffix", required=true) @PathParam("PhotoStoreKeySuffix") final String photoStoreKeySuffix,
                                             @Context final Request request) {

        return getFluxtreamCapturePhoto(uid, request, new FluxtreamCapturePhotoFetchStrategy() {
            private final String photoStoreKey = uid + "." + photoStoreKeySuffix;

            @Nullable
            @Override
            public FluxtreamCapturePhotoStore.Photo getPhoto() throws FluxtreamCapturePhotoStore.StorageException {
                return fluxtreamCapturePhotoStore.getPhoto(photoStoreKey);
            }

            @NotNull
            @Override
            public String getPhotoIdentifier() {
                return photoStoreKey;
            }
        });
    }

    @GET
    @Path("/photoThumbnail/{UID}/{PhotoId}/{ThumbnailIndex}")
    @ApiResponses({
            @ApiResponse(code=200, message="Photo Image data (png/jpg)"),
            @ApiResponse(code=403, message="In case of unauthorized access")
    })
    @ApiOperation(value="Retrieve a specific photo thumbnail")
    public Response getFluxtreamCapturePhotoThumbnail(@ApiParam(value="User ID", required=true) @PathParam("UID") final long uid,
                                                      @ApiParam(value="Photo ID", required=true) @PathParam("PhotoId") final long photoId,
                                                      @ApiParam(value="Thumbnail ID", required=true) @PathParam("ThumbnailIndex") final int thumbnailIndex,
                                                      @Context final Request request) {

        return getFluxtreamCapturePhoto(uid, request, new FluxtreamCapturePhotoFetchStrategy() {
            @Nullable
            @Override
            public FluxtreamCapturePhotoStore.Photo getPhoto() {
                return fluxtreamCapturePhotoStore.getPhotoThumbnail(uid, photoId, thumbnailIndex);
            }

            @NotNull
            @Override
            public String getPhotoIdentifier() {
                return uid + "/" + photoId + "/" + thumbnailIndex;
            }
        });
    }

    // Based on code from http://stackoverflow.com/questions/3496209/input-and-output-binary-streams-using-jersey/12573173#12573173
    private Response getFluxtreamCapturePhoto(final long uid,
                                              final Request request,
                                              @NotNull final FluxtreamCapturePhotoFetchStrategy photoFetchStrategy) {

        // Check authorization: is the logged-in user the same as the UID in the key?  If not, does the logged-in user
        // have coaching access AND access to the FluxtreamCapture connector?
        boolean accessAllowed = false;
        Long loggedInUserId = null;
        try {
            loggedInUserId = AuthHelper.getGuestId();
            accessAllowed = isOwnerOrAdmin(uid);
            if (!accessAllowed) {
                final CoachingBuddy coachee = buddiesService.getTrustingBuddy(loggedInUserId, uid);
                if (coachee != null) {
                    accessAllowed = coachee.hasAccessToConnector("fluxtream_capture");
                }
            }
        }
        catch (Exception e) {
            LOG.error("BodyTrackController.getFluxtreamCapturePhoto(): Exception while trying to check authorization.", e);
        }

        if (accessAllowed) {

            final FluxtreamCapturePhotoStore.Photo photo;
            try {
                photo = photoFetchStrategy.getPhoto();
            }
            catch (Exception e) {
                final String message = "Exception while trying to get photo [" + photoFetchStrategy.getPhotoIdentifier() + "]";
                LOG.error("BodyTrackController.getFluxtreamCapturePhoto(): " + message, e);
                return jsonResponseHelper.internalServerError(message);
            }

            if (photo == null) {
                final String message = "Photo [" + photoFetchStrategy.getPhotoIdentifier() + "] requested by user [" + loggedInUserId + "] not found";
                LOG.error("BodyTrackController.getFluxtreamCapturePhoto(): " + message);
                return jsonResponseHelper.notFound(message);
            }

            final CacheControl cc = new CacheControl();
            cc.setNoTransform(true);
            cc.setMustRevalidate(false);
            cc.setNoCache(false);
            cc.setMaxAge(ONE_WEEK_IN_SECONDS);

            EntityTag etag;
            try {
                etag = new EntityTag(HashUtils.computeMd5Hash(photo.getPhotoBytes()));

                final Response.ResponseBuilder responseBuilder = request.evaluatePreconditions(etag);
                if (responseBuilder != null) {
                    // Preconditions are not met, returning HTTP 304 'not-modified'
                    return responseBuilder.cacheControl(cc).build();
                }
            }
            catch (NoSuchAlgorithmException e) {
                LOG.warn("NoSuchAlgorithmException caught while trying to create an MD5 hash for photo [" + photo.getIdentifier() + "].  No Etag will be specified in the response.");
                etag = null;
            }

            // Start building the response
            Response.ResponseBuilder responseBuilder = Response.ok().cacheControl(cc);
            if (etag != null) {
                responseBuilder = responseBuilder.tag(etag);
            }

            // Add the Last Modified header to the response, if we know it
            final Long lastUpdatedTimestamp = photo.getLastUpdatedTimestamp();
            if (lastUpdatedTimestamp != null) {
                responseBuilder = responseBuilder.lastModified(new Date(lastUpdatedTimestamp));
            }

            return responseBuilder
                    .type(photo.getImageType().getMediaType())
                    .expires(new DateTime().plusMonths(1).toDate())
                    .entity(photo.getPhotoBytes()).build();
        }

        return jsonResponseHelper.forbidden("User [" + loggedInUserId + "] is not authorized to view photo [" + photoFetchStrategy.getPhotoIdentifier() + "]");
    }

    @GET
    @Path("/tiles/{UID}/{DeviceNickname}.{ChannelName}/{Level}.{Offset}.json")
    @ApiOperation(value="Get data tile for a given channel", response=BodyTrackHelper.GetTileResponse.class)
    @ApiResponses({
            @ApiResponse(code=403, message="In case of unauthorized access")
    })
    @Produces({MediaType.APPLICATION_JSON})
    public Response fetchTile(@ApiParam(value="User ID", required=true) @PathParam("UID") Long uid,
                              @ApiParam(value="Device Name", required=true) @PathParam("DeviceNickname") String deviceNickname,
                              @ApiParam(value="Channel Name", required=true) @PathParam("ChannelName") String channelName,
                              @ApiParam(value="Level of tile", required=true) @PathParam("Level") int level,
                              @ApiParam(value="Offset of tile", required=true) @PathParam("Offset") long offset){
        try{
            long loggedInUserId = AuthHelper.getGuestId();
            boolean accessAllowed = isOwnerOrAdmin(uid);
            CoachingBuddy coachee = buddiesService.getTrustingBuddy(loggedInUserId, uid);
            if (!accessAllowed&&coachee==null){
                uid = null;
            }
//            if (coachee!=null) {
//                ApiKey apiKey = getApiKeyFromDeviceNickname(deviceNickname, coachee.guestId);
//                if (apiKey==null)
//                    return Response.status(Response.Status.BAD_REQUEST).entity("Couldn't find connector with device nickname=" + deviceNickname).build();
//                else if (buddiesService.getSharedConnector(apiKey.getId(), AuthHelper.getGuestId())==null)
//                    return Response.status(Response.Status.UNAUTHORIZED).entity("Access denied to device " + deviceNickname).build();
//            }
            return Response.ok(bodyTrackHelper.fetchTile(uid, deviceNickname, channelName, level, offset)).build();
        } catch (Exception e) {
            return Response.status(Response.Status.UNAUTHORIZED).entity("Access Denied").build();
        }
    }

    private ApiKey getApiKeyFromDeviceNickname(String deviceNickname, long guestId) {
        final List<ApiKey> apiKeys = guestService.getApiKeys(guestId, Connector.fromDeviceNickname(deviceNickname));
        // bodytrack doesn't have the ability to handle multiple instances of the same connector yet, so returning
        // the first matching ApiKey
        if (apiKeys.size()>0)
            return apiKeys.get(0);
        return null;
    }

    @GET
    @Path("/users/{UID}/views")
    @ApiOperation(value="Get a list of available views", response=BodyTrackHelper.ViewsList.class)
    @Produces({MediaType.APPLICATION_JSON})
    @ApiResponses({
            @ApiResponse(code=403, message="In case of unauthorized access")
    })
    public Response getViews(@ApiParam(value="User ID", required=true) @PathParam("UID") Long uid) {
        try{
            long loggedInUserId = AuthHelper.getGuestId();
            boolean accessAllowed = isOwnerOrAdmin(uid);
            CoachingBuddy coachee = buddiesService.getTrustingBuddy(loggedInUserId, uid);
            if (!accessAllowed&&coachee==null){
                uid = null;
            }
            return Response.ok(bodyTrackHelper.listViews(uid)).build();
        }
        catch (Exception e){
            return Response.status(Response.Status.UNAUTHORIZED).entity("Access Denied").build();
        }
    }

    @GET
    @Path("/users/{UID}/views/{id}")
    @ApiOperation(value="Retrieve a specific view", response=BodyTrackHelper.ViewJSON.class)
    @ApiResponses({
            @ApiResponse(code=403, message="In case of unauthorized access")
    })
    @Produces({MediaType.APPLICATION_JSON})
    public Response bodyTrackView(@ApiParam(value="User ID", required=true) @PathParam("UID") Long uid,
                                  @ApiParam(value="View ID", required= true) @PathParam("id") long id) {
        try{
            long loggedInUserId = AuthHelper.getGuestId();
            boolean accessAllowed = isOwnerOrAdmin(uid);
            CoachingBuddy coachee = buddiesService.getTrustingBuddy(loggedInUserId, uid);

            if (!accessAllowed && coachee==null) {
                uid = null;
            }
            String result = bodyTrackHelper.getView(uid,id);
            if (result!=null)
                return Response.ok(result).build();
            else
                return Response.serverError().entity("Failed to get view").build();
        }
        catch (Exception e){
            return Response.status(Response.Status.UNAUTHORIZED).entity("Access Denied").build();
        }
    }

    @POST
    @Path("/users/{UID}/views")
    @ApiOperation(value="Create a new view with given name and data", response = BodyTrackHelper.AddViewResult.class)
    @ApiResponses({
            @ApiResponse(code=403, message="In case of unauthorized access")
    })
    @Produces({MediaType.APPLICATION_JSON})
    public Response setView(@ApiParam(value="User ID", required = true) @PathParam("UID") Long uid,
                            @ApiParam(value="View name", required = true) @FormParam("name") String name,
                            @ApiParam(value="View data", required = true@FormParam("data") String data) {
        try{
            long loggedInUserId = AuthHelper.getGuestId();
            boolean accessAllowed = isOwnerOrAdmin(uid);
            CoachingBuddy coachee = buddiesService.getTrustingBuddy(loggedInUserId, uid);

            if (!accessAllowed && coachee==null) {
                uid = null;
            }
            return Response.ok(bodyTrackHelper.saveView(uid, name, data)).build();
        }
        catch (Exception e){
            return Response.status(Response.Status.UNAUTHORIZED).entity("Access Denied").build();
        }
    }

    @GET
    @Path("/users/{UID}/sources/list")
    @ApiOperation(value="Retrieves a list of devices and channels that data can be retrieved from", response=BodyTrackHelper.SourcesResponse.class)
    @ApiResponses({
            @ApiResponse(code=403, message="In case of unauthorized access")
    })
    @Produces({MediaType.APPLICATION_JSON})
    public Response getSourceList(@ApiParam(value= "User ID", required= true) @PathParam("UID") Long uid) {
        try{
            final long loggedInUserId = AuthHelper.getGuestId();
            boolean accessAllowed = isOwnerOrAdmin(uid);
            CoachingBuddy coachee = null;
            if (!accessAllowed) {
                coachee = buddiesService.getTrustingBuddy(loggedInUserId, uid);
                accessAllowed = (coachee!=null);
            }
            if (!accessAllowed){
                uid = null;
            }
            return Response.ok(bodyTrackHelper.listSources(uid, coachee)).build();
        }
        catch (Exception e){
            return Response.status(Response.Status.UNAUTHORIZED).entity("Access Denied").build();
        }
    }

    @GET
    @Path(value = "/users/{UID}/sources/{source}/default_graph_specs")
    @ApiOperation(value = "Retrieves the default grapher settings for a device", response=BodyTrackHelper.SourceInfo.class)
    @ApiResponses({
            @ApiResponse(code=403, message="In case of unauthorized access")
    })
    @Produces({MediaType.APPLICATION_JSON})
    public Response bodyTrackGetDefaultGraphSpecs(@ApiParam(value="User ID", required = true) @PathParam("UID") Long uid,
                                                  @ApiParam(value="Device name", required=true) @PathParam("source") String name) {
        try{
            long loggedInUserId = AuthHelper.getGuestId();
            boolean accessAllowed = isOwnerOrAdmin(uid);
            CoachingBuddy coachee = buddiesService.getTrustingBuddy(loggedInUserId, uid);

            if (!accessAllowed && coachee==null) {
                uid = null;
            }
            return Response.ok(bodyTrackHelper.getSourceInfo(uid, name)).build();
        }
        catch (Exception e){
            return Response.status(Response.Status.UNAUTHORIZED).entity("Access Denied").build();
        }
    }

    @GET
    @Path(value = "/users/{UID}/tags")
    @ApiOperation(value = "Retrieve all tags for a user", response=Tag.class, responseContainer="Array")
    @ApiResponses({
            @ApiResponse(code=403, message="In case of unauthorized access")
    })
    @Produces({MediaType.APPLICATION_JSON})
    public Response getAllTagsForUser(@ApiParam(value="User ID", required = true) @PathParam("UID") Long uid) {
        try {
            long loggedInUserId = AuthHelper.getGuestId();
            boolean accessAllowed = isOwnerOrAdmin(uid);
            CoachingBuddy coachee = buddiesService.getTrustingBuddy(loggedInUserId, uid);
            if (!accessAllowed && coachee == null) {
                uid = null;
            }
            return Response.ok(bodyTrackHelper.getAllTagsForUser(uid)).build();
        }
        catch (Exception e) {
            return Response.status(Response.Status.UNAUTHORIZED).entity("Access Denied").build();
        }
    }

    @POST
    @Path("/users/{UID}/channels/{DeviceNickname}.{ChannelName}/set")
    @ApiOperation(value = "Set the default style for a channel")
    @ApiResponses({
            @ApiResponse(code=200, message="Channel style set"),
            @ApiResponse(code=403, message="In case of unauthorized access")
    })
    public Response setDefaultStyle(@ApiParam(value="User ID", required = true) @PathParam("UID") Long uid,
                                    @ApiParam(value="Device name", required = true) @PathParam("DeviceNickname") String deviceNickname,
                                    @ApiParam(value="Channel name", required = true) @PathParam("ChannelName") String channelName,
                                    @ApiParam(value="Style data", required = true) @FormParam("user_default_style") String style) {
        try{
            if (!isOwnerOrAdmin(uid)){
                uid = null;
            }
            bodyTrackHelper.setDefaultStyle(uid,deviceNickname,channelName,style);
            return Response.ok("Channel style set").build();
        }
        catch (Exception e){
            return Response.status(Response.Status.UNAUTHORIZED).entity("Access Denied").build();
        }
    }

    @GET
    @Path("/timespans/{UID}/{ConnectorName}.{ObjectTypeName}/{Level}.{Offset}.json")
    @ApiOperation(value = "Retrieve a timespan tile", response=TimespanTileResponse.class)
    @Produces({MediaType.APPLICATION_JSON})
    public Response fetchTimespanTile(@ApiParam(value="User ID", required = true) @PathParam("UID") Long uid,
                                      @ApiParam(value="Connector Name", required = true) @PathParam("ConnectorName") String connectorName,
                                      @ApiParam(value="Object Type Name", required = true) @PathParam("ObjectTypeName") String objectTypeName,
                                      @ApiParam(value="Tile level", required = true) @PathParam("Level") int level,
                                      @ApiParam(value="Tile offset", required = true) @PathParam("Offset") long offset) {
        try{
            long loggedInUserId = AuthHelper.getGuestId();
            boolean accessAllowed = isOwnerOrAdmin(uid);
            CoachingBuddy coachee = buddiesService.getTrustingBuddy(loggedInUserId, uid);

            if (!accessAllowed && coachee==null) {
                uid = null;
            }

            if (uid == null) {
                return Response.status(Response.Status.BAD_REQUEST).entity("Invalid User ID (null)").build();
            }

            List<ApiKey> keys = guestService.getApiKeys(uid);
            ApiKey api = null;

            api = getApiKeyFromConnectorName(connectorName, keys, api);

            if (coachee!=null && buddiesService.getSharedConnector(api.getId(), AuthHelper.getGuestId())==null)
                return Response.status(Response.Status.UNAUTHORIZED).entity("Access Denied to connector " + connectorName).build();

            if (api == null) {
                return Response.status(Response.Status.BAD_REQUEST).entity("Invalid Channel (null)").build();
            }


            final long startTimeMillis = (long)(LevelOffsetHelper.offsetAtLevelToUnixTime(level, offset) * 1000);
            final long endTimeMillis = (long)(LevelOffsetHelper.offsetAtLevelToUnixTime(level, offset + 1) * 1000);

            final AbstractBodytrackResponder bodytrackResponder = api.getConnector().getBodytrackResponder(beanFactory);
            final List<TimespanModel> timespans = bodytrackResponder.getTimespans(startTimeMillis, endTimeMillis, api, objectTypeName);
            TimespanTileResponse response = new TimespanTileResponse(timespans);
            return Response.ok(gson.toJson(response)).build();

        }
        catch (Exception e) {
            LOG.error("BodyTrackController.fetchTimespanTile(): Exception while trying to fetch timespans: ", e);
            return Response.status(Response.Status.UNAUTHORIZED).entity("Access Denied").build();
        }

    }

    private ApiKey  getApiKeyFromConnectorName(String connectorName, List<ApiKey> keys, ApiKey api) {
        for (ApiKey key : keys){
            Connector connector = key.getConnector();
            if (connector.getName().equals(connectorName)||
                connector.getPrettyName().equals(connectorName)){
                api = key;
                break;
            }
        }
        return api;
    }

    public class TimespanTileResponse{

        public List<TimespanModel> data = new ArrayList<TimespanModel>();
        public String type = "timespan";

        public TimespanTileResponse(List<TimespanModel> data){
            this.data = data;
        }
    }


    @GET
    @Path("/photos/{UID}/{ConnectorPrettyName}.{ObjectTypeName}/{Level}.{Offset}.json")
    @ApiOperation(value = "Retrieve a photo tile", response=PhotoItem.class)
    @ApiResponses({
            @ApiResponse(code=403, message="In case of unauthorized access")
    })
    @Produces({MediaType.APPLICATION_JSON})
    public Response fetchPhotoTile(@ApiParam(value="User ID", required = true) @PathParam("UID") Long uid,
                                   @ApiParam(value="Connector name", required = true) @PathParam("ConnectorPrettyName") String connectorPrettyName,
                                   @ApiParam(value="Object type name", required = true) @PathParam("ObjectTypeName") String objectTypeName,
                                   @ApiParam(value="Tile level", required = true) @PathParam("Level") int level,
                                   @ApiParam(value="Tile offset", required = true) @PathParam("Offset") long offset,
                                   @ApiParam(value="Tags for filtering", required = true) @QueryParam("tags") String tagsStr,
                                   @ApiParam(value="Tag matching strategy", required = true) @QueryParam("tag-match") String tagMatchingStrategyName) {
        try {
            final TagFilter.FilteringStrategy tagFilteringStrategy = TagFilter.FilteringStrategy.findByName(tagMatchingStrategyName);

            if (isUnauthorized(uid)) {
                return Response.status(Response.Status.UNAUTHORIZED).entity("Invalid User ID (null)").build();
            }

            // first use Level and Offset to calculate the desired start and end times
            final long startTimeMillis = (long)(LevelOffsetHelper.offsetAtLevelToUnixTime(level, offset) * 1000);
            final long endTimeMillis = (long)(LevelOffsetHelper.offsetAtLevelToUnixTime(level, offset + 1) * 1000);

            final TimeInterval timeInterval = new SimpleTimeInterval(startTimeMillis, endTimeMillis, TimeUnit.ARBITRARY, TimeZone.getTimeZone("UTC"));

            // fetch the photos for this time interval, and for the desired device/channel
            final TagFilter tagFilter = TagFilter.create(Tag.parseTagsIntoStrings(tagsStr, Tag.COMMA_DELIMITER), tagFilteringStrategy);
            final SortedSet<PhotoService.Photo> photos = photoService.getPhotos(uid, timeInterval, connectorPrettyName, objectTypeName, tagFilter);

            // Define the min interval to be 1/20th of the span of the tile.  Value is in seconds
            final double minInterval = LevelOffsetHelper.levelToDuration(level) / 20.0;

            // Now filter the photos using the minInterval as follows:
            //  * min_interval specifies the minimum number of seconds between images.
            //  * Always include the first photo, set count to 1
            //  * When processing a given photo B, compare the time of this photo with the previous included
            //    photo A.  If image B is < min_interval seconds after image A, then increase count field in image
            //    A and ignore image B.  If image B is >= min_interval seconds after image A, then include image
            //    B with count=1
            PhotoItem photoA = null;
            final List<PhotoItem> filteredPhotos = new ArrayList<PhotoItem>();
            for (final PhotoService.Photo photoB : photos) {
                if (photoA == null) {
                    photoA = new PhotoItem(photoB);
                    filteredPhotos.add(photoA);
                }
                else {
                    // Already have a photoA, compare times to see if we should keep this one
                    final long photoBStartTimeSecs = photoB.getAbstractPhotoFacetVO().start / 1000;
                    if (photoBStartTimeSecs > (photoA.begin_d + minInterval)) {
                        // Enough of a gap between A and B, so keep this one and set to be new A
                        photoA = new PhotoItem(photoB);
                        filteredPhotos.add(photoA);
                    } else {
                        // Not enough of a gap, increment count on photoA
                        photoA.incrementCount();
                    }
                }
            }

            if (LOG_DEBUG.isDebugEnabled()) {
                LOG_DEBUG.debug("BodyTrackController.fetchPhotoTile(): num photos filtered from " + photos.size() + " to " + filteredPhotos.size());
            }

            return Response.ok(gson.toJson(filteredPhotos)).build();
        }
        catch (Exception e) {
            LOG.error("BodyTrackController.fetchPhotoTile(): Exception while trying to fetch photos: ", e);
            return Response.status(Response.Status.UNAUTHORIZED).entity("Access Denied").build();
        }
    }

    @GET
    @Path("/photos/{UID}/{ConnectorPrettyName}.{ObjectTypeName}/{unixTime}/{count}")
    @ApiOperation(value="Get photos at a given time", response=PhotoItem.class)
    @ApiResponses({
            @ApiResponse(code=403, message="In case of unauthorized access")
    })
    @Produces({MediaType.APPLICATION_JSON})
    public Response getPhotosBeforeOrAfterTime(@ApiParam(value="User ID", required = true) @PathParam("UID") long uid,
                                               @ApiParam(value="Connector name", required = true) @PathParam("ConnectorPrettyName") String connectorPrettyName,
                                               @ApiParam(value="Object type name", required = true) @PathParam("ObjectTypeName") String objectTypeName,
                                               @ApiParam(value="Timestamp (epoch seconds)", required = true) @PathParam("unixTime") double unixTimeInSecs,
                                               @ApiParam(value="Photo count limit", required = true) @PathParam("count") int desiredCount,
                                               @ApiParam(value="Is before time", required = true) @QueryParam("isBefore") boolean isGetPhotosBeforeTime,
                                               @ApiParam(value="Tags for matching", required = true) @QueryParam("tags") String tagsStr,
                                               @ApiParam(value="Tag matching strategy", required = true) @QueryParam("tag-match") String tagMatchingStrategyName) {
        try {
            final TagFilter.FilteringStrategy tagFilteringStrategy = TagFilter.FilteringStrategy.findByName(tagMatchingStrategyName);

            if (isUnauthorized(uid)) {
                return Response.status(Response.Status.UNAUTHORIZED).entity("Invalid User ID (null)").build();
            }

            final TagFilter tagFilter = TagFilter.create(Tag.parseTagsIntoStrings(tagsStr, Tag.COMMA_DELIMITER), tagFilteringStrategy);
            final SortedSet<PhotoService.Photo> photos = photoService.getPhotos(uid, (long)(unixTimeInSecs * 1000), connectorPrettyName, objectTypeName, desiredCount, isGetPhotosBeforeTime, tagFilter);

            // create the JSON response
            final List<PhotoItem> photoItems = new ArrayList<PhotoItem>();
            for (final PhotoService.Photo photo : photos) {
                photoItems.add(new PhotoItem(photo));
            }
            return Response.ok(gson.toJson(photoItems)).build();
        }
        catch (Exception e) {
            LOG.error("BodyTrackController.getPhotosBeforeOrAfterTime(): Exception while trying to fetch log items: ", e);
            return Response.status(Response.Status.UNAUTHORIZED).entity("Access Denied").build();
        }
    }

    public boolean isUnauthorized(Long uid) {
        long loggedInUserId = AuthHelper.getGuestId();
        boolean accessAllowed = isOwnerOrAdmin(uid);
        CoachingBuddy coachee = buddiesService.getTrustingBuddy(loggedInUserId, uid);

        return !accessAllowed && coachee==null;
    }

    @GET
    @Path("/metadata/{UID}/{ConnectorName}.{ObjectTypeName}/{facetId}/get")
    @ApiOperation(value="Get the metadata for a facet", response=FacetMetadata.class)
    @Produces({MediaType.APPLICATION_JSON})
    public Response getFacetMetadata(@ApiParam(value="User ID", required = true) @PathParam("UID") Long uid,
                                     @ApiParam(value="Connector name", required = true) final @PathParam("ConnectorName") String connectorName,
                                     @ApiParam(value="Object type name", required = true) final @PathParam("ObjectTypeName") String objectTypeName,
                                     @ApiParam(value="Facet ID", required = true) final @PathParam("facetId") long facetId) {
        if (isUnauthorized(uid)) {
            return Response.status(Response.Status.UNAUTHORIZED).build();
        }

        return executeFacetMetaDataOperation(uid, connectorName, objectTypeName, facetId, new FacetMetaDataOperation() {
            @Override
            @NotNull
            public Response executeOperation(@NotNull final AbstractFacet facet) {
                return Response.status(Response.Status.OK).entity(gson.toJson(new FacetMetadata(facet))).type(MediaType.APPLICATION_JSON).build();
            }
        });
    }

    @POST
    @Path("/metadata/{UID}/{ConnectorName}.{ObjectTypeName}/{facetId}/set")
    @ApiOperation(value="Set the metadata for a facet", response=FacetMetadata.class)
    @ApiResponses({
            @ApiResponse(code=403, message="In case of unauthorized access")
    })
    @Produces({MediaType.APPLICATION_JSON})
    public Response setFacetMetadata(@ApiParam(value="User ID", required = true) final @PathParam("UID") long uid,
                                     @ApiParam(value="Connector name", required = true) final @PathParam("ConnectorName") String connectorName,
                                     @ApiParam(value="Object type name", required = true) final @PathParam("ObjectTypeName") String objectTypeName,
                                     @ApiParam(value="Facet ID", required = true) final @PathParam("facetId") long facetId,
                                     @ApiParam(value="Comment", required = true) final @FormParam("comment") String comment,
                                     @ApiParam(value="Tags", required = true) final @FormParam("tags") String tags) {

        // don't bother doing anything if comment and tags are both null
        if (comment != null || tags != null) {
            return executeFacetMetaDataOperation(uid, connectorName, objectTypeName, facetId, new FacetMetaDataOperation() {
                @Override
                @NotNull
                public Response executeOperation(@NotNull final AbstractFacet facet) throws Exception {
                    if (LOG.isInfoEnabled()) {
                        LOG.info("BodyTrackController.setFacetMetadata(): Attempting to set metadata for facet [" + facetId + "] for connector [" + connectorName + "] and object type [" + objectTypeName + "]");
                    }

                    final FacetMetadataModifier facetMetadataModifier = new FacetMetadataModifier(uid, facetId, comment, tags);
                    final AbstractFacet modifiedFacet = apiDataService.createOrReadModifyWrite(facet.getClass(), facetMetadataModifier.getFacetFinderQuery(), facetMetadataModifier, facet.apiKeyId);

                    if (modifiedFacet != null) {
                        return Response.ok(new FacetMetadata(modifiedFacet)).build();
                    }

                    return jsonResponseHelper.forbidden("User [" + uid + "] is not allowed to set metadata for facet [" + facetId + "] for connector [" + connectorName + "] and object type [" + objectTypeName + "]");
                }
            });
        }
        return jsonResponseHelper.badRequest("Nothing changed since comment and tags were both null");
    }

    @DELETE
    @Path("/photo/{UID}/{facetId}")
    @ApiOperation(value="Delete a FluxtreamCapture photo")
    @ApiResponses({
            @ApiResponse(code=200, message="n/a"),
            @ApiResponse(code=403, message="In case of unauthorized access")
    })
    public Response deletePhoto(@ApiParam(value="User ID", required = true) final @PathParam("UID") long uid,
                                @ApiParam(value="Facet ID", required = true) final @PathParam("facetId") long facetId) {
        // only the photo's owner (or admin) is allowed to delete a photo
        if (!isOwnerOrAdmin(uid))
            return Response.status(Response.Status.UNAUTHORIZED).build();

        return executeFacetMetaDataOperation(uid, "FluxtreamCapture", "photo", facetId, new FacetMetaDataOperation() {
            @Override
            @NotNull
            public Response executeOperation(@NotNull final AbstractFacet facet) {
                FluxtreamCapturePhotoFacet photoFacet = (FluxtreamCapturePhotoFacet) facet;
                final String photoStoreKey = FluxtreamCapturePhoto.createPhotoStoreKey(photoFacet.guestId, photoFacet.getCaptureYYYYDDD(), photoFacet.start, photoFacet.getHash());
                try {
                    fluxtreamCapturePhotoStore.deletePhoto(photoStoreKey);
                    facetDao.delete(facet);
                } catch (FluxtreamCapturePhotoStore.StorageException e) {
                    return Response.serverError().build();
                }
                return Response.status(Response.Status.OK).build();
            }
        });

    }

    @ApiModel
    public static class FacetMetadata {
        @ApiModelProperty(value="The facet's user comment (if any)", required=true)
        public String comment;
        public SortedSet<String> tags = new TreeSet<String>();

        private FacetMetadata(@NotNull AbstractFacet facet) {
            this.comment = facet.comment;
            this.tags.addAll(facet.getTagsAsStrings());
        }
    }

    private static interface FacetMetaDataOperation {
        @NotNull
        Response executeOperation(@NotNull final AbstractFacet facet) throws Exception;
    }

    private Response executeFacetMetaDataOperation(final long uid,
                                                   final String connectorName,
                                                   final String objectTypeName,
                                                   final long facetId,
                                                   final FacetMetaDataOperation operation) {
            // Try to find the connector by pretty name, and then if that fails the find by actual name
            Connector connector = ConnectorUtils.findConnectorByPrettyName(guestService, uid, connectorName);
            if (connector == null) {
                connector = Connector.getConnector(connectorName);
            }

            if (connector != null) {
                // Check authorization: is the logged-in user the same as the UID in the key?  If not, does the logged-in user
                // have coaching access AND access to the FluxtreamCapture connector?
                boolean accessAllowed = false;
                Long loggedInUserId = null;
                try {
                    loggedInUserId = AuthHelper.getGuestId();
                    accessAllowed = isOwnerOrAdmin(uid);
                    if (!accessAllowed) {
                        final CoachingBuddy coachee = buddiesService.getTrustingBuddy(loggedInUserId, uid);
                        if (coachee != null) {
                            accessAllowed = coachee.hasAccessToConnector(connector.getName());
                        }
                    }
                }
                catch (Exception e) {
                    LOG.error("BodyTrackController.setFacetMetadata(): Exception while trying to check authorization.", e);
                }

                if (accessAllowed) {

                    final ObjectType objectType = ObjectType.getObjectType(connector, objectTypeName);
                    if (objectType != null) {
                        ApiKey apiKey = guestService.getApiKey(uid, connector);
                        final AbstractFacet facet = apiDataService.getFacetById(apiKey, objectType, facetId);
                        if (facet != null) {
                            try {
                                return operation.executeOperation(facet);
                            }
                            catch (Exception e) {
                                final String message = "Unexpected error while trying to operate on metadata for facet [" + facetId + "] for connector [" + connectorName + "] and object type [" + objectType + "]";
                                LOG_DEBUG.error(message, e);
                                return jsonResponseHelper.internalServerError(message);
                            }
                        }
                        return jsonResponseHelper.notFound("Unknown facet [" + facetId + "] for connector [" + connectorName + "] and object type [" + objectType + "] and guestId [" + uid + "]");
                    }
                    return jsonResponseHelper.notFound("Unknown object type [" + objectTypeName + "] for connector [" + connectorName + "]");
                }
                return jsonResponseHelper.forbidden("User [" + loggedInUserId + "] is not authorized to access or modify metadata for facets owned by user [" + uid + "] in connector [" + connectorName + "]");
            }
            return jsonResponseHelper.notFound("Unknown connector [" + connectorName + "]");
    }

    private static final class FacetMetadataModifier implements ApiDataService.FacetModifier<AbstractFacet> {
        @NotNull
        private final ApiDataService.FacetQuery facetFinderQuery;
        @Nullable
        private final String comment;
        @Nullable
        private final String tagsStr;

        public FacetMetadataModifier(final long guestId, final long facetId, @Nullable final String comment, @Nullable final String tagsStr) {
            this.comment = comment;
            this.tagsStr = tagsStr;
            facetFinderQuery = new ApiDataService.FacetQuery("e.id = ? and e.guestId = ?", facetId, guestId);
        }

        @Override
        public AbstractFacet createOrModify(final AbstractFacet existingFacet, final Long apiKeyId) {
            // the case where the existing facet doesn't exist and is null should never happen here
            if (existingFacet != null) {
                if (comment != null) {
                    existingFacet.comment = comment;
                }

                if (tagsStr != null) {
                    existingFacet.clearTags();
                    existingFacet.addTags(tagsStr, Tag.COMMA_DELIMITER);
                }
            }

            return existingFacet;
        }

        @NotNull
        public ApiDataService.FacetQuery getFacetFinderQuery() {
            return facetFinderQuery;
        }
    }

    private boolean isOwnerOrAdmin(long targetUid){
        Guest guest = AuthHelper.getGuest();
        return targetUid == guest.getId() || guest.hasRole(Guest.ROLE_ADMIN);
    }

    @ApiModel
    public static class PhotoItem {
        private static final DateTimeFormatter DATE_TIME_FORMATTER = ISODateTimeFormat.dateTime();

        @ApiModelProperty(value="Not Safe For Work", required=true)
        public boolean nsfw = false;
        @ApiModelProperty
        public String id;
        @ApiModelProperty
        public String description;
        @ApiModelProperty
        public String comment;
        @ApiModelProperty
        public double begin_d;
        @ApiModelProperty
        public String begin;
        @ApiModelProperty
        public double end_d;
        @ApiModelProperty
        public String end;
        @ApiModelProperty
        public String dev_id;
        @ApiModelProperty
        public String dev_nickname;
        @ApiModelProperty
        public String object_type_name;
        @ApiModelProperty
        public String channel_name;
        @ApiModelProperty
        public String url;
        @ApiModelProperty
        public ArrayList<String> tags = new ArrayList<String>();
        @ApiModelProperty
        public ArrayList<PhotoItemThumbnail> thumbnails = new ArrayList<PhotoItemThumbnail>();
        @ApiModelProperty
        public int count = 1;
        @ApiModelProperty
        public int orientation;
        @ApiModelProperty
        public String time_type;

        public PhotoItem(final PhotoService.Photo photo) {
            final AbstractPhotoFacetVO photoFacetVO = photo.getAbstractPhotoFacetVO();

            this.id = photo.getConnector().prettyName() + "." + photo.getObjectType().getName() + "." + photoFacetVO.id;
            this.description = photoFacetVO.description == null ? "" : photoFacetVO.description;
            this.comment = photoFacetVO.comment == null ? "" : photoFacetVO.comment;
            this.begin_d = photoFacetVO.start / 1000.0; // convert millis to seconds
            this.begin = DATE_TIME_FORMATTER.print(photoFacetVO.start);
            this.end_d = this.begin_d;
            this.end = this.begin;
            this.dev_id = photo.getConnector().getName();
            this.dev_nickname = photo.getConnector().prettyName();
            this.object_type_name = photo.getObjectType().getName();
            this.time_type = photoFacetVO.timeType;
            this.channel_name = PhotoService.DEFAULT_PHOTOS_CHANNEL_NAME;   // photo channels are always named the same
            final List<DimensionModel> thumbnailSizes = photoFacetVO.getThumbnailSizes();
            if ((thumbnailSizes != null) && (!thumbnailSizes.isEmpty())) {
                int i = 0;
                for (final DimensionModel thumbnailDimension : thumbnailSizes) {
                    final String url = photoFacetVO.getThumbnail(i);
                    thumbnails.add(new PhotoItemThumbnail(url, thumbnailDimension.width, thumbnailDimension.height));
                    i++;
                }
            }

            this.url = photoFacetVO.getPhotoUrl();

            // copy the tags
            final SortedSet<String> facetTags = photoFacetVO.getTags();
            if ((facetTags != null) && (!facetTags.isEmpty())) {
                this.tags.addAll(facetTags);
            }

            // get the image orientation, defaulting to upright portrait
            final ImageOrientation tempOrientation = photoFacetVO.getOrientation();
            this.orientation = (tempOrientation == null ? ImageOrientation.ORIENTATION_1 : tempOrientation).getId();
        }

        public void incrementCount() {
            this.count++;
        }
    }

    private static class PhotoItemThumbnail {
        String url;
        int width;
        int height;

        private PhotoItemThumbnail(final String url, final int width, final int height) {
            this.url = url;
            this.width = width;
            this.height = height;
        }
    }

    private static final class LevelOffsetHelper {

        /** Number of bins per tile */
        private static final int NUM_BINS_PER_TILE = 512;

        /**
         * Returns the time in seconds of a bin in a tile at a given level. This is 2^level seconds, so level 0 bins
         * are 1 second, level 4 are 16 secs, etc.
         */
        private static double levelToBinSeconds(final int level) {
            return Math.pow(2, level);
        }

        /** Returns the duration in seconds of a tile at a given level.  For level 0 this is TILE_BIN_NUM seconds. */
        private static double levelToDuration(final int level) {
            return levelToBinSeconds(level) * NUM_BINS_PER_TILE;
        }

        /**
         * Returns the unixtime of the start of a tile at a given offset and level.  This is the duration at that
         * level times the offset.
         */
        private static double offsetAtLevelToUnixTime(final int level, final long offset) {
            return levelToDuration(level) * offset;
        }
    }

    @ApiModel(value = "Upload response returned from BodyTrack datastore.")
    public static final class BodyTrackUploadResponse {
        // We only store the bare minimum here because it might be a security/privacy issue to include everything (Randy
        // explained to Chris on 2012.10.31 that we probably don't want to make channel ranges and such visible by
        // default.  Plus, if debugging is on in the datastore, file paths might also be included in the response JSON).
        @ApiModelProperty(value = "Number of data points successfully added", required = true)
        public String successful_records;
        @ApiModelProperty(value = "Number of data points unsuccessfully added", required = true)
        public String failed_records;
        @ApiModelProperty(value = "Whether the upload failed", required = true)
        public String failure;
    }

    @ApiModel
    public class PhotoUploadResponsePayload {
        @NotNull
        @Expose
        @ApiModelProperty
        public final String operation;

        @NotNull
        @Expose
        @ApiModelProperty
        public final String key;

        @Nullable
        @Expose
        @ApiModelProperty
        public final Long id;

        public PhotoUploadResponsePayload(@NotNull final FluxtreamCapturePhotoStore.Operation operation, @Nullable final Long databaseRecordId, @NotNull final String photoStoreKey) {
            this.id = databaseRecordId;
            this.operation = operation.getName();
            this.key = photoStoreKey;
        }
    }

    private interface FluxtreamCapturePhotoFetchStrategy {

        @Nullable
        FluxtreamCapturePhotoStore.Photo getPhoto() throws FluxtreamCapturePhotoStore.StorageException;

        @NotNull
        String getPhotoIdentifier();
    }
}
TOP

Related Classes of org.fluxtream.core.api.BodyTrackController

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.