65.9K
CodeProject 正在变化。 阅读更多。
Home

高级JSON表单规范 - 第八章:高级表单功能

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2016 年 7 月 18 日

LGPL3

9分钟阅读

viewsIcon

18720

downloadIcon

721

JSON表单规范。

章节

引言

这是8章中的最后一章,它涵盖了此表单规范的以下功能

  1. 表单跳转逻辑
  2. 表单元数据收集
  3. 重复屏幕
  4. 表单实例加密

本文档附有四个独立的表单,分别对应描述的四种功能。

跳转逻辑

跳转逻辑是一项功能,它允许根据上一屏幕的条目值来显示或隐藏/绕过表单屏幕。下面显示的屏幕来自Skip Screen Demo form。鼓励读者下载并使用CCA-Mobile应用程序来测试此表单

通过测试Skip Screen Demo表单,读者会注意到,如果上面显示的第一个屏幕的整数输入值大于10,则显示第二个表单屏幕,否则显示第三个表单屏幕。换句话说,第二个或第三个屏幕的显示取决于在第一个屏幕中输入的值。

允许这种可能性的JSON表单定义使用下面代码块中所示的Skip Screen Demo表单示例进行描述

/*

{
"formName": "Skip Screen Demo", 
"formID": "0fe3f144-d242-4959-9473-7098719029cd", 
"formDescription": "This is the example form for Chapter 8 (Skip Screen)", 
"canSavePartial": true, 
"formScreens": [
{
"mainScreen":  {
"screenID": "A Number", 
"screenDisplayArray": [
{
"localeCode": "en", 
"screenLabel": "1. Enter an even number", 
"screenHint": "This number must be between 1 - 20"
}], 
"screenwidgetType": "integerInput", 
"inputRequired": true, 
"widgetSchema": "
\"A Number\": {
\"type\": \"integer\",
\"minimum\": 1,
\"maximum\": 20,
\"multipleOf\": 2
}"
}
}, 
{
"mainScreen":  {
"screenID": "Info 1", 
"screenDisplayArray": [
{
"localeCode": "en", 
"screenLabel": "Please read the text below"
}], 
"screenwidgetType": "info", 
"inputRequired": false, 
"widgetSchema": "
\"Info 1\": {
\"type\": \"string\"
}", 
"screenBindObject":  {
"screenID": "A Number", 
"widgetName": "A Number", 
"widgetValue": ["10"], 
"bindScreenOperator": ">"
}, 
"infoDisplayArray": [
{
"localeCode": "en", 
"localeText": "You entered a number greater than 10!"
}]
}
}, 
{
"mainScreen":  {
"screenID": "Info 2", 
"screenDisplayArray": [
{
"localeCode": "en", 
"screenLabel": "Please read the text below."
}], 
"screenwidgetType": "info", 
"inputRequired": false, 
"widgetSchema": "
\"Info 2\": {
\"type\": \"string\"
}", 
"screenBindObject":  {
"screenID": "A Number", 
"widgetName": "A Number", 
"widgetValue": ["10"], 
"bindScreenOperator": "<="
}, 
"infoDisplayArray": [
{
"localeCode": "en", 
"localeText": "You entered a number less than or equal to 10"
}]
}
}]
}

*/

请注意,表单由三个屏幕组成,其screenID值为:A NumberInfo 1Info 2

Info 1屏幕中,请注意screenBindObject参数。此对象包含四个属性,即screenIDwidgetNamewidgetValuebindScreenOperator。显示此表单的应用程序执行以下操作,以确定是否可以将带有screenBindObject参数的屏幕显示给用户。

  1. 应用程序提取screenBindObject参数中的screenID值。
  2. 应用程序在表单中搜索具有步骤1中提取的screen ID值的屏幕。
  3. 如果在步骤1中未找到具有提取的screenID的屏幕,则当前表单屏幕将不会显示。
  4. 另一方面,如果找到了带有提取的screenID的屏幕,则应用程序在该找到的屏幕中搜索与screenBindObject参数中的widgetName值匹配的输入的。注意,用于此比较的运算符是screenBindObject参数中bindScreenOperator的值。
  5. 如果步骤4中找到输入值,则将screenBindObject参数中的widgetValue与此输入的进行比较。请注意,用于此比较的运算符是screenBindObject参数中bindScreenOperator的值。
  6. 如果比较结果为true,则屏幕将显示给用户,否则将跳过该屏幕。

请查看Info 2屏幕的screenBindObject参数。

表单元数据

元数据的收集对用户是透明的。元数据包括有关显示表单的设备的信息,或有关用户如何填写表单的信息。元数据虽然被定义为一个屏幕,但它永远不会被显示。下面的代码块显示了一个典型的不可见元数据表单屏幕的JSON定义示例。

