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

686
LINES

< > BotCompany Repo | #1027604 // unstructure (v16, better class finding, dev.)

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

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