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}