1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one 3 * or more contributor license agreements. See the NOTICE file 4 * distributed with this work for additional information 5 * regarding copyright ownership. The ASF licenses this file 6 * to you under the Apache License, Version 2.0 (the 7 * "License"); you may not use this file except in compliance 8 * with the License. You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, 13 * software distributed under the License is distributed on an 14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 * KIND, either express or implied. See the License for the 16 * specific language governing permissions and limitations 17 * under the License. 18 */ 19 module hunt.shiro.config.Ini; 20 21 import hunt.collection; 22 import hunt.Exceptions; 23 import hunt.logging.Logger; 24 import hunt.Object; 25 import hunt.util.StringBuilder; 26 import hunt.util.ObjectUtils; 27 28 import std.array; 29 import std.file; 30 import std.path; 31 import std.range; 32 import std.string; 33 import std.stdio; 34 import std.ascii; 35 36 37 /** 38 * A class representing the <a href="http://en.wikipedia.org/wiki/INI_file">INI</a> text configuration format. 39 * <p/> 40 * An Ini instance is a map of {@link IniSection IniSection}s, keyed by section name. Each 41 * {@code IniSection} is itself a map of {@code string} name/value pairs. Name/value pairs are guaranteed to be unique 42 * within each {@code IniSection} only - not across the entire {@code Ini} instance. 43 * 44 * @since 1.0 45 */ 46 class Ini : Map!(string, IniSection) { 47 48 enum string DEFAULT_SECTION_NAME = ""; //empty string means the first unnamed section 49 enum string DEFAULT_CHARSET_NAME = "UTF-8"; 50 51 enum string COMMENT_POUND = "#"; 52 enum string COMMENT_SEMICOLON = ";"; 53 enum string SECTION_PREFIX = "["; 54 enum string SECTION_SUFFIX = "]"; 55 56 private Map!(string, IniSection) sections; 57 58 /** 59 * Creates a new empty {@code Ini} instance. 60 */ 61 this() { 62 this.sections = new LinkedHashMap!(string, IniSection)(); 63 } 64 65 /** 66 * Creates a new {@code Ini} instance with the specified defaults. 67 * 68 * @param defaults the default sections and/or key-value pairs to copy into the new instance. 69 */ 70 this(Ini defaults) { 71 this(); 72 if (defaults is null) { 73 throw new NullPointerException("Defaults cannot be null."); 74 } 75 foreach (IniSection section ; defaults.getSections()) { 76 IniSection copy = new IniSection(section); 77 this.sections.put(section.getName(), copy); 78 } 79 } 80 81 /** 82 * Returns {@code true} if no sections have been configured, or if there are sections, but the sections themselves 83 * are all empty, {@code false} otherwise. 84 * 85 * @return {@code true} if no sections have been configured, or if there are sections, but the sections themselves 86 * are all empty, {@code false} otherwise. 87 */ 88 bool isEmpty() { 89 IniSection[] sections = this.sections.values(); 90 foreach (IniSection section ; sections) { 91 if (!section.isEmpty()) { 92 return false; 93 } 94 } 95 return true; 96 } 97 98 /** 99 * Returns the names of all sections managed by this {@code Ini} instance or an empty collection if there are 100 * no sections. 101 * 102 * @return the names of all sections managed by this {@code Ini} instance or an empty collection if there are 103 * no sections. 104 */ 105 string[] getSectionNames() { 106 return sections.byKey.array(); 107 } 108 109 /** 110 * Returns the sections managed by this {@code Ini} instance or an empty collection if there are 111 * no sections. 112 * 113 * @return the sections managed by this {@code Ini} instance or an empty collection if there are 114 * no sections. 115 */ 116 IniSection[] getSections() { 117 return sections.values(); 118 } 119 120 /** 121 * Returns the {@link IniSection} with the given name or {@code null} if no section with that name exists. 122 * 123 * @param sectionName the name of the section to retrieve. 124 * @return the {@link IniSection} with the given name or {@code null} if no section with that name exists. 125 */ 126 IniSection getSection(string sectionName) { 127 string name = cleanName(sectionName); 128 return sections.get(name); 129 } 130 131 /** 132 * Ensures a section with the specified name exists, adding a new one if it does not yet exist. 133 * 134 * @param sectionName the name of the section to ensure existence 135 * @return the section created if it did not yet exist, or the existing IniSection that already existed. 136 */ 137 IniSection addSection(string sectionName) { 138 string name = cleanName(sectionName); 139 IniSection section = getSection(name); 140 if (section is null) { 141 section = new IniSection(name); 142 this.sections.put(name, section); 143 } 144 return section; 145 } 146 147 /** 148 * Removes the section with the specified name and returns it, or {@code null} if the section did not exist. 149 * 150 * @param sectionName the name of the section to remove. 151 * @return the section with the specified name or {@code null} if the section did not exist. 152 */ 153 IniSection removeSection(string sectionName) { 154 string name = cleanName(sectionName); 155 return this.sections.remove(name); 156 } 157 158 private static string cleanName(string sectionName) { 159 string name = strip(sectionName); 160 if (name.empty) { 161 trace("Specified name was null or empty. Defaulting to the default section (name = \"\")"); 162 name = DEFAULT_SECTION_NAME; 163 } 164 return name; 165 } 166 167 /** 168 * Sets a name/value pair for the section with the given {@code sectionName}. If the section does not yet exist, 169 * it will be created. If the {@code sectionName} is null or empty, the name/value pair will be placed in the 170 * default (unnamed, empty string) section. 171 * 172 * @param sectionName the name of the section to add the name/value pair 173 * @param propertyName the name of the property to add 174 * @param propertyValue the property value 175 */ 176 void setSectionProperty(string sectionName, string propertyName, string propertyValue) { 177 string name = cleanName(sectionName); 178 IniSection section = getSection(name); 179 if (section is null) { 180 section = addSection(name); 181 } 182 section.put(propertyName, propertyValue); 183 } 184 185 /** 186 * Returns the value of the specified section property, or {@code null} if the section or property do not exist. 187 * 188 * @param sectionName the name of the section to retrieve to acquire the property value 189 * @param propertyName the name of the section property for which to return the value 190 * @return the value of the specified section property, or {@code null} if the section or property do not exist. 191 */ 192 string getSectionProperty(string sectionName, string propertyName) { 193 IniSection section = getSection(sectionName); 194 return section !is null ? section.get(propertyName) : null; 195 } 196 197 /** 198 * Returns the value of the specified section property, or the {@code defaultValue} if the section or 199 * property do not exist. 200 * 201 * @param sectionName the name of the section to add the name/value pair 202 * @param propertyName the name of the property to add 203 * @param defaultValue the default value to return if the section or property do not exist. 204 * @return the value of the specified section property, or the {@code defaultValue} if the section or 205 * property do not exist. 206 */ 207 string getSectionProperty(string sectionName, string propertyName, string defaultValue) { 208 string value = getSectionProperty(sectionName, propertyName); 209 return value !is null ? value : defaultValue; 210 } 211 212 /** 213 * Creates a new {@code Ini} instance loaded with the INI-formatted data in the resource at the given path. The 214 * resource path may be any value interpretable by the 215 * {@link ResourceUtils#getInputStreamForPath(string) ResourceUtils.getInputStreamForPath} method. 216 * 217 * @param resourcePath the resource location of the INI data to load when creating the {@code Ini} instance. 218 * @return a new {@code Ini} instance loaded with the INI-formatted data in the resource at the given path. 219 * @throws ConfigurationException if the path cannot be loaded into an {@code Ini} instance. 220 */ 221 static Ini fromResourcePath(string resourcePath) { 222 if (resourcePath.empty()) { 223 throw new IllegalArgumentException("Resource Path argument cannot be null or empty."); 224 } 225 Ini ini = new Ini(); 226 ini.loadFromPath(resourcePath); 227 return ini; 228 } 229 230 /** 231 * Loads data from the specified resource path into this current {@code Ini} instance. The 232 * resource path may be any value interpretable by the 233 * {@link ResourceUtils#getInputStreamForPath(string) ResourceUtils.getInputStreamForPath} method. 234 * 235 * @param resourcePath the resource location of the INI data to load into this instance. 236 * @throws ConfigurationException if the path cannot be loaded 237 */ 238 void loadFromPath(string resourcePath) { 239 string rootPath = dirName(thisExePath()); 240 string filename = buildPath(rootPath, resourcePath); 241 if (!exists(filename) || isDir(filename)) { 242 throw new ConfigurationException("The config file doesn't exist: " ~ filename); 243 } 244 245 File f = File(filename, "r"); 246 if (!f.isOpen()) 247 return; 248 scope (exit) 249 f.close(); 250 251 // https://dlang.org/phobos/std_stdio.html#byLine 252 parsing!(typeof(f.byLine()), true)(f.byLine()); 253 } 254 255 /** 256 * Loads the specified raw INI-formatted text into this instance. 257 * 258 * @param iniConfig the raw INI-formatted text to load into this instance. 259 * @throws ConfigurationException if the text cannot be loaded 260 */ 261 void load(string iniConfig) { 262 parsing(iniConfig.lineSplitter()); 263 } 264 265 private void parsing(T, bool needDup=false)(T lines) { 266 // trace(typeid(T)); 267 268 string sectionName = DEFAULT_SECTION_NAME; 269 StringBuilder sectionContent = new StringBuilder(); 270 271 while (!lines.empty()) { 272 string rawLine = cast(string)lines.front(); 273 // version(HUNT_DEBUG_CONFIG) trace(rawLine); 274 scope(exit) 275 lines.popFront(); 276 string line = strip(rawLine); 277 278 if (line.empty() || line.startsWith(COMMENT_POUND) || line.startsWith(COMMENT_SEMICOLON)) { 279 //skip empty lines and comments: 280 continue; 281 } 282 283 static if(needDup) { 284 line = line.idup(); 285 } 286 287 string newSectionName = getSectionName(line); 288 if (!newSectionName.empty()) { 289 // infof("sectionName=%s, newSectionName=%s", sectionName, newSectionName); 290 //found a new section - convert the currently buffered one into a IniSection object 291 addSection(sectionName, sectionContent); 292 293 //reset the buffer for the new section: 294 sectionContent = new StringBuilder(); 295 sectionName = newSectionName; 296 297 version(HUNT_DEBUG) { 298 trace("Parsing " ~ SECTION_PREFIX ~ sectionName ~ SECTION_SUFFIX); 299 } 300 } else { 301 //normal line - add it to the existing content buffer: 302 sectionContent.append(rawLine).append("\n"); 303 } 304 } 305 306 307 //finish any remaining buffered content: 308 addSection(sectionName, sectionContent); 309 } 310 311 /** 312 * Loads the INI-formatted text backed by the given InputStream into this instance. This implementation will 313 * close the input stream after it has finished loading. It is expected that the stream's contents are 314 * UTF-8 encoded. 315 * 316 * @param is the {@code InputStream} from which to read the INI-formatted text 317 * @throws ConfigurationException if unable 318 */ 319 // void load(InputStream is) { 320 // if (is is null) { 321 // throw new NullPointerException("InputStream argument cannot be null."); 322 // } 323 // InputStreamReader isr; 324 // try { 325 // isr = new InputStreamReader(is, DEFAULT_CHARSET_NAME); 326 // } catch (UnsupportedEncodingException e) { 327 // throw new ConfigurationException(e); 328 // } 329 // load(isr); 330 // } 331 332 /** 333 * Loads the INI-formatted text backed by the given Reader into this instance. This implementation will close the 334 * reader after it has finished loading. 335 * 336 * @param reader the {@code Reader} from which to read the INI-formatted text 337 */ 338 // void load(Reader reader) { 339 // Scanner scanner = new Scanner(reader); 340 // try { 341 // load(scanner); 342 // } finally { 343 // try { 344 // scanner.close(); 345 // } catch (Exception e) { 346 // trace("Unable to cleanly close the InputStream scanner. Non-critical - ignoring.", e); 347 // } 348 // } 349 // } 350 351 /** 352 * Merges the contents of <code>m</code>'s {@link IniSection} objects into self. 353 * This differs from {@link Ini#putAll(Map)}, in that each section is merged with the existing one. 354 * For example the following two ini blocks are merged and the result is the third<BR/> 355 * <p> 356 * Initial: 357 * <pre> 358 * <code>[section1] 359 * key1 = value1 360 * 361 * [section2] 362 * key2 = value2 363 * </code> </pre> 364 * 365 * To be merged: 366 * <pre> 367 * <code>[section1] 368 * foo = bar 369 * 370 * [section2] 371 * key2 = new value 372 * </code> </pre> 373 * 374 * Result: 375 * <pre> 376 * <code>[section1] 377 * key1 = value1 378 * foo = bar 379 * 380 * [section2] 381 * key2 = new value 382 * </code> </pre> 383 * 384 * </p> 385 * 386 * @param m map to be merged 387 * @since 1.4 388 */ 389 void merge(Map!(string, IniSection) m) { 390 391 if (m !is null) { 392 foreach (string key, IniSection value; m) { 393 IniSection section = this.getSection(key); 394 if (section is null) { 395 section = addSection(key); 396 } 397 section.putAll(value); 398 } 399 } 400 } 401 402 private void addSection(string name, StringBuilder content) { 403 if (content.length() > 0) { 404 string contentString = content.toString(); 405 string cleaned = strip(contentString); 406 if (!cleaned.empty) { 407 IniSection section = new IniSection(name, contentString); 408 if (!section.isEmpty()) { 409 sections.put(name, section); 410 } 411 } 412 } 413 } 414 415 protected static bool isSectionHeader(string line) { 416 string s = strip(line); 417 return !s.empty && s.startsWith(SECTION_PREFIX) && s.endsWith(SECTION_SUFFIX); 418 } 419 420 protected static string getSectionName(string line) { 421 string s = strip(line); 422 if (isSectionHeader(s)) { 423 return cleanName(s[1 .. $ - 1]); 424 } 425 return null; 426 } 427 428 429 bool replace(string key, IniSection oldValue, IniSection newValue) { 430 IniSection curValue = get(key); 431 if (curValue != oldValue || !containsKey(key)) { 432 return false; 433 } 434 put(key, newValue); 435 return true; 436 } 437 438 IniSection replace(string key, IniSection value) { 439 IniSection curValue = IniSection.init; 440 if (containsKey(key)) { 441 curValue = put(key, value); 442 } 443 return curValue; 444 } 445 446 447 IniSection putIfAbsent(string key, IniSection value) { 448 IniSection v = IniSection.init; 449 450 if (!containsKey(key)) 451 v = put(key, value); 452 453 return v; 454 } 455 456 bool remove(string key, IniSection value) { 457 IniSection curValue = get(key); 458 if (curValue != value || !containsKey(key)) 459 return false; 460 remove(key); 461 return true; 462 } 463 464 IniSection opIndex(string key) { 465 return get(key); 466 } 467 468 int opApply(scope int delegate(ref string, ref IniSection) dg) { 469 int result = 0; 470 471 foreach(string key, IniSection value; sections) { 472 result = dg(key, value); 473 } 474 475 return result; 476 } 477 478 int opApply(scope int delegate(MapEntry!(string, IniSection) entry) dg) { 479 int result = 0; 480 481 foreach(MapEntry!(string, IniSection) entry; sections) { 482 result = dg(entry); 483 } 484 485 return result; 486 } 487 488 InputRange!string byKey() { 489 return sections.byKey(); 490 } 491 492 InputRange!IniSection byValue() { 493 return sections.byValue(); 494 } 495 496 bool opEquals(IObject o) { 497 return opEquals(cast(Object) o); 498 } 499 500 override bool opEquals(Object obj) { 501 Ini ini = cast(Ini) obj; 502 if (ini !is null) { 503 return this.sections.opEquals(ini.sections); 504 } 505 return false; 506 } 507 508 override size_t toHash() @trusted nothrow { 509 return this.sections.toHash(); 510 } 511 512 override string toString() { 513 if (this.sections is null || this.sections.isEmpty()) { 514 return "<empty INI>"; 515 } else { 516 StringBuilder sb = new StringBuilder("sections="); 517 int i = 0; 518 foreach (IniSection section ; this.sections.values()) { 519 if (i > 0) { 520 sb.append(","); 521 } 522 sb.append(section.toString()); 523 i++; 524 } 525 return sb.toString(); 526 } 527 } 528 529 int size() { 530 return this.sections.size(); 531 } 532 533 bool containsKey(string key) { 534 return this.sections.containsKey(key); 535 } 536 537 bool containsValue(IniSection value) { 538 return this.sections.containsValue(value); 539 } 540 541 IniSection get(string key) { 542 return this.sections.get(key); 543 } 544 545 IniSection put(string key, IniSection value) { 546 return this.sections.put(key, value); 547 } 548 549 IniSection remove(string key) { 550 return this.sections.remove(key); 551 } 552 553 void putAll(Map!(string, IniSection) m) { 554 this.sections.putAll(m); 555 } 556 557 void clear() { 558 this.sections.clear(); 559 } 560 561 // Set<string> keySet() { 562 // return Collections.unmodifiableSet(this.sections.keySet()); 563 // } 564 565 IniSection[] values() { 566 return this.sections.values(); 567 } 568 569 // Set<Entry!(string, IniSection)> entrySet() { 570 // return Collections.unmodifiableSet(this.sections.entrySet()); 571 // } 572 573 mixin CloneMemberTemplate!(typeof(this)); 574 575 } 576 577 578 579 /** 580 * An {@code IniSection} is string-key-to-string-value Map, identifiable by a 581 * {@link #getName() name} unique within an {@link Ini} instance. 582 */ 583 class IniSection : Map!(string, string) { 584 585 enum char ESCAPE_TOKEN = '\\'; 586 587 private string name; 588 private Map!(string, string) props; 589 590 private this(string name) { 591 trace("section: ", name); 592 if (name.empty) { 593 throw new NullPointerException("name"); 594 } 595 this.name = name; 596 this.props = new LinkedHashMap!(string, string)(); 597 } 598 599 private this(string name, string sectionContent) { 600 trace("section: ", name); 601 if (name.empty) { 602 throw new NullPointerException("name"); 603 } 604 this.name = name; 605 Map!(string, string) props; 606 if (!sectionContent.empty() ) { 607 props = toMapProps(sectionContent); 608 } else { 609 props = new LinkedHashMap!(string, string)(); 610 } 611 if ( props !is null ) { 612 this.props = props; 613 } else { 614 this.props = new LinkedHashMap!(string, string)(); 615 } 616 } 617 618 private this(IniSection defaults) { 619 this(defaults.getName()); 620 putAll(defaults.props); 621 } 622 623 //Protected to access in a test case - NOT considered part of Shiro's API 624 625 static bool isContinued(string line) { 626 if (line.empty()) { 627 return false; 628 } 629 int length = cast(int)line.length; 630 //find the number of backslashes at the end of the line. If an even number, the 631 //backslashes are considered escaped. If an odd number, the line is considered continued on the next line 632 int backslashCount = 0; 633 for (int i = length - 1; i > 0; i--) { 634 if (line[i] == ESCAPE_TOKEN) { 635 backslashCount++; 636 } else { 637 break; 638 } 639 } 640 return backslashCount % 2 != 0; 641 } 642 643 private static bool isKeyValueSeparatorChar(char c) { 644 return isWhite(c) || c == ':' || c == '='; 645 } 646 647 private static bool isCharEscaped(string s, int index) { 648 return index > 0 && s[index - 1] == ESCAPE_TOKEN; 649 } 650 651 //Protected to access in a test case - NOT considered part of Shiro's API 652 static string[] splitKeyValue(string keyValueLine) { 653 string line = strip(keyValueLine); 654 if (line.empty()) { 655 return null; 656 } 657 StringBuilder keyBuffer = new StringBuilder(); 658 StringBuilder valueBuffer = new StringBuilder(); 659 660 bool buildingKey = true; //we'll build the value next: 661 662 for (int i = 0; i < cast(int)line.length; i++) { 663 char c = line[i]; 664 665 if (buildingKey) { 666 if (isKeyValueSeparatorChar(c) && !isCharEscaped(line, i)) { 667 buildingKey = false;//now start building the value 668 } else { 669 keyBuffer.append(c); 670 } 671 } else { 672 if (valueBuffer.length() == 0 && isKeyValueSeparatorChar(c) && !isCharEscaped(line, i)) { 673 //swallow the separator chars before we start building the value 674 } else { 675 valueBuffer.append(c); 676 } 677 } 678 } 679 680 string key = strip(keyBuffer.toString()); 681 string value = strip(valueBuffer.toString()); 682 683 if (key.empty() || value.empty()) { 684 version(HUNT_DEBUG) warningf("key/value is empty: %s = %s, line: %s", key, value, line); 685 string msg = "Line argument must contain a key and a value. Only one string token was found."; 686 throw new IllegalArgumentException(msg); 687 } 688 689 version(HUNT_DEBUG_CONFIG) tracef("Discovered key/value pair: %s = %s", key, value); 690 691 return [key, value]; 692 } 693 694 private static Map!(string, string) toMapProps(string content) { 695 string line; 696 Map!(string, string) props = new LinkedHashMap!(string, string)(); 697 StringBuilder lineBuffer = new StringBuilder(); 698 auto scanner = content.lineSplitter(); 699 700 while (!scanner.empty()) { 701 line = strip(scanner.front); 702 scanner.popFront(); 703 if (isContinued(line)) { 704 //strip off the last continuation backslash: 705 line = line[0 .. $ - 1]; 706 lineBuffer.append(line); 707 continue; 708 } else { 709 lineBuffer.append(line); 710 } 711 line = lineBuffer.toString(); 712 lineBuffer = new StringBuilder(); 713 string[] kvPair = splitKeyValue(line); 714 if(kvPair !is null) 715 props.put(kvPair[0], kvPair[1]); 716 } 717 718 return props; 719 } 720 721 string getName() { 722 return this.name; 723 } 724 725 void clear() { 726 this.props.clear(); 727 } 728 729 bool containsKey(string key) { 730 return this.props.containsKey(key); 731 } 732 733 bool containsValue(string value) { 734 return this.props.containsValue(value); 735 } 736 737 // Set<Entry!(string, string)> entrySet() { 738 // return this.props.entrySet(); 739 // } 740 741 string get(string key) { 742 return this.props.get(key); 743 } 744 745 bool isEmpty() { 746 return this.props.isEmpty(); 747 } 748 749 // Set<string> keySet() { 750 // return this.props.keySet(); 751 // } 752 753 string put(string key, string value) { 754 return this.props.put(key, value); 755 } 756 757 void putAll(Map!(string, string) m) { 758 this.props.putAll(m); 759 } 760 761 string remove(string key) { 762 return this.props.remove(key); 763 } 764 765 int size() { 766 return this.props.size(); 767 } 768 769 string[] values() { 770 return this.props.values(); 771 } 772 773 string opIndex(string key) { 774 return get(key); 775 } 776 777 bool replace(string key, string oldValue, string newValue) { 778 string curValue = get(key); 779 if (curValue != oldValue || !containsKey(key)) { 780 return false; 781 } 782 put(key, newValue); 783 return true; 784 } 785 786 string replace(string key, string value) { 787 string curValue = string.init; 788 if (containsKey(key)) { 789 curValue = put(key, value); 790 } 791 return curValue; 792 } 793 794 795 string putIfAbsent(string key, string value) { 796 string v = string.init; 797 798 if (!containsKey(key)) 799 v = put(key, value); 800 801 return v; 802 } 803 804 bool remove(string key, string value) { 805 string curValue = get(key); 806 if (curValue != value || !containsKey(key)) 807 return false; 808 remove(key); 809 return true; 810 } 811 812 813 int opApply(scope int delegate(ref string, ref string) dg) { 814 int result = 0; 815 foreach(string key, string value; this.props) { 816 result = dg(key, value); 817 } 818 return result; 819 } 820 821 int opApply(scope int delegate(MapEntry!(string, string) entry) dg) { 822 int result = 0; 823 foreach(MapEntry!(string, string) entry; this.props) { 824 result = dg(entry); 825 } 826 return result; 827 } 828 829 InputRange!string byKey() { 830 return this.props.byKey(); 831 } 832 833 InputRange!string byValue() { 834 return this.props.byValue(); 835 } 836 837 bool opEquals(IObject o) { 838 return opEquals(cast(Object) o); 839 } 840 841 override string toString() { 842 string name = getName(); 843 if (Ini.DEFAULT_SECTION_NAME == name) { 844 return "<default>"; 845 } 846 return name; 847 } 848 849 override 850 bool opEquals(Object obj) { 851 IniSection other = cast(IniSection) obj; 852 if (other !is null) { 853 return getName() == other.getName() && this.props == other.props; 854 } 855 return false; 856 } 857 858 override size_t toHash() @trusted nothrow { 859 return hashOf(this.name) * 31 + this.props.toHash(); 860 } 861 862 mixin CloneMemberTemplate!(typeof(this)); 863 }