SDK Grails backend reference

Last updated on October 18th, 2019 at 06:24 am

package white.label.app.backend

import grails.converters.JSON
import groovyx.net.http.ContentType
import groovyx.net.http.HTTPBuilder
import groovyx.net.http.HttpResponseException
import groovyx.net.http.Method
import org.grails.web.json.JSONObject

/**
 * All communication is done with JSON data and contentType.
 * All communication from the SDK has to be secured with HTTP Basic Auth.
 * Because Basic Authentication is used, SSL is a requirement.
 */
class ApiController {

     String storageurl = "https://ask-for-it.motion-s.com"
     String storageauthheader = "Basic ${"service_account:password".bytes.encodeBase64().toString()}"
     static File BASE = new File(System.getProperty('user.home'), "apifiles")

/**
     * Returns an array of trip data elements for the given device id.
     * @Optional This action is not required by the SDK
     */
    def index(String deviceid) {
        if (!deviceid) {
            render(['error': "no deviceid parameter provided"] as JSON)
            return
        }

        def filePath = new File(BASE, deviceid)
        //fixme this produces a large json. consider filtering out relevant data / and pagination
        response.setContentType('application/json')
        response.outputStream << '['
        File[] files = filePath.listFiles()
        files.eachWithIndex { file, index ->
            response.outputStream << file.text
            if ((index + 1) < files.size()) {
                response.outputStream << ','
            }
        }
        response.outputStream << ']'
        response.outputStream.flush()
    }

    /**
     * This is the callback endpoint for the profiler. This receives all the profiling result of individual trips.
     *
     * Example payload (not matching the example trip in the other action):
     *
     *{
  "uid": "a53bceb86d8758c8016d8759f45c0000",
  "metrics_version": "nauto-pilot.git@0.19",
  "executionTimeMs": 635,
  "metricsVersion": "nauto-pilot.git@0.19",
  "origin": "app-identifier-ask-motion-s-for-it",
  "context": {
    "avg_slope": 0.11566666666666665,
    "driver_id": null,
    "distance": 5415,
    "end_longitude": 9.13933,
    "end_latitude": 48.94151,
    "start_datetime_local": "2017-12-05 07:17:22+01:00",
    "end_datetime_local": "2017-12-05 07:17:42+01:00",
    "duration": 20,
    "start_latitude": 48.9515,
    "road_quality_distance": {
      "poor": 0,
      "fair": 2.155,
      "good": 0
    },
    "end_datetime_utc": "2017-12-05 06:17:42",
    "weather_distance": {
      "Cloudy or fog": 5.415
    },
    "fleet_id": null,
    "number_of_locations": 3,
    "road_type_distance": {
      "urban": 5.415,
      "rural": 0,
      "motorway": 0
    },
    "trip_id": null,
    "avg_speed": 974.7,
    "max_speed": 9.86,
    "countries": [
      "Deutschland"
    ],
    "end_location_name": "Bietigheim-Bissingen, Deutschland",
    "start_location_name": "Bietigheim-Bissingen, Deutschland",
    "start_longitude": 9.13604,
    "avg_roughness": 2.4000000000000004,
    "road_type_locations": {
      "urban": 2,
      "rural": 0,
      "motorway": 0
    },
    "avg_traffic_speed": 37.666666666666664,
    "start_datetime_utc": "2017-12-05 06:17:22"
  },
  "risk": {
    "summary": {
      "safety_score": 92.35,
      "risk_score": 7.65,
      "meta_data": {},
      "risk_method": "ratio_of_cdf",
      "risk_sum": 4.13,
      "nb_of_contributory_factors": 23
    },
    "events": [
      {
        "from_date": "2017-12-05 06:17:22",
        "to_date": "2017-12-05 06:17:23",
        "latitude": 48.9515,
        "road_type": "urban",
        "meta_data": {
          "speed_kmh": 8.86,
          "acceleration_ms2": null,
          "bearing_rate": null,
          "bearing": 194.34,
          "peak_time": null,
          "traffic_speed": 38,
          "peak_magnitude": null,
          "trigger": null,
          "g_force": null
        },
        "contributory_factor_name": "rain_sleet_snow_or_fog",
        "link_id": -1080033497,
        "longitude": 9.13604
      }
    ]
  },
  "event": "PROFILED",
  "device": {
    "lastUpdated": "2019-10-01T12:33:19Z",
    "metaData": null,
    "managedUrl": null,
    "dateCreated": "2019-10-01T12:33:19Z",
    "origin": "app-identifier-ask-motion-s-for-it",
    "name": null,
    "detailsRequested": true,
    "imei": null,
    "model": null,
    "id": "a53bceb86d816a3f016d874f88b90022",
    "foreignKey": "18bb9f27-54b7-eüc58e75xxx"
  },
  "foreignKey": "11111111769"
}
     * @Optional This action is not required by the SDK
     */
    def consume() {
        JSONObject json = request.JSON as JSONObject
        try {
            String fkey = json.getString('foreignKey')
            JSONObject device = json.getJSONObject('device')
            String deviceid = device.getString('foreignKey')

            log.info("Session: $deviceid/$fkey")

            File directory = new File(BASE, deviceid)
            directory.mkdirs()
            File file = new File(directory, fkey)
            file.text = json.toString()

            log.info file.getAbsolutePath()
            render(['status': 'ok', 'file': file.getPath()] as JSON)
        } catch (e) {
            log.error e.message, e
            log.error json as String
            response.status = 500
            render(['error': e.message] as JSON)
        }
    }

