Implementing a token maintainer

The token maintainer is a BDL Web Services server program that handles push token registration from mobile apps.

Basics

In order to implement a push notification mechanism, you need to set up a server part (token maintainer and push notification server), in conjunction with a push notification framework such as Google Cloud Messaging (GCM) or Apple Push Notification service (APNs). In addition, you need to handle notification events in your mobile app. This section describes how to implement the token maintainer, the server program that maintains the list of registered devices (i.e. registration tokens for GCM or device tokens for APNs).

Note: The max length of a push client token can vary according to the push framework provider. If you need to store registration tokens in a database, check the max size for a token and consider using a large column type such as VARCHAR(250).

The same code base can be used for Android (using GCM) and iOS (using APNs) applications: The token maintainer will basically handle RESTful HTTP requests coming from the internet for token registration and token un-registration. For each of these requests, the program will insert a new record or delete an existing record in a dedicated database table.

Note: The database used to store tokens must be created before starting the token maintainer program. By default, the program uses SQLite (dbmsqt) and the name of the database is "tokendb". To create this SQLite database, simply create an empty file with this name.

The push provider/server program can then query the tokens table to build the list of target devices for push notifications.

In the context of APNS, the token maintainer must also handle badge numbers for each registered device: When connsuming notification messages, the iOS app must inform the token maintainer that the badge number has changed. This function is implemented with the "badge_number" command.

The token maintainer is a Web Services server program which must be deployed behind a GAS to handle load balancing. You can, however, write code to test your program in development without a GAS.

The act of registering/unregistering push tokens is application specific: When registering tokens, you typically want to add application user information. Genero BDL allows you to implement a token maintainer in a simple way.

Note: When executing this token maintainer program with APNs, you must pass the "APNS" command line argument to execute APNs feedback queries.

MAIN block and database creation

Start with the MAIN block, and the connection to a database. In this tutorial, we use SQLite as the database. The program will automatically create the database file and the tokens table if it does not yet exist.
...

MAIN
    CALL open_create_db()
    CALL handle_registrations()
END MAIN

FUNCTION open_create_db()
    DEFINE dbsrc VARCHAR(100),
           x INTEGER
    LET dbsrc = "tokendb+driver='dbmsqt'"
    CONNECT TO dbsrc
    WHENEVER ERROR CONTINUE
    SELECT COUNT(*) INTO x FROM tokens
    WHENEVER ERROR STOP
    IF SQLCA.SQLCODE<0 THEN
       CREATE TABLE tokens (
              id INTEGER NOT NULL PRIMARY KEY,
              sender_id VARCHAR(150),
              registration_token VARCHAR(250) NOT NULL UNIQUE,
              badge_number INTEGER NOT NULL,
              app_user VARCHAR(50) NOT NULL, -- UNIQUE
              reg_date DATETIME YEAR TO FRACTION(3) NOT NULL
       )
    END IF
END FUNCTION

Handling registration and unregistration requests

The next function is typical Web Service server code using the Web Services API to handle RESTful requests. Note that the TCP port is defined as a constant that is used to set FGLAPPSERVER automatically when not running behind the GAS:

IMPORT util
IMPORT com

CONSTANT DEFAULT_PORT = 9999

MAIN
    ...
    CALL handle_registrations()
END MAIN