/*
{
"mainScreen":  {
"screenID": "866d44b7-609f-4459-82be-7d1ef61f48b4", 
"screenDisplayArray": [
{
"localeCode": "en", 
"screenLabel": "2853e838-c0ac-42f8-860e-f8c500b798a5"
}], 
"screenwidgetType": "metaData", 
"inputRequired": false, 
"widgetSchema": "
\"metaData\": {
\"type\": \"object\",
\"properties\":  {
\"startDate\": {
\"type\": \"object\",
\"properties\":  {
\"dateYear\": {
\"type\": \"integer\",
\"minimum\": 0,
\"maximum\": 9999
},
\"dateMonth\": {
\"type\": \"integer\",
\"minimum\": 1,
\"maximum\": 12
},
\"dateDay\": {
\"type\": \"integer\",
\"minimum\": 1,
\"maximum\": 31
},
\"saveAsUtc\": {
\"type\": \"boolean\",
\"default\": false
}
},
\"required\": [\"dateYear\", \"dateMonth\", \"dateDay\"],
\"minProperties\": 3,
\"maxProperties\": 3,
\"additionalProperties\": false
},
\"endDate\": {
\"type\": \"object\",
\"properties\":  {
\"dateYear\": {
\"type\": \"integer\",
\"minimum\": 0,
\"maximum\": 9999
},
\"dateMonth\": {
\"type\": \"integer\",
\"minimum\": 1,
\"maximum\": 12
},
\"dateDay\": {
\"type\": \"integer\",
\"minimum\": 1,
\"maximum\": 31
},
\"saveAsUtc\": {
\"type\": \"boolean\",
\"default\": false
}
},
\"required\": [\"dateYear\", \"dateMonth\", \"dateDay\"],
\"minProperties\": 3,
\"maxProperties\": 3,
\"additionalProperties\": false
},
\"deviceID\": {
\"type\": \"string\"
},
\"phoneNum\": {
\"type\": \"string\"
},
\"simSerial\": {
\"type\": \"string\"
},
\"subscriberID\": {
\"type\": \"string\"
},
\"timeSpan\": {
\"type\": \"object\",
\"properties\":  {
\"days\": {
\"type\": \"integer\"
},
\"hours\": {
\"type\": \"integer\"
},
\"minutes\": {
\"type\": \"integer\"
},
\"seconds\": {
\"type\": \"integer\"
}
},
\"required\": [\"days\", \"hours\", \"minutes\", \"seconds\"],
\"minProperties\": 4,
\"maxProperties\": 4,
\"additionalProperties\": false
}
},
\"required\": [\"startDate\", \"endDate\", \"deviceID\", 
\"phoneNum\", \"simSerial\", \"subscriberID\", \"timeSpan\"],
\"additionalProperties\": false
}"
}
}

*/

上面的元数据屏幕定义是本文档附带的Metadata demo form的摘录。screenwidgetType设置为metaData。该屏幕的widgetSchema值的主要属性如下所述

  1. startDate:这是用户开始填写表单的日期。请注意,与日期和时间小部件一样,表单设计者可以指定此信息是否以其协调世界时(UTC)等效形式保存。
  2. endDate:这是用户完成表单的日期。表单设计者还可以指定此日期是否以其协调世界时(UTC)值形式存储。
  3. deviceID:这是设备国际移动设备身份码(IMEI),如果设备有的话。
  4. phoneNum:这是附加到用户识别模块(SIM)的电话号码,如果设备上有的话。
  5. simSerial:这是用户识别模块(SIM)上的序列号,如果设备上有的话。
  6. subscriberID:这是移动网络分配给SIM卡的国际移动用户身份码(IMSI)
  7. timeSpan:这是完成表单所需的天、小时、分钟和秒的时长。

下面是Metadata Demo form已完成实例的示例。

/*

{
"metaData":  {
"startDate":  {
"dateYear": 2016, 
"dateMonth": 6, 
"dateDay": 28
}, 
"endDate":  {
"dateYear": 2016, 
"dateMonth": 6, 
"dateDay": 28
}, 
"deviceID": "9908876543234577", 
"phoneNum": "N/A", 
"simSerial": "76888076543244", 
"subscriberID": "877668888009888", 
"timeSpan":  {
"days": 0, 
"hours": 0, 
"minutes": 0, 
"seconds": 8
}
}
}

*/

请注意,表单在同一天完成,耗时8秒。还请注意,phoneNum参数的值为N/AN/A用于元数据参数,这些参数的值不存在或无法获取。

重复屏幕

表单有时会遇到一个问题需要一个或多个答案的情况。例如,可能会要求受访者列出他/她去过的国家数量。重复屏幕通过在运行时根据受访者输入的国家数量创建多个输入屏幕来满足这些情况。

