| 16 | | |
| 17 | | class Tracer |
| 18 | | inherits Object |
| 19 | | """ |
| 20 | | Used to implement the trace statement. |
| 21 | | """ |
| 22 | | |
| 23 | | var _isActive = true |
| 24 | | var _willAutoFlush = true |
| 25 | | var _dest as TextWriter |
| 26 | | var _separator = '; ' |
| 27 | | var _prefix = 'trace: ' |
| 28 | | var _willOutputDirectoryNames = false |
| 29 | | |
| 30 | | def init |
| 31 | | """ |
| 32 | | Initializes the tracer with Console.out as the destination. |
| 33 | | """ |
| 34 | | .init(Console.out) |
| 35 | | |
| 36 | | def init(dest as TextWriter) |
| 37 | | base.init |
| 38 | | _dest = dest |
| 39 | | |
| 40 | | pro isActive from var |
| 41 | | """ |
| 42 | | When false, the `trace` methods will produce no output. |
| 43 | | """ |
| 44 | | |
| 45 | | pro willAutoFlush from var |
| 46 | | """ |
| 47 | | When true, `destination.flush` is invoked after every trace. |
| 48 | | Defaults to `true`. |
| 49 | | """ |
| 50 | | |
| 51 | | pro willOutputDirectoryNames from var |
| 52 | | """ |
| 53 | | When true, full path names with directories are output. |
| 54 | | Otherwise, only the base filename of a source file is output. |
| 55 | | Defaults to `false`. |
| 56 | | """ |
| 57 | | |
| 58 | | pro destination from _dest |
| 59 | | """ |
| 60 | | The TextWriter where all trace output is sent to. |
| 61 | | """ |
| 62 | | |
| 63 | | pro separator from var |
| 64 | | """ |
| 65 | | The separator string used between items of both name/value pairs and source information. |
| 66 | | Default is '; '. |
| 67 | | """ |
| 68 | | |
| 69 | | pro prefix from var |
| 70 | | """ |
| 71 | | The prefix string used at the beginning of every trace. |
| 72 | | Default is 'trace: '. |
| 73 | | """ |
| 74 | | |
| 75 | | def trace(source as SourceSite) |
| 76 | | if .isActive |
| 77 | | print to _dest, '[.prefix][source.oneLiner(.separator, .willOutputDirectoryNames)]' |
| 78 | | if .willAutoFlush |
| 79 | | _dest.flush |
| 80 | | |
| 81 | | def trace(source as SourceSite, nameValues as vari Object) |
| 82 | | require nameValues.length % 2 == 0 |
| 83 | | if .isActive |
| 84 | | _dest.write(.prefix) |
| 85 | | sep = .separator |
| 86 | | i = 0 |
| 87 | | while i < nameValues.length-1 |
| 88 | | name = nameValues[i] to String |
| 89 | | value = CobraImp._techStringMaker.makeString(nameValues[i+1]) |
| 90 | | _dest.write('[name]=[value][sep]') |
| 91 | | i += 2 |
| 92 | | _dest.writeLine(source.oneLiner(.separator, .willOutputDirectoryNames)) |
| 93 | | if .willAutoFlush |
| 94 | | _dest.flush |
| 95 | | |
| 96 | | class Visitor |
| 97 | | is abstract |
| 98 | | """ |
| 99 | | The visitor design pattern is a way of separating an algorithm from an object structure upon |
| 100 | | which it operates. This enables the addition of new operations to existing object structures |
| 101 | | without modifying those structures. [1] |
| 102 | | |
| 103 | | This class implements the visitor pattern, but via reflection so as to not require that the |
| 104 | | classes being visited implement any additional methods for "double dispatch". This approach |
| 105 | | enables less coding and an even greater degree of modularity. It also enables visitation |
| 106 | | directly on final classes, structs, library classes, etc. with no wrapper classes. [2] |
| 107 | | |
| 108 | | In order to leverage this class, you must create a concrete subclass. |
| 109 | | |
| 110 | | Subclasses must override .methodName and then simply implement visitation methods with that |
| 111 | | name and the appropriate parameter types. |
| 112 | | |
| 113 | | When enumerating through subobjects, invoke .dispatch to get type specific dispatch. |
| 114 | | |
| 115 | | If you add a subclass to the class hierarchy that is being visited then you must either |
| 116 | | (1) remember to implement a method in your visitor class for that specific type, or |
| 117 | | (2) be content when a visitation method for one of its ancestor classes is invoked for it. |
| 118 | | |
| 119 | | If you look at the example much further below, you will get a better idea of what to do. |
| 120 | | Another read of the notes after examining the example could be useful. |
| 121 | | |
| 122 | | Alternatives to the visitor pattern: |
| 123 | | |
| 124 | | * Class extensions. You could put multiple, related class extensions in the same file. |
| 125 | | A pro is that this can make the additional functionality feel like a natural part of the |
| 126 | | classes. A con is that state must be passed from method to method. Also, method signatures |
| 127 | | will have to be updated if additional state is added (unless state is grouped into a |
| 128 | | single context/container object). With the visitor pattern, the state can be stored in the |
| 129 | | instance of the visitor. |
| 130 | | |
| 131 | | * Partial classes. These have the same advantages of class extensions, but without the |
| 132 | | disadvantage of having to pass state around. However, the con is that this is not |
| 133 | | available if the classes are external to your project. For example, if the classes come |
| 134 | | from a library or you are writing a plugin for an application, then partial classes are |
| 135 | | not available. (Also, as of 2008-07-07, Cobra has not yet implemented partial classes.) |
| 136 | | |
| 137 | | References: |
| 138 | | |
| 139 | | [1] http://en.wikipedia.org/wiki/Visitor_pattern |
| 140 | | [2] http://www.javaworld.com/javaworld/javatips/jw-javatip98.html |
| 141 | | |
| 142 | | Future: |
| 143 | | |
| 144 | | * Could add a 'strict' mode where if the argument type is not an exact match for the found |
| 145 | | method, an exception is raised. This would help when you don't desire that subclasses are |
| 146 | | silently handled as described above. |
| 147 | | """ |
| 148 | | |
| 149 | | var _willCache = true |
| 150 | | var _methods = Dictionary<of Type, MethodInfo>() |
| 151 | | var _methodName as String |
| 152 | | |
| 153 | | def init |
| 154 | | _methodName = .methodName |
| 155 | | if _methodName[0].isLower |
| 156 | | _methodName = _methodName[0].toUpper.toString + _methodName[1:] |
| 157 | | |
| 158 | | get methodName as String is abstract |
| 159 | | """ |
| 160 | | Returns the method name looked up in .dispatch. |
| 161 | | """ |
| 162 | | # ensure result.length > 0 |
| 163 | | |
| 164 | | def dispatch(obj as Object?) |
| 165 | | """ |
| 166 | | Performs type specific dispatch on the given object. |
| 167 | | A subclass can override this method to change the dispatch logic. |
| 168 | | """ |
| 169 | | if obj is nil, return |
| 170 | | objType = obj.getType |
| 171 | | methInfo as MethodInfo? |
| 172 | | if _willCache, _methods.tryGetValue(objType, out methInfo) |
| 173 | | if methInfo is nil |
| 174 | | methInfo = .getType.getMethod(_methodName, @[objType]) |
| 175 | | if methInfo is nil |
| 176 | | throw InvalidOperationException('Cannot find a method "[_methodName]" with arg type "[objType]"') |
| 177 | | _methods[objType] = methInfo to ! |
| 178 | | methInfo.invoke(this, @[obj]) # would be nice to speed this up |
| 179 | | |
| 180 | | def dispatch(objects as System.Collections.IEnumerable) |
| 181 | | """ |
| 182 | | Performs type specific dispatch on each object in order. |
| 183 | | """ |
| 184 | | for obj in objects, .dispatch(obj) |
| 185 | | |
| 836 | | class HtmlExceptionReportWriter |
| 837 | | inherits ExceptionReportWriter |
| 838 | | |
| 839 | | var _tw as TextWriter? |
| 840 | | var _dumpObjectCount as int |
| 841 | | var _maxDumpObjectCount = 250 |
| 842 | | |
| 843 | | def init |
| 844 | | .maxDumpObjectCount = CobraCore.maxDumpObjectCount |
| 845 | | |
| 846 | | get tw from var |
| 847 | | |
| 848 | | pro maxDumpObjectCount from var |
| 849 | | """ |
| 850 | | Controls the maximum number of objects dumped in the exception report. |
| 851 | | Defaults to 250 which can easily result in a 5MB exception report. |
| 852 | | """ |
| 853 | | |
| 854 | | def writeReport(tw as TextWriter, exc as Exception?) |
| 855 | | base.writeReport(tw, exc) |
| 856 | | Console.out.writeLine |
| 857 | | |
| 858 | | def writeReport(tw as TextWriter, exc as Exception?, frames as Stack<of CobraFrame>?) is override |
| 859 | | # dump the most recent stack frames first since the HTML file will be displayed at the top in the browser |
| 860 | | |
| 861 | | _tw = tw |
| 862 | | _dumpObjectCount = 0 |
| 863 | | |
| 864 | | tw.writeLine('<html>') |
| 865 | | tw.writeLine('<head>') |
| 866 | | exePath = CobraCore.findCobraExe |
| 867 | | if exePath |
| 868 | | path = Path.combine(Path.getDirectoryName(exePath), 'styles-exception-report.css') |
| 869 | | path = 'file://' + path.replace(Path.directorySeparatorChar, c'/') |
| 870 | | tw.writeLine('<link href="[path]" rel=stylesheet type="text/css">') |
| 871 | | tw.writeLine('<link href=styles-exception-report.css rel=stylesheet type="text/css">') |
| 872 | | tw.writeLine('<meta http-equiv="content-type" content="text/html; charset=utf-8">') |
| 873 | | tw.writeLine('</head>') |
| 874 | | tw.writeLine('<body>') |
| 875 | | tw.writeLine('<div class=sstHeading>Cobra Exception Report</div>') |
| 876 | | |
| 877 | | tw.writeLine('<div class=topLinks>') |
| 878 | | tw.writeLine('<a href=http://Cobra-Language.com/>Cobra</a> ') |
| 879 | | tw.writeLine('<a href=http://Cobra-Language.com/downloads/>Downloads</a> ') |
| 880 | | tw.writeLine('<a href=http://Cobra-Language.com/docs/>Docs</a> ') |
| 881 | | tw.writeLine('<a href=http://Cobra-Language.com/how-to/>How To</a> ') |
| 882 | | tw.writeLine('<a href=http://Cobra-Language.com/samples/>Samples</a> ') |
| 883 | | tw.writeLine('<a href=http://CobraLang.BlogSpot.com/>Blog</a> ') |
| 884 | | tw.writeLine('<a href=http://Cobra-Language.com/forums/>Discussion</a> ') |
| 885 | | tw.writeLine('<a href=http://Cobra-Language.com/source/>Source</a> ') |
| 886 | | tw.writeLine('<a href=http://Cobra-Language.com/docs/contact/>Contact</a>') |
| 887 | | tw.writeLine('</div>') |
| 888 | | |
| 889 | | tw.writeLine('<div class=section>') |
| 890 | | tw.writeLine('<div class=title>Header</div>') |
| 891 | | tw.writeLine('<table class=keyValues border=0 cellpadding=1 cellspacing=1>') |
| 892 | | if CobraCore.isRunningOnMono |
| 893 | | # Mono 1.9 on Mac OS X 10.4 chokes so hard on ProcessName that it won't even throw an exception |
| 894 | | name = '' |
| 895 | | else |
| 896 | | name = System.Diagnostics.Process.getCurrentProcess.processName |
| 897 | | if name.endsWith('mono') |
| 898 | | for part in Environment.commandLine.split(c' ') |
| 899 | | if part.endsWith('.exe') |
| 900 | | name = Path.getFileName(part) to ! # CC: method should have: ensure arg and arg.length implies result; um, that'll be awhile to both put in and interpret at compile time! |
| 901 | | break |
| 902 | | if name <> '' |
| 903 | | _headerPair('Program', name) |
| 904 | | _headerPair('When', DateTime.now) |
| 905 | | _headerPair('Command Line', Environment.commandLine) |
| 906 | | _headerPair('Current Directory', Environment.currentDirectory) |
| 907 | | _headerPair('Machine Name', Environment.machineName) |
| 908 | | _headerPair('Cobra', CobraCore.version) |
| 909 | | _headerPair('CLR', Environment.version) |
| 910 | | if Environment.workingSet to decimal # CC: axe cast. workingSet is really C# 'long' or Cobra 'int64' |
| 911 | | _headerPair('Working Set', Environment.workingSet) |
| 912 | | tw.writeLine('</table>') |
| 913 | | tw.writeLine('</div> <!-- section -->') |
| 914 | | |
| 915 | | objects = ObjectCatalog() |
| 916 | | |
| 917 | | if exc |
| 918 | | tw.writeLine('<div class=section>') |
| 919 | | tw.writeLine('<div class=title>Exception</div>') |
| 920 | | objects.record(exc) |
| 921 | | .dumpObjectAsHtml(objects.serialNumFor(exc), exc, objects) |
| 922 | | |
| 923 | | startingSerialNum = objects.minSerialNum |
| 924 | | tw.writeLine('<div class=section>') |
| 925 | | tw.writeLine('<div class=title>Stack Frames</div>') |
| 926 | | if frames is nil |
| 927 | | tw.writeLine('<p>There is no detailed stack trace. You can turn this on with "cobra -dst ..." and see significantly more information about the state of the program including the details of every argument and local variable. There is a performance cost, but the slowdown is likely worth it if you are unable to diagnose this problem.</p>') |
| 928 | | else |
| 929 | | if not frames.count |
| 930 | | tw.writeLine('<p>No stack frames.</p>') |
| 931 | | else |
| 932 | | frameList = List<of CobraFrame>(frames) |
| 933 | | frameList.reverse |
| 934 | | tw.writeLine('<table class=stack border=0 cellpadding=2 cellspacing=0>') |
| 935 | | i = 0 |
| 936 | | for frame in frameList |
| 937 | | frame.dumpHtml(tw, i, objects, ref .willDumpHtmlFor) |
| 938 | | i += 1 |
| 939 | | tw.writeLine('</table>') |
| 940 | | tw.writeLine('</div> <!-- section -->') |
| 941 | | |
| 942 | | tw.writeLine('<div class=section>') |
| 943 | | tw.writeLine('<div class=title>Objects</div>') |
| 944 | | serialNum = if(startingSerialNum > 0, startingSerialNum+1, objects.minSerialNum) |
| 945 | | while objects.contains(serialNum) and _dumpObjectCount <= _maxDumpObjectCount |
| 946 | | .dumpObjectAsHtml(serialNum, objects.objectFor(serialNum), objects) |
| 947 | | serialNum += 1 |
| 948 | | tw.writeLine('</div> <!-- section -->') |
| 949 | | |
| 950 | | tw.writeLine('</body>') |
| 951 | | tw.writeLine('</html>') |
| 952 | | |
| 953 | | def _headerPair(key as String, value as Object?) |
| 954 | | key = .htmlEncode(key) |
| 955 | | value ?= '' |
| 956 | | value = .htmlEncode(value.toString) |
| 957 | | .tw.writeLine('<tr class=keyValue> <td class=key> [key] </td> <td> = </td> <td class=value> [value] </td> </tr>') |
| 958 | | |
| 959 | | def htmlEncode(obj as Object) as String |
| 960 | | return CobraCore.htmlEncode(obj) |
| 961 | | |
| 962 | | def htmlEncode(s as String) as String |
| 963 | | return CobraCore.htmlEncode(s) |
| 964 | | |
| 965 | | def htmlFormat(s as String) as Html |
| 966 | | s = .htmlEncode(s) |
| 967 | | # re = Regex(r'\n[ ]+', ... # CC: ack, no delegates so can't get length |
| 968 | | sb = StringBuilder(s.length*2) |
| 969 | | state = 0 |
| 970 | | for c in s |
| 971 | | branch state |
| 972 | | on 0 |
| 973 | | sb.append(c) |
| 974 | | if c == c'\n' |
| 975 | | state = 1 |
| 976 | | on 1 |
| 977 | | if c == c' ' |
| 978 | | sb.append(' ') |
| 979 | | else if c == c'\n' |
| 980 | | sb.append(c) |
| 981 | | else |
| 982 | | sb.append(c) |
| 983 | | state = 0 |
| 984 | | s = sb.toString |
| 985 | | s = s.replace('\r', '').replace('\n', '<br>') |
| 986 | | return Html(s) |
| 987 | | |
| 988 | | def willDumpHtmlFor(obj as Object?) as bool |
| 989 | | if obj is nil |
| 990 | | return false |
| 991 | | if obj inherits int |
| 992 | | return false |
| 993 | | # TODO: int64 and other int sizes |
| 994 | | if obj inherits String, return false |
| 995 | | if obj inherits decimal or obj inherits float, return false |
| 996 | | if obj inherits bool or obj inherits char, return false |
| 997 | | if obj inherits Html, return false |
| 998 | | if obj inherits CobraDirectString, return false |
| 999 | | if obj.getType.isEnum, return false |
| 1000 | | fullName = obj.getType.fullName |
| 1001 | | if fullName == 'System.Security.Policy.Evidence' # 2007-07-11 CE: causes problems, at least on mono 1.2.4 |
| 1002 | | return false |
| 1003 | | if fullName == 'System.IntPtr' # not interesting. # TODO: skip any type that has zero properties and does not implement custom exception reporting |
| 1004 | | return false |
| 1005 | | return true |
| 1006 | | |
| 1007 | | def willDumpHtmlForConservative(obj as Object?) as bool |
| 1008 | | if not .willDumpHtmlFor(obj), return false |
| 1009 | | if obj.getType.isNested, return false |
| 1010 | | if 'IEquatableOf' in obj.getType.name, return false |
| 1011 | | return true |
| 1012 | | |
| 1013 | | def dumpObjectAsHtml(serialNum as int, obj as Object, objects as ObjectCatalog?) |
| 1014 | | tw = .tw |
| 1015 | | if _dumpObjectCount % 10 == 0 # TODO: change 10 to a public property. value < 1 means not to write progress |
| 1016 | | Console.out.write('.') |
| 1017 | | Console.out.flush |
| 1018 | | _dumpObjectCount += 1 |
| 1019 | | if objects |
| 1020 | | objects.record(obj) |
| 1021 | | tw.writeLine('<a name=Object[serialNum]></a>') |
| 1022 | | tw.writeLine('<table class=object border=0 cellpadding=0 cellspacing=0>') |
| 1023 | | tw.writeLine('<tr class=objectTitle> <td class=objectTitle colspan=2> [.htmlEncode(CobraCore.typeName(obj.getType))] <font size=-1>([serialNum])</font> </td> </tr>') |
| 1024 | | try |
| 1025 | | value = obj.toString to Object? |
| 1026 | | catch exc as Exception |
| 1027 | | value = 'Caught exception while reading value: [exc.toString]' |
| 1028 | | row = 1 |
| 1029 | | if obj inherits IHasSourceSite |
| 1030 | | .writeObjectPair1(row, 'at', obj.sourceSite.htmlForAt) |
| 1031 | | row += 1 |
| 1032 | | .writeObjectPair1(row, 'toString', .htmlFormat(value.toString)) |
| 1033 | | propInfos = List<of PropertyInfo>(obj.getType.getProperties) |
| 1034 | | propInfos.sort(ref .comparePropertyInfoNames) # CC: propInfos.sort(def(a as PropertyInfo, b as PropertyInfo)=a.name.compareTo(b.name)) |
| 1035 | | for propInfo in propInfos |
| 1036 | | if propInfo.canRead and not propInfo.getGetMethod.isStatic and propInfo.name not in ['Clone', 'Copy', 'Item'] |
| 1037 | | if propInfo.name == 'MetadataToken' # problems on mono. http://bugzilla.ximian.com/show_bug.cgi?id=82161 |
| 1038 | | value = '(SKIPPED)' |
| 1039 | | else |
| 1040 | | try |
| 1041 | | value = propInfo.getValue(obj, nil) |
| 1042 | | catch exc as Exception |
| 1043 | | value = .htmlFormat('Caught exception while reading value: [exc.toString]') |
| 1044 | | success |
| 1045 | | value = .htmlForValue(value, objects) |
| 1046 | | .writeObjectPair1(row%2+1, propInfo.name, value) |
| 1047 | | row += 1 |
| 1048 | | extendMethod = obj.getType.getMethod('ExtendObjectTable') |
| 1049 | | if extendMethod |
| 1050 | | view = PrivateObjectViewForExceptionReport(this, row, objects) |
| 1051 | | extendMethod.invoke(obj, @[view]) |
| 1052 | | i = view.rowNum |
| 1053 | | # TODO: ack! cannot do this until qualified type problems are fixed up |
| 1054 | | #if obj inherits System.Collections.IList |
| 1055 | | if obj.getType.name.startsWith('List`') |
| 1056 | | dobj = obj to dynamic |
| 1057 | | count = dobj.count to int |
| 1058 | | for j = 0 .. count |
| 1059 | | try |
| 1060 | | value = dobj[j] |
| 1061 | | catch exc as Exception |
| 1062 | | value = .htmlFormat('Caught exception while reading indexed value [j]: [exc.toString]') |
| 1063 | | success |
| 1064 | | value = .htmlForValue(value, objects) |
| 1065 | | .writeObjectPair1(i%2+1, r'[' + '[j]]', value) |
| 1066 | | i += 1 |
| 1067 | | else if obj.getType.name.startsWith('Dictionary') # TODO: use if implements/inherits |
| 1068 | | dobj = obj to dynamic |
| 1069 | | for key in dobj.keys |
| 1070 | | htmlKey as dynamic? |
| 1071 | | try |
| 1072 | | htmlKey = CobraCore.toTechString(key) |
| 1073 | | catch exc as Exception |
| 1074 | | htmlKey = .htmlFormat('Caught exception for toTechString(key): [exc.toString]') |
| 1075 | | success |
| 1076 | | htmlKey = .htmlEncode(htmlKey) |
| 1077 | | try |
| 1078 | | value = dobj[key] |
| 1079 | | catch exc as Exception |
| 1080 | | value = .htmlEncode('Caught exception while reading keyed value [htmlKey]: [exc.toString]') |
| 1081 | | success |
| 1082 | | value = .htmlForValue(value, objects) |
| 1083 | | .writeObjectPair1(i%2+1, r'[' + htmlKey + ']', value) |
| 1084 | | i += 1 |
| 1085 | | |
| 1086 | | tw.writeLine('</table>') |
| 1087 | | |
| 1088 | | def comparePropertyInfoNames(a as PropertyInfo, b as PropertyInfo) as int |
| 1089 | | return a.name.compareTo(b.name) |
| 1090 | | |
| 1091 | | def htmlForValue(value as dynamic?, objects as ObjectCatalog?) as dynamic |
| 1092 | | """ |
| 1093 | | Returns the .toTechString of the value, encoded for HTML. |
| 1094 | | Gracefully handles exceptions and also creates links to objects. |
| 1095 | | """ |
| 1096 | | if value inherits Html # this feature is used by AssertException, but it's interesting that this would then make Html() objects less recognizable when they show up in the report as a normal part of the program that failed |
| 1097 | | return value |
| 1098 | | s as dynamic? |
| 1099 | | try |
| 1100 | | try |
| 1101 | | s = CobraCore.toTechString(value) |
| 1102 | | catch exc as Exception |
| 1103 | | s = .htmlFormat('Caught exception while converting value toTechString: [exc.toString]') |
| 1104 | | success |
| 1105 | | s = .htmlFormat(s to !) |
| 1106 | | if value and objects |
| 1107 | | willLink = false |
| 1108 | | if objects.contains(value to passthrough) |
| 1109 | | willLink = true |
| 1110 | | else if .willDumpHtmlForConservative(value) and objects |
| 1111 | | objects.record(value to passthrough) |
| 1112 | | willLink = true |
| 1113 | | if willLink |
| 1114 | | s = '<a class=objectDetails href=#Object[objects.serialNumFor(value to passthrough)]>[s]</a>' |
| 1115 | | catch topExc as SystemException # for example, System.Security.Policy.Evidence as a dictionary key causes problems (at least on mono 1.2.4) |
| 1116 | | try |
| 1117 | | s = value.toString |
| 1118 | | catch toStringExc as Exception |
| 1119 | | s = '(htmlForValue: Exception during toString: [toStringExc];[Environment.newLine]Exception during processing: [topExc])' |
| 1120 | | success |
| 1121 | | s = '(htmlForValue: value=[s]; Exception during processing: [topExc])' |
| 1122 | | try |
| 1123 | | s = .htmlFormat(s to !) |
| 1124 | | catch Exception |
| 1125 | | pass # forget it |
| 1126 | | return s to ! |
| 1127 | | |
| 1128 | | def writeObjectPair1(i as int, key as String, valueHtml as Object?) is internal |
| 1129 | | require |
| 1130 | | i == 1 or i == 2 |
| 1131 | | key.length |
| 1132 | | body |
| 1133 | | key = key[0].toString.toLower + key[1:] |
| 1134 | | key = .htmlEncode(key) |
| 1135 | | .writeObjectPair2(i, key, valueHtml) |
| 1136 | | |
| 1137 | | def writeObjectPair2(i as int, preFormattedKey as String, valueHtml as Object?) is internal |
| 1138 | | require |
| 1139 | | i == 1 or i == 2 |
| 1140 | | preFormattedKey.length |
| 1141 | | body |
| 1142 | | .tw.writeLine('<tr class=keyValue[i]> <td class=key[i]> [preFormattedKey] </td> <td class=value[i]> [valueHtml ? ''] </td> </tr>') |
| 1143 | | |
| 1144 | | interface IObjectView |
| 1145 | | """ |
| 1146 | | TODO: document this |
| 1147 | | """ |
| 1148 | | |
| 1149 | | def addEntry(key as String, value as Object?) |
| 1150 | | |
| 1151 | | class PrivateObjectViewForExceptionReport |
| 1152 | | is internal |
| 1153 | | implements IObjectView |
| 1154 | | |
| 1155 | | var _reportWriter as HtmlExceptionReportWriter |
| 1156 | | var _rowNum as int |
| 1157 | | var _objects as ObjectCatalog? |
| 1158 | | |
| 1159 | | def init(reportWriter as HtmlExceptionReportWriter, rowNum as int, objects as ObjectCatalog?) |
| 1160 | | _reportWriter = reportWriter |
| 1161 | | _rowNum = rowNum |
| 1162 | | _objects = objects |
| 1163 | | |
| 1164 | | get rowNum from var |
| 1165 | | |
| 1166 | | def addEntry(key as String, value as Object?) |
| 1167 | | key = _reportWriter.htmlEncode(key).replace(' ', ' ') # the double space replace here works well with the indentation scheme used in AssertException |
| 1168 | | html = _reportWriter.htmlForValue(value, _objects) |
| 1169 | | _reportWriter.writeObjectPair2(_rowNum%2+1, key, html) |
| 1170 | | _rowNum += 1 |
| 1171 | | |
| 1172 | | sig WillDumpHtmlForMethod(value as dynamic?) as bool |
| 1173 | | |