added java bytecode reader (not complete yet)
This commit is contained in:
parent
fd62bc9167
commit
78940c4469
20 changed files with 631 additions and 0 deletions
124
src/main/java/nl/sander/jsontoy2/java/ClassObject.java
Normal file
124
src/main/java/nl/sander/jsontoy2/java/ClassObject.java
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
package nl.sander.jsontoy2.java;
|
||||
|
||||
import nl.sander.jsontoy2.java.constantpool.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
public class ClassObject<T> {
|
||||
|
||||
private int constantPoolCount;
|
||||
private ConstantPoolEntry[] constantPool;
|
||||
private int constantPoolIndex = 0;
|
||||
|
||||
public int getConstantPoolCount() {
|
||||
return constantPoolCount;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < constantPoolCount - 1; i++) {
|
||||
ConstantPoolEntry entry = constantPool[i];
|
||||
builder.append(entry.toString());
|
||||
builder.append(String.format("%n"));
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private void add(ConstantPoolEntry entry) {
|
||||
constantPool[constantPoolIndex++] = entry;
|
||||
}
|
||||
|
||||
public Set<Field> getFields() {
|
||||
return Arrays.stream(constantPool)
|
||||
.filter(e -> e instanceof FieldRefEntry)
|
||||
.map(FieldRefEntry.class::cast)
|
||||
.map(f -> {
|
||||
NameAndType nat = getNameAndType(f);
|
||||
return new Field(nat.getName(), nat.getType());
|
||||
})
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private NameAndType getNameAndType(FieldRefEntry f) {
|
||||
NameAndTypeEntry natEntry = (NameAndTypeEntry) constantPool[f.getNameAndTypeIndex() - 1];
|
||||
return new NameAndType(getUtf8(natEntry.getNameIndex()), getUtf8(natEntry.getTypeIndex()));
|
||||
}
|
||||
|
||||
private String getUtf8(short index) {
|
||||
return ((Utf8Entry) constantPool[index - 1]).getUtf8();
|
||||
}
|
||||
|
||||
public static class Builder<T> {
|
||||
|
||||
private final ClassObject<T> classObject = new ClassObject<>();
|
||||
|
||||
public ClassObject<T> build() {
|
||||
return classObject;
|
||||
}
|
||||
|
||||
public Builder<T> constantPoolCount(int constantPoolCount) {
|
||||
classObject.constantPoolCount = constantPoolCount;
|
||||
classObject.constantPool = new ConstantPoolEntry[constantPoolCount];
|
||||
return this;
|
||||
}
|
||||
|
||||
public void constantPoolEntry(String utf8) {
|
||||
classObject.add(new Utf8Entry(utf8));
|
||||
}
|
||||
|
||||
public void constantPoolEntry(int i) {
|
||||
classObject.add(new IntEntry(i));
|
||||
}
|
||||
|
||||
public void constantPoolEntry(float f) {
|
||||
classObject.add(new FloatEntry(f));
|
||||
}
|
||||
|
||||
public void constantPoolEntry(long f) {
|
||||
classObject.add(new LongEntry(f));
|
||||
}
|
||||
|
||||
public void constantPoolEntry(double d) {
|
||||
classObject.add(new DoubleEntry(d));
|
||||
}
|
||||
|
||||
public void constantPoolClassEntry(short nameIndex) {
|
||||
classObject.add(new ClassEntry(nameIndex));
|
||||
}
|
||||
|
||||
public void constantPoolStringEntry(short utf8Index) {
|
||||
classObject.add(new StringEntry(utf8Index));
|
||||
}
|
||||
|
||||
public void constantPoolFieldRefEntry(short classIndex, short nameAndTypeIndex) {
|
||||
classObject.add(new FieldRefEntry(classIndex, nameAndTypeIndex));
|
||||
}
|
||||
|
||||
public void constantPoolMethodRefEntry(short classIndex, short nameAndTypeIndex) {
|
||||
classObject.add(new MethodRefEntry(classIndex, nameAndTypeIndex));
|
||||
}
|
||||
|
||||
public void constantPoolInterfaceMethodRefEntry(short classIndex, short nameAndTypeIndex) {
|
||||
classObject.add(new InterfaceMethodRefEntry(classIndex, nameAndTypeIndex));
|
||||
}
|
||||
|
||||
public void constantPoolNameAndTypeEntry(short nameIndex, short typeIndex) {
|
||||
classObject.add(new NameAndTypeEntry(nameIndex, typeIndex));
|
||||
}
|
||||
|
||||
public void constantPoolMethodHandleEntry(short referenceKind, short referenceIndex) {
|
||||
classObject.add(new MethodHandleEntry(referenceKind, referenceIndex));
|
||||
}
|
||||
|
||||
public void constantPoolMethodTypeEntry(short descriptorIndex) {
|
||||
classObject.add(new MethodTypeEntry(descriptorIndex));
|
||||
}
|
||||
|
||||
public void constantPoolInvokeDynamicEntry(short bootstrapMethodAttrIndex, short nameAndTypeIndex) {
|
||||
classObject.add(new InvokeDynamicEntry(bootstrapMethodAttrIndex, nameAndTypeIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
152
src/main/java/nl/sander/jsontoy2/java/ClassParser.java
Normal file
152
src/main/java/nl/sander/jsontoy2/java/ClassParser.java
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
package nl.sander.jsontoy2.java;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class ClassParser {
|
||||
|
||||
public <T> ClassObject<T> parse(Class<T> type) {
|
||||
DataInputStream in = new DataInputStream(new BufferedInputStream(type.getResourceAsStream(getResourceName(type))));
|
||||
expect(in, 0xCAFEBABE);
|
||||
ClassObject.Builder<T> builder = new ClassObject.Builder<>();
|
||||
skip(in, 4); //skip version
|
||||
int constantPoolCount = readShort(in);
|
||||
builder.constantPoolCount(constantPoolCount);
|
||||
for (int i = 1; i < constantPoolCount; i++) {
|
||||
readConstantPoolEntry(in, builder);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private <T> void readConstantPoolEntry(DataInputStream in, ClassObject.Builder<T> builder) {
|
||||
byte tag = readByte(in);
|
||||
switch (tag) {
|
||||
case 1: readUtf8(in, builder);
|
||||
break;
|
||||
case 2: throw new IllegalStateException("2: invalid classpool tag");
|
||||
case 3: builder.constantPoolEntry(readInt(in));
|
||||
break;
|
||||
case 4: builder.constantPoolEntry(readFloat(in));
|
||||
break;
|
||||
case 5: builder.constantPoolEntry(readLong(in));
|
||||
break;
|
||||
case 6: builder.constantPoolEntry(readDouble(in));
|
||||
break;
|
||||
case 7: builder.constantPoolClassEntry(readShort(in));
|
||||
break;
|
||||
case 8: builder.constantPoolStringEntry(readShort(in));
|
||||
break;
|
||||
case 9: builder.constantPoolFieldRefEntry(readShort(in), readShort(in));
|
||||
break;
|
||||
case 10: builder.constantPoolMethodRefEntry(readShort(in), readShort(in));
|
||||
break;
|
||||
case 11: builder.constantPoolInterfaceMethodRefEntry(readShort(in), readShort(in));
|
||||
break;
|
||||
case 12: builder.constantPoolNameAndTypeEntry(readShort(in), readShort(in));
|
||||
break;
|
||||
case 15: builder.constantPoolMethodHandleEntry(readShort(in), readShort(in));
|
||||
break;
|
||||
case 16: builder.constantPoolMethodTypeEntry(readShort(in));
|
||||
break;
|
||||
case 18: builder.constantPoolInvokeDynamicEntry(readShort(in), readShort(in));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void readUtf8(DataInputStream in, ClassObject.Builder<?> builder) {
|
||||
short length = readShort(in);
|
||||
String utf8 = readString(in, length);
|
||||
builder.constantPoolEntry(utf8);
|
||||
}
|
||||
|
||||
private String readString(DataInputStream in, short length) {
|
||||
try {
|
||||
byte[] bytes = in.readNBytes(length);
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
}
|
||||
|
||||
private long skip(InputStream in, long bytecount) {
|
||||
try {
|
||||
return in.skip(bytecount);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private byte readByte(DataInputStream in) {
|
||||
try {
|
||||
return in.readByte();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private short readShort(DataInputStream in) {
|
||||
try {
|
||||
return in.readShort();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private int readInt(DataInputStream in) {
|
||||
try {
|
||||
return in.readInt();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private float readFloat(DataInputStream in) {
|
||||
try {
|
||||
return in.readFloat();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private long readLong(DataInputStream in) {
|
||||
try {
|
||||
return in.readLong();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private double readDouble(DataInputStream in) {
|
||||
try {
|
||||
return in.readDouble();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void expect(DataInputStream in, int expected) {
|
||||
try {
|
||||
int i = in.readInt();
|
||||
if (i != expected) {
|
||||
throw new IllegalStateException("class file not valid");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private <T> String getResourceName(Class<T> type) {
|
||||
StringBuilder typeName = new StringBuilder("/" + type.getName());
|
||||
|
||||
for (int i = 0; i < typeName.length(); i++) {
|
||||
if (typeName.charAt(i) == '.') {
|
||||
typeName.setCharAt(i, '/');
|
||||
}
|
||||
}
|
||||
typeName.append(".class");
|
||||
return typeName.toString();
|
||||
}
|
||||
}
|
||||
44
src/main/java/nl/sander/jsontoy2/java/Field.java
Normal file
44
src/main/java/nl/sander/jsontoy2/java/Field.java
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package nl.sander.jsontoy2.java;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class Field {
|
||||
|
||||
private final String name;
|
||||
private final String type;
|
||||
|
||||
public Field(String name, String type) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Field field = (Field) o;
|
||||
return name.equals(field.name) &&
|
||||
type.equals(field.type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Field{" +
|
||||
"name='" + name + '\'' +
|
||||
", type='" + type + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package nl.sander.jsontoy2.java.constantpool;
|
||||
|
||||
public class ClassEntry extends ConstantPoolEntry {
|
||||
private final short nameIndex;
|
||||
|
||||
public ClassEntry(short nameIndex) {
|
||||
this.nameIndex = nameIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ClassEntry{" +
|
||||
"nameIndex=" + nameIndex +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package nl.sander.jsontoy2.java.constantpool;
|
||||
|
||||
public abstract class ConstantPoolEntry {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package nl.sander.jsontoy2.java.constantpool;
|
||||
|
||||
public class DoubleEntry extends ConstantPoolEntry {
|
||||
private final double doubleVal;
|
||||
|
||||
public DoubleEntry(double doubleVal) {
|
||||
this.doubleVal = doubleVal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DoubleEntry{" +
|
||||
"doubleVal=" + doubleVal +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package nl.sander.jsontoy2.java.constantpool;
|
||||
|
||||
public class FieldRefEntry extends ConstantPoolEntry {
|
||||
private final short classIndex;
|
||||
private final short nameAndTypeIndex;
|
||||
|
||||
public FieldRefEntry(short classIndex, short nameAndTypeIndex) {
|
||||
this.classIndex = classIndex;
|
||||
this.nameAndTypeIndex = nameAndTypeIndex;
|
||||
}
|
||||
|
||||
public short getClassIndex() {
|
||||
return classIndex;
|
||||
}
|
||||
|
||||
public short getNameAndTypeIndex() {
|
||||
return nameAndTypeIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "FieldRefEntry{" +
|
||||
"classIndex=" + classIndex +
|
||||
", nameAndTypeIndex=" + nameAndTypeIndex +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package nl.sander.jsontoy2.java.constantpool;
|
||||
|
||||
public class FloatEntry extends ConstantPoolEntry {
|
||||
private final float floatVal;
|
||||
|
||||
public FloatEntry(float floatVal) {
|
||||
this.floatVal = floatVal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "FloatEntry{" +
|
||||
"floatVal=" + floatVal +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package nl.sander.jsontoy2.java.constantpool;
|
||||
|
||||
public class IntEntry extends ConstantPoolEntry {
|
||||
private final int intVal;
|
||||
|
||||
public IntEntry(int integer) {
|
||||
this.intVal = integer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "IntEntry{" +
|
||||
"intVal=" + intVal +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package nl.sander.jsontoy2.java.constantpool;
|
||||
|
||||
public class InterfaceMethodRefEntry extends ConstantPoolEntry {
|
||||
private final short classIndex;
|
||||
private final short nameAndTypeIndex;
|
||||
|
||||
public InterfaceMethodRefEntry(short classIndex, short nameAndTypeIndex) {
|
||||
this.classIndex = classIndex;
|
||||
this.nameAndTypeIndex = nameAndTypeIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "InterfaceMethodRefEntry{" +
|
||||
"classIndex=" + classIndex +
|
||||
", nameAndTypeIndex=" + nameAndTypeIndex +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package nl.sander.jsontoy2.java.constantpool;
|
||||
|
||||
public class InvokeDynamicEntry extends ConstantPoolEntry {
|
||||
private final short bootstrapMethodAttrIndex;
|
||||
private final short nameAndTypeIndex;
|
||||
|
||||
public InvokeDynamicEntry(short bootstrapMethodAttrIndex, short nameAndTypeIndex) {
|
||||
this.bootstrapMethodAttrIndex = bootstrapMethodAttrIndex;
|
||||
this.nameAndTypeIndex = nameAndTypeIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "InvokeDynamicEntry{" +
|
||||
"bootstrapMethodAttrIndex=" + bootstrapMethodAttrIndex +
|
||||
", nameAndTypeIndex=" + nameAndTypeIndex +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package nl.sander.jsontoy2.java.constantpool;
|
||||
|
||||
public class LongEntry extends ConstantPoolEntry {
|
||||
|
||||
private final long longVal;
|
||||
|
||||
public LongEntry(long longVal) {
|
||||
this.longVal = longVal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LongEntry{" +
|
||||
"longVal=" + longVal +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package nl.sander.jsontoy2.java.constantpool;
|
||||
|
||||
public class MethodHandleEntry extends ConstantPoolEntry {
|
||||
private final short referenceKind;
|
||||
private final short referenceIndex;
|
||||
|
||||
public MethodHandleEntry(short referenceKind, short referenceIndex) {
|
||||
this.referenceKind = referenceKind;
|
||||
this.referenceIndex = referenceIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MethodHandleEntry{" +
|
||||
"referenceKind=" + referenceKind +
|
||||
", referenceIndex=" + referenceIndex +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package nl.sander.jsontoy2.java.constantpool;
|
||||
|
||||
public class MethodRefEntry extends ConstantPoolEntry {
|
||||
private final short classIndex;
|
||||
private final short nameAndTypeIndex;
|
||||
|
||||
public MethodRefEntry(short classIndex, short nameAndTypeIndex) {
|
||||
this.classIndex = classIndex;
|
||||
this.nameAndTypeIndex = nameAndTypeIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MethodRefEntry{" +
|
||||
"classIndex=" + classIndex +
|
||||
", nameAndTypeIndex=" + nameAndTypeIndex +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package nl.sander.jsontoy2.java.constantpool;
|
||||
|
||||
public class MethodTypeEntry extends ConstantPoolEntry {
|
||||
private final short descriptorIndex;
|
||||
|
||||
public MethodTypeEntry(short descriptorIndex) {
|
||||
this.descriptorIndex = descriptorIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MethodTypeEntry{" +
|
||||
"descriptorIndex=" + descriptorIndex +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package nl.sander.jsontoy2.java.constantpool;
|
||||
|
||||
public class NameAndType {
|
||||
private final String name;
|
||||
private final String type;
|
||||
|
||||
public NameAndType(String name, String type) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package nl.sander.jsontoy2.java.constantpool;
|
||||
|
||||
public class NameAndTypeEntry extends ConstantPoolEntry {
|
||||
private final short nameIndex;
|
||||
private final short typeIndex;
|
||||
|
||||
public NameAndTypeEntry(short nameIndex, short typeIndex) {
|
||||
this.nameIndex = nameIndex;
|
||||
this.typeIndex = typeIndex;
|
||||
}
|
||||
|
||||
public short getNameIndex() {
|
||||
return nameIndex;
|
||||
}
|
||||
|
||||
public short getTypeIndex() {
|
||||
return typeIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "NameAndTypeEntry{" +
|
||||
"nameIndex=" + nameIndex +
|
||||
", typeIndex=" + typeIndex +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package nl.sander.jsontoy2.java.constantpool;
|
||||
|
||||
public class StringEntry extends ConstantPoolEntry {
|
||||
private final short utf8Index;
|
||||
|
||||
public StringEntry(short utf8Index) {
|
||||
this.utf8Index = utf8Index;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "StringEntry{" +
|
||||
"utf8Index=" + utf8Index +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package nl.sander.jsontoy2.java.constantpool;
|
||||
|
||||
public class Utf8Entry extends ConstantPoolEntry {
|
||||
private final String stringVal;
|
||||
|
||||
public Utf8Entry(String utf8) {
|
||||
this.stringVal = utf8;
|
||||
}
|
||||
|
||||
|
||||
public String getUtf8() {
|
||||
return stringVal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Utf8Entry{" +
|
||||
"stringVal='" + stringVal + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
23
src/test/java/nl/sander/jsontoy2/java/ClassParserTest.java
Normal file
23
src/test/java/nl/sander/jsontoy2/java/ClassParserTest.java
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package nl.sander.jsontoy2.java;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class ClassParserTest {
|
||||
|
||||
private int field;
|
||||
|
||||
@Test
|
||||
public void testReadClass() {
|
||||
ClassObject<ClassParserTest> object = new ClassParser().parse(ClassParserTest.class);
|
||||
assertEquals(Set.of(new Field("field", "I")), object.getFields());
|
||||
}
|
||||
|
||||
// if not included, field is not in the compiled code.
|
||||
public int getField() {
|
||||
return field;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue