View Javadoc

1   package org.kuali.student.common.dictionary.service.impl;
2   
3   import java.util.ArrayList;
4   import java.util.Collections;
5   import java.util.Comparator;
6   import java.util.HashSet;
7   import java.util.LinkedHashMap;
8   import java.util.List;
9   import java.util.Map;
10  import java.util.Set;
11  
12  import org.kuali.student.common.dictionary.dto.CaseConstraint;
13  import org.kuali.student.common.dictionary.dto.CommonLookupParam;
14  import org.kuali.student.common.dictionary.dto.Constraint;
15  import org.kuali.student.common.dictionary.dto.DataType;
16  import org.kuali.student.common.dictionary.dto.FieldDefinition;
17  import org.kuali.student.common.dictionary.dto.LookupConstraint;
18  import org.kuali.student.common.dictionary.dto.ObjectStructureDefinition;
19  import org.kuali.student.common.dictionary.dto.RequiredConstraint;
20  import org.kuali.student.common.dictionary.dto.ValidCharsConstraint;
21  import org.kuali.student.common.dictionary.dto.WhenConstraint;
22  
23  public class DictionaryFormatter
24  {
25  
26   private StringBuilder builder = new StringBuilder (5000);
27   private ObjectStructureDefinition os;
28   private String rowSeperator = "\n";
29   private String colSeperator = "|";
30   private String name;
31   private String className;
32   private boolean processSubstructures = false;
33   private int level;
34   private Map<String, ObjectStructureDefinition> subStructuresToProcess =
35                                                  new LinkedHashMap ();
36   private Set<ObjectStructureDefinition> subStructuresAlreadyProcessed;
37  
38   public DictionaryFormatter (String name,
39                               String className,
40                               ObjectStructureDefinition os,
41                               Set<ObjectStructureDefinition> subStructuresAlreadyProcessed,
42                               int level,
43                               boolean processSubstructures)
44   {
45    this.name = name;
46    this.className = className;
47    this.os = os;
48    this.subStructuresAlreadyProcessed = subStructuresAlreadyProcessed;
49    this.level = level;
50    this.processSubstructures = processSubstructures;
51   }
52   public static final String UNBOUNDED = "unbounded";
53  
54   public String getRowSeperator ()
55   {
56    return rowSeperator;
57   }
58  
59   public void setRowSeperator (String rowSeperator)
60   {
61    this.rowSeperator = rowSeperator;
62   }
63  
64   public String getColSeparator ()
65   {
66    return colSeperator;
67   }
68  
69   public void setColSeparator (String separator)
70   {
71    this.colSeperator = separator;
72   }
73  
74   private String pad (String str, int size)
75   {
76    StringBuilder padStr = new StringBuilder (size);
77    padStr.append (str);
78    while (padStr.length () < size)
79    {
80     padStr.append (' ');
81    }
82    return padStr.toString ();
83   }
84  
85   public String formatForWiki ()
86   {
87    builder.append (rowSeperator);
88  //  builder.append ("======= start dump of object structure definition ========");
89    builder.append (rowSeperator);
90    builder.append ("h" + level + ". " + calcNotSoSimpleName (name));
91    builder.append ("{anchor:" + name + "}");
92    builder.append (rowSeperator);
93    if (className != null)
94    {
95     builder.append ("The corresponding java class for this dictionary object is "
96                     + os.getName ());
97    }
98    if (os.isHasMetaData ())
99    {
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  }