文档版本 | 开发工具 | 测试平台 | 工程名字 | 日期 | 作者 | 备注 |
---|---|---|---|---|---|---|
V1.0 | 2016.06.10 | lutianfei | none |
国际化
- 软件的国际化:软件开发时,要使它能同时应对世界不同地区和国家的访问,并针对不同地区和国家的访问,提供相应的、符合来访者阅读习惯的页面或数据。
- 国际化又称为 i18n:internationalization
- 软件实现国际化,需具备哪些特征:
- 对于程序中固定使用的文本元素,例如菜单栏、导航条等中使用的文本元素、或错误提示信息,状态信息等,需要根据来访者的地区和国家,选择不同语言的文本为之服务。
- 对于程序动态产生的数据,例如(日期,货币等),软件应能根据当前所在的国家或地区的文化习惯进行显示。
固定文本元素的国际化
- 针对于不同的国家与地区要显示的信息,都配置到配置文件中,根据当前访问者的国家或语言来从不同的配置文件中获取信息,展示在页面上。
- 对于软件中的菜单栏、导航条、错误提示信息,状态信息等这些固定不变的文本信息,可以把它们写在一个properties文件中,并根据不同的国家编写不同的properties文件。这一组properties文件称之为一个
资源包
。 - 在JavaAPI中提供了一个
ResourceBundle 类
用于描述一个资源包,并且 ResourceBundle类提供了相应的方法getBundle
,这个方法可以根据来访者的国家地区自动获取与之对应的资源文件予以显示。
创建资源包和资源文件
- 一个资源包中的每个资源文件都必须拥有共同的
basename
基名。除了基名,每个资源文件的名称中还必须有标识其本地信息的附加部分。例如:一个资源包的基名是“myproperties”,则与中文、英文环境相对应的资源文件名则为:“myproperites_zh_cn.properties” “myproperites_en_US.properties”
- 资源包中所有properties文件必须具有相同基名basename, 在basename后可以通过
_
拼接国家和语言信息 例如:basename_语言_国家.properties
- myproperties_zh.properties 中文的配置文件 这里zh就是语言
- myproperties_en.properties 英文的配置文件 这里en就是语言
- myproperties_zh_CN.properties 中国中文配置文件 这里zh语言 CN国家
- myproperties.properties 没有国家和语言信息,默认资源文件,如果来访者的国家和语言的资源文件没有找到,就会读取默认资源文件
资源文件书写格式
- 资源文件的内容通常采用
关键字=值
的形式,软件根据关键字检索值显示在页面上。一个资源包中的所有资源文件的关键字必须相同,值则为相应国家的文字。 - 并且资源文件中采用的是properties格式文件,所以文件中的所有字符都必须是ASCII字码,对于像中文这样的非ACSII字符,须先进行编码。(java提供了一个native2ascII命令用于编码)。属性文件是不能保存中文的
- 问题:关于配置文件?
- 所谓的配置文件就是一组properties文件,它们叫做资源包。
- ResourceBundler,它是用于从资源包中获取数据的。
编程实现固定文本的国际化
- 1、加载src下properties文件
- ResourceBundle bundle = ResourceBundle.getBundle(basename);
- 2、读取properties文件中内容
- String value = bundle.getString(key);
- 也可以在ResourceBundle.getBundle时 传入一个代表国家和地区Locale对象
- ResourceBundle bundle = ResourceBundle.getBundle(“info”, Locale.CHINA); 指定读取中国配置文件
- 如果与该locale对象匹配的资源包子类找不到。一般情况下,则选用操作系统默认资源文件予以显示
- 优先级:指定Locale > 系统默认区域和语言 > 资源包默认的
编码演示
- properties文件操作以及通过ResourceBundler来获取资源包中信息.
- 1.资源包文件一般都放置在classpath下(对于myeclipse就是src下)
- 2.关于ResourceBundle使用
- 创建:
ResourceBundle bundle = ResourceBundle.getBundle("message");
ResourceBundle bundle = ResourceBundle.getBundle("message",Locale.US);
* 获取:
`bundle.getString(String name);`
* 扩展:关于properties文件中中文问题处理?
* 在jdk中有一个命令native2ascii.exe。
* 1.进行一次翻译
* `native2ascii` 回车
* 中文 回车
* 2.批量翻译
* native2ascii 源文件路径 目录文件路径
* 例如: native2ascii d:/a.txt d:/a.properties
国际化的登录页面
- 1.创建登录页面
- 2.创建配置文件
- 3.在登录页面上根据不同的国家获取ResourceBundle
- 4.在页面上需要国际化的位置,通过ResourceBundle.getString()来获取信息.
- 问题:在页面上使用了jsp脚本.解决方案:使用标签 。
- 在jstl标签库中提供了国际化标签.
日期国际化 DateFormat类
- 作用:
- 1.可以将一个Date对象格式化成指定效果的String format方法
- 2.可以将一个String解析成Date对象 parse方法
- DateFormat 类还定义了一些用于描述日期/时间的显示模式的 int 型的常量,包括FULL, LONG, MEDIUM, DEFAULT, SHORT,实例化DateFormat对象时,可以使用这些常量,控制日期/时间的显示长度
- 实例化DateFormat类有九种方式
//无参数
DateFormat df1 = DateFormat.getDateInstance(); // 只有年月日
DateFormat df2 = DateFormat.getTimeInstance(); // 只有小时分钟秒
DateFormat df3 = DateFormat.getDateTimeInstance();// 两个都有
//有参数
DateFormat df1 = DateFormat.getDateInstance(DateFormat.FULL); // 只有年月日
DateFormat df2 = DateFormat.getTimeInstance(DateFormat.MEDIUM); // 只有小时分钟秒
DateFormat df3 = DateFormat.getDateTimeInstance(DateFormat.LONG,DateFormat.SHORT);// 两个都有
//国际化
DateFormat df1 = DateFormat.getDateInstance(DateFormat.FULL,Locale.US); // 只有年月日
DateFormat df2 = DateFormat.getTimeInstance(DateFormat.MEDIUM,Locale.US); // 只有小时分钟秒
DateFormat df3 = DateFormat.getDateTimeInstance(DateFormat.LONG,DateFormat.SHORT,Locale.US);// 两个都有
数字格式化 NumberFormat类
- 实例化NumberFormat类时,可以使用locale对象作为参数,也可以不使用,下面列出的是使用参数的。
- getNumberInstance(Locale locale):以参数locale对象所标识的本地信息来获得具有多种用途的NumberFormat实例对象
- getIntegerInstance(Locale locale):以参数locale对象所标识的本地信息来获得处理整数的NumberFormat实例对象
- getCurrencyInstance(Locale locale):以参数locale对象所标识的本地信息来获得处理货币的NumberFormat实例对象
- getPercentInstance(Locale locale):以参数locale对象所标识的本地信息来获得处理百分比数值的NumberFormat实例对象
- 1.对数值进行格式化
- NumberFormat nf = NumberFormat.getIntegerInstance();
- 2.对数值进行百分比
- NumberFormat nf = NumberFormat.getPercentInstance(Locale.FRANCE);
- 3.对数值进行以货币显示
- NumberFormat nf = NumberFormat.getCurrencyInstance(Locale.US);
// 1.数值操作
@Test
public void fun1() {
NumberFormat nf = NumberFormat.getIntegerInstance();
String s = nf.format(19.98765);
System.out.println(s);
}
// 2.查分比
@Test
public void fun2() {
NumberFormat nf = NumberFormat.getPercentInstance(Locale.FRANCE);
String s = nf.format(0.98);
System.out.println(s);
}
// 3.货币
@Test
public void fun3() {
NumberFormat nf = NumberFormat.getCurrencyInstance(Locale.US);
System.out.println(nf.format(1090));
}
动态文本国际化
- 如果一个字符串中包含了多个与国际化相关的数据,可以使用MessageFormat类对这些数据进行批量处理。
- 例如:
- At 12:30 pm on jul 3,1998, a hurricance destroyed 99 houses and caused $1000000 of damage
- 以上字符串中包含了时间、数字、货币等多个与国际化相关的数据,对于这种字符串,可以使用MessageFormat类对其国际化相关的数据进行批量处理。
- MessageFormat 类如何进行批量处理呢?
- 1.MessageFormat类允许开发人员用占位符{0}{1}{2}…替换掉字符串中的敏感数据(即国际化相关的数据)。
- 2.MessageFormat类在格式化输出包含占位符的文本时,messageFormat类可以接收一个参数数组,以替换文本中的每一个占位符。
- 模式字符串:
On {0}, a hurricance destroyed {1} houses and caused {2} of damage.
- MessageFormat类的使用
- format(String pattern, Object… arguments) static
- pattern 模式字符串
- arguments 参数数组
- format(String pattern, Object… arguments) static
- 自定义Locale
- MessageFormat(String pattern, Locale locale)
- format(Object obj)
- 占位符有三种书写方式:
- {argumentIndex}: 0-9 之间的数字,表示要格式化对象数据在参数数组中的索引号
- {argumentIndex,formatType}: 参数的格式化类型
- {argumentIndex,formatType,FormatStyle}: 格式化的样式,它的值必须是与格式化类型相匹配的合法模式、或表示合法模式的字符串。
- formatType可以取的值有:number date time
- formatStyle可以取的值有
- number类型可以取:integer currency percent
- date类型可以取的:short medium full long
- time类型可以取的:short medium full long
@Test
public void fun2() {
// At 12:30 pm on jul 3,1998, a hurricance destroyed 99 houses and
// caused $1000000 of damage
String msg = "At {0,time,short} on {0,date,long}, a hurricance destroyed {1,number,integer} houses and caused {2,number,currency} of damage";
Calendar c = Calendar.getInstance();
c.set(1998, 6, 3, 12, 30,0);
Date date = c.getTime();
MessageFormat mf = new MessageFormat(msg, Locale.US);
String value = mf.format(new Object[] { date, 99, 1000000 });
System.out.println(value);
}
AJAX
AJAX
即Asynchronous Javascript And XML
(异步JavaScript和XML),是指一种创建交互式网页应用的网页开发技术。- AJAX = 异步 JavaScript和XML(标准通用标记语言的子集)。
- AJAX 是一种用于创建快速动态网页的技术
- 使用ajax目的是为了提高用户的感受。
- 问题:异步是什么?
- 异步操作的核心
XMLHttpRequest
对象. - 传统web交互模型,浏览器直接将请求发送给服务器,服务器回送响应,直接发给浏览器
- Ajax交互模型,浏览器首先将请求 发送 Ajax引擎(以XMLHttpRequest为核心),AJax引擎再将请求发送给 服务器,服务器回送响应先发给Ajax引擎,再由引擎传给浏览器显示
- 异步操作的核心
- 同步交互模式,客户端提交请求,等待,在响应回到客户端前,客户端无法进行其他操作
- 异步交互模型,客户端将请求提交给Ajax引擎,客户端可以继续操作,由Ajax引擎来完成与服务武器端通信,当响应回来后,Ajax引擎会更新客户页面,在客户端提交请求后,用户可以继续操作,而无需等待 。
- Google : suggest建议、邮件定时保存、map地图
AJAX快速入门
- ajax核心就是
XMLHttpRequest
对象. - 1.创建XMLHttpRequest对象
- 不同浏览器提供不同的支持
- IE浏览器
- new ActiveXObject(“Msxml2.XMLHTTP”);
- new new ActiveXObject(“Microsoft.XMLHTTP”);
- 其它浏览器(火狐)
- new XMLHttpRequest();
- XMLHttpRequest
- 方法
open
(method,url, asynch) :建立对服务器的调用- 其中method表示HTTP调用方法,一般使用”GET“,”POST”
- url表示调用的服务器的地址
- asynch表示是否采用异步方式,true表示异步,一般这个参数不写
send
(content) :向服务器发送请求
- 属性
onreadystatechange
:状态回调函数responseText
/responseXML
:服务器的响应字符串status
:服务器返回的HTTP状态码statusText
: 服务器返回的HTTP状态信息readyState
:对象状态- 0 = 未初始化 1 = 正在加载
- 2 = 已加载 3 = 交互中
- 4 = 完成
- 方法
- 得到XMLHttpRequest对象.(js对象)
- 在w3school文档中的 xmldom文档中就可以查找到 dom XMLHttpRequest对象
var xmlhttp=null;
if (window.XMLHttpRequest)
{// code for all new browsers
xmlhttp=new XMLHttpRequest();
}
else if (window.ActiveXObject)
{// code for IE5 and IE6
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
- 2.注册回调函数
xmlhttp.onreadystatechange=function(){
};
- 3.使用open方法建立与服务器的连接
- 只是用于设置请求方式 以及url,它不发送请求.
- 4.向服务器端发送数据 send
- 它是用于发送请求的。
- send(null);null代表没有参数 如果有参数可以写成:”username=tom&password=123”
- 5.在回调函数中对返回数据进行处理
- 1.XMLHttpRequest对象有一个属性 readyState
- 它代表的是XMLHttpRequest对象的状态。
- 0.代表XMLHttpRequest对象创建
- 1.open操作
- 2.send操作
- 3.接收到了响应数据,但是只有响应头,正文还没有接收。
- 4.所有http响应接收完成。
- 2.status
- 由服务器返回的 HTTP 状态代码,如 200 表示成功
- 3.在回调函数中可以通过以下方式获取服务器返回的数据
- 1.responseText
- 2.responseXML
- 回调函数编写
- 1.XMLHttpRequest对象有一个属性 readyState
if(xmlHttp.readyState == 4){
// 判断数据是否正确
if(xmlHttp.status == 200){
// 响应有效
...
}
}
- 客户端向服务器提交数据
- 1、get方式发送数据
- xmlHttp.open(“GET”,”url?key=value”); // 参数已经在url上
- xmlHttp.send(null);
- 2、post方式发送数据
- xmlHttp.open(“POST”,”url”); // 不需要写参数
- xmlHttp.setRequestHeader(“CONTENT-TYPE”,”application/x-www-form-urlencoded”); // post发送参数前,需要设置编码格式
- xmlHttp.send(“name=xxx&pwd=xxx”); // 发送post数据
- 1、get方式发送数据
- 关于ajax操作中请求参数的设置问题:
- 1.对于get请求方式,参数设置
- 直接在url后面拼接
- 例如:”${pageContext.request.contextPath}/ajax2?name=tom”
- 2.对于post请求方式,参数设置
- xmlhttp.open(“POST”,”${pageContext.request.contextPath}/ajax2”);
- xmlhttp.send(“name=tom”);
- 注意:如果是post请求方式,还需要设置一个http请求头。
- xmlhttp.setRequestHeader(“”,”“);
- 例如:
- xmlhttp.open(“POST”,”${pageContext.request.contextPath}/ajax2”);
- xmlhttp.setRequestHeader(“content-type”,”application/x-www-form-urlencoded”);
- xmlhttp.send(“name=tom”);
- 1.对于get请求方式,参数设置
ajax案例1–验证用户名是否可以使用
- HTML片段的数据处理
- 通过xmlHttp.responseText获得返回数据
- 通过DOM查找获得元素
- 调用元素的 innerHTML进行操作
var xmlHttp = createXMLHttpRequest();
xmlHttp.onreadystatechange = function(){
if(xmlHttp.readyState ==4 ){
if(xmlHttp.status == 200){
...
}
}
};
xmlHttp.open("GET","checkUsername?username="+username);
xmlHttp.send(null);
ajax案例2–显示商品信息
- 显示 — 返回HTML片段
- 通过product.jsp 生成 HTML片段,返回客户端,客户端Ajax引擎接收,通过innerHTML 将tbale元素 嵌入到页面内部
- 第一个版本:
- 1.创建一个Product类
- private int id;
- private String name;
- private double price;
- 2.创建ajax4.jsp
<a href="javascript:void(0)" id="p">显示商品信息</a>
<div id="d"></div>
* 在回调函数中得到服务器返回的信息innerHTML到div中.
- 3.在Ajax4Servlet中
- 将
List<Product>
中的数据,手动拼接成了html代码,写回到浏览器端.
- 将
builder.append("<table border=‘1‘><tr><td>商品编号</td><td>商品名称</td><td>商品价格</td></tr>");
for (Product p : ps) {
builder.append("<tr><td>" + p.getId() + "</td><td>" + p.getName()
+ "</td><td>" + p.getPrice() + "</td></tr>");
}
builder.append("</table>");
- 第二个版本
- 创建一个product.jsp页面,在页面上去组装table,直接将数据返回了.
- 1.在Ajax4Servlet中
- request.setAttribute(“ps”, ps);
- request.getRequestDispatcher(“/product.jsp”).forward(request, response);
- 2.在product.jsp页面上
<table border=‘1‘>
<tr>
<td>商品编号</td>
<td>商品名称</td>
<td>商品价格</td>
</tr>
<c:forEach items="${ps}" var="p">
<tr>
<td>${p.id }</td>
<td>${p.name }</td>
<td>${p.price }</td>
</tr>
</c:forEach>
</table>
- 第三个版本
- 在服务器端得到数据,只将要显示的内容返回,而不返回html代码
- 而html代码的拼接,在浏览器端完成。
- 问题:服务器返回什么样的数据格式?
- json:它是一种轻量级的数据交换格式。
[{‘id‘:‘1‘,‘name‘:‘洗衣机‘,‘price‘:‘1800‘},{‘id‘:‘2‘,‘name‘:‘电视机‘,‘price‘:‘3800‘}]
在js中{name:value,name1:valu1}这就是一个js对象.
[{},{}]这代表有两个对象装入到了一个数组中。
Json入门
- json 是一种javascript轻量级数据交互格式,主要应用于Ajax编程在java中;可以通过jsonlib插件,在java对象与json之间做转换。
- 格式一:
{key:value,key:value,key:value }
键值对直接用,
分开,键值之间用:
,键本身必须是字符串常量- {name : ‘张三’}
- {‘name’:’张三’}
- 是等价的
- 值 加不加引号,是有区别的,不加引号是变量,加引号常量字符串
- 格式二: ·[值1, 值2 ,值3 ]· 数组结构
- 组合后复杂格式
- [{name:’aaa’}, {name:’bbb’}, {name:ccc}] 表示三个对象数组
JSON应用场景
- AJAX**请求参数** 和响应数据
json-lib使用
- 是 java类库 ,支持 javabean map list array 转换 json格式字符串, 支持将json字符串转换 javabean对象
- 导入jar包(6个)
- 1) 转换数组 、List集合 到json格式字符串 - 使用
JSONArray
- 1) 转换数组 、List集合 到json格式字符串 - 使用
String[] arr = {"sada","fdssd","dfsd","sadas"};
JSONArray jsonArray = JSONArray.fromObject(arr);
System.out.println(jsonArray.toString());
* 2) 将JavaBean/Map解析成JSON串:使用`JSONObject`
Product p1 = new Product();
p1.setName("三星手机");
p1.setPrice(3999);
JSONObject jsonObject = JSONObject.fromObject(p1);
System.out.println(jsonObject);
* 3) 通过JsonConfig对象 配置对象哪些属性不参与转换
JsonConfig jsonConfig = new JsonConfig();
jsonConfig.setExcludes(new String[]{"price"});
JSONObject jsonObject = JSONObject.fromObject(p1, jsonConfig);
System.out.println(jsonObject);
- 重写练习二 返回结果数据是json格式数据
- 客户端获得json格式字符串后,转换为 javascript对象 ——- eval(“(“+jsonstr+”)”);
ajax操作中服务器端返回xml处理
- XMHttpRequest.resposneXML;—–>得到的是一个Document对象.
- 操作:可以自己将xml文件中的内容取出来,写回到浏览器端。也可以请求转发到一个xml文件,将这个文件信息写回到
- 浏览器端,注意 response.setContextType(“text/xml;charset=utf-8”);
- 问题:如果没有xml文件,我们的数据是从数据库中查找到了,想要将其以xml格式返回怎样处理?
- 可以使用xml插件处理
xstream
,它可以在java对象与xml之间做转换.
- 可以使用xml插件处理
- xstream使用:
- 1.导包:2个.
- 2.使用
- 1.将java对象转换成xml
- XStream xs=new XStream();
- String xml=xs.toXML(java对象);
- 问题:生成的xml中的名称是类的全名.
- 两种方式:
- 1.编码实现
- xs.alias(“person”, Person.class);
- 2.使用注解(Annotation)
- @XStreamAlias(别名) 对类和变量设置别名
- @XStreamAsAttribute 设置变量生成属性
- @XStreamOmitField 设置变量 不生成到XML
- @XStreamImplicit(itemFieldName = “hobbies”) 设置集合类型变量 别名
- 1.编码实现
- 两种方式:
- 使注解生效
- xStream.autodetectAnnotations(true);
- 1.将java对象转换成xml
基于xml返回数据Ajax省市联动
- XML结果数据生成
- 问题:服务器端如何将java对象,生成XML格式数据 ? 需要第三方类库支持 XStream
- XStream is a simple library to serialize objects to XML and back again.
- 如果实现 对象 — xml 只需要 xstream-1.3.1.jar
- 如果实现 xml —- 对象 需要 xstream-1.3.1.jar 、xpp3_min-1.1.4c.jar
- 将 xstream-1.3.1.jar 、xpp3_min-1.1.4c.jar 复制工程lib 目录下
- 核心方法
- xSteam.alias(name,Class); 将类型解析或者序列化 定义一个别名
- toXML(obj) 将对象序列化XML
- fromXML(inputStream/xml片段) 将xml信息解析对象
提供便捷注解
@XStreamAlias(别名) 对类和变量设置别名
@XStreamAsAttribute 设置变量生成属性
@XStreamOmitField 设置变量 不生成到XML
@XStreamImplicit(itemFieldName = “hobbies”) 设置集合类型变量 别名
使注解生效
xStream.autodetectAnnotations(true);
使用 xmlHttp.responseXML 接收解析成为 document对象
练习:省市级联
- 注意:如果服务器返回的是json数据,我们在浏览器端接收数据用
eval
()转换。- var json=eval(xmlhttp.responseText);
- 有些情况下,转换会出问题需加括号解决
- var json=eval(“(“+xmlhttp.responseText+”)”)
<title>ajax开发---json返回</title>
<script type="text/javascript"
src="${pageContext.request.contextPath}/my.js"></script>
<script type="text/javascript">
var jsonObj; //声明全局,因为要在多个函数中使用.
window.onload = function() {
var province = document.getElementById("province");//省份下拉框
//第一步:得到XMLHttpRequest对象.
var xmlhttp = getXmlHttpRequest();
//2.设置回调函数
xmlhttp.onreadystatechange = function() {
//5.处理响应数据 当信息全部返回,并且是成功
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
jsonObj = eval("(" + xmlhttp.responseText + ")");
//得到省份名称
for ( var i = 0; i < jsonObj.length; i++) {
var pname = jsonObj[i].name;
var option = document.createElement("option");
option.text = pname;
province.add(option);
}
}
};
//post请求方式,参数设置
xmlhttp.open("GET", "${pageContext.request.contextPath}/ajax2");
xmlhttp.send(null);
};
//创建一个函数,用于向城市下拉框中添加值.
function fillCity() {
var province = document.getElementById("province");//省份下拉框
var city = document.getElementById("city");//城市下拉框.
//每一次向城市中添加信息时,将信息重置。
city.innerHTML = "<option>--请选择城市--</option>";
var pname = province.options[province.selectedIndex].text;
for ( var i = 0; i < jsonObj.length; i++) {
var pElementName = jsonObj[i].name;
if (pname == pElementName) {
var citys = jsonObj[i].citys;
for ( var j = 0; j < citys.length; j++) {
var cname = citys[j].name;
var option = document.createElement("option");
option.text = cname;
city.add(option);
}
}
}
}
</script>
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/xml;charset=utf-8");
// 1.得到数据
List<Province> ps = new ArrayList<Province>();
Province p1 = new Province();
p1.setName("黑龙江");
List<City> citys1 = new ArrayList<City>();
City city1 = new City();
city1.setName("哈尔滨");
City city11 = new City();
city11.setName("齐齐哈尔");
City city111 = new City();
city111.setName("大庆");
citys1.add(city1);
citys1.add(city11);
citys1.add(city111);
p1.setCitys(citys1);
Province p2 = new Province();
p2.setName("吉林");
List<City> citys2 = new ArrayList<City>();
City city2 = new City();
city2.setName("长春");
City city22 = new City();
city22.setName("吉林");
City city222 = new City();
city222.setName("四平");
citys2.add(city2);
citys2.add(city22);
citys2.add(city222);
p2.setCitys(citys2);
ps.add(p1);
ps.add(p2);
// 转换成json
String json = JSONArray.fromObject(ps).toString();
response.getWriter().write(json);
response.getWriter().close();
}
时间: 2024-10-07 16:34:21