下图是本文档中附带的Repeat Screen Demo FormCCA-Mobile应用程序中显示的截图。在左侧的屏幕上,受访者输入数字4表示访问过的国家数量。受访者通过单击按钮被进一步要求输入国家名称。右侧的屏幕是提示输入第一个国家名称的屏幕。向左滑动将显示类似的输入屏幕,提示输入第二个国家名称。此过程一直持续到用户输入第四个国家名称为止。

下面显示了上面描述的重复屏幕的定义。

/*
{
"mainScreen":  {
"screenID": "Num Countries", 
"screenDisplayArray": [
{
"localeCode": "en", 
"screenLabel": "1. Enter the number of countries you have visited.", 
"screenHint": "Click the button below to enter the name of these countries."
}], 
"screenwidgetType": "repeat", 
"inputRequired": true, 
"widgetSchema": "
\"Num Countries\": {
\"type\": \"array\",
\"items\": 
{
\"type\": \"object\",
\"properties\":  {
\"Name of Country\": {
\"type\": \"string\"
}
},
\"required\": [\"Name of Country\"],
\"additionalProperties\": false
},
\"additionalItems\": false
}"
}, 
"subScreenList": [
{
"screenID": "Name of Country", 
"screenDisplayArray": [
{
"localeCode": "en", 
"screenLabel": "Enter the name of the country below", 
"screenHint": "Please do not leave blank"
}], 
"screenwidgetType": "textInput", 
"inputRequired": true, 
"widgetSchema": "
\"Name of Country\": {
\"type\": \"string\"
}"
}]
}
*/

请注意,screenwidgetType设置为repeat,而widgetSchema是用于保存用户输入的所有国家名称的数组的转义模式。

如前所述,重复屏幕会根据输入重复一个或多个子屏幕。子屏幕定义包含在重复屏幕定义中的subScreenList JSON数组中。

请注意,ID为 Name of Country的子屏幕的widgetSchema中包含的模式与Num Countries JSON数组的项相同。

下面是此重复屏幕输入已完成实例的示例。

/*

{
"Num Countries": [
{
"Name of Country": "Country A"
}, 
{
"Name of Country": "Country B"
}, 
{
"Name of Country": "Country C"
}, 
{
"Name of Country": "Country D"
}], 
...
}

*/

子屏幕的重复次数可以在表单设计时受到限制。下面的屏幕图像将重复次数限制为两次。

这种重复次数限制是通过设置重复屏幕的widgetSchema中包含的数组的maxItems值来实现的。例如,下面重复屏幕定义中的Fave Countries数组设置为2

/*

{
"mainScreen":  {
"screenID": "Fave Countries", 
"screenDisplayArray": [
{
"localeCode": "en", 
"screenLabel": "2. Enter the name of the top two favourite 
                of all the countries you have visited.", 
"screenHint": "Click the button below to enter the names."
}], 
"screenwidgetType": "repeat", 
"inputRequired": true, 
"widgetSchema": "
\"Fave Countries\": {
\"type\": \"array\",
\"items\": 
{
\"type\": \"object\",
\"properties\":  {
\"Country Name\": {
\"type\": \"string\"
}
},
\"required\": [\"Country Name\"],
\"additionalProperties\": false
},
\"maxItems\": 2,
\"additionalItems\": false
}"
}, 
"subScreenList": [
{
"screenID": "Country Name", 
"screenDisplayArray": [
{
"localeCode": "en", 
"screenLabel": "Enter the name of the favourite country", 
"screenHint": "Please do not leave blank."
}], 
"screenwidgetType": "textInput", 
"inputRequired": true, 
"widgetSchema": "
\"Country Name\": {
\"type\": \"string\"
}"
}
*/

下面是受限制的重复屏幕已完成实例的示例。

/*

{
..., 
"Fave Countries": [
{
"Country Name": "Country C"
}, 
{
"Country Name": "Country B"
}]
}

*/

表单实例加密

本节的内容是定义此JSON表单规范的主要动机之一。其背后的想法是允许在设备以及其他任何地方对表单实例进行加密,一旦它们完成。这种加密机制需要实现以下目标;

  1. 每个已完成的表单实例都必须使用不同的加密密钥进行加密。这确保了如果一个已完成的实例被泄露,其他已完成的实例将保持安全。
  2. 加密必须使用强大的对称协议进行,例如高级加密标准(AES)
  3. 用于加密已完成表单实例的密钥将使用Diffie–Hellman (D–H)密钥交换机制进行计算。
  4. 加密数据必须使用哈希算法进行签名,例如SHA256。

为了实现此安全功能,将生成一个安全的加密公钥-私钥对。此密钥对的私钥部分由表单设计者安全保管,公钥部分将进行Base64编码并包含在表单定义中formPublicKey参数的值中。请参见下面Secure Form Demo form定义中的示例

