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}