001package gu.dtalk.client; 002 003import java.io.File; 004import java.io.IOException; 005import java.util.Scanner; 006 007import org.slf4j.Logger; 008import org.slf4j.LoggerFactory; 009 010import com.alibaba.fastjson.JSONObject; 011import com.google.common.base.MoreObjects; 012import com.google.common.base.Predicate; 013import com.google.common.base.Strings; 014import com.google.common.base.Throwables; 015import com.google.common.net.HostAndPort; 016 017import gu.dtalk.CmdItem; 018import gu.dtalk.BaseItem; 019import gu.dtalk.BaseOption; 020import gu.dtalk.ItemType; 021import gu.dtalk.MenuItem; 022import gu.dtalk.redis.RedisConfigType; 023import gu.dtalk.Ack; 024import gu.dtalk.Ack.Status; 025import gu.simplemq.Channel; 026import gu.simplemq.redis.JedisPoolLazy; 027import gu.simplemq.redis.RedisFactory; 028import gu.simplemq.redis.RedisPublisher; 029import gu.simplemq.redis.RedisSubscriber; 030import net.gdface.utils.BinaryUtils; 031import net.gdface.utils.NetworkUtil; 032import static gu.dtalk.CommonConstant.*; 033import static gu.dtalk.CommonUtils.*; 034import static com.google.common.base.Preconditions.*; 035 036/** 037 * 字符终端实现基类 038 * @author guyadong 039 * 040 */ 041public abstract class BaseConsole { 042 protected static final Logger logger = LoggerFactory.getLogger(BaseConsole.class); 043 final RedisSubscriber subscriber; 044 final RedisPublisher publisher; 045 /** 046 * 请求频道名,用于终端向设备端发送菜单命令(item)请求 047 * 这个频道名,在与设备端成功连接后,由设备端提供 048 */ 049 protected String reqChannel = null; 050 /** 051 * 终端的MAC地址 052 */ 053 protected final byte[] temminalMac; 054 /** 055 * 响应频道名,用于终端接收设备端的响应消息 056 * 这是个与终端MAC地址相关的常量,设备端只要知道终端的MAC就能得到它的响应频道名 057 */ 058 private final String ackchname; 059 /** 060 * 连接频道名,用于终端向设备端发送连接请求 061 * 这是个与设备端MAC地址相关的常量,终端只要知道设备端的MAC就能得到它的连接频道名 062 */ 063 protected final String connchname; 064 private final RenderEngine renderEngine = new RenderEngine(); 065 private final Channel<JSONObject> ackChannel; 066 /** 067 * 出错时是否显示详细调用堆栈 068 */ 069 private boolean stackTrace = false; 070 /** 071 * 构造方法 072 * @param devmac 要连接的设备MAC地址,测试设备程序在本地运行时可为空。 073 * @param config redis连接配置类型 074 */ 075 public BaseConsole(String devmac, RedisConfigType config) { 076 JedisPoolLazy pool = JedisPoolLazy.getInstance(config.readRedisParam(),false); 077 subscriber = RedisFactory.getSubscriber(pool); 078 publisher = RedisFactory.getPublisher(pool); 079 temminalMac = getSelfMac(config); 080 System.out.printf("TERMINAL MAC address: %s\n", NetworkUtil.formatMac(temminalMac, ":")); 081 082 ackchname = getAckChannel(temminalMac); 083 ConnectorAdapter msgAdapter = new ConnectorAdapter().setOnValidPwd(new Predicate<String>() { 084 085 @Override 086 public boolean apply(String input) { 087 reqChannel = input; 088 ackChannel.setAdapter(renderEngine); 089 return false; 090 } 091 }); 092 ackChannel = new Channel<JSONObject>( ackchname, JSONObject.class).setAdapter(msgAdapter); 093 094 if(Strings.isNullOrEmpty(devmac)){ 095 // 使用本地地址做为设备MAC地址 096 devmac = BinaryUtils.toHex(temminalMac); 097 System.out.println("use local MAC for target DEVICE"); 098 } 099 System.out.printf("DEVICE MAC address: %s\n", devmac); 100 101 connchname = getConnChannel(devmac); 102 103 } 104 protected static byte[] getSelfMac(RedisConfigType type){ 105 try { 106 HostAndPort hostAndPort = type.getHostAndPort(); 107 String host = hostAndPort.getHost(); 108 int port = hostAndPort.getPort(); 109 // 使用localhost获取本机MAC地址会返回空数组,所以这里使用一个互联地址来获取 110 if(host.equals("127.0.0.1") || host.equalsIgnoreCase("localhost")){ 111 return NetworkUtil.getCurrentMac("www.cnnic.net.cn", 80); 112 } 113 return NetworkUtil.getCurrentMac(host, port); 114 } catch (IOException e) { 115 throw new RuntimeException(e); 116 } 117 } 118 /** 119 * 尝试连接目标设备 120 */ 121 public void connect() { 122 123 subscriber.register(ackChannel); 124 125 } 126 protected static String scanLine(Predicate<String>validate){ 127 Scanner scaner = new Scanner(System.in); 128 try{ 129 130 return scanLine(validate,scaner); 131 }finally { 132 //scaner.close(); 133 } 134 } 135 private static String scanLine(Predicate<String>validate,Scanner scaner){ 136 scaner.reset(); 137 scaner.useDelimiter("\r?\n"); 138 while (scaner.hasNextLine()) { 139 String str = scaner.next(); 140 if(str.isEmpty()){ 141 return ""; 142 } 143 try{ 144 if(validate.apply(str)){ 145 return str; 146 } 147 }catch (Exception e) { 148 System.out.println(e.getMessage()); 149 } 150 } 151 return ""; 152 } 153 154 /** 155 * 输入目标设备的MAC地址 156 * @return MAC地址 157 */ 158 protected static String inputMac(){ 159 System.out.println("Input MAC address of Device,such as '00:00:7f:2a:39:4A' or '00e8992730FF':" 160 + "(input empty string if target device demo running on localhost)" 161 ); 162 return scanLine(new Predicate<String>() { 163 @Override 164 public boolean apply(String input) { 165 String mac = parseMac(input); 166 if(!mac.isEmpty()){ 167 return true; 168 } 169 System.out.println("ERROR:Invalid mac adress"); 170 return false; 171 } 172 }); 173 174 } 175 private void waitResp(long timestamp){ 176 int waitCount = 30; 177 TextMessageAdapter<?> adapter = (TextMessageAdapter<?>) ackChannel.getAdapter(); 178 while(adapter.getLastResp() < timestamp && waitCount > 0){ 179 try { 180 Thread.sleep(100); 181 waitCount --; 182 } catch (InterruptedException e) { 183 System.exit(-1); 184 } 185 } 186 if(waitCount ==0 ){ 187 System.out.println("TIMEOUT for response"); 188 System.exit(-1); 189 } 190 } 191 private JSONObject makeItemJSON(String path){ 192 checkArgument(!Strings.isNullOrEmpty(path)); 193 JSONObject json = new JSONObject(); 194 if(path.equals("/")){ 195 json.fluentPut(ITEM_FIELD_PATH, path) 196 .fluentPut(ITEM_FIELD_CATALOG, ItemType.MENU); 197 }else{ 198 BaseItem item; 199 BaseItem currentLevel = checkNotNull(renderEngine.getCurrentLevel(),"currentLevel is null"); 200 if(".".equals(path)){ 201 // 如果没有根据path找到对应的item则抛出异常 202 item = currentLevel; 203 path = item.getPath(); 204 }else{ 205 // 如果没有根据path找到对应的item则抛出异常 206 item = checkNotNull(currentLevel.getChildByPath(path),"NOT FOUND item %s",path); 207 } 208 json.fluentPut(ITEM_FIELD_PATH,path) 209 .fluentPut(ITEM_FIELD_CATALOG, item.getCatalog()); 210 } 211 212 return json; 213 } 214 protected <T>boolean syncPublish(Channel<T>channel,T json){ 215 try{ 216 long timestamp = System.currentTimeMillis(); 217 long rc = publisher.publish(channel, json); 218 // 没有接收端则抛出异常 219 checkState(rc != 0,"target device DISCONNECT"); 220 waitResp(timestamp); 221 return true; 222 }catch(Exception e){ 223 System.out.println(e.getMessage()); 224 System.exit(0); 225 } 226 return false; 227 } 228 private boolean syncPublishReq(Object json){ 229 Channel<Object> reqCh = new Channel<Object>(checkNotNull(reqChannel), Object.class); 230 return syncPublish(reqCh, json); 231 } 232 233 /** 234 * 接受键盘输入选项内容 235 * @param scaner 236 * @param json 237 * @return 输入不为空返回true,否则返回false 238 */ 239 private boolean inputOption(Scanner scaner,final JSONObject json){ 240 checkArgument(json !=null && ItemType.OPTION == json.getObject(ITEM_FIELD_CATALOG, ItemType.class)); 241 BaseItem item = renderEngine.getCurrentLevel().getChildByPath(json.getString(ITEM_FIELD_PATH)); 242 checkArgument(item instanceof BaseOption<?>); 243 BaseOption<?> option = (BaseOption<?>)item; 244 String desc = Strings.isNullOrEmpty(option.getDescription()) ? "" : "("+option.getDescription()+")"; 245 // 显示提示信息 246 System.out.printf("INPUT VALUE for %s(%s)%s(input empty for skip):",option.getUiName(),option.getName(),desc); 247 String value = scanLine(new Predicate<String>() { 248 249 @Override 250 public boolean apply(String input) { 251 if(isImage(json,renderEngine.getCurrentLevel())){ 252 try { 253 json.fluentPut(OPTION_FIELD_VALUE, BinaryUtils.getBytesNotEmpty(new File(input))); 254 } catch (Exception e) { 255 Throwables.throwIfUnchecked(e); 256 throw new RuntimeException(e); 257 } 258 }else{ 259 json.fluentPut(OPTION_FIELD_VALUE, input); 260 } 261 return true; 262 } 263 }, scaner); 264 return !value.isEmpty(); 265 } 266 private boolean inputCmd(Scanner scaner,JSONObject json){ 267 checkArgument(json !=null && ItemType.CMD == json.getObject(ITEM_FIELD_CATALOG, ItemType.class)); 268 BaseItem item = renderEngine.getCurrentLevel().getChildByPath(json.getString(ITEM_FIELD_PATH)); 269 checkArgument(item instanceof CmdItem); 270 CmdItem cmd = (CmdItem)item; 271 for(BaseOption<?> param:cmd.getParameters()){ 272 JSONObject optjson = makeItemJSON(param.getPath()); 273 while(inputOption(scaner,optjson)){ 274 if(syncPublishReq(optjson)){ 275 checkState(isAck(renderEngine.getLastRespObj())); 276 Status status = ((JSONObject)renderEngine.getLastRespObj()).getObject(ACK_FIELD_STATUS, Status.class); 277 if(status != Status.OK){ 278 // 参数值无效,继续提示输入 279 continue; 280 } 281 break; 282 } 283 return false; 284 } 285 // 继续下一个参数 286 } 287 return true; 288 } 289 /** 290 * 键盘命令交互 291 */ 292 protected void cmdInteractive(){ 293 294 // 第一次进入发送命令显示根菜单 295 if(!syncPublishReq(makeItemJSON("/"))){ 296 return ; 297 } 298 Scanner scaner = new Scanner(System.in); 299 try{ 300 while (scaner.hasNextLine()) { 301 String str = scaner.next(); 302 if(str.isEmpty()){ 303 continue; 304 } 305 try{ 306 JSONObject json = makeItemJSON(str); 307 switch (json.getObject(ITEM_FIELD_CATALOG,ItemType.class)) { 308 case MENU: 309 // 进入菜单 310 syncPublishReq(json); 311 break; 312 case OPTION:{ 313 Ack<?> ack=null; 314 // 修改参数 315 do{ 316 if(inputOption(scaner,json)){ 317 syncPublishReq(json); 318 }else{ 319 // 输入空行则返回 320 break; 321 } 322 // 获取响应消息内容,如果输入响应错误则提示继续 323 ack = renderEngine.getLastAck(); 324 325 }while(ack != null && !Status.OK.equals(ack.getStatus())); 326 // 刷新当前菜单 327 syncPublishReq(makeItemJSON(renderEngine.getCurrentLevel().getPath())); 328 break; 329 } 330 case CMD:{ 331 // 执行命令前先保存当前菜单,因为执行命令后当前菜单会变化, 332 // 而isQuit需要的参数是执行命令前的菜单位置 333 MenuItem lastLevel = renderEngine.getCurrentLevel(); 334 // 执行命令 335 if(inputCmd(scaner,json)){ 336 syncPublishReq(json); 337 }else{ 338 // 输入空行则返回 339 break; 340 } 341 if(isQuit(json,lastLevel)){ 342 return; 343 } 344 345 break; 346 } 347 default: 348 break; 349 } 350 }catch (Exception e) { 351 System.out.println(e.getMessage()); 352 } 353 } 354 }finally { 355 scaner.close(); 356 } 357 return; 358 } 359 protected static String parseMac(String input){ 360 input = MoreObjects.firstNonNull(input, "").trim(); 361 if(input.matches(MAC_REG)){ 362 return input.replace(":", "").toLowerCase(); 363 } 364 return ""; 365 } 366 protected void waitTextRenderEngine(){ 367 int waitCount = 30; 368 TextMessageAdapter<?> adapter = (TextMessageAdapter<?>) ackChannel.getAdapter(); 369 while( !(adapter instanceof RenderEngine) && waitCount > 0){ 370 try { 371 Thread.sleep(100); 372 waitCount --; 373 } catch (InterruptedException e) { 374 System.exit(-1); 375 } 376 } 377 if(waitCount ==0 ){ 378 System.out.println("TIMEOUT for response"); 379 System.exit(-1); 380 } 381 } 382 /** 383 * 启动终端 384 */ 385 public void start(){ 386 try{ 387 Channel<String> testch = new Channel<String>(connchname, String.class); 388 long rc = publisher.publish(testch, "\"hello,dtalk\""); 389 // 目标设备没有上线 390 checkState(rc != 0,"TARGET DEVICE NOT online"); 391 if(rc>1){ 392 // 有两个设备侦听同一个连接频道 393 System.out.println("WARN:DUPLICATED TARGET DEVICE WITH same MAC address"); 394 } 395 connect(); 396 if(authorize()){ 397 waitTextRenderEngine(); 398 cmdInteractive(); 399 } 400 }catch (Exception e) { 401 if(stackTrace){ 402 logger.error(e.getMessage(),e); 403 }else{ 404 System.out.println(e.getMessage()); 405 } 406 return ; 407 } 408 } 409 /** 410 * 安全验证, 411 * 用于实现连接dtalk引擎的安全验证过程 412 * @return 验证通过返回{@code true},否则返回{@code false} 413 */ 414 protected abstract boolean authorize(); 415 /** 416 * @param stackTrace 要设置的 stackTrace 417 * @return 当前对象 418 */ 419 public BaseConsole setStackTrace(boolean stackTrace) { 420 this.stackTrace = stackTrace; 421 return this; 422 } 423}