/*

{
"formName": "Secure Form Demo", 
"formID": "d875a1c5-5962-4d5e-ae0d-8d9bad9a9b9f", 
"formDescription": "This is the example form for Chapter 8 (Secure Form)", 
"formPublicKey": "DqYOFPae0DPbburzNF7gvRJvJMtmWc3nV79Paa2ZFVpxOSvp997bwuYLMMa0OxGEOavtdpk5o0
 s84Z5mgpTz3aCZkNYo8Sdks7THWCHsQqKyoPTN2dOAqHwTxinr6VbBbK9Bjoe12Lf5gV8LZzxkFt0HFWQx23DjcG9KIe
 PnMQtIDl3iApiukDQLsMTccFFPwdr+/bcPeWazEXTUYecBPLkO1vusIqC1FcXWBTuxoM/4Cu7r4Z2MT3MnQBIt3flX", 
"canSavePartial": false, 
"formScreens": [
{
"mainScreen":  {
"screenID": "Name", 
"screenDisplayArray": [
{
"localeCode": "en", 
"screenLabel": "1. Enter candidate's name in the text box below.", 
"screenHint": "Please do not leave blank."
}], 
"screenwidgetType": "textInput", 
"inputRequired": true, 
"widgetSchema": "
\"Name\": {
\"type\": \"string\"
}"
}
}, 
{
"mainScreen":  {
"screenID": "Vote", 
"screenDisplayArray": [
{
"localeCode": "en", 
"screenLabel": "2. Please enter candidate's vote count below.", 
"screenHint": "Enter an integer value between 0 - 100."
}], 
"screenwidgetType": "integerInput", 
"inputRequired": true, 
"widgetSchema": "
\"Vote\": {
\"type\": \"integer\",
\"minimum\": 0,
\"maximum\": 100
}"
}
}]
}
*/

请注意,canSavePartial参数设置为false,这意味着不允许部分完成的表单实例。

下面是上面定义的表单已完成实例的示例

/*

{
"encryptedData": "jvQ5Ewa0zZInj56m3BMKh91wvL/0RVDhCYYI93bLI76rUg==", 

"dataSignature": "8PIFfjS+HVFSUWD0CVVOUr/7lUDePtF+3WJUrQ/VE7g=",

"publicKey": "fEVu+M95Qsa/tZ48svN0yuwhhaqIKj73wylMzn1aonp+0MR7HVYu5D7s3cd0mY/
 N95zKfDrN+t5BNCsiHtk60CUNU7dAIgqDdFSOOkWXDDPZ03ZUGQQkhK+MUg+XnYNd5InEpNM0Cyj99jB+
 MSIqD3UJZvhlpNK8VoYFHIv5QPw7auxAyiH7+tU6p6vzxPeyd8LqY1pb1q0fYR5CfUnuyvMET+
 iIX9pD+AFUvu3DfUFha0PiMmVv7llIDIMBYz1n"
}

*/

带有formPublicKey参数的表单的已完成实例始终采用上图所示的JSON格式。为了理解为什么会这样,有必要描述已完成实例如何被加密和存储。过程如下

  1. 显示表单的设备会生成一个随机的安全加密公钥和私钥对。公钥以其Base64格式编码,并设置为publicKey JSON字符串变量的值。
  2. 设备提取随表单包含的formPublicKey参数的值。
  3. 设备使用步骤1中生成的私钥和表单的公钥,使用D-H密钥交换机制计算出加密密钥。生成此密钥后,步骤1中生成的私钥将被安全丢弃。
  4. 在步骤3中计算出的加密密钥用于加密纯文本已完成的表单实例。加密的表单实例以Base64格式编码,并设置为encryptedData JSON字符串变量的值。
  5. 表单的加密实例使用哈希算法进行签名。此计算出的签名的Base64等效值被设置为dataSignature JSON字符串变量。

要解密加密的表单实例,将执行以下步骤

  1. 计算encryptedData JSON string值的哈希值。此计算值与dataSignature JSON string变量的值进行比较。如果匹配,则继续解密过程,否则中止。
  2. 使用包含在表单中的公钥以及publicKey JSON string值关联的私钥,使用D-H算法计算出解密密钥。
  3. 然后使用计算出的密钥来解密encryptedData JSON string值。

加密和解密过程的步骤仅用于说明。实现者可能会选择另一条路线。尽管如此,有兴趣的读者可以查看此链接了解本节中提供的加密过程的数学原理。

注意:加密仅适用于文本数据。它不涵盖图形数据,如照片和签名。

看点

有兴趣的读者应遵循这些演练中的表单设计部分,并使用此GUI工具来了解更多关于基于多种用例场景设计表单的信息。

历史

  • 2016 年 7 月 18 日:第一个版本
  • 2016 年 11 月 3 日:对此工作进行了更正
© . All rights reserved.