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}