     /**
     * This is just proxying the upload trip request to the motion-s storage buffer server. 
     * This example uploads whole trips in one call. 
     * In case the trips are very long, there is a possibility to upload chunk by chunk, but in general <code>addEndpointSessionWithLocations</code> is preffered.
     * See error messages for explantion of the return codes. 
     * You should not retry uploading if the error code was 409 or 406.
      *
      * Example payload from SDK:
      * {
      *     "startTimestamp": 1512454642000,
      *     "endTimestamp": 1512554642000,
      *     "device": "18bb9f27-54b7",
      *     "foreignKey":"18bb9f27",
      *     "origin": "app-identifier-ask-motion-s-for-it",
      *     "locations":[
      *         {"id_loc": 1,
      *         "latitude":48.952283,
      *         "longitude":9.136845,
      *         "bearing":194.344284,
      *         "speedKmh":8.8560,
      *         "recordDateUTCTimestamp":1512454642000},
      *         {"id_loc": 2,
      *         "latitude":48.922283,
      *         "longitude":9.136845,
      *         "bearing":194.344284,
      *         "speedKmh":9.8560,
      *         "recordDateUTCTimestamp":1512454643000},
      *         {"id_loc": 3,
      *         "latitude":48.942283,
      *         "longitude":9.136845,
      *         "bearing":194.344284,
      *         "speedKmh":8.9560,
      *         "recordDateUTCTimestamp":1512454644000}
      *        ]
      * }
      *
      * Examle response from storage backend:
      * {
      *     "id": "a53bceb86d816a3f016d8706602c0021",
      *     "dateCreatedTimestamp": 1569928405049,
      *     "startTimestamp": 1512454642000,
      *     "endTimestamp": 1512554642000,
      *     "device": {
      *         "id": "a53bceb86d38c432016d38d1ff3d0000",
      *         "dateCreated": "2019-09-16T06:45:49Z",
      *         "detailsRequested": true,
      *         "foreignKey": "18bb9f27-54b7",
      *         "imei": null,
      *         "lastUpdated": "2019-09-16T06:45:49Z",
      *         "managedUrl": null,
      *         "metaData": null,
      *         "model": null,
      *         "name": null,
      *         "origin": "app-identifier-ask-motion-s-for-it"
      *     },
      *     "foreignKey": "18bb9f27",
      *     "origin": "app-identifier-ask-motion-s-for-it",
      *     "consumed": false
      * }
      * @Required This action is required by the SDK to send the trip data
     */
    def store(){
        JSONObject jo = request.JSON as JSONObject
        String data = jo.toString()
        try {
            log.info("posting ${data.substring(0, 100)}... to $storageurl")
            def res = post("apiv2", "addEndpointSessionWithLocations", data)
            log.info(res as String)
            render(res as JSON)
        } catch (HttpResponseException hix) {
            if (hix.statusCode == 409) {
                log.error("Storage reported this chunk already as persisted: ${data.substring(0, 100)}")
            } else if (hix.statusCode == 406) {
                log.error("Storage reported this session id as invalid or complete: ${data.substring(0, 100)}")
            } else {
                log.error "Storage replied: ${hix.statusCode} for locations in session: ${data.substring(0, 100)}"
                //throw hix
            }

            response.setStatus(500)
            render(['error': hix.message] as JSON)
        }
    }


    //--------------------------------------- helper methods
    private Map post(String controller, String action, String data) {
        def http = new HTTPBuilder()
        def storage = "$storageurl/$controller/$action" as String
        def res = [:]
        http.request(storage, Method.POST, ContentType.JSON) { req ->
            body = data
            headers.'Authorization' = storageauthheader
            headers.Accept = 'application/json'
            log.info("Posting to URL: ${uri as String} size: ${(data).size()}")
            response.success = { resp, result ->
                res = [result: result]
            }
        }
        return res
    }

}

 

Categories: