Test_HttpApiServer.java 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. package com.github.xiangyuecn.areacity.query;
  2. //要是编译不过,就直接删掉这个文件就好了
  3. //要是编译不过,就直接删掉这个文件就好了
  4. //要是编译不过,就直接删掉这个文件就好了
  5. import java.io.IOException;
  6. import java.net.InetSocketAddress;
  7. import java.net.URI;
  8. import java.net.URLDecoder;
  9. import java.text.SimpleDateFormat;
  10. import java.util.Date;
  11. import java.util.HashMap;
  12. import java.util.Map.Entry;
  13. import java.util.regex.Matcher;
  14. import java.util.regex.Pattern;
  15. import org.locationtech.jts.geom.Geometry;
  16. import org.locationtech.jts.io.WKTReader;
  17. import com.github.xiangyuecn.areacity.query.AreaCityQuery.Func;
  18. import com.github.xiangyuecn.areacity.query.AreaCityQuery.QueryInitInfo;
  19. import com.github.xiangyuecn.areacity.query.AreaCityQuery.QueryResult;
  20. //jre rt.jar com.sun,Eclips不允许引用:Access restriction: The type 'HttpServer' is not API
  21. //Eclips修改项目配置 Java Compiler -> Errors/Warnings -> Deprecated and restricted API,将Error的改成Warning即可
  22. import com.sun.net.httpserver.Headers;
  23. import com.sun.net.httpserver.HttpExchange;
  24. import com.sun.net.httpserver.HttpHandler;
  25. import com.sun.net.httpserver.HttpServer;
  26. /**
  27. * AreaCityQuery测试本地轻量HTTP API服务
  28. *
  29. * GitHub: https://github.com/xiangyuecn/AreaCity-Query-Geometry (github可换成gitee)
  30. * 省市区县乡镇区划边界数据: https://github.com/xiangyuecn/AreaCity-JsSpider-StatsGov (github可换成gitee)
  31. */
  32. public class Test_HttpApiServer {
  33. /** 是否允许输出大量WKT数据,默认不允许,只能输出最大20M的数据;如果要设为true,请确保没有 -Xmx300m 限制Java使用小内存 **/
  34. static public boolean AllowResponseBigWKT=false;
  35. static private String Desc;
  36. static public boolean Create(String bindIP, int bindPort) {
  37. Desc="========== 本地轻量HTTP API服务 ==========";
  38. Desc+="\n可通过 http://127.0.0.1:"+bindPort+"/ 访问本服务、文档、实例状态,提供的接口:";
  39. Desc+="\n\n - GET /queryPoint?lng=&lat=&tolerance=&returnWKTKey= 查询出包含此坐标点的所有边界图形的属性数据;lng必填经度,lat必填纬度,returnWKTKey可选要额外返回边界的wkt文本数据放到此key下。tolerance可选,距离范围容差值,单位米,比如2500相当于一个以此坐标为中心点、半径为2.5km的圆形范围,-1不限制距离;当坐标位于界线外侧(如海岸线、境界线)时QueryPoint方法将不会有边界图形能够匹配包含此坐标(就算距离只相差1cm),传了此参数后,会查询出在这个范围内和此坐标点距离最近的边界数据,并且结果属性中会额外添加PointDistance(图形与坐标的距离,单位米)、PointDistanceID(图形唯一标识符)两个值。";
  40. Desc+="\n\n - GET /queryGeometry?wkt=&returnWKTKey= 查询出和此图形(点、线、面)有交点的所有边界图形的属性数据(包括边界相交);wkt必填任意图形,returnWKTKey可选要额外返回边界的wkt文本数据放到此key下。";
  41. Desc+="\n\n - GET /readWKT?id=&pid=&deep=&extPath=&returnWKTKey= 读取边界图形的WKT文本数据;前四个参数可以组合查询或查一个参数(边界的属性中必须要有这些字段才能查询出来),id:查询指定id|unique_id的边界;pid:查询此pid下的所有边界;deep:限制只返回特定层级的数据,取值:0省1市2区想3乡镇;extPath:查询和ext_path完全相同值的边界,首尾支持*通配符(如:*武汉*);returnWKTKey回边界的wkt文本数据放到此key下,默认值polygon_wkt,填0不返回wkt文本数据;注意:默认只允许输出最大20M的WKT数据,请参考下面的注意事项。";
  42. Desc+="\n\n - GET /debugReadGeometryGridSplitsWKT?id=&pid=&deep=&extPath=&returnWKTKey= Debug读取边界网格划分图形WKT文本数据;参数和/readWKT接口一致。";
  43. Desc+="\n\n - JSON响应:{c:0, v:{...}, m:\"错误消息\"} c=0代表接口调用成功,v为内容;c=其他值调用错误,m为错误消息。";
  44. Desc+="\n\n - 指定查询实例:接口前面加/0-"+(AreaCityQuery.Instances.length-1)+"/,或使用instance=0-"+(AreaCityQuery.Instances.length-1)+"参数来指定需要调用的静态实例,默认为AreaCityQuery.Instances[0]实例;允许同时使用多个数据文件来分别初始化多个实例,然后查询时指定需要调用哪个实例。";
  45. Desc+="\n\n - 注意:所有输入坐标参数的坐标系必须和初始化时使用的geojson数据的坐标系一致,否则坐标可能会有比较大的偏移,导致查询结果不正确。";
  46. Desc+="\n\n - 注意:如果要输出大量WKT数据,请调大Java内存,不然可能是 -Xmx300m 启动的只允许使用小内存,并且修改服务源码内的AllowResponseBigWKT=true,否则只允许输出最大20M的WKT数据。";
  47. System.out.println(Desc);
  48. System.out.println();
  49. System.out.println("绑定IP: "+bindIP+", Port: "+bindPort+", 正在启动HTTP API服务...");
  50. boolean startOK=false;
  51. try {
  52. __Start(bindIP, bindPort);
  53. startOK=true;
  54. System.out.println("HTTP API服务正在运行,输入 exit 退出服务...");
  55. while(true){
  56. String inStr=Test.ReadIn().trim();
  57. if(inStr.equals("exit")) {
  58. System.out.println("bye! 已退出HTTP API服务。");
  59. System.out.println();
  60. httpServer.stop(0);
  61. httpServer=null;
  62. break;
  63. }
  64. System.out.println("如需退出HTTP API服务请输入exit");
  65. }
  66. }catch (Exception e) {
  67. e.printStackTrace();
  68. if(!startOK) {
  69. System.out.println("创建HTTP服务异常:"+e.getMessage());
  70. System.out.println();
  71. return false;
  72. }
  73. }
  74. return true;
  75. }
  76. static private void Req_queryPoint(HashMap<String, String> query, String[] response, String[] responseErr, int[] status, String[] contentType, HashMap<String, String> respHeader) throws Exception {
  77. double lng=ToNum(query.get("lng"), 999);
  78. double lat=ToNum(query.get("lat"), 999);
  79. int tolerance=(int)ToLong(query.get("tolerance"), 0);
  80. String returnWKTKey=query.get("returnWKTKey");
  81. if(lng<-180 || lat<-90 || lng>180 || lat>90) {
  82. responseErr[0]="坐标参数值无效";
  83. return;
  84. }
  85. AreaCityQuery instance=GetInstance(query, responseErr);
  86. if(instance==null) return;
  87. QueryResult res=new QueryResult();
  88. if(returnWKTKey!=null && returnWKTKey.length()>0) {
  89. res.Set_ReturnWKTKey=returnWKTKey;
  90. }
  91. if(tolerance==0) {
  92. instance.QueryPoint(lng, lat, null, res);
  93. }else {
  94. instance.QueryPointWithTolerance(lng, lat, null, res, tolerance);
  95. }
  96. response[0]=ResToJSON(res);
  97. }
  98. static private void Req_queryGeometry(HashMap<String, String> query, String[] response, String[] responseErr, int[] status, String[] contentType, HashMap<String, String> respHeader) throws Exception {
  99. String wkt=query.get("wkt");
  100. String returnWKTKey=query.get("returnWKTKey");
  101. if(wkt==null || wkt.length()==0) {
  102. responseErr[0]="wkt参数无效";
  103. return;
  104. }
  105. Geometry geom;
  106. try {
  107. geom=new WKTReader(AreaCityQuery.Factory).read(wkt);
  108. }catch (Exception e) {
  109. responseErr[0]="wkt参数解析失败:"+e.getMessage();
  110. return;
  111. }
  112. AreaCityQuery instance=GetInstance(query, responseErr);
  113. if(instance==null) return;
  114. QueryResult res=new QueryResult();
  115. if(returnWKTKey!=null && returnWKTKey.length()>0) {
  116. res.Set_ReturnWKTKey=returnWKTKey;
  117. }
  118. instance.QueryGeometry(geom, null, res);
  119. response[0]=ResToJSON(res);
  120. }
  121. static private void Req_readWKT(boolean debugReadGrid, HashMap<String, String> query, String[] response, String[] responseErr, int[] status, String[] contentType, HashMap<String, String> respHeader) throws Exception {
  122. long id=ToLong(query.get("id"), -1);
  123. long pid=ToLong(query.get("pid"), -1);
  124. long deep=ToLong(query.get("deep"), -1);
  125. String extPath=query.get("extPath"); if(extPath==null) extPath="";
  126. String returnWKTKey=query.get("returnWKTKey");
  127. if(id==-1 && pid==-1 && deep==-1 && extPath.length()==0) {
  128. responseErr[0]="请求参数无效";
  129. return;
  130. }
  131. if(returnWKTKey==null || returnWKTKey.length()==0) {
  132. returnWKTKey="polygon_wkt";
  133. }
  134. if("0".equals(returnWKTKey)) {
  135. returnWKTKey=null;
  136. }
  137. AreaCityQuery instance=GetInstance(query, responseErr);
  138. if(instance==null) return;
  139. String exp=extPath;
  140. if(extPath!=null && extPath.length()>0) {
  141. if(exp.equals("*")) {
  142. exp="";
  143. } else {
  144. if(exp.startsWith("*")) {
  145. exp=exp.substring(1);
  146. }else {
  147. exp="\""+exp;
  148. }
  149. if(exp.endsWith("*")) {
  150. exp=exp.substring(0, exp.length()-1);
  151. }else {
  152. exp=exp+"\"";
  153. }
  154. }
  155. }
  156. String exp_=exp;
  157. String extPath_=extPath;
  158. int[] readCount=new int[] {0};
  159. boolean[] isWktSizeErr=new boolean[] {false};
  160. int[] wktSize=new int[] {0};
  161. Func<String, Boolean> where=new Func<String, Boolean>() {
  162. @Override
  163. public Boolean Exec(String prop) throws Exception {
  164. String prop2=(","+prop.substring(1, prop.length()-1)+",").replace("\"", "").replace(" ", ""); //不解析json,简单处理
  165. if(id!=-1) {
  166. if(!prop2.contains(",id:"+id+",") && !prop2.contains(",unique_id:"+id+",")) {
  167. return false;
  168. }
  169. }
  170. if(pid!=-1) {
  171. if(!prop2.contains(",pid:"+pid+",")) {
  172. return false;
  173. }
  174. }
  175. if(deep!=-1) {
  176. if(!prop2.contains(",deep:"+deep+",")) {
  177. return false;
  178. }
  179. }
  180. if(extPath_.length()>0) {
  181. int i0=prop.indexOf("ext_path");
  182. if(i0==-1)return false;
  183. int i1=prop.indexOf(",", i0);
  184. if(i1==-1)i1=prop.length();
  185. if(!prop.substring(i0+9, i1).contains(exp_)) {
  186. return false;
  187. }
  188. }
  189. readCount[0]++;
  190. if(isWktSizeErr[0]) {
  191. return false;
  192. }
  193. return true;
  194. }
  195. };
  196. Func<String[], Boolean> onFind=new Func<String[], Boolean>() {
  197. @Override
  198. public Boolean Exec(String[] val) throws Exception {
  199. wktSize[0]+=val[1].length();
  200. if(!AllowResponseBigWKT && wktSize[0]>20*1024*1024) {
  201. isWktSizeErr[0]=true;
  202. return false;
  203. }
  204. return true;
  205. }
  206. };
  207. QueryResult res;
  208. if(debugReadGrid) {
  209. res=instance.Debug_ReadGeometryGridSplitsWKT(returnWKTKey, null, where, onFind);
  210. } else {
  211. res=instance.ReadWKT_FromWkbsFile(returnWKTKey, null, where, onFind);
  212. }
  213. if(isWktSizeErr[0]) {
  214. responseErr[0]="已匹配到"+readCount[0]+"条数据,但WKT数据量超过20M限制,可修改服务源码内的AllowResponseBigWKT=true来解除限制";
  215. return;
  216. }
  217. response[0]=ResToJSON(res);
  218. }
  219. static private AreaCityQuery GetInstance(HashMap<String, String> query, String[] responseErr) {
  220. int idx=(int)ToLong(query.get("instance"), 0);
  221. if(idx<0 || idx>=AreaCityQuery.Instances.length) {
  222. responseErr[0]="AreaCityQuery实例值"+idx+"无效";
  223. return null;
  224. }
  225. AreaCityQuery val=AreaCityQuery.Instances[idx];
  226. try {
  227. val.CheckInitIsOK();
  228. }catch(Exception e) {
  229. responseErr[0]="AreaCityQuery实例"+idx+"未初始化完成:"+e.getMessage();
  230. return null;
  231. }
  232. return val;
  233. }
  234. static private String ResToJSON(QueryResult res) {
  235. StringBuilder json=new StringBuilder();
  236. json.append("{\"list\":[");//手撸json
  237. for(int i=0,L=res.Result.size();i<L;i++) {
  238. if(i>0) json.append(",");
  239. json.append(res.Result.get(i));
  240. res.Result.set(i, null);//已读取了结果就释放掉内存
  241. }
  242. json.append("]}");
  243. return json.toString();
  244. }
  245. static private String StringInnerJson(String str) {
  246. if (str==null || str.length()==0) {
  247. return "";
  248. }
  249. int len = str.length();
  250. StringBuilder sb = new StringBuilder(len * 2);
  251. char chr;
  252. for (int i = 0; i < len; i++) {
  253. chr = str.charAt(i);
  254. switch (chr) {
  255. case '"':
  256. sb.append('\\').append('"'); break;
  257. case '\\':
  258. sb.append('\\').append('\\'); break;
  259. case '\n':
  260. sb.append('\\').append('n'); break;
  261. case '\r':
  262. sb.append('\\').append('r'); break;
  263. default:
  264. sb.append(chr);
  265. break;
  266. }
  267. }
  268. return sb.toString();
  269. }
  270. static private long ToLong(String val, long def) {
  271. if(val==null || val.length()==0) {
  272. return def;
  273. }
  274. try {
  275. return Long.parseLong(val);
  276. }catch (Exception e) {
  277. return def;
  278. }
  279. }
  280. static private double ToNum(String val, double def) {
  281. if(val==null || val.length()==0) {
  282. return def;
  283. }
  284. try {
  285. return Double.parseDouble(val);
  286. }catch (Exception e) {
  287. return def;
  288. }
  289. }
  290. static private HttpServer httpServer;
  291. static private Pattern Exp_PathInstance=Pattern.compile("^/(\\d+)(/.+)");
  292. static private void __Start(String bindIP, int bindPort) throws Exception {
  293. if(httpServer!=null) {
  294. try {
  295. httpServer.stop(0);
  296. }catch(Exception e) {}
  297. }
  298. Func<HttpExchange, Object> fn=new Func<HttpExchange, Object>() {
  299. @Override
  300. public Object Exec(HttpExchange context) throws Exception {
  301. URI url=context.getRequestURI();
  302. String path=url.getPath(); if(path==null||path.length()==0)path="/";
  303. String queryStr=url.getQuery(); if(queryStr==null) queryStr="";
  304. String method=context.getRequestMethod(); if(method==null)method="";
  305. method=method.toUpperCase();
  306. HashMap<String, String> query=new HashMap<>();
  307. String apiPath=path;
  308. Matcher m=Exp_PathInstance.matcher(apiPath);
  309. if(m.find()) {
  310. apiPath=m.group(2);
  311. query.put("instance", m.group(1));
  312. }
  313. String[] queryArr=queryStr.split("&");
  314. for(String s : queryArr) {
  315. if(s.length()>0) {
  316. String[] kv=s.split("=");
  317. if(kv.length==2) {
  318. query.put(kv[0], URLDecoder.decode(kv[1], "utf-8"));
  319. }
  320. }
  321. }
  322. int[] status=new int[] { 200 };
  323. String[] contentType=new String[] {"text/json; charset=utf-8"};
  324. HashMap<String, String> respHeader=new HashMap<>();
  325. respHeader.put("Access-Control-Allow-Origin", "*");
  326. boolean isApi=true, isHtml=false;
  327. String[] response=new String[] { "" };
  328. String[] responseErr=new String[] { "" };
  329. try {
  330. if(!method.equals("POST") && !method.equals("GET")) {
  331. isApi=false; isHtml=true;
  332. response[0]="Method: "+method;
  333. } else if(apiPath.equals("/queryPoint")){
  334. Req_queryPoint(query, response, responseErr, status, contentType, respHeader);
  335. } else if (apiPath.equals("/queryGeometry")) {
  336. Req_queryGeometry(query, response, responseErr, status, contentType, respHeader);
  337. } else if (apiPath.equals("/readWKT")) {
  338. Req_readWKT(false, query, response, responseErr, status, contentType, respHeader);
  339. } else if (apiPath.equals("/debugReadGeometryGridSplitsWKT")) {
  340. Req_readWKT(true, query, response, responseErr, status, contentType, respHeader);
  341. } else if (path.equals("/")) {
  342. isApi=false; isHtml=true;
  343. String html="\n\n\n\n【请求IP】\n"+context.getRemoteAddress().getAddress().getHostAddress();
  344. if(context.getRemoteAddress().getAddress().isLoopbackAddress()) {
  345. html+="\n\n【静态实例列表】仅服务器本地访问可见";
  346. for(int i=0;i<AreaCityQuery.Instances.length;i++) {
  347. AreaCityQuery item=AreaCityQuery.Instances[i];
  348. if(item.GetInitStatus()==2) {
  349. QueryInitInfo info=item.GetInitInfo();
  350. html+="\n实例"+i+": Instances["+i+"] "+(item.IsStoreInMemory()?"Init_StoreInMemory":"Init_StoreInWkbsFile");
  351. html+="\n Geometry "+info.GeometryCount+" 个(Grid切分Polygon "+info.PolygonCount+" 个)";
  352. html+="\n Data文件: "+info.FilePath_Data;
  353. html+="\n Wkbs文件: "+info.FilePath_SaveWkbs;
  354. }
  355. }
  356. }
  357. html+="\n\n==========";
  358. response[0]="<h1>AreaCityQuery HttpApiServer Running!</h1>"
  359. +"\n<pre style='word-break:break-all;white-space:pre-wrap'>\n"
  360. +Desc+html+"\n</pre>";
  361. } else {
  362. isApi=false; isHtml=true;
  363. status[0]=404;
  364. response[0]="<h1>请求路径 "+path+" 不存在!</h1>";
  365. }
  366. } catch (Throwable e) {
  367. e.printStackTrace();
  368. if(e instanceof OutOfMemoryError) {
  369. System.gc();
  370. }
  371. responseErr[0]="接口调用异常:"+e.getMessage();
  372. }
  373. if(isApi) {
  374. if(responseErr[0].length()>0) {//手撸json
  375. response[0]="{\"c\":1,\"v\":null,\"m\":\""+StringInnerJson(responseErr[0])+"\"}";
  376. } else {
  377. response[0]="{\"c\":0,\"v\":"+response[0]+",\"m\":\"\"}";
  378. }
  379. }
  380. if(isHtml) {
  381. contentType[0]="text/html; charset=utf-8";
  382. }
  383. respHeader.put("Content-Type", contentType[0]);
  384. Headers header=context.getResponseHeaders();
  385. for(Entry<String, String> kv : respHeader.entrySet()) {
  386. header.set(kv.getKey(), kv.getValue());
  387. }
  388. byte[] sendData=response[0].getBytes("utf-8");
  389. context.sendResponseHeaders(status[0], sendData.length);
  390. context.getResponseBody().write(sendData);
  391. StringBuilder log=new StringBuilder();
  392. log.append("["+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())+"]");
  393. log.append(status[0]);
  394. log.append(" "+method);
  395. log.append(" "+path);
  396. if(queryStr.length()>0) {
  397. log.append("?"+queryStr);
  398. }
  399. log.append(" "+sendData.length);
  400. System.out.println(log);
  401. return null;
  402. }
  403. };
  404. // https://www.apiref.com/java11-zh/jdk.httpserver/com/sun/net/httpserver/HttpServer.html
  405. httpServer = HttpServer.create(new InetSocketAddress(bindIP, bindPort), 0);
  406. httpServer.createContext("/", new HttpHandler() {
  407. @Override
  408. public void handle(HttpExchange context) throws IOException {
  409. try {
  410. fn.Exec(context);
  411. } catch (Throwable e) {
  412. e.printStackTrace();
  413. if(e instanceof OutOfMemoryError) {
  414. System.gc();
  415. }
  416. }
  417. context.close();
  418. }
  419. });
  420. httpServer.start();
  421. }
  422. }