FUNCTION handle_registrations()
    DEFINE req com.HTTPServiceRequest,
           url, method, version, content_type STRING,
           reg_data, reg_result STRING
    IF LENGTH(fgl_getenv("FGLAPPSERVER"))==0 THEN
       -- Normally, FGLAPPSERVER is set by the GAS
       DISPLAY SFMT("Setting FGLAPPSERVER to %1", DEFAULT_PORT)
       CALL fgl_setenv("FGLAPPSERVER", DEFAULT_PORT)
    END IF
    CALL com.WebServiceEngine.Start()
    WHILE TRUE
       TRY
          LET req = com.WebServiceEngine.getHTTPServiceRequest(20)
       CATCH
          IF STATUS==-15565 THEN
             CALL show_verb("TCP socket probably closed by GAS, stopping process...")
             EXIT PROGRAM 0
          ELSE
             DISPLAY "Unexpected getHTTPServiceRequest() exception: ", STATUS
             DISPLAY "Reason: ", SQLCA.SQLERRM
             EXIT PROGRAM 1
          END IF
       END TRY
       IF req IS NULL THEN -- timeout
          DISPLAY SFMT("HTTP request timeout...: %1", CURRENT YEAR TO FRACTION)
          CALL check_apns_feedback()
          CALL show_tokens()
          CONTINUE WHILE
       END IF
       LET url = req.getURL()
       LET method = req.getMethod()
       IF method IS NULL OR method != "POST" THEN
          IF method == "GET" THEN
             CALL req.sendTextResponse(200,NULL,"Hello from token maintainer...")
          ELSE
             DISPLAY SFMT("Unexpected HTTP request: %1", method)
             CALL req.sendTextResponse(400,NULL,"Only POST requests supported")
          END IF
          CONTINUE WHILE
       END IF
       LET version = req.getRequestVersion()
       IF version IS NULL OR version != "1.1" THEN
          DISPLAY SFMT("Unexpected HTTP request version: %1", version)
          CONTINUE WHILE
       END IF
       LET content_type = req.getRequestHeader("Content-Type")
       IF content_type IS NULL
          OR content_type NOT MATCHES "application/json*" -- ;Charset=UTF-8
       THEN
          DISPLAY SFMT("Unexpected HTTP request header Content-Type: %1", content_type)
          CALL req.sendTextResponse(400,NULL,"Bad request")
          CONTINUE WHILE
       END IF
       TRY
          CALL req.readTextRequest() RETURNING reg_data
       CATCH
          DISPLAY SFMT("Unexpected HTTP request read exception: %1", STATUS)
       END TRY
       LET reg_result = process_command(url, reg_data)
       CALL req.setResponseCharset("UTF-8")
       CALL req.setResponseHeader("Content-Type","application/json")
       CALL req.sendTextResponse(200,NULL,reg_result)
    END WHILE
END FUNCTION

Processing registration and unregistration commands

The next function is called when a RESTful request is to be processed. The URL will define the type of command to be executed by the server:
  • If the URL contains "/token_maintainer/register", a new token must be inserted in the database.
  • If the URL contains "/token_maintainer/unregister", an existing token must be deleted from the database.
FUNCTION process_command(url, data)
    DEFINE url, data STRING
    DEFINE data_rec RECORD
               sender_id VARCHAR(150),
               registration_token VARCHAR(250),
               app_user VARCHAR(50)
           END RECORD,
           p_id INTEGER,
           p_ts DATETIME YEAR TO FRACTION(3),
           result_rec RECORD
               status INTEGER,
               message STRING
           END RECORD,
           result STRING
    LET result_rec.status = 0
    TRY
       CASE
         WHEN url MATCHES "*token_maintainer/register"
           CALL util.JSON.parse( data, data_rec )
           SELECT id INTO p_id FROM tokens
                  WHERE registration_token = data_rec.registration_token
           IF p_id > 0 THEN
              LET result_rec.status = 1
              LET result_rec.message = SFMT("Token already registered:\n [%1]", data_rec.registration_token)
              GOTO pc_end
           END IF
           SELECT MAX(id) + 1 INTO p_id FROM tokens
           IF p_id IS NULL THEN LET p_id=1 END IF
           LET p_ts = util.Datetime.toUTC(CURRENT YEAR TO FRACTION(3))
           WHENEVER ERROR CONTINUE
           INSERT INTO tokens
               VALUES( p_id, data_rec.sender_id, data_rec.registration_token, 0, data_rec.app_user, p_ts )
           WHENEVER ERROR STOP
           IF SQLCA.SQLCODE==0 THEN
              LET result_rec.message = SFMT("Token is now registered:\n [%1]", data_rec.registration_token)
           ELSE
              LET result_rec.status = -2
              LET result_rec.message = SFMT("Could not insert token in database:\n [%1]", data_rec.registration_token)
           END IF
         WHEN url MATCHES "*token_maintainer/unregister"
           CALL util.JSON.parse( data, data_rec )
           DELETE FROM tokens
                 WHERE registration_token = data_rec.registration_token
           IF SQLCA.SQLERRD[3]==1 THEN
              LET result_rec.message = SFMT("Token unregistered:\n [%1]", data_rec.registration_token)
           ELSE
              LET result_rec.status = -3
              LET result_rec.message = SFMT("Could not find token in database:\n [%1]", data_rec.registration_token)
           END IF
         WHEN url MATCHES "*token_maintainer/badge_number"
            CALL util.JSON.parse( data, data_rec )
            WHENEVER ERROR CONTINUE
              UPDATE tokens
                 SET badge_number = data_rec.badge_number
               WHERE registration_token = data_rec.registration_token
            WHENEVER ERROR STOP
            IF SQLCA.SQLCODE==0 THEN
               LET result_rec.message = SFMT("Badge number update succeeded for Token:\n [%1]\n New value for badge number :[%2]\n", data_rec.registration_token, data_rec.badge_number)
            ELSE
               LET result_rec.status = -4
               LET result_rec.message = SFMT("Could not update badge number for token in database:\n [%1]", data_rec.registration_token)
            END IF
       END CASE
    CATCH
       LET result_rec.status = -1
       LET result_rec.message = SFMT("Failed to register token:\n [%1]", data_rec.registration_token)
    END TRY
