Added types to FlowableExcelMapper
This commit is contained in:
@@ -2,35 +2,69 @@ package com.example.parser;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
|
import org.apache.poi.ss.usermodel.DateUtil;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.*;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class FlowableExcelMapper {
|
public class FlowableExcelMapper {
|
||||||
|
|
||||||
protected static ObjectMapper objectMapper = new ObjectMapper();
|
protected static ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
protected static JavaTimeModule javaTimeModule = new JavaTimeModule();
|
||||||
protected static FlowableExcelParser flowableExcelParser = new FlowableExcelParser();
|
protected static FlowableExcelParser flowableExcelParser = new FlowableExcelParser();
|
||||||
|
|
||||||
|
public FlowableExcelMapper() {
|
||||||
|
objectMapper.registerModule(javaTimeModule);
|
||||||
|
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
|
||||||
|
}
|
||||||
|
|
||||||
public ArrayNode excelToJsonNode(String excelPath) {
|
public ArrayNode excelToJsonNode(String excelPath) {
|
||||||
ArrayList<LinkedHashMap<String, Object>> rowList = flowableExcelParser.parseExcel(excelPath);
|
ArrayList<ArrayList<Object>> rawList = flowableExcelParser.parseExcel(excelPath);
|
||||||
|
ArrayList<Object> paths = new ArrayList<>();
|
||||||
|
ArrayList<Object> types = new ArrayList<>();
|
||||||
ArrayNode arrayNode = objectMapper.createArrayNode();
|
ArrayNode arrayNode = objectMapper.createArrayNode();
|
||||||
for (LinkedHashMap<String, Object> row : rowList) {
|
for (int rowNum = 0; rowNum < rawList.size(); rowNum++) {
|
||||||
JsonNode jsonNode = null;
|
if (rowNum == 0) {
|
||||||
Set<Map.Entry<String, Object>> entries = row.entrySet();
|
// The 1st row contains the paths
|
||||||
for (Map.Entry<String, Object> entry : entries) {
|
paths.addAll(rawList.get(rowNum));
|
||||||
jsonNode = addNode(jsonNode, entry.getKey(), entry.getValue());
|
} else if (rowNum == 1) {
|
||||||
|
// The 1st row contains the types of the variables
|
||||||
|
types.addAll(rawList.get(rowNum));
|
||||||
|
} else {
|
||||||
|
// All other rows contain the values of the variables
|
||||||
|
JsonNode jsonNode = null;
|
||||||
|
ArrayList<Object> row = rawList.get(rowNum);
|
||||||
|
for (int cellNum = 0; cellNum < row.size(); cellNum++) {
|
||||||
|
Object content = row.get(cellNum);
|
||||||
|
String type = types.get(cellNum).toString();
|
||||||
|
switch (type) {
|
||||||
|
case "String": content = content.toString(); break;
|
||||||
|
case "Boolean": content = Boolean.parseBoolean(content.toString()); break;
|
||||||
|
case "Integer": content = (int) Double.parseDouble(content.toString()); break;
|
||||||
|
case "Double": content = Double.parseDouble(content.toString()); break;
|
||||||
|
case "Date":
|
||||||
|
// Excel considers dates as local dates, but we want them as UTC
|
||||||
|
Date dateTime = DateUtil.getJavaDate(Double.parseDouble(content.toString()));
|
||||||
|
content = dateTime.toInstant().plusSeconds(ZonedDateTime.now().getOffset().getTotalSeconds());
|
||||||
|
break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
jsonNode = addNode(jsonNode, paths.get(cellNum).toString(), content);
|
||||||
|
}
|
||||||
|
arrayNode.add(jsonNode);
|
||||||
}
|
}
|
||||||
arrayNode.add(jsonNode);
|
|
||||||
}
|
}
|
||||||
return arrayNode;
|
return arrayNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public JsonNode addNode(JsonNode parent, String path, Object value) {
|
protected JsonNode addNode(JsonNode parent, String path, Object value) {
|
||||||
objectMapper = new ObjectMapper();
|
|
||||||
|
|
||||||
if (parent == null) {
|
if (parent == null) {
|
||||||
parent = objectMapper.createObjectNode();
|
parent = objectMapper.createObjectNode();
|
||||||
}
|
}
|
||||||
@@ -87,7 +121,7 @@ public class FlowableExcelMapper {
|
|||||||
return parent;
|
return parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int indexOfFirstDelimiter(String str, String delimiters) {
|
protected int indexOfFirstDelimiter(String str, String delimiters) {
|
||||||
for (int i = 0; i < str.length(); i++) {
|
for (int i = 0; i < str.length(); i++) {
|
||||||
for (char c : delimiters.toCharArray()) {
|
for (char c : delimiters.toCharArray()) {
|
||||||
if (str.charAt(i) == c) {
|
if (str.charAt(i) == c) {
|
||||||
|
|||||||
@@ -8,56 +8,63 @@ import org.springframework.stereotype.Component;
|
|||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class FlowableExcelParser {
|
public class FlowableExcelParser {
|
||||||
|
|
||||||
public ArrayList<LinkedHashMap<String, Object>> parseExcel(String resourcePath) {
|
// resourcePath defines the location of the Excel file
|
||||||
ArrayList<LinkedHashMap<String, Object>> rowList = new ArrayList<>();
|
// Return all cells in the 1st sheet which have values as objects (String, boolean or double)
|
||||||
|
// The 1st row defines the amount of cells per row
|
||||||
|
// Parsing the sheet stops after the 1st empty row
|
||||||
|
public ArrayList<ArrayList<Object>> parseExcel(String resourcePath) {
|
||||||
|
ArrayList<ArrayList<Object>> rows = new ArrayList<>();
|
||||||
try (FileInputStream fileInputStream = new FileInputStream(resourcePath)) {
|
try (FileInputStream fileInputStream = new FileInputStream(resourcePath)) {
|
||||||
Workbook workbook = new XSSFWorkbook(fileInputStream);
|
Workbook workbook = new XSSFWorkbook(fileInputStream);
|
||||||
Sheet sheet = workbook.getSheetAt(0);
|
Sheet sheet = workbook.getSheetAt(0);
|
||||||
ArrayList<String> colHeaders = new ArrayList<>();
|
int maxCells = 0;
|
||||||
for (Row row : sheet) {
|
for (int rowNum = 0; rowNum < sheet.getLastRowNum(); rowNum++) {
|
||||||
if (row.getRowNum() == 0) {
|
Row row = sheet.getRow(rowNum);
|
||||||
// Header row
|
if (row == null) {
|
||||||
for (int colNum = 0; colNum < row.getLastCellNum(); colNum++) {
|
break;
|
||||||
Cell cell = row.getCell(colNum);
|
}
|
||||||
if (cell == null || cell.getCellType() == CellType.BLANK || cell.getCellType() == CellType._NONE) {
|
if (maxCells == 0) {
|
||||||
// Take all columns until the first empty header cell
|
maxCells = row.getLastCellNum();
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
colHeaders.add(cell.getStringCellValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
boolean allCellsNull = true;
|
boolean allCellsNull = true;
|
||||||
LinkedHashMap<String, Object> rowMap = new LinkedHashMap<>();
|
ArrayList<Object> cells = new ArrayList<>();
|
||||||
for (int colNum = 0; colNum < colHeaders.size(); colNum++) {
|
for (int cellNum = 0; cellNum < maxCells; cellNum++) {
|
||||||
Object content = null;
|
Cell cell = row.getCell(cellNum);
|
||||||
Cell cell = row.getCell(colNum);
|
if(rowNum == 0) {
|
||||||
CellValue cellValue = workbook.getCreationHelper().createFormulaEvaluator().evaluate(cell);
|
if (cell == null || cell.getCellType() == CellType.BLANK || cell.getCellType() == CellType._NONE) {
|
||||||
if (cellValue != null && cell.getCellType() != CellType.BLANK && cell.getCellType() != CellType._NONE) {
|
// Count cells of 1st row until the first empty cell
|
||||||
switch (cellValue.getCellType()) {
|
maxCells = cellNum;
|
||||||
case STRING : content = cellValue.getStringValue(); break;
|
break;
|
||||||
case BOOLEAN : content = cellValue.getBooleanValue(); break;
|
|
||||||
case NUMERIC : content = cellValue.getNumberValue(); break;
|
|
||||||
}
|
}
|
||||||
allCellsNull = false;
|
|
||||||
}
|
}
|
||||||
rowMap.put(colHeaders.get(colNum), content);
|
CellValue cellValue = workbook.getCreationHelper().createFormulaEvaluator().evaluate(cell);
|
||||||
|
Object content = null;
|
||||||
|
if (cellValue != null) {
|
||||||
|
CellType cellType = cell.getCellType();
|
||||||
|
if (cellType != CellType.BLANK && cellType != CellType._NONE) {
|
||||||
|
switch (cellValue.getCellType()) {
|
||||||
|
case STRING : content = cellValue.getStringValue(); break;
|
||||||
|
case BOOLEAN : content = cellValue.getBooleanValue(); break;
|
||||||
|
case NUMERIC : content = cellValue.getNumberValue(); break;
|
||||||
|
}
|
||||||
|
allCellsNull = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cells.add(content);
|
||||||
}
|
}
|
||||||
if (allCellsNull) {
|
if (allCellsNull) {
|
||||||
// Take all rows until the first empty row
|
// Take all rows until the first empty row
|
||||||
return rowList;
|
return rows;
|
||||||
}
|
}
|
||||||
rowList.add(rowMap);
|
rows.add(cells);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
return rowList;
|
return rows;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public class ParserApp {
|
|||||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
private static final FlowableJsonParser flowableJsonParser = new FlowableJsonParser();
|
private static final FlowableJsonParser flowableJsonParser = new FlowableJsonParser();
|
||||||
private static final FlowableExcelParser flowableExcelParser = new FlowableExcelParser();
|
private static final FlowableExcelParser flowableExcelParser = new FlowableExcelParser();
|
||||||
|
private static final FlowableExcelMapper flowableExcelMapper = new FlowableExcelMapper();
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(ParserApp.class, args);
|
SpringApplication.run(ParserApp.class, args);
|
||||||
@@ -28,9 +29,12 @@ public class ParserApp {
|
|||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
ClassLoader classLoader = ParserApp.class.getClassLoader();
|
URL resourceUrl = ParserApp.class.getClassLoader().getResource("flowableExcelParserExampleData.xlsx");
|
||||||
URL resourceUrl = classLoader.getResource("flowableExcelParserExampleData.xlsx");
|
|
||||||
assert resourceUrl != null;
|
assert resourceUrl != null;
|
||||||
logger.info("parsed excel={}", flowableExcelParser.parseExcel(resourceUrl.getPath()));
|
logger.info("parsed excel={}", flowableExcelParser.parseExcel(resourceUrl.getPath()));
|
||||||
|
|
||||||
|
resourceUrl = ParserApp.class.getClassLoader().getResource("flowableExcelMapperExampleData.xlsx");
|
||||||
|
assert resourceUrl != null;
|
||||||
|
logger.info("mapped excel={}", flowableExcelMapper.excelToJsonNode(resourceUrl.getPath()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -18,23 +18,9 @@ class FlowableExcelMapperTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private FlowableExcelMapper flowableExcelMapper;
|
private FlowableExcelMapper flowableExcelMapper;
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ParserApp.class);
|
private static final Logger logger = LoggerFactory.getLogger(FlowableExcelMapperTest.class);
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void addNodeTest() {
|
|
||||||
JsonNode node = null;
|
|
||||||
node = flowableExcelMapper.addNode(node, "3*0*2", "String1");
|
|
||||||
node = flowableExcelMapper.addNode(node, "3*0*3", "String1");
|
|
||||||
node = flowableExcelMapper.addNode(node, "3*1.1", 234);
|
|
||||||
node = flowableExcelMapper.addNode(node, "3*1.2", 234);
|
|
||||||
node = flowableExcelMapper.addNode(node, "3*2", false);
|
|
||||||
node = flowableExcelMapper.addNode(node, "4.5", "String1");
|
|
||||||
node = flowableExcelMapper.addNode(node, "4.6", 234);
|
|
||||||
node = flowableExcelMapper.addNode(node, "4.7", false);
|
|
||||||
logger.info("node2={}{}", System.lineSeparator(), node.toPrettyString());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String getResourcePath(String path) {
|
protected String getResourcePath(String path) {
|
||||||
ClassLoader classLoader = FlowableExcelMapperTest.class.getClassLoader();
|
ClassLoader classLoader = FlowableExcelMapperTest.class.getClassLoader();
|
||||||
URL resourceUrl = classLoader.getResource(path);
|
URL resourceUrl = classLoader.getResource(path);
|
||||||
@@ -44,7 +30,7 @@ class FlowableExcelMapperTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void excelToJsonNodeTest() {
|
public void excelToJsonNodeTest() {
|
||||||
JsonNode node = flowableExcelMapper.excelToJsonNode(getResourcePath("flowableExcelMapperTestData1.xlsx"));
|
JsonNode node = flowableExcelMapper.excelToJsonNode(getResourcePath("flowableExcelMapperTestData.xlsx"));
|
||||||
logger.info("node2={}{}", System.lineSeparator(), node.toPrettyString());
|
logger.info("node2={}{}", System.lineSeparator(), node.toPrettyString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,26 +20,18 @@ class FlowableExcelParserTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private FlowableExcelParser flowableExcelParser;
|
private FlowableExcelParser flowableExcelParser;
|
||||||
|
|
||||||
private Stream<Arguments> GetArgumentsFromExcel(String path) {
|
protected String getResourcePath(String path) {
|
||||||
ArrayList<Arguments> argumentList = new ArrayList<>();
|
|
||||||
ClassLoader classLoader = FlowableExcelParserTest.class.getClassLoader();
|
ClassLoader classLoader = FlowableExcelParserTest.class.getClassLoader();
|
||||||
URL resourceUrl = classLoader.getResource(path);
|
URL resourceUrl = classLoader.getResource(path);
|
||||||
Assertions.assertThat(resourceUrl).isNotNull();
|
Assertions.assertThat(resourceUrl).isNotNull();
|
||||||
ArrayList<LinkedHashMap<String, Object>> rowList = flowableExcelParser.parseExcel(resourceUrl.getPath());
|
return resourceUrl.getPath();
|
||||||
Assertions.assertThat(rowList).isNotNull();
|
}
|
||||||
for (LinkedHashMap<String, Object> row : rowList) {
|
|
||||||
LinkedHashMap<String, Object> inputMap = new LinkedHashMap<>();
|
protected Stream<Arguments> GetArgumentsFromExcel(String path) {
|
||||||
LinkedHashMap<String, Object> outputMap = new LinkedHashMap<>();
|
ArrayList<Arguments> argumentList = new ArrayList<>();
|
||||||
for (Map.Entry<String, Object> entry : row.entrySet()) {
|
ArrayList<ArrayList<Object>> rowList = flowableExcelParser.parseExcel(getResourcePath(path));
|
||||||
String key = entry.getKey();
|
for (ArrayList<Object> row : rowList) {
|
||||||
if (List.of("input1", "input2", "input3").contains(key)) {
|
argumentList.add(Arguments.of(row));
|
||||||
inputMap.put(key, entry.getValue());
|
|
||||||
}
|
|
||||||
if (List.of("output1", "output2").contains(key)) {
|
|
||||||
outputMap.put(key, entry.getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
argumentList.add(Arguments.of(inputMap, outputMap));
|
|
||||||
}
|
}
|
||||||
return argumentList.stream();
|
return argumentList.stream();
|
||||||
}
|
}
|
||||||
@@ -51,18 +43,12 @@ class FlowableExcelParserTest {
|
|||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource("booleanExcelParserTestDataProvider")
|
@MethodSource("booleanExcelParserTestDataProvider")
|
||||||
void booleanExcelParserTest(LinkedHashMap<String, Object> inputMap, LinkedHashMap<String, Object> outputMap) {
|
void booleanExcelParserTest(ArrayList<Object> row) {
|
||||||
for (Map.Entry<String, Object> entry : outputMap.entrySet()) {
|
Boolean valOr = (Boolean) row.get(0) || (Boolean) row.get(1) || (Boolean) row.get(2);
|
||||||
String key = entry.getKey();
|
Assertions.assertThat(row.get(3)).isEqualTo(valOr);
|
||||||
if (key.equals("output1")) {
|
|
||||||
Boolean val = (Boolean)inputMap.get("input1") || (Boolean)inputMap.get("input2") || (Boolean)inputMap.get("input3");
|
Boolean valAnd = (Boolean) row.get(0) && (Boolean) row.get(1) && (Boolean) row.get(2);
|
||||||
Assertions.assertThat(entry.getValue()).isEqualTo(val);
|
Assertions.assertThat(row.get(4)).isEqualTo(valAnd);
|
||||||
}
|
|
||||||
if (key.equals("output2")) {
|
|
||||||
Boolean val = (Boolean)inputMap.get("input1") && (Boolean)inputMap.get("input2") && (Boolean)inputMap.get("input3");
|
|
||||||
Assertions.assertThat(entry.getValue()).isEqualTo(val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@@ -72,22 +58,15 @@ class FlowableExcelParserTest {
|
|||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource("stringExcelParserTestDataProvider")
|
@MethodSource("stringExcelParserTestDataProvider")
|
||||||
void stringExcelParserTest(LinkedHashMap<String, Object> inputMap, LinkedHashMap<String, Object> outputMap) {
|
void stringExcelParserTest(ArrayList<Object> row) {
|
||||||
for (Map.Entry<String, Object> entry : outputMap.entrySet()) {
|
String val12 = row.get(0).toString() + row.get(1).toString();
|
||||||
String key = entry.getKey();
|
Assertions.assertThat(row.get(3)).isEqualTo(val12);
|
||||||
if (key.equals("output1")) {
|
|
||||||
String val = inputMap.get("input1").toString() + inputMap.get("input2").toString();
|
String val23 = row.get(1).toString() + row.get(2).toString();
|
||||||
Assertions.assertThat(entry.getValue()).isEqualTo(val);
|
Assertions.assertThat(row.get(4)).isEqualTo(val23);
|
||||||
}
|
|
||||||
if (key.equals("output2")) {
|
String val123 = row.get(0).toString() + row.get(1).toString() + row.get(2).toString();
|
||||||
String val = inputMap.get("input2").toString() + inputMap.get("input3").toString();
|
Assertions.assertThat(row.get(5)).isEqualTo(val123);
|
||||||
Assertions.assertThat(entry.getValue()).isEqualTo(val);
|
|
||||||
}
|
|
||||||
if (key.equals("output3")) {
|
|
||||||
String val = inputMap.get("input1").toString() + inputMap.get("input2").toString() + inputMap.get("input3").toString();
|
|
||||||
Assertions.assertThat(entry.getValue()).isEqualTo(val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@@ -97,21 +76,14 @@ class FlowableExcelParserTest {
|
|||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource("numberExcelParserTestDataProvider")
|
@MethodSource("numberExcelParserTestDataProvider")
|
||||||
void numberExcelParserTest(LinkedHashMap<String, Object> inputMap, LinkedHashMap<String, Object> outputMap) {
|
void numberExcelParserTest(ArrayList<Object> row) {
|
||||||
for (Map.Entry<String, Object> entry : outputMap.entrySet()) {
|
Double val12 = (Double) row.get(0) + (Double) row.get(1);
|
||||||
String key = entry.getKey();
|
Assertions.assertThat(row.get(3)).isEqualTo(val12);
|
||||||
if (key.equals("output1")) {
|
|
||||||
Double val = (Double)inputMap.get("input1") + (Double)inputMap.get("input2");
|
Double val23 = (Double) row.get(1) + (Double) row.get(2);
|
||||||
Assertions.assertThat(entry.getValue()).isEqualTo(val);
|
Assertions.assertThat(row.get(4)).isEqualTo(val23);
|
||||||
}
|
|
||||||
if (key.equals("output2")) {
|
Double val123 = (Double) row.get(0) + (Double) row.get(1) + (Double) row.get(2);
|
||||||
Double val = (Double)inputMap.get("input2") + (Double)inputMap.get("input3");
|
Assertions.assertThat(row.get(5)).isEqualTo(val123);
|
||||||
Assertions.assertThat(entry.getValue()).isEqualTo(val);
|
|
||||||
}
|
|
||||||
if (key.equals("output3")) {
|
|
||||||
Double val = (Double)inputMap.get("input1") + (Double)inputMap.get("input2") + (Double)inputMap.get("input3");
|
|
||||||
Assertions.assertThat(entry.getValue()).isEqualTo(val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user