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