LABEL pc_end:
    DISPLAY result_rec.message
    LET result = util.JSON.stringify(result_rec)
    RETURN result
END FUNCTION

Showing the current registered tokens

The following function is called after a WebServiceEngine timeout, when no request is to be processed. Its purpose is just to show the current list of registered tokens in a server log (stdout):
FUNCTION show_tokens()
    DEFINE rec RECORD
               id INTEGER,
               sender_id VARCHAR(150),
               registration_token VARCHAR(250),
               badge_number INTEGER,
               app_user VARCHAR(50),
               reg_date DATETIME YEAR TO FRACTION(3)
           END RECORD
    DECLARE c1 CURSOR FOR SELECT * FROM tokens ORDER BY id
    FOREACH c1 INTO rec.*
        IF rec.sender_id IS NULL THEN
           LET rec.sender_id = "(null)"
        END IF
        DISPLAY "   ", rec.id, ": ",
                       rec.app_user[1,10], " / ",
                       rec.sender_id[1,20],"... / ",
                       "(",rec.badge_number USING "<<<<&", ") ",
                       rec.registration_token[1,20],"..."
    END FOREACH
    IF rec.id == 0 THEN
       DISPLAY "No tokens registered yet..."
    END IF
END FUNCTION

APNs feedback checking

When using Apple Push Notification service, the device token maintainer can also handle device unregistration by querying the APNs feedback service. The APNs feedback service will provide the list of device tokens that are no longer valid because the app on the devices has unregistered.

Note: When using the APNs feedback service, an SSL certificate needs to be defined in FGLPROFILE as described in APNs SSL certificate.

To get the list of device tokens failed for remote notifications, send HTTP POST request to the following URL:

tcps://feedback.push.apple.com:2196

The token maintainer can use this service to clean up the token database.

The next function is called after a timeout when no request needs to be processed by the token maintainer:
FUNCTION check_apns_feedback()
    DEFINE req com.TCPRequest,
           resp com.TCPResponse,
           feedback DYNAMIC ARRAY OF RECORD
                        timestamp INTEGER,
                        deviceToken STRING
                    END RECORD,
           timestamp DATETIME YEAR TO FRACTION(3),
           token VARCHAR(250),
           i INTEGER,
           data BYTE

    IF arg_val(1)!="APNS" THEN RETURN END IF
    DISPLAY "Checking APNS feedback service..."

    LOCATE data IN MEMORY

    TRY
        LET req = com.TCPRequest.create( "tcps://feedback.push.apple.com:2196" )
        CALL req.setKeepConnection(true)
        CALL req.setTimeout(2)
        CALL req.doRequest()
        LET resp = req.getResponse()
        CALL resp.getDataResponse(data)
        CALL com.APNS.DecodeFeedback(data,feedback)
        FOR i=1 TO feedback.getLength()
            LET timestamp = util.Datetime.fromSecondsSinceEpoch(feedback[i].timestamp)
            LET timestamp = util.Datetime.toUTC(timestamp)
            LET token = feedback[i].deviceToken
            DELETE FROM tokens
                  WHERE registration_token = token
                    AND reg_date < timestamp
        END FOR
    CATCH
        CASE STATUS
            WHEN -15553 DISPLAY "APNS feedback: Timeout: No feedback message"
            WHEN -15566 DISPLAY "APNS feedback: Operation failed :", SQLCA.SQLERRM
            WHEN -15564 DISPLAY "APNS feedback: Server has shutdown"
            OTHERWISE   DISPLAY "APNS feedback: ERROR :",STATUS
        END CASE
    END TRY
END FUNCTION

For more details about APNs feedback service, see The Feedback Service in Apple's APNs documentation.