001package gu.dtalk;
002
003import com.alibaba.fastjson.JSON;
004import com.alibaba.fastjson.annotation.JSONField;
005import com.google.common.base.Function;
006import com.google.common.base.Joiner;
007import com.google.common.base.MoreObjects;
008import com.google.common.base.Strings;
009import com.google.common.collect.Lists;
010import com.google.common.collect.Maps;
011
012import static com.google.common.base.Preconditions.*;
013import java.util.ArrayList;
014import java.util.Arrays;
015import java.util.Collection;
016import java.util.Collections;
017import java.util.LinkedHashMap;
018import java.util.List;
019import java.util.Map;
020
021/**
022 * 菜单选项抽象类<br>
023 * 所有选项的基类
024 * @author guyadong
025 *
026 */
027public abstract class BaseItem{
028        
029        /**
030         * 条目名称([a-zA-Z0-9_],不允许有空格)
031         */
032        private String name;
033        /**
034         * 条目的界面显示名称,如果不指定则使用{@link #name}
035         */
036        private String uiName;
037        /**
038         * 当前对象父节点
039         */
040        @JSONField(serialize = false,deserialize = false)
041        private BaseItem parent;
042        /**
043         * 当前对象在整个菜单树形结构中的全路径
044         */
045        private String path = null;
046        /**
047         * 当前条目是否禁用
048         */
049        private boolean disable=false;
050        /**
051         * 对当前条目的说明文字
052         */
053        @JSONField(deserialize = false)
054        private String description = "";
055        /**
056         * 当前条目下的子条目
057         */
058        protected final LinkedHashMap<String,BaseItem> items = new LinkedHashMap<>();
059        /**
060         * 从{@link BaseItem}对象中返回条目路径的转换器
061         */
062        private static final Function<BaseItem,String> PATH_FUN = new Function<BaseItem,String>(){
063                @Override
064                public String apply(BaseItem input) {
065                        return input.getPath();
066                }};
067        public BaseItem() {
068        }
069        /**
070         * @return 条目名称
071         */
072        public String getName() {               
073                return name;
074        }
075        /**
076         * @param name 允许的字符[a-zA-Z0-9_],不允许有空格
077         * @return 
078         */
079        public BaseItem setName(String name) {
080                name = checkNotNull(name,"name is null").trim();
081                checkArgument(name.isEmpty() || name.matches("^[a-zA-Z]\\w+$"),
082                                "invalid option name '%s',allow character:[a-zA-Z0-9_],not space char allowed,start with alphabet",name);
083                this.name = name;
084                return this;
085        }
086
087        /**
088         * 返回父结点
089         * @return
090         */
091        public BaseItem getParent() {
092                return parent;
093        }
094        /**
095         * 检查循环引用, 如果为循环引用则抛出异常
096         */
097        private void checkCycleRef(){
098                BaseItem node = this;
099                while(node.parent != null){
100                        checkState(node.parent != this, "CYCLE REFERENCE");
101                        node = node.parent;
102                }
103        }
104        /**
105         * 设置父结点
106         * @param parent
107         * @return
108         */
109        BaseItem setParent(BaseItem parent) {
110                checkArgument(parent ==null || parent.isContainer(),"INVALID parent");
111                checkArgument(parent == null || !parent.getChilds().contains(this),"DUPLICATE element in parent [%s]",this.getName());
112                this.parent = parent;
113                checkCycleRef();
114                refreshPath();
115                return this;
116        }
117        /**
118         * @return  是否为容器(可包含item)
119         */
120        public abstract boolean isContainer();
121        /**
122         * @return 返回item分类类型
123         */
124        public abstract ItemType getCatalog();
125        /**
126         * 生成能对象在菜单中全路径名
127         * @param indexInstead 是否用索引值代替名字
128         * @return 全路径名
129         */
130        private String createPath(boolean indexInstead){
131                List<String> list = new ArrayList<>();
132                for(BaseItem item = this; item.parent !=null ; item = (BaseItem) item.parent){
133                        if(indexInstead){
134                                list.add(Integer.toString(parent.getChilds().indexOf(item)));
135                        }else{
136                                list.add(item.getName());
137                        }
138                }
139                return "/" + Joiner.on('/').join(Lists.reverse(list));
140        }
141        /**
142         * 重新计算当前条目及子条目的路径
143         */
144        private void refreshPath(){             
145                this.path = createPath(false);
146                for(BaseItem child:items.values()){
147                        child.refreshPath();
148                }
149        }
150        /**
151         * 路径名归一化,以'/'开始,不以'/'结尾
152         * @param path
153         * @return
154         */
155        private String normalizePath(String path){
156                path = MoreObjects.firstNonNull(path, "").trim();
157                if(path.length()>1 ){
158                        if(!path.startsWith("/")){
159                                path = "/" + path;
160                        }
161                        if(path.endsWith("/")){
162                                path = path.substring(0, path.length()-1);                                      
163                        }
164                }
165                return path;
166        }
167        /**
168         * @return 当前对象在整个菜单树形结构中的全路径
169         */
170        public String getPath() {
171                if(path == null){
172                        refreshPath();
173                }
174                return path;
175        }
176        /**
177         * 设置当前对象在整个菜单树形结构中的全路径
178         * @param path
179         * @return 当前对象
180         */
181        public BaseItem setPath(String path) {
182                this.path = normalizePath(path);
183                return this;
184        }
185        /**
186         * @return 当前条目是否禁用
187         */
188        public boolean isDisable() {
189                return disable;
190        }
191        /**
192         * 设置当前条目是否禁用
193         * @param disable
194         * @return 当前对象
195         */
196        public BaseItem setDisable(boolean disable) {
197                this.disable = disable;
198                return this;
199        }
200        /**
201         * @return 对当前条目的说明文字
202
203         */
204        public String getDescription() {
205                return description;
206        }
207        /**
208         * 设置对当前条目的说明文字
209         * @param description
210         * @return 当前对象
211         */
212        public BaseItem setDescription(String description) {
213                this.description = description;
214                return this;
215        }
216        /**
217         * @return 条目的界面显示名称
218         */
219        public String getUiName() {
220                return Strings.isNullOrEmpty(uiName) ? name : uiName;
221        }
222        /**
223         * 设置条目的界面显示名称
224         * @param uiName
225         * @return 当前对象
226         */
227        public BaseItem setUiName(String uiName) {
228                this.uiName = uiName;
229                return this;
230        }
231        public String json(){
232                return JSON.toJSONString(this);
233        }
234        @Override
235        public String toString() {
236                return json();
237        }
238        @Override
239        public int hashCode() {
240                final int prime = 31;
241                int result = 1;
242                result = prime * result + ((name == null) ? 0 : name.hashCode());
243                return result;
244        }
245        @Override
246        public boolean equals(Object obj) {
247                if (this == obj)
248                        return true;
249                if (obj == null)
250                        return false;
251                if (!(obj instanceof BaseItem))
252                        return false;
253                BaseItem other = (BaseItem) obj;
254                if (name == null) {
255                        if (other.name != null)
256                                return false;
257                } else if (!name.equals(other.name))
258                        return false;
259                return true;
260        }
261        /**
262         * 返回{@code path}指定的路径查找当前对象下的子条目<br>
263         * @param path
264         * @return 返回子条目,没有找到返回{@code null}
265         */
266        public BaseItem getChildByPath(String path){
267                path = MoreObjects.firstNonNull(path, "").trim();
268                String relpath = path;
269                if(path.startsWith("/")){
270                        if(path.startsWith(getPath())){
271                                relpath = path.substring(getPath().length());
272                        }else{
273                                String inxpath = createPath(true); 
274                                if(path.startsWith(inxpath)){
275                                        relpath = path.substring(inxpath.length());
276                                }else{
277                                        return null;
278                                }
279                        }
280                }
281                if(relpath.isEmpty()){
282                        return null;
283                }
284                if(relpath.startsWith("/")){
285                        relpath = relpath.substring(1);
286                }
287                if(relpath.endsWith("/")){
288                        relpath = relpath.substring(0,relpath.length()-1);
289                }
290                String[] nodes = relpath.split("/");
291                BaseItem child = this;
292                for(String node:nodes){
293                        child = child.getChild(node);
294                        if(child == null){
295                                return null;
296                        }
297                }
298                return child;
299        
300        }
301        /**
302         * 根据{@code path}指定的路径查找对象,
303         * 先在当前对象中查找,如果找不到,从根结点查找
304         * @param path
305         * @return 返回找到的{@link BaseItem},找不到返回{@code null}
306         */
307        public BaseItem find(String path){
308                // 当前对象查找
309                BaseItem child = getChildByPath(path);
310                if (child !=null) {
311                        return child;
312                }
313                BaseItem root = this;
314                for(;root.getParent() != null;root = root.getParent()){}
315                // 从根菜单查找
316                return root.getPath().equals(path) ? this : root.getChildByPath(path);
317        }
318        /**
319         * 根据{@code path}指定的路径查找对象,
320         * 与{@link #find(String)}基本相同,只是当找不到指定的对象时抛出异常
321         * @param path
322         * @return 返回找到的{@link BaseItem}对象
323         * @throws IllegalArgumentException 没找到指定的对象
324         */
325        public BaseItem findChecked(String path){
326                return checkNotNull(find(path),"NOT FOUND ITEM [%s]",path);             
327        }
328        /**
329         * 根据path指定的路径查找menu对象, 先在当前对象中查找,如果找不到,从根结点查找
330         * @param path
331         * @return 返回找到的{@link CmdItem}对象,找不到返回null
332         */
333        public MenuItem findMenu(String path){
334                BaseItem item = find(path);
335                return (item instanceof MenuItem) ? (MenuItem)item : null;
336        }
337        /**
338         * 根据path指定的路径查找menu对象, 与{@link #findCmd(String)}基本相同,只是当找不到指定的对象时抛出异常
339         * @param path
340         * @return 返回找到的{@link MenuItem}对象
341         * @throws IllegalArgumentException 没找到指定的对象
342         */
343        public MenuItem findMenuChecked(String path){
344                return checkNotNull(findMenu(path),"NOT FOUND MENU [%s]",path);
345        }
346        /**
347         * 根据path指定的路径查找cmd对象, 先在当前对象中查找,如果找不到,从根结点查找
348         * @param path
349         * @return 返回找到的{@link CmdItem}对象,找不到返回null
350         */
351        public CmdItem findCmd(String path){
352                BaseItem item = find(path);
353                return (item instanceof CmdItem) ? (CmdItem)item : null;
354        }
355        /**
356         * 根据path指定的路径查找cmd对象, 与{@link #findCmd(String)}基本相同,只是当找不到指定的对象时抛出异常
357         * @param path
358         * @return 返回找到的{@link CmdItem}对象
359         * @throws IllegalArgumentException 没找到指定的对象
360         */
361        public CmdItem findCmdChecked(String path){
362                return checkNotNull(findCmd(path),"NOT FOUND CMD [%s]",path);
363        }
364        /**
365         * 根据path指定的路径查找对象, 先在当前对象中查找,如果找不到,从根结点查找
366         * @param path
367         * @return 返回找到的{@link BaseItem},找不到返回{@code null}
368         */
369        @SuppressWarnings("unchecked")
370        public <T>BaseOption<T> findOption(String path){
371                BaseItem item = find(path);
372                return (item instanceof BaseOption) ? (BaseOption<T>)item : null;
373        }
374        
375        /**
376         * 根据{@code path}指定的路径查找option对象,
377         * 与{@link #findOption(String)}基本相同,只是当找不到指定的对象时抛出异常
378         * @return 返回找到的{@link BaseItem}
379         * @throws IllegalArgumentException 没找到指定的对象
380         * @see #findOption(String)
381         */
382        public <T>BaseOption<T> findOptionChecked(String path){
383                BaseOption<T> opt = findOption(path);
384                return checkNotNull(opt,"NOT FOUND OPTION [%s]",path);
385        }
386        /**
387         * @param path
388         * @return
389         * @see #find(String)
390         */
391        public Base64Option findBase64Option(String path){
392                BaseItem item = find(path);
393                return (item instanceof Base64Option) ? (Base64Option)item : null;
394        }
395        /**
396         * @param path
397         * @return
398         * @see #find(String)
399         */
400        public BoolOption findBoolOption(String path){
401                BaseItem item = find(path);
402                return (item instanceof BoolOption) ? (BoolOption)item : null;
403        }
404        /**
405         * @param path
406         * @return
407         * @see #find(String)
408         */
409        public IPv4Option findIPv4Option(String path){
410                BaseItem item = find(path);
411                return (item instanceof IPv4Option) ? (IPv4Option)item : null;
412        }
413        /**
414         * @param path
415         * @return
416         * @see #find(String)
417         */
418        public MACOption findMACOption(String path){
419                BaseItem item = find(path);
420                return (item instanceof MACOption) ? (MACOption)item : null;
421        }
422        /**
423         * @param path
424         * @return
425         * @see #find(String)
426         */
427        public IntOption findIntOption(String path){
428                BaseItem item = find(path);
429                return (item instanceof IntOption) ? (IntOption)item : null;
430        }
431        /**
432         * @param path
433         * @return
434         * @see #find(String)
435         */
436        public FloatOption findFloatOption(String path){
437                BaseItem item = find(path);
438                return (item instanceof FloatOption) ? (FloatOption)item : null;
439        }
440        /**
441         * @param path
442         * @return
443         * @see #find(String)
444         */
445        public DateOption findDateOption(String path){
446                BaseItem item = find(path);
447                return (item instanceof DateOption) ? (DateOption)item : null;
448        }
449        /**
450         * @param path
451         * @return
452         * @see #find(String)
453         */
454        public StringOption findStringOption(String path){
455                BaseItem item = find(path);
456                return (item instanceof StringOption) ? (StringOption)item : null;
457        }
458        /**
459         * @param path
460         * @return
461         * @see #find(String)
462         */
463        public PasswordOption findPasswordOption(String path){
464                BaseItem item = find(path);
465                return (item instanceof PasswordOption) ? (PasswordOption)item : null;
466        }
467        /**
468         * @param path
469         * @return
470         * @see #find(String)
471         */
472        public UrlOption findUrlOption(String path){
473                BaseItem item = find(path);
474                return (item instanceof UrlOption) ? (UrlOption)item : null;
475        }
476        /**
477         * @param path
478         * @return
479         * @see #find(String)
480         */
481        public ImageOption findImageOption(String path){
482                BaseItem item = find(path);
483                return (item instanceof ImageOption) ? (ImageOption)item : null;
484        }
485        /**
486         * @param path
487         * @return
488         * @see #find(String)
489         */
490        @SuppressWarnings("unchecked")
491        public <T>CheckOption<T> findCheckOption(String path){
492                BaseItem item = find(path);
493                return (item instanceof CheckOption) ? (CheckOption<T>)item : null;
494        }
495        /**
496         * @param path
497         * @return
498         * @see #find(String)
499         */
500        @SuppressWarnings("unchecked")
501        public <T>SwitchOption<T> findSwitchOption(String path){
502                BaseItem item = find(path);
503                return (item instanceof SwitchOption) ? (SwitchOption<T>)item : null;
504        }
505        /**
506         * 返回所有子条目
507         * @return
508         */
509        public List<BaseItem> getChilds() {
510                return Lists.newArrayList(items.values());
511        }
512        /**
513         * 设置子条目(会清除原有的子条目)
514         * @param childs
515         * @return 当前对象
516         */
517        public BaseItem setChilds(List<BaseItem> childs) {
518                items.clear();
519                return addChilds(childs);
520        }
521        /**
522         * 添加子条目
523         * @param childs
524         * @return 当前对象
525         */
526        public BaseItem addChilds(BaseItem ... childs) {
527                return addChilds(Arrays.asList(childs));
528        }
529        /**
530         * 添加子条目
531         * @param childs
532         * @return 当前对象
533         */
534        public BaseItem addChilds(Collection<BaseItem> childs) {
535                childs = MoreObjects.firstNonNull(childs, Collections.<BaseItem>emptyList());
536                for(BaseItem child:childs){
537                        if(!items.containsKey(child.getName())){
538                                child.setParent(this);
539                                items.put(child.getName(), child);
540                        }
541                }
542                return this;
543        }
544        /**
545         * @return 返回子条目的数量
546         */
547        public int childCount() {
548                return items.size();
549        }
550        /**
551         * @return 所有子条目的名称-路径映射
552         */
553        public Map<String, String> childNames(){
554                return Maps.newLinkedHashMap(Maps.transformValues(items, PATH_FUN));
555        }
556        /**
557         * @return 是否有子条目
558         */
559        public boolean isEmpty() {
560                return items.isEmpty();
561        }
562        /**
563         * 根据{@code name}指定的条目名称查找当前对象下的子条目<br>
564         * 如果{@code name}为数字则为子条目索引
565         * @param name
566         * @return 子条目,没找到返回{@code null}
567         */
568        public BaseItem getChild(final String name) {
569                BaseItem item = items.get(name);
570                if (null == item ){
571                        try{
572                                // 如果name为数字则返回数字
573                                return getChilds().get(Integer.valueOf(name));
574                        }catch (Exception  e) {}
575                }
576                return item;
577        }
578        /**
579         * 用{@code item}更新同名的子对象,如果对象不存在则跳过
580         * @param item
581         */
582        public void updateChild(BaseItem item){
583                if(items.containsKey(item.getName())){
584                        items.remove(item.getName());
585                        addChilds(item);
586                }
587        }
588}