扩展一个参数解析器 |
由 REST 服务发布机制简述 可知,HTTP 请求在达到 REST 应用对象,交给资源实现类处理的时候,先要解析 HTTP 请求中的参数,然后才会进入业务逻辑进行处理。参数解析的工作由参数解析器(Decoder)进行,即可以实现将请求参数转换为 Java 对象。
SuperMap iServer 中提供的参数解析器有 JsonDecoder 和 XMLDecoder,分别用于解析 Json 格式的参数和 XML 格式的参数。在使用 SuperMap iServer REST 服务时,参数的类型由 HTTP 请求中的 X-RequestEntity-ContentType 请求头和 X-UrlEntity-ContentType 请求头标识,分别标识请求体中的参数的类型和 URI 中的参数的类型。它们为 application/json 时,表明 HTTP 请求中的参数是按 Json 格式组织的,SuperMap iServer 就会使用 JsonDecoder 去解析参数;当为 application/xml 时,SuperMap iServer 就会使用 XMLDecoder 去解析参数。如果 X-RequestEntity-ContentType 或 X-UrlEntity-ContentType 请求头为空,服务器默认用 JsonDecoder 进行参数解析。
如果你在使用 SuperMap iServer REST 服务的时候,想要使用一种新的格式(Json、XML 之外)作为 HTTP 请求中参数的组织形式,就需要对参数解析器进行扩展。SuperMap iServer 提供了良好的参数解析器扩展机制,扩展一个参数解析器的过程可分如下两步:
在 SuperMap iServer 的 REST 实现框架中,提供了 com.supermap.services.rest.decoders.Decoder 抽象类用于参数解析器,所有的参数解析器都继承自该类。例如,在 SuperMap iServer 中已提供的 JsonDecoder、XMLDecoder 等。
用户扩展新的参数解析器,既可以继承 Decoder 抽象类,对其中的抽象方法进行实现,也可以继承现有的参数解析器类,改写其中的方法。其中,重要的方法如下表所示,详请参见 JavaDoc 文档:
方法名称 | 含义 |
createSupportedMediaTypes() | 创建该参数解析器支持的媒体类型列表。即指明能处理什么类型的参数。 |
toObject(String, Class) | 将参数字符串转换成指定类型的 Java 对象。 |
toList(String, Class) | 将参数字符串转换成指定元素类型的 java.util.List 对象。 |
toMap(String, Map<String, Class>) | 将参数字符串转换成指定类型的 java.util.Map 映射集。 |
toSet(String, Class) | 将参数字符串转换成指定元素类型的 java.util.Set 对象。 |
继承 Decoder 抽象类(或其子类),实现(或重写)上表中的方法,就可以实现一个自定义的参数解析器。
作为示例,这里仅举一个特例来说明扩展参数解析器的过程,对 distance 资源中执行 GET 请求时,对如下特定格式的自定义参数进行解析(该格式仅针对 distance 资源):
point2Ds=[(x= 23.00,y=34.00);(x= 53.55,y=12.66);(x= 73.88,y=12.6)]&unit=RADIAN
注:正常的 Json 格式的参数如下所示:
point2Ds=[{x: 23.00,y:34.00},{x: 53.55,y:12.66},{x: 73.88,y:12.6}]&unit=RADIAN
MyDecoder 的层次结构如下:
package com.supermap.sample.extendREST;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.restlet.data.MediaType;
import com.supermap.services.components.commontypes.Point2D;
import com.supermap.services.rest.decoders.Decoder;
/**
* 针对 distance 资源的扩展参数解析器类。
*
* JSON 格式的参数:point2Ds=[{x: 23.00,y:34.00},{x: 53.55,y:12.66},{x: 73.88,y:12.6}]&unit=METER
* 预期可解析参数:point2Ds=[(x:23.00,y:34.00);(x:53.55,y:12.66);(x:73.88,y:12.6)]&unit=METER
*/
public class MyDecoder extends Decoder{
@Override
protected List<MediaType> createSupportedMediaTypes() {
// TODO Auto-generated method stub
List<MediaType> mediaTypes = new ArrayList<MediaType>();
mediaTypes.add(new MediaType("application/custom"));
return mediaTypes;
}
@Override
public List toList(String text, Class elementClass) throws Exception {
List result = null;
Object obj = this.toObject(text, Point2D.class);
if(obj == null){
return null;
}
Class eleClz = obj.getClass().getComponentType();
if(eleClz .equals(Point2D.class)){
List list = new ArrayList();
Point2D[] points = (Point2D[])(obj);
for(Point2D point: points){
list.add(point);
}
result = list;
}
return result;
}
@Override
public Map<String, Object> toMap(String str, Map<String, Class> nameClassMapping) {
Map<String, Object> mapping = new HashMap() ;
try{
mapping = (Map<String, Object>)this.toObject(str, Point2D.class);
}catch(Exception e){
e.printStackTrace();
}
return mapping;
}
@Override
public Object toObject(String mytypeStr, Class targetClass) throws Exception {
if(targetClass == null){
throw new NullPointerException("param targetClass can not be null");
}
if(mytypeStr == null){
return null;
}
mytypeStr = mytypeStr.trim();
Object resultObj = null;
//转换数组。
//mytypeStr 以“[”开头,“]”结尾时,表示一个 point2Ds
if(mytypeStr.startsWith("[") && mytypeStr.endsWith("]")){
mytypeStr = mytypeStr.substring(1);
mytypeStr = mytypeStr.substring(0,mytypeStr.length() - 1);
mytypeStr = mytypeStr.trim();
if(mytypeStr .equals("")){
return new Object[0];
}else{
//数组中只有一个 Point2D
if(mytypeStr.lastIndexOf("(") == 0){
Point2D[] points = new Point2D[1];
//
Point2D point = (Point2D)this.toObject(mytypeStr, Point2D.class);
points[0] = point;
resultObj = points;
}else{
String[] objValueStrs = mytypeStr.split(";");
Point2D[] points = new Point2D[objValueStrs.length];
for(int i = 0 ; i < points.length ;i++){
String objValueStr = objValueStrs[i];
Point2D point = (Point2D)this.toObject(objValueStr, Point2D.class);
points[i] = point;
}
resultObj = points;
}
}
}else if (Enum.class.isAssignableFrom(targetClass)) {
//当 targetClass 是枚举类型时,按枚举类型解析 mytypeStr
//这里 targetClass 是 Unit 时,把 mytypeStr 转化成一个 Unit 枚举对象
resultObj = Enum.valueOf(targetClass, mytypeStr);
}else{
Map<String ,String> nameValueMapping = this.getMap(mytypeStr);
if(nameValueMapping == null){
return null;
}
if(targetClass.equals(Point2D.class)){
if(nameValueMapping.size() == 0){
return new Point2D();
}else{
Point2D pp = new Point2D();
String strX = nameValueMapping.get("x");
String strY = nameValueMapping.get("y");
try{
double x = Double.parseDouble(strX);
double y = Double.parseDouble(strY);
pp.x = x;
pp.y = y;
resultObj = pp;
}catch(NumberFormatException e){
e.printStackTrace();
}
}
}
}
return resultObj;
}
@Override
public Set toSet(String arg0, Class arg1) throws Exception {
Set result = null;
Object obj = this.toObject(arg0, Point2D.class);
if(obj == null){
return null;
}
Class eleClz = obj.getClass().getComponentType();
if(eleClz .equals(Point2D.class)){
Set set = new TreeSet();
Point2D[] points = (Point2D[])(obj);
for(Point2D point: points){
set.add(point);
}
result = set;
}
return result;
}
private Map<String ,String> getMap(String str){
Map<String, String> mapping = new HashMap<String, String>();
if(str == null){
return null;
}
str = str.trim();
if(str.startsWith("(") && str.endsWith(")")){
str = str.substring(1);
str = str.substring(0,str.length() -1);
str = str.trim();
if(str .equals("")){
return new HashMap();
}else{
String[] entryStr = str .split(",");
for(int i=0;i<entryStr.length;i++){
entryStr[i]=entryStr[i].trim();
if(entryStr[i] .indexOf(':') == -1){
continue;
}
String[] element=entryStr[i].split(":");
if(element.length != 2){
System.out.println("字符串" + entryStr + "包含多个:");
}
if(element[1]!=null){
try {
mapping.put(element[0].trim(),element[1].trim());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}else{
throw new IllegalArgumentException("字符串格式不对");
}
return mapping;
}
}
至此,一个特殊的参数解析器类就完成了。你可以在%SuperMap iServer_HOME%/samples/code/ExtendREST 下找到该实例程序的源代码。
MyDecoder 类,编译后,需要打成 Jar 包放到%SuperMap iServer_HOME%/webapps/iserver/WEB-INF/lib 目录下,这里将它打进 extendREST.jar 中。
将自定义的参数解析器配置到 REST 服务的流程跟将自定义的表述生成器配置到 REST 服务的流程类似。也是先注册成为 Bean 组件,然后再配置到单个资源或所有资源。
注册 MyDecoder 类为 Bean 组件,需要在 REST 应用配置文件中添加一个<bean/>节点,REST 应用配置文件 iserver-rest-appContext.xml 位于%SuperMap iServer_HOME%/webapps/iserver/WEB-INF 目录下。如下所示,将 MyDecoder 类注册成了 MyDecoder Bean 组件(class 的路径为 MyDecoder 类在 Jar 包中的路径):
<bean id="MyDecoder" class="com.supermap.sample.extendREST.MyDecoder"/>
配置到单个资源在资源配置文件中进行,配置到所有资源,在 iserver-rest-appContext.xml 中的<util:map id="restConfig"/>中,由 key="systemDecoders"的<entry/>节点中进行。
注:iserver-rest-resources.xml、iserver-rest-appContext.xml 位于%SuperMap iServer_HOME%/webapps/iserver/WEB-INF 目录下。
作为本文中这个特殊的参数解析器 MyEncoder,在注册成名为 MyEncoder 的 Bean 组件后,只能配置到 distance 资源。distance 资源的资源配置信息位于 %SuperMap iServer_HOME%/webapps/iserver/WEB-INF/lib/iserver-all-{version}.jar/resource/rest 下的 mappingResources.xml 中。如下所示:
<resource>
<configID>distance</configID>
<urlTemplate>/maps/{mapName}/distance</urlTemplate>
<resourceType>ArithmeticResource</resourceType>
<implementClass>com.supermap.services.rest.resources.impl.DistanceResource</implementClass>
<extensionEncoderBeanNames></extensionEncoderBeanNames>
<extensionDecoderBeanNames>MyDecoder</extensionDecoderBeanNames>
</resource>
至此,一个参数解析器的扩展就完成了。
重启 SuperMap iServer REST 服务,这时,distance 资源就能接受特定格式的参数。
需要提醒的是,本文的开篇曾经介绍过,服务器端对参数解析器的选择是基于 HTTP 请求中 X-RequestEntity-ContentType 和 X-UrlEntity-ContentType 请求头的值的。在示例代码的 createSupportedMediaTypes 方法(即加粗的部分)中我们看到,MyDecoder 参数解析器能解析的参数对应的媒体类型是“application/custom”。所以,在对 distance 资源执行 GET 请求时,需要设置请求头以标识参数的媒体类型,distance 资源中, 参数放在 URI 中,请求体中没有参数,所以设置 X-UrlEntity-ContentType 请求头的值为“application/custom”,X-RequestEntity-ContentType 请求头可以不管。 这时服务器才会选择正确的参数解析器(即 MyDecoder)进行参数(URI 中的参数)解析,从而得出正确的结果。
利用 JavaScript 编写的简单客户端程序如下(请参见“使用 REST API”中的示例介绍):
function getDistance()
{
//获取 XMLHttpRequest 对象
var commit=getcommit();
//以 json 格式的表述格式为例
var uri="http://localhost:8090/iserver/services/components-rest/rest/maps/世界地图/distance.json";
//URI 中的参数
var params1="point2Ds=[(x: 23.00,y:34.00);(x:53.55,y:12.66);(x:73.88,y:12.6)]&unit=METER";
//请求体中的参数,这里为 null
var params2=null;
commit.open("GET",uri+'?'+params1,false,"","");
//设置 X-UrlEntity-ContentType 为 application/custom,以标识 URI 中的参数的媒体类型
commit.setRequestHeader('X-UrlEntity-ContentType', 'application/custom');
commit.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
commit.send(params2);
//显示服务器端的响应内容
alert(commit.responseText);
}
效果如下,SuperMap iServer 正确解析了给定的特殊格式参数,返回正确的结果:
你可以在%SuperMap iServer_HOME%/samples/code/ExtendREST/distance 目录下找到该客户端程序的源代码。