001 package org.kuali.student.common.dictionary.service.impl; 002 003 import java.util.ArrayList; 004 import java.util.Collections; 005 import java.util.Comparator; 006 import java.util.HashSet; 007 import java.util.LinkedHashMap; 008 import java.util.List; 009 import java.util.Map; 010 import java.util.Set; 011 012 import org.kuali.student.common.dictionary.dto.CaseConstraint; 013 import org.kuali.student.common.dictionary.dto.CommonLookupParam; 014 import org.kuali.student.common.dictionary.dto.Constraint; 015 import org.kuali.student.common.dictionary.dto.DataType; 016 import org.kuali.student.common.dictionary.dto.FieldDefinition; 017 import org.kuali.student.common.dictionary.dto.LookupConstraint; 018 import org.kuali.student.common.dictionary.dto.ObjectStructureDefinition; 019 import org.kuali.student.common.dictionary.dto.RequiredConstraint; 020 import org.kuali.student.common.dictionary.dto.ValidCharsConstraint; 021 import org.kuali.student.common.dictionary.dto.WhenConstraint; 022 023 public class DictionaryFormatter 024 { 025 026 private StringBuilder builder = new StringBuilder (5000); 027 private ObjectStructureDefinition os; 028 private String rowSeperator = "\n"; 029 private String colSeperator = "|"; 030 private String name; 031 private String className; 032 private boolean processSubstructures = false; 033 private int level; 034 private Map<String, ObjectStructureDefinition> subStructuresToProcess = 035 new LinkedHashMap (); 036 private Set<ObjectStructureDefinition> subStructuresAlreadyProcessed; 037 038 public DictionaryFormatter (String name, 039 String className, 040 ObjectStructureDefinition os, 041 Set<ObjectStructureDefinition> subStructuresAlreadyProcessed, 042 int level, 043 boolean processSubstructures) 044 { 045 this.name = name; 046 this.className = className; 047 this.os = os; 048 this.subStructuresAlreadyProcessed = subStructuresAlreadyProcessed; 049 this.level = level; 050 this.processSubstructures = processSubstructures; 051 } 052 public static final String UNBOUNDED = "unbounded"; 053 054 public String getRowSeperator () 055 { 056 return rowSeperator; 057 } 058 059 public void setRowSeperator (String rowSeperator) 060 { 061 this.rowSeperator = rowSeperator; 062 } 063 064 public String getColSeparator () 065 { 066 return colSeperator; 067 } 068 069 public void setColSeparator (String separator) 070 { 071 this.colSeperator = separator; 072 } 073 074 private String pad (String str, int size) 075 { 076 StringBuilder padStr = new StringBuilder (size); 077 padStr.append (str); 078 while (padStr.length () < size) 079 { 080 padStr.append (' '); 081 } 082 return padStr.toString (); 083 } 084 085 public String formatForWiki () 086 { 087 builder.append (rowSeperator); 088 // builder.append ("======= start dump of object structure definition ========"); 089 builder.append (rowSeperator); 090 builder.append ("h" + level + ". " + calcNotSoSimpleName (name)); 091 builder.append ("{anchor:" + name + "}"); 092 builder.append (rowSeperator); 093 if (className != null) 094 { 095 builder.append ("The corresponding java class for this dictionary object is " 096 + os.getName ()); 097 } 098 if (os.isHasMetaData ()) 099 { 100 builder.append (rowSeperator); 101 builder.append ("The dictionary says this object holds metadata"); 102 } 103 builder.append (rowSeperator); 104 builder.append (colSeperator); 105 builder.append (colSeperator); 106 builder.append ("Field"); 107 builder.append (colSeperator); 108 builder.append (colSeperator); 109 builder.append ("Required?"); 110 builder.append (colSeperator); 111 builder.append (colSeperator); 112 builder.append ("DataType"); 113 builder.append (colSeperator); 114 builder.append (colSeperator); 115 builder.append ("Length"); 116 builder.append (colSeperator); 117 builder.append (colSeperator); 118 builder.append ("Dynamic or Hidden"); 119 builder.append (colSeperator); 120 builder.append (colSeperator); 121 builder.append ("Default"); 122 builder.append (colSeperator); 123 builder.append (colSeperator); 124 builder.append ("Repeats?"); 125 builder.append (colSeperator); 126 builder.append (colSeperator); 127 builder.append ("Valid Characters"); 128 builder.append (colSeperator); 129 builder.append (colSeperator); 130 builder.append ("Lookup"); 131 builder.append (colSeperator); 132 builder.append (colSeperator); 133 builder.append ("Cross Field"); 134 builder.append (colSeperator); 135 builder.append (colSeperator); 136 builder.append (rowSeperator); 137 for (FieldDefinition fd : getSortedFields ()) 138 { 139 builder.append (colSeperator); 140 builder.append (pad (fd.getName (), 30)); 141 builder.append (colSeperator); 142 builder.append (pad (calcRequired (fd), 10)); 143 builder.append (colSeperator); 144 builder.append (pad (calcDataType (fd), 25)); 145 builder.append (colSeperator); 146 builder.append (pad (calcLength (fd), 15)); 147 builder.append (colSeperator); 148 builder.append (pad (calcDynamicOrHidden (fd), 7)); 149 builder.append (colSeperator); 150 builder.append (pad (calcDefaultValue (fd), 15)); 151 builder.append (colSeperator); 152 builder.append (calcRepeating (fd)); 153 builder.append (colSeperator); 154 builder.append (calcValidCharsMinMax (fd)); 155 builder.append (colSeperator); 156 builder.append (calcLookup (fd)); 157 builder.append (colSeperator); 158 builder.append (calcCrossField (fd)); 159 builder.append (colSeperator); 160 builder.append (rowSeperator); 161 } 162 List<String> discrepancies = null; 163 if (className == null) 164 { 165 discrepancies = new ArrayList (1); 166 discrepancies.add ( 167 "There is no corresponding java class for this dictionary object structure"); 168 } 169 else 170 { 171 discrepancies = new Dictionary2BeanComparer (className, os).compare (); 172 } 173 if (discrepancies.size () > 0) 174 { 175 builder.append ("h" + (level + 1) + ". " + discrepancies.size () 176 + " discrepancie(s) found in " 177 + calcSimpleName (name)); 178 builder.append (rowSeperator); 179 builder.append (formatAsString (discrepancies)); 180 builder.append (rowSeperator); 181 } 182 183 // builder.append ("======= end dump of object structure definition ========"); 184 builder.append (rowSeperator); 185 Set<ObjectStructureDefinition> subStructuresAlreadyProcessedBeforeProcessingSubStructures = 186 new HashSet (); 187 subStructuresAlreadyProcessedBeforeProcessingSubStructures.addAll ( 188 subStructuresAlreadyProcessed); 189 for (String subName : this.subStructuresToProcess.keySet ()) 190 { 191 ObjectStructureDefinition subOs = this.subStructuresToProcess.get (subName); 192 if ( ! subStructuresAlreadyProcessedBeforeProcessingSubStructures.contains ( 193 subOs)) 194 { 195 this.subStructuresAlreadyProcessed.add (subOs); 196 // System.out.println ("formatting substructure " + subName); 197 Class<?> subClazz = getClass (subOs.getName ()); 198 DictionaryFormatter formatter = 199 new DictionaryFormatter (subName, subOs.getName (), 200 subOs, 201 subStructuresAlreadyProcessed, 202 level + 1, 203 this.processSubstructures); 204 builder.append (formatter.formatForWiki ()); 205 builder.append (rowSeperator); 206 } 207 } 208 209 return builder.toString (); 210 } 211 212 213 214 private List<FieldDefinition> getSortedFields () 215 { 216 List<FieldDefinition> fields = os.getAttributes (); 217 Collections.sort (fields, new FieldDefinitionNameComparator ()); 218 return fields; 219 } 220 221 private static class FieldDefinitionNameComparator implements Comparator <FieldDefinition> 222 { 223 @Override 224 public int compare (FieldDefinition o1, FieldDefinition o2) 225 { 226 return o1.getName ().toLowerCase ().compareTo (o2.getName ().toLowerCase ()); 227 } 228 229 } 230 231 private Class getClass (String className) 232 { 233 try 234 { 235 return Class.forName (className); 236 } 237 catch (ClassNotFoundException ex) 238 { 239 return null; 240 // throw new IllegalArgumentException ("Could not find class for " + className); 241 } 242 } 243 244 private String formatAsString (List<String> discrepancies) 245 { 246 int i = 0; 247 StringBuilder builder = new StringBuilder (); 248 for (String discrep : discrepancies) 249 { 250 i ++; 251 builder.append (i + ". " + discrep + "\n"); 252 } 253 return builder.toString (); 254 } 255 256 private String calcDataType (FieldDefinition fd) 257 { 258 if (fd.getDataType ().equals (DataType.COMPLEX)) 259 { 260 if (fd.getDataObjectStructure () == null) 261 { 262 throw new IllegalArgumentException ( 263 fd.getName () + " is complex but does not have a sub-structure defined"); 264 } 265 Class subClazz = this.getClass (fd.getDataObjectStructure ().getName ()); 266 String subStrucName = calcComplexSubStructureName (fd); 267 // process if explicity asking for substructures OR the field is a freestanding field 268 // so it won't be processed by just processing all of the DTO's and their sub-objects 269 if (this.processSubstructures || subClazz == null) 270 { 271 if ( ! this.subStructuresAlreadyProcessed.contains ( 272 fd.getDataObjectStructure ())) 273 { 274 // System.out.println ("Adding " + subStrucName + " to set to be processed"); 275 this.subStructuresToProcess.put (subStrucName, fd.getDataObjectStructure ()); 276 } 277 } 278 return "[" + calcNotSoSimpleName (subStrucName) + "|#" + subStrucName + "]"; 279 } 280 return fd.getDataType ().toString (); 281 } 282 283 private String calcDefaultValue (FieldDefinition fd) 284 { 285 if (fd.getDefaultValue () != null) 286 { 287 return fd.getDefaultValue ().toString (); 288 } 289 return " "; 290 } 291 292 293 private String calcDynamicOrHidden (FieldDefinition fd) 294 { 295 if (fd.isHide ()) 296 { 297 if (fd.isDynamic ()) 298 { 299 return "dynamic and hidden"; 300 } 301 return "hidden"; 302 } 303 if (fd.isDynamic ()) 304 { 305 return "dynamic"; 306 } 307 return " "; 308 } 309 310 private String calcComplexSubStructureName (FieldDefinition fd) 311 { 312 if (this.processSubstructures) 313 { 314 return name + "." + fd.getName () + "." + calcSimpleName ( 315 fd.getDataObjectStructure ().getName ()); 316 } 317 return calcSimpleName (fd.getDataObjectStructure ().getName ()); 318 } 319 320 private String calcSimpleName (String name) 321 { 322 if (name.lastIndexOf (".") != -1) 323 { 324 name = name.substring (name.lastIndexOf (".") + 1); 325 } 326 return name; 327 } 328 329 private String calcNotSoSimpleName (String name) 330 { 331 if (name.lastIndexOf (".") == -1) 332 { 333 return name; 334 } 335 String simpleName = calcSimpleName (name); 336 String fieldName = calcSimpleName (name.substring (0, name.length () 337 - simpleName.length () 338 - 1)); 339 return fieldName + "." + simpleName; 340 } 341 342 private String calcRequired (FieldDefinition fd) 343 { 344 if (fd.getMaxOccurs () != null) 345 { 346 if ( ! fd.getMaxOccurs ().equals (UNBOUNDED)) 347 { 348 if (Integer.parseInt (fd.getMaxOccurs ()) == 0) 349 { 350 return "Not allowed"; 351 } 352 } 353 } 354 355 if (fd.getMinOccurs () != null) 356 { 357 if (fd.getMinOccurs () >= 1) 358 { 359 return "required"; 360 } 361 } 362 363 return " "; 364 // return "optional"; 365 } 366 private static final String LINK_TO_DEFINITIONS = 367 "KULSTG:Formatted View of Base Dictionary#Valid Character Definitions"; 368 369 private String calcValidChars (FieldDefinition fd) 370 { 371 if (fd.getValidChars () == null) 372 { 373 return " "; 374 } 375 return calcValidChars (fd.getValidChars ()); 376 } 377 378 private String calcValidChars (ValidCharsConstraint cons) 379 { 380 String labelKey = cons.getLabelKey (); 381 if (labelKey == null) 382 { 383 labelKey = "validation.validChars"; 384 } 385 String validChars = escapeWiki (cons.getValue ()); 386 String descr = "[" + labelKey + "|" + LINK_TO_DEFINITIONS + "]" + "\\\\\n" 387 + validChars; 388 return descr; 389 } 390 391 private String escapeWiki (String str) 392 { 393 StringBuilder bldr = new StringBuilder (str.length ()); 394 for (int i = 0; i < str.length (); i ++) 395 { 396 char c = str.charAt (i); 397 switch (c) 398 { 399 case '{': 400 case '}': 401 case '[': 402 case ']': 403 case '|': 404 bldr.append ('\\'); 405 } 406 bldr.append (c); 407 } 408 return bldr.toString (); 409 } 410 411 private String calcLookup (FieldDefinition fd) 412 { 413 if (fd.getLookupDefinition () == null) 414 { 415 return " "; 416 } 417 return calcLookup (fd.getLookupDefinition ()); 418 } 419 420 private String calcLookup (LookupConstraint lc) 421 { 422 StringBuilder bldr = new StringBuilder (); 423 bldr.append (lc.getId ()); 424 // this is the search description not the lookup description 425 // builder.append (" - "); 426 // builder.append (lc.getDesc ()); 427 String and = ""; 428 bldr.append ("\\\\"); 429 bldr.append ("\n"); 430 bldr.append ("Implemented using search: "); 431 String searchPage = calcWikiSearchPage (lc.getSearchTypeId ()); 432 bldr.append ("[" + lc.getSearchTypeId () + "|" + searchPage + "#" 433 + lc.getSearchTypeId () + "]"); 434 List<CommonLookupParam> configuredParameters = filterConfiguredParams ( 435 lc.getParams ()); 436 if (configuredParameters.size () > 0) 437 { 438 bldr.append ("\\\\"); 439 bldr.append ("\n"); 440 bldr.append (" where "); 441 and = ""; 442 for (CommonLookupParam param : configuredParameters) 443 { 444 bldr.append (and); 445 and = " and "; 446 bldr.append (param.getName ()); 447 bldr.append ("="); 448 if (param.getDefaultValueString () != null) 449 { 450 bldr.append (param.getDefaultValueString ()); 451 continue; 452 } 453 if (param.getDefaultValueList () != null) 454 { 455 String comma = ""; 456 for (String defValue : param.getDefaultValueList ()) 457 { 458 bldr.append (comma); 459 comma = ", "; 460 bldr.append (defValue); 461 } 462 } 463 } 464 } 465 return bldr.toString (); 466 } 467 468 private String calcValidCharsMinMax (FieldDefinition fd) 469 { 470 String validChars = calcValidChars (fd); 471 String minMax = calcMinMax (fd); 472 String and = " and "; 473 if (validChars.trim ().equals ("")) 474 { 475 return minMax; 476 } 477 if (minMax.trim ().equals ("")) 478 { 479 return validChars; 480 } 481 return validChars + "\\\\\n" + minMax; 482 } 483 484 private String calcMinMax (FieldDefinition fd) 485 { 486 if (fd.getExclusiveMin () == null) 487 { 488 if (fd.getInclusiveMax () == null) 489 { 490 return " "; 491 } 492 return "Must be <= " + fd.getInclusiveMax (); 493 } 494 if (fd.getInclusiveMax () == null) 495 { 496 return "Must be > " + fd.getExclusiveMin (); 497 } 498 return "Must be > " + fd.getExclusiveMin () + " and < " 499 + fd.getInclusiveMax (); 500 } 501 private static final String PAGE_PREFIX = "Formatted View of "; 502 private static final String PAGE_SUFFIX = " Searches"; 503 504 private String calcWikiSearchPage (String searchType) 505 { 506 return PAGE_PREFIX + calcWikigPageAbbrev (searchType) + PAGE_SUFFIX; 507 } 508 509 private String calcWikigPageAbbrev (String searchType) 510 { 511 if (searchType == null) 512 { 513 return null; 514 } 515 if (searchType.equals ("enumeration.management.search")) 516 { 517 return "EM"; 518 } 519 if (searchType.startsWith ("lu.")) 520 { 521 return "LU"; 522 } 523 if (searchType.startsWith ("cluset.")) 524 { 525 return "LU"; 526 } 527 if (searchType.startsWith ("lo.")) 528 { 529 return "LO"; 530 } 531 if (searchType.startsWith ("lrc.")) 532 { 533 return "LRC"; 534 } 535 if (searchType.startsWith ("comment.")) 536 { 537 return "Comment"; 538 } 539 if (searchType.startsWith ("org.")) 540 { 541 return "Organization"; 542 } 543 if (searchType.startsWith ("atp.")) 544 { 545 return "ATP"; 546 } 547 if (searchType.startsWith ("subjectCode.")) 548 { 549 return "SC"; 550 } 551 throw new IllegalArgumentException ("Unknown type of search: " + searchType); 552 } 553 554 private List<CommonLookupParam> filterConfiguredParams ( 555 List<CommonLookupParam> params) 556 { 557 List list = new ArrayList (); 558 if (params == null) 559 { 560 return list; 561 } 562 if (params.size () == 0) 563 { 564 return list; 565 } 566 for (CommonLookupParam param : params) 567 { 568 if (param.getDefaultValueString () != null) 569 { 570 list.add (param); 571 continue; 572 } 573 if (param.getDefaultValueList () != null) 574 { 575 list.add (param); 576 } 577 } 578 return list; 579 } 580 581 private String calcRepeating (FieldDefinition fd) 582 { 583 if (fd.getMaxOccurs () == null) 584 { 585 return "???"; 586 } 587 if (fd.getMaxOccurs ().equals (UNBOUNDED)) 588 { 589 if (fd.getMinOccurs () != null && fd.getMinOccurs () > 1) 590 { 591 return "repeating: minimum " + fd.getMinOccurs () + " times"; 592 } 593 return "repeating: unlimited"; 594 } 595 if (Integer.parseInt (fd.getMaxOccurs ()) == 0) 596 { 597 return "NOT USED"; 598 } 599 if (Integer.parseInt (fd.getMaxOccurs ()) == 1) 600 { 601 return " "; 602 // return "single"; 603 } 604 605 if (fd.getMinOccurs () != null) 606 { 607 if (fd.getMinOccurs () > 1) 608 { 609 return "repeating: " + fd.getMinOccurs () + " to " + fd.getMaxOccurs () 610 + " times"; 611 } 612 } 613 return "repeating: maximum " + fd.getMaxOccurs () + " times"; 614 } 615 616 private String calcLength (FieldDefinition fd) 617 { 618 if (fd.getMaxLength () != null) 619 { 620 if (fd.getMinLength () != null && fd.getMinLength () != 0) 621 { 622 if (Integer.parseInt (fd.getMaxLength ()) == fd.getMinLength ()) 623 { 624 return ("(must be " + fd.getMaxLength () + ")"); 625 } 626 return "(" + fd.getMinLength () + " to " + fd.getMaxLength () + ")"; 627 } 628 return "(up to " + fd.getMaxLength () + ")"; 629 } 630 if (fd.getMinLength () != null) 631 { 632 return "(over " + fd.getMinLength () + ")"; 633 } 634 return " "; 635 } 636 637 private String calcCrossField (FieldDefinition fd) 638 { 639 StringBuilder b = new StringBuilder (); 640 String semicolon = ""; 641 String cfr = calcCrossFieldRequire (fd); 642 if (cfr != null) 643 { 644 b.append (semicolon); 645 semicolon = "; "; 646 b.append (cfr); 647 } 648 String cfw = calcCrossFieldWhen (fd); 649 if (cfw != null) 650 { 651 b.append (semicolon); 652 semicolon = "; "; 653 b.append (cfw); 654 } 655 if (b.length () == 0) 656 { 657 return " "; 658 } 659 return b.toString (); 660 } 661 662 private String calcCrossFieldRequire (FieldDefinition fd) 663 { 664 if (fd.getRequireConstraint () == null) 665 { 666 return null; 667 } 668 if (fd.getRequireConstraint ().size () == 0) 669 { 670 return null; 671 } 672 StringBuilder b = new StringBuilder (); 673 String comma = ""; 674 b.append ("if not empty then "); 675 for (RequiredConstraint rc : fd.getRequireConstraint ()) 676 { 677 b.append (comma); 678 comma = ", "; 679 b.append (rc.getFieldPath ()); 680 } 681 if (fd.getRequireConstraint ().size () == 1) 682 { 683 b.append (" is"); 684 } 685 else 686 { 687 b.append (" are"); 688 } 689 b.append (" also required"); 690 return b.toString (); 691 } 692 693 private String calcCrossFieldWhen (FieldDefinition fd) 694 { 695 if (fd.getCaseConstraint () == null) 696 { 697 return null; 698 } 699 StringBuilder b = new StringBuilder (); 700 CaseConstraint cc = fd.getCaseConstraint (); 701 for (WhenConstraint wc : cc.getWhenConstraint ()) 702 { 703 b.append ("\\\\"); 704 b.append ("\n"); 705 b.append ("when "); 706 b.append (cc.getFieldPath ()); 707 b.append (" "); 708 if ( ! cc.isCaseSensitive ()) 709 { 710 b.append ("ignoring case "); 711 } 712 b.append (cc.getOperator ()); 713 b.append (" "); 714 715 b.append ("\\\\"); 716 b.append ("\n"); 717 String comma = ""; 718 for (Object value : wc.getValues ()) 719 { 720 b.append (comma); 721 comma = " or "; 722 b.append (asString (value)); 723 } 724 b.append ("\\\\"); 725 b.append ("\n"); 726 b.append ("then override constraint:" 727 + calcOverride (fd, wc.getConstraint ())); 728 } 729 return b.toString (); 730 } 731 732 private String calcOverride (FieldDefinition fd, Constraint cons) 733 { 734 StringBuilder b = new StringBuilder (); 735 b.append (calcOverride ("serviceSide", fd.isServerSide (), 736 cons.isServerSide ())); 737 b.append (calcOverride ("exclusiveMin", fd.getExclusiveMin (), 738 cons.getExclusiveMin ())); 739 b.append (calcOverride ("inclusiveMax", fd.getInclusiveMax (), 740 cons.getInclusiveMax ())); 741 String minOccursMessage = calcOverride ("minOccurs", fd.getMinOccurs (), 742 cons.getMinOccurs ()); 743 if ( ! minOccursMessage.trim ().equals ("")) 744 { 745 if (cons.getMinOccurs () != null && cons.getMinOccurs () == 1) 746 { 747 minOccursMessage = " REQUIRED"; 748 } 749 } 750 b.append (minOccursMessage); 751 b.append (calcOverride ("validchars", fd.getValidChars (), 752 cons.getValidChars ())); 753 b.append (calcOverride ("lookup", fd.getLookupDefinition (), 754 cons.getLookupDefinition ())); 755 //TODO: other more complex constraints 756 return b.toString (); 757 } 758 759 private String calcOverride (String attribute, LookupConstraint val1, 760 LookupConstraint val2) 761 { 762 if (val1 == val2) 763 { 764 return ""; 765 } 766 if (val1 == null && val2 != null) 767 { 768 return " add lookup " + this.calcLookup (val2); 769 } 770 if (val1 != null && val2 == null) 771 { 772 return " remove lookup constraint"; 773 } 774 return " change lookup to " + calcLookup (val2); 775 } 776 777 private String calcOverride (String attribute, ValidCharsConstraint val1, 778 ValidCharsConstraint val2) 779 { 780 if (val1 == val2) 781 { 782 return ""; 783 } 784 if (val1 == null && val2 != null) 785 { 786 return " add validchars " + calcValidChars (val2); 787 } 788 if (val1 != null && val2 == null) 789 { 790 return " remove validchars constraint"; 791 } 792 return " change validchars to " + calcValidChars (val2); 793 } 794 795 private String calcOverride (String attribute, boolean val1, boolean val2) 796 { 797 if (val1 == val2) 798 { 799 return ""; 800 } 801 return " " + attribute + "=" + val2; 802 } 803 804 private String calcOverride (String attribute, String val1, String val2) 805 { 806 if (val1 == null && val2 == null) 807 { 808 return ""; 809 } 810 if (val1 == val2) 811 { 812 return ""; 813 } 814 if (val1 == null) 815 { 816 return " " + attribute + "=" + val2; 817 } 818 if (val1.equals (val2)) 819 { 820 return ""; 821 } 822 return " " + attribute + "=" + val2; 823 } 824 825 private String calcOverride (String attribute, Object val1, Object val2) 826 { 827 if (val1 == null && val2 == null) 828 { 829 return ""; 830 } 831 if (val1 == val2) 832 { 833 return ""; 834 } 835 if (val1 == null) 836 { 837 return " " + attribute + "=" + val2; 838 } 839 if (val1.equals (val2)) 840 { 841 return ""; 842 } 843 return " " + attribute + "=" + asString (val2); 844 } 845 846 private String asString (Object value) 847 { 848 if (value == null) 849 { 850 return "null"; 851 } 852 if (value instanceof String) 853 { 854 return (String) value; 855 } 856 return value.toString (); 857 } 858 }