Not logged in.  Login/Logout/Register | List snippets | | Create snippet | Upload image | Upload data

682
LINES

< > BotCompany Repo | #1027605 // unstructure (v15, new custom deserialization)

JavaX fragment (include) [tags: use-pretranspiled]

Libraryless. Click here for Pure Java version (4813L/29K).

1  
static Object unstructure(String text) {
2  
  ret unstructure(text, false);
3  
}
4  
5  
static Object unstructure(String text, final boolean allDynamic) {
6  
  ret unstructure(text, allDynamic, null);
7  
}
8  
9  
static int structure_internStringsLongerThan = 50;
10  
static int unstructure_unquoteBufSize = 100;
11  
12  
static int unstructure_tokrefs; // stats
13  
14  
abstract sclass unstructure_Receiver {
15  
  abstract void set(O o);
16  
}
17  
18  
// classFinder: func(name) -> class (optional)
19  
static Object unstructure(String text, boolean allDynamic,
20  
  O classFinder) {
21  
  if (text == null) ret null;
22  
  ret unstructure_tok(javaTokC_noMLS_iterator(text), allDynamic, classFinder);
23  
}
24  
25  
static O unstructure_reader(BufferedReader reader) {
26  
  ret unstructure_tok(javaTokC_noMLS_onReader(reader), false, null);
27  
}
28  
29  
static O unstructure_tok(final Producer<S> tok, final boolean allDynamic, final O _classFinder) {
30  
  final boolean debug = unstructure_debug;
31  
  
32  
  final class X {
33  
    int i = -1;
34  
    final O classFinder = _classFinder != null ? _classFinder : _defaultClassFinder();
35  
    new HashMap<Integer, O> refs;
36  
    new HashMap<Integer, O> tokrefs;
37  
    new HashSet<S> concepts;
38  
    new HashMap<S, Class> classesMap;
39  
    new L<Runnable> stack;
40  
    S curT;
41  
    char[] unquoteBuf = new char[unstructure_unquoteBufSize];
42  
    
43  
    S unquote(S s) {
44  
      ret unquoteUsingCharArray(s, unquoteBuf); 
45  
    }
46  
47  
    // look at current token
48  
    S t() {
49  
      ret curT;
50  
    }
51  
    
52  
    // get current token, move to next
53  
    S tpp() {
54  
      S t = curT;
55  
      consume();
56  
      ret t;
57  
    }
58  
    
59  
    void parse(final unstructure_Receiver out) {
60  
      S t = t();
61  
      
62  
      int refID = 0;
63  
      if (structure_isMarker(t, 0, l(t))) {
64  
        refID = parseInt(t.substring(1));
65  
        consume();
66  
      }
67  
      final int _refID = refID;
68  
      
69  
      // if (debug) print("parse: " + quote(t));
70  
      
71  
      final int tokIndex = i;  
72  
      parse_inner(refID, tokIndex, new unstructure_Receiver {
73  
        void set(O o) {
74  
          if (_refID != 0)
75  
            refs.put(_refID, o);
76  
          if (o != null)
77  
            tokrefs.put(tokIndex, o);
78  
          out.set(o);
79  
        }
80  
      });
81  
    }
82  
    
83  
    void parse_inner(int refID, int tokIndex, final unstructure_Receiver out) {
84  
      S t = t();
85  
      
86  
      // if (debug) print("parse_inner: " + quote(t));
87  
      
88  
      Class c = classesMap.get(t);
89  
      if (c == null) {
90  
        if (t.startsWith("\"")) {
91  
          S s = internIfLongerThan(unquote(tpp()), structure_internStringsLongerThan);
92  
          out.set(s); ret;
93  
        }
94  
        
95  
        if (t.startsWith("'")) {
96  
          out.set(unquoteCharacter(tpp())); ret;
97  
        }
98  
        if (t.equals("bigint")) {
99  
          out.set(parseBigInt()); ret;
100  
        }
101  
        if (t.equals("d")) {
102  
          out.set(parseDouble()); ret;
103  
        }
104  
        if (t.equals("fl")) {
105  
          out.set(parseFloat()); ret;
106  
        }
107  
        if (t.equals("sh")) {
108  
          consume();
109  
          t = tpp();
110  
          if (t.equals("-")) {
111  
            t = tpp();
112  
            out.set((short) (-parseInt(t)); ret;
113  
          }
114  
          out.set((short) parseInt(t)); ret;
115  
        }
116  
        if (t.equals("-")) {
117  
          consume();
118  
          t = tpp();
119  
          out.set(isLongConstant(t) ? (O) (-parseLong(t)) : (O) (-parseInt(t))); ret;
120  
        }
121  
        if (isInteger(t) || isLongConstant(t)) {
122  
          consume();
123  
          //if (debug) print("isLongConstant " + quote(t) + " => " + isLongConstant(t));
124  
          if (isLongConstant(t)) {
125  
            out.set(parseLong(t)); ret;
126  
          }
127  
          long l = parseLong(t);
128  
          bool isInt = l == (int) l;
129  
          ifdef unstructure_debug
130  
            print("l=" + l + ", isInt: " + isInt);
131  
          endifdef
132  
          out.set(isInt ? (O) Integer.valueOf((int) l) : (O) Long.valueOf(l)); ret;
133  
        }
134  
        if (t.equals("false") || t.equals("f")) {
135  
          consume(); out.set(false); ret;
136  
        }
137  
        if (t.equals("true") || t.equals("t")) {
138  
          consume(); out.set(true); ret;
139  
        }
140  
        if (t.equals("-")) {
141  
          consume();
142  
          t = tpp();
143  
          out.set(isLongConstant(t) ? (O) (-parseLong(t)) : (O) (-parseInt(t))); ret;
144  
        }
145  
        if (isInteger(t) || isLongConstant(t)) {
146  
          consume();
147  
          //if (debug) print("isLongConstant " + quote(t) + " => " + isLongConstant(t));
148  
          if (isLongConstant(t)) {
149  
            out.set(parseLong(t)); ret;
150  
          }
151  
          long l = parseLong(t);
152  
          bool isInt = l == (int) l;
153  
          ifdef unstructure_debug
154  
            print("l=" + l + ", isInt: " + isInt);
155  
          endifdef
156  
          out.set(isInt ? (O) Integer.valueOf((int) l) : (O) Long.valueOf(l)); ret;
157  
        }
158  
        
159  
        if (t.equals("File")) {
160  
          consume();
161  
          File f = new File(unquote(tpp()));
162  
          out.set(f); ret;
163  
        }
164  
        
165  
        if (t.startsWith("r") && isInteger(t.substring(1))) {
166  
          consume();
167  
          int ref = Integer.parseInt(t.substring(1));
168  
          O o = refs.get(ref);
169  
          if (o == null)
170  
            fail("unsatisfied back reference " + ref);
171  
          out.set(o); ret;
172  
        }
173  
      
174  
        if (t.startsWith("t") && isInteger(t.substring(1))) {
175  
          consume();
176  
          int ref = Integer.parseInt(t.substring(1));
177  
          O o = tokrefs.get(ref);
178  
          if (o == null)
179  
            fail("unsatisfied token reference " + ref + " at " + tokIndex);
180  
          out.set(o); ret;
181  
        }
182  
        
183  
        if (t.equals("hashset")) ret with parseHashSet(out);
184  
        if (t.equals("lhs")) ret with parseLinkedHashSet(out);
185  
        if (t.equals("treeset")) ret with parseTreeSet(out);
186  
        if (t.equals("ciset")) ret with parseCISet(out);
187  
        
188  
        if (eqOneOf(t, "hashmap", "hm")) {
189  
          consume();
190  
          parseMap(new HashMap, out);
191  
          ret;
192  
        }
193  
        if (t.equals("lhm")) {
194  
          consume();
195  
          parseMap(new LinkedHashMap, out);
196  
          ret;
197  
        }
198  
        if (t.equals("tm")) {
199  
          consume();
200  
          parseMap(new TreeMap, out);
201  
          ret;
202  
        }
203  
        if (t.equals("cimap")) {
204  
          consume();
205  
          parseMap(ciMap(), out);
206  
          ret;
207  
        }
208  
        
209  
        if (t.equals("ll")) {
210  
          consume();
211  
          ret with parseList(new LinkedList, out);
212  
        }
213  
214  
        if (t.equals("syncLL")) { // legacy
215  
          consume();
216  
          ret with parseList(synchroLinkedList(), out);
217  
        }
218  
219  
        if (t.equals("sync")) {
220  
          consume();
221  
          ret with parse(new unstructure_Receiver {
222  
            void set(O value) {
223  
              if (value instanceof Map) {
224  
                ifndef Android // Java 7
225  
                if (value instanceof NavigableMap)
226  
                  ret with out.set(Collections.synchronizedNavigableMap((NavigableMap) value));
227  
                endifndef
228  
                if (value instanceof SortedMap)
229  
                  ret with out.set(Collections.synchronizedSortedMap((SortedMap) value));
230  
                ret with out.set(Collections.synchronizedMap((Map) value));
231  
              } else
232  
                ret with out.set(Collections.synchronizedList((L) value);
233  
            }
234  
          });
235  
        }
236  
        
237  
        if (t.equals("{")) {
238  
          parseMap(out); ret;
239  
        }
240  
        if (t.equals("[")) {
241  
          this.parseList(new ArrayList, out); ret;
242  
        }
243  
        if (t.equals("bitset")) {
244  
          parseBitSet(out); ret;
245  
        }
246  
        if (t.equals("array") || t.equals("intarray") || t.equals("dblarray")) {
247  
          parseArray(out); ret;
248  
        }
249  
        if (t.equals("ba")) {
250  
          consume();
251  
          S hex = unquote(tpp());
252  
          out.set(hexToBytes(hex)); ret;
253  
        }
254  
        if (t.equals("boolarray")) {
255  
          consume();
256  
          int n = parseInt(tpp());
257  
          S hex = unquote(tpp());
258  
          out.set(boolArrayFromBytes(hexToBytes(hex), n)); ret;
259  
        }
260  
        if (t.equals("class")) {
261  
          out.set(parseClass()); ret;
262  
        }
263  
        if (t.equals("l")) {
264  
          parseLisp(out); ret;
265  
        }
266  
        if (t.equals("null")) {
267  
          consume(); out.set(null); ret;
268  
        }
269  
        
270  
        if (eq(t, "c")) {
271  
          consume();
272  
          t = t();
273  
          assertTrue(isJavaIdentifier(t));
274  
          concepts.add(t);
275  
        }
276  
        
277  
        // custom deserialization (new static method method)
278  
        if (eq(t, "cu")) {
279  
          consume();
280  
          t = tpp();
281  
          assertTrue(isJavaIdentifier(t));
282  
          S fullClassName = "main$" + t;
283  
          Class _c = classFinder != null ? (Class) callF(classFinder, fullClassName) : findClass_fullName(fullClassName);
284  
          if (_c == null) fail("Class not found: " + fullClassName);
285  
          parse(new unstructure_Receiver {
286  
            void set(O value) {
287  
              ifdef unstructure_debug
288  
                print("Consumed custom object, next token: " + t());
289  
              endifdef
290  
              out.set(call(_c, "_deserialize", value);
291  
            }
292  
          });
293  
          ret;
294  
        }
295  
      }
296  
      
297  
      if (eq(t, "j")) {
298  
        consume("j");
299  
        out.set(parseJava()); ret;
300  
      }
301  
302  
      if (c == null && !isJavaIdentifier(t))
303  
        throw new RuntimeException("Unknown token " + (i+1) + ": " + quote(t));
304  
        
305  
      // any other class name (or package name)
306  
      consume();
307  
      S className, fullClassName;
308  
      
309  
      // Is it a package name?
310  
      if (eq(t(), ".")) {
311  
        consume();
312  
        className = fullClassName = t + "." + assertIdentifier(tpp());
313  
      } else {
314  
        className = t;
315  
        fullClassName = "main$" + t;
316  
      }
317  
      
318  
      if (c == null) {
319  
        // First, find class
320  
        if (allDynamic) c = null;
321  
        else c = classFinder != null ? (Class) callF(classFinder, fullClassName) : findClass_fullName(fullClassName);
322  
        if (c != null)
323  
          classesMap.put(className, c);
324  
      }
325  
          
326  
      // Check if it has an outer reference
327  
      bool hasBracket = eq(t(), "(");
328  
      if (hasBracket) consume();
329  
      bool hasOuter = hasBracket && eq(t(), "this$1");
330  
      
331  
      DynamicObject dO = null;
332  
      O o = null;
333  
      fS thingName = t;
334  
      if (c != null) {
335  
        o = hasOuter ? nuStubInnerObject(c, classFinder) : nuEmptyObject(c);
336  
        if (o instanceof DynamicObject) dO = (DynamicObject) o;
337  
      } else {
338  
        if (concepts.contains(t) && (c = findClass("Concept")) != null)
339  
          o = dO = (DynamicObject) nuEmptyObject(c);
340  
        else
341  
          dO = new DynamicObject;
342  
        dO.className = className;
343  
        ifdef unstructure_debug
344  
          print("Made dynamic object " + t + " " + shortClassName(dO));
345  
        endifdef
346  
      }
347  
      
348  
      // Save in references list early because contents of object
349  
      // might link back to main object
350  
      
351  
      if (refID != 0)
352  
        refs.put(refID, o != null ? o : dO);
353  
      tokrefs.put(tokIndex, o != null ? o : dO);
354  
      
355  
      // NOW parse the fields!
356  
      
357  
      final new LinkedHashMap<S, O> fields; // preserve order
358  
      final O _o = o;
359  
      final DynamicObject _dO = dO;
360  
      if (hasBracket) {
361  
        stack.add(r {
362  
          ifdef unstructure_debug
363  
            print("in object values, token: " + t());
364  
          endifdef
365  
          if (eq(t(), ",")) consume();
366  
          if (eq(t(), ")")) {
367  
            consume(")");
368  
            objRead(_o, _dO, fields, hasOuter);
369  
            out.set(_o != null ? _o : _dO);
370  
          } else {
371  
            final S key = unquote(tpp());
372  
            S t = tpp();
373  
            if (!eq(t, "="))
374  
              fail("= expected, got " + t + " after " + quote(key) + " in object " + thingName /*+ " " + sfu(fields)*/);
375  
            stack.add(this);
376  
            parse(new unstructure_Receiver {
377  
              void set(O value) {
378  
                fields.put(key, value);
379  
                /*ifdef unstructure_debug
380  
                  print("Got field value " + value + ", next token: " + t());
381  
                endifdef*/
382  
                //if (eq(t(), ",")) consume();
383  
              }
384  
            });
385  
          }
386  
        });
387  
      } else {
388  
        objRead(o, dO, fields, hasOuter);
389  
        out.set(o != null ? o : dO);
390  
      }
391  
    }
392  
    
393  
    void objRead(O o, DynamicObject dO, Map<S, O> fields, bool hasOuter) {
394  
      ifdef unstructure_debug
395  
      print("objRead " + className(o) + " " + className(dO) + " " + struct(fields));
396  
      endifdef
397  
      if (o != null) {
398  
        if (dO != null) {
399  
          ifdef unstructure_debug
400  
            printStructure("setOptAllDyn", fields);
401  
          endifdef
402  
          setOptAllDyn_pcall(dO, fields);
403  
        } else {
404  
          setOptAll_pcall(o, fields);
405  
          ifdef unstructure_debug
406  
            print("objRead now: " + struct(o));
407  
          endifdef
408  
        }
409  
        if (hasOuter)
410  
          fixOuterRefs(o);
411  
      } else for (Map.Entry<S, O> e : fields.entrySet())
412  
        setDynObjectValue(dO, intern(e.getKey()), e.getValue());
413  
414  
      if (o != null)
415  
        pcallOpt_noArgs(o, "_doneLoading");
416  
    }
417  
    
418  
    void parseSet(final Set set, final unstructure_Receiver out) {
419  
      this.parseList(new ArrayList, new unstructure_Receiver {
420  
        void set(O o) {
421  
          set.addAll((L) o);
422  
          out.set(set);
423  
        }
424  
      });
425  
    }
426  
    
427  
    void parseLisp(final unstructure_Receiver out) {
428  
      ifclass Lisp
429  
        consume("l");
430  
        consume("(");
431  
        final new ArrayList list;
432  
        stack.add(r {
433  
          if (eq(t(), ")")) {
434  
            consume(")");
435  
            out.set(Lisp((S) list.get(0), subList(list, 1)));
436  
          } else {
437  
            stack.add(this);
438  
            parse(new unstructure_Receiver {
439  
              void set(O o) {
440  
                list.add(o);
441  
                if (eq(t(), ",")) consume();
442  
              }
443  
            });
444  
          }
445  
        });
446  
        if (false) // skip fail line
447  
      endif
448  
      
449  
      fail("class Lisp not included");
450  
    }
451  
    
452  
    void parseBitSet(final unstructure_Receiver out) {
453  
      consume("bitset");
454  
      consume("{");
455  
      final new BitSet bs;
456  
      stack.add(r {
457  
        if (eq(t(), "}")) {
458  
          consume("}");
459  
          out.set(bs);
460  
        } else {
461  
          stack.add(this);
462  
          parse(new unstructure_Receiver {
463  
            void set(O o) {
464  
              bs.set((Integer) o);
465  
              if (eq(t(), ",")) consume();
466  
            }
467  
          });
468  
        }
469  
      });
470  
    }
471  
    
472  
    void parseList(final L list, final unstructure_Receiver out) {
473  
      tokrefs.put(i, list);
474  
      consume("[");
475  
      stack.add(r {
476  
        if (eq(t(), "]")) {
477  
          consume();
478  
          ifdef unstructure_debug
479  
            print("Consumed list, next token: " + t());
480  
          endifdef
481  
          out.set(list);
482  
        } else {
483  
          stack.add(this);
484  
          parse(new unstructure_Receiver {
485  
            void set(O o) {
486  
              //if (debug) print("List element type: " + getClassName(o));
487  
              list.add(o);
488  
              if (eq(t(), ",")) consume();
489  
            }
490  
          });
491  
        }
492  
      });
493  
    }
494  
    
495  
    void parseArray(final unstructure_Receiver out) {
496  
      final S type = tpp();
497  
      consume("{");
498  
      final List list = new ArrayList;
499  
      
500  
      stack.add(r {
501  
        if (eq(t(), "}")) {
502  
          consume("}");
503  
          out.set(
504  
            type.equals("intarray") ? toIntArray(list)
505  
            : type.equals("dblarray") ? toDoubleArray(list)
506  
            : list.toArray());
507  
        } else {
508  
          stack.add(this);
509  
          parse(new unstructure_Receiver {
510  
            void set(O o) {
511  
              list.add(o);
512  
              if (eq(t(), ",")) consume();
513  
            }
514  
          });
515  
        }
516  
      });
517  
    }
518  
    
519  
    Object parseClass() {
520  
      consume("class");
521  
      consume("(");
522  
      S name = unquote(tpp());
523  
      consume(")");
524  
      Class c = allDynamic ? null : classFinder != null ? (Class) callF(classFinder, name) : findClass_fullName(name);
525  
      if (c != null) ret c;
526  
      new DynamicObject dO;
527  
      dO.className = "java.lang.Class";
528  
      name = dropPrefix("main$", name);
529  
      dO.fieldValues.put("name", name);
530  
      ret dO;
531  
    }
532  
    
533  
    Object parseBigInt() {
534  
      consume("bigint");
535  
      consume("(");
536  
      S val = tpp();
537  
      if (eq(val, "-"))
538  
        val = "-" + tpp();
539  
      consume(")");
540  
      ret new BigInteger(val);
541  
    }
542  
    
543  
    Object parseDouble() {
544  
      consume("d");
545  
      consume("(");
546  
      S val = unquote(tpp());
547  
      consume(")");
548  
      ret Double.parseDouble(val);
549  
    }
550  
    
551  
    Object parseFloat() {
552  
      consume("fl");
553  
      S val;
554  
      if (eq(t(), "(")) {
555  
        consume("(");
556  
        val = unquote(tpp());
557  
        consume(")");
558  
      } else {
559  
        val = unquote(tpp());
560  
      }
561  
      ret Float.parseFloat(val);
562  
    }
563  
    
564  
    void parseHashSet(unstructure_Receiver out) {
565  
      consume("hashset");
566  
      parseSet(new HashSet, out);
567  
    }
568  
    
569  
    void parseLinkedHashSet(unstructure_Receiver out) {
570  
      consume("lhs");
571  
      parseSet(new LinkedHashSet, out);
572  
    }
573  
    
574  
    void parseTreeSet(unstructure_Receiver out) {
575  
      consume("treeset");
576  
      parseSet(new TreeSet, out);
577  
    }
578  
    
579  
    void parseCISet(unstructure_Receiver out) {
580  
      consume("ciset");
581  
      parseSet(ciSet(), out);
582  
    }
583  
    
584  
    void parseMap(unstructure_Receiver out) {
585  
      parseMap(new TreeMap, out);
586  
    }
587  
    
588  
    O parseJava() {
589  
      S j = unquote(tpp());
590  
      new Matches m;
591  
      if (jmatch("java.awt.Color[r=*,g=*,b=*]", j, m))
592  
        ret nuObject("java.awt.Color", parseInt($1), parseInt($2), parseInt($3));
593  
      else {
594  
        warn("Unknown Java object: " + j);
595  
        null;
596  
      }
597  
    }
598  
    
599  
    void parseMap(final Map map, final unstructure_Receiver out) {
600  
      consume("{");
601  
      stack.add(new Runnable {
602  
        bool v;
603  
        O key;
604  
        
605  
        public void run() { 
606  
          if (v) {
607  
            v = false;
608  
            stack.add(this);
609  
            if (!eq(tpp(), "="))
610  
              fail("= expected, got " + t() + " in map of size " + l(map));
611  
612  
            parse(new unstructure_Receiver {
613  
              void set(O value) {
614  
                map.put(key, value);
615  
                ifdef unstructure_debug
616  
                  print("parseMap: Got value " + getClassName(value) + ", next token: " + quote(t()));
617  
                endifdef
618  
                if (eq(t(), ",")) consume();
619  
              }
620  
            });
621  
          } else {
622  
            if (eq(t(), "}")) {
623  
              consume("}");
624  
              out.set(map);
625  
            } else {
626  
              v = true;
627  
              stack.add(this);
628  
              parse(new unstructure_Receiver {
629  
                void set(O o) {
630  
                  key = o;
631  
                }
632  
              });
633  
            }
634  
          } // if v else
635  
        } // run()
636  
      });
637  
    }
638  
    
639  
    /*void parseSub(unstructure_Receiver out) {
640  
      int n = l(stack);
641  
      parse(out);
642  
      while (l(stack) > n)
643  
        stack
644  
    }*/
645  
    
646  
    void consume() { curT = tok.next(); ++i; }
647  
    
648  
    void consume(S s) {
649  
      if (!eq(t(), s)) {
650  
        /*S prevToken = i-1 >= 0 ? tok.get(i-1) : "";
651  
        S nextTokens = join(tok.subList(i, Math.min(i+2, tok.size())));
652  
        fail(quote(s) + " expected: " + prevToken + " " + nextTokens + " (" + i + "/" + tok.size() + ")");*/
653  
        fail(quote(s) + " expected, got " + quote(t()));
654  
      }
655  
      consume();
656  
    }
657  
    
658  
    // outer wrapper function getting first token and unwinding the stack
659  
    void parse_initial(unstructure_Receiver out) {
660  
      consume(); // get first token
661  
      parse(out);
662  
      while (nempty(stack))
663  
        popLast(stack).run();
664  
    }
665  
  }
666  
  
667  
  Bool b = DynamicObject_loading!;
668  
  DynamicObject_loading.set(true);
669  
  try {
670  
    final new Var v;
671  
    new X x;
672  
    x.parse_initial(new unstructure_Receiver {
673  
      void set(O o) { v.set(o); }
674  
    });
675  
    unstructure_tokrefs = x.tokrefs.size();
676  
    ret v.get();
677  
  } finally {
678  
    DynamicObject_loading.set(b);
679  
  }
680  
}
681  
682  
static boolean unstructure_debug;

Author comment

Began life as a copy of #1025231

download  show line numbers  debug dex  old transpilations   

Travelled to 7 computer(s): bhatertpkbcr, mqqgnosmbjvj, pyentgdyhuwx, pzhvpgtvlbxg, tvejysmllsmz, vouqrxazstgt, xrpafgyirdlv

No comments. add comment

Snippet ID: #1027605
Snippet name: unstructure (v15, new custom deserialization)
Eternal ID of this version: #1027605/3
Text MD5: 3d660694176e29f5740cda4bbdc2970a
Transpilation MD5: 04b271d7da97366948ceeac0ac83451f
Author: stefan
Category: javax
Type: JavaX fragment (include)
Public (visible to everyone): Yes
Archived (hidden from active list): No
Created/modified: 2020-08-27 17:11:57
Source code size: 20438 bytes / 682 lines
Pitched / IR pitched: No / No
Views / Downloads: 190 / 248
Version history: 2 change(s)
Referenced in: [show references]