mirror of
https://github.com/tiennm99/zfoo.git
synced 2026-08-06 02:24:25 +00:00
feat[storage]: 新增解析json数据格式
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
package com.zfoo.storage.interpreter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 配置文件资源
|
||||
*
|
||||
*/
|
||||
public class JsonResource {
|
||||
// 文件名
|
||||
private String name;
|
||||
//配置表字段名
|
||||
private List<ColumnMeta> columns = new ArrayList<>();
|
||||
// 配置表数据
|
||||
private List<List<String>> data = new ArrayList<>();
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public List<ColumnMeta> getColumns() {
|
||||
return columns;
|
||||
}
|
||||
|
||||
public void setColumns(List<ColumnMeta> columns) {
|
||||
this.columns = columns;
|
||||
}
|
||||
|
||||
public List<List<String>> getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public void setData(List<List<String>> data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public static class ColumnMeta {
|
||||
//字段名
|
||||
private String name;
|
||||
//类型
|
||||
private String type;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package com.zfoo.storage.interpreter;
|
||||
|
||||
import com.zfoo.protocol.exception.RunException;
|
||||
import com.zfoo.protocol.util.JsonUtils;
|
||||
import com.zfoo.protocol.util.ReflectionUtils;
|
||||
import com.zfoo.protocol.util.StringUtils;
|
||||
import com.zfoo.storage.model.anno.Id;
|
||||
import com.zfoo.storage.strategy.*;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.springframework.context.support.ConversionServiceFactoryBean;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class JsonResourceReader implements IResourceReader {
|
||||
|
||||
private static final TypeDescriptor TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
|
||||
|
||||
private static final ConversionServiceFactoryBean conversionServiceFactoryBean = new ConversionServiceFactoryBean();
|
||||
|
||||
static {
|
||||
var converters = new HashSet<>();
|
||||
converters.add(new JsonToArrayConverter());
|
||||
converters.add(new JsonToMapConverter());
|
||||
converters.add(new JsonToObjectConverter());
|
||||
converters.add(new StringToClassConverter());
|
||||
converters.add(new StringToDateConverter());
|
||||
converters.add(new StringToMapConverter());
|
||||
conversionServiceFactoryBean.setConverters(converters);
|
||||
conversionServiceFactoryBean.afterPropertiesSet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<T> read(InputStream inputStream, Class<T> clazz) {
|
||||
String txt = readTxt(inputStream, clazz);
|
||||
JsonResource JsonResource = JsonUtils.string2Object(txt, JsonResource.class);
|
||||
|
||||
var result = new ArrayList<T>();
|
||||
|
||||
// 默认取到第一个sheet页
|
||||
var fieldInfos = getFieldInfos(JsonResource, clazz);
|
||||
|
||||
var iterator = JsonResource.getData().iterator();
|
||||
// 从ROW_SERVER这行开始读取数据
|
||||
while (iterator.hasNext()) {
|
||||
var row = iterator.next();
|
||||
var instance = ReflectionUtils.newInstance(clazz);
|
||||
|
||||
for (var fieldInfo : fieldInfos) {
|
||||
var content = row.get(fieldInfo.index);
|
||||
if (StringUtils.isNotEmpty(content) || fieldInfo.field.getType() == String.class) {
|
||||
inject(instance, fieldInfo.field, content);
|
||||
}
|
||||
}
|
||||
result.add(instance);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void inject(Object instance, Field field, String content) {
|
||||
try {
|
||||
var targetType = new TypeDescriptor(field);
|
||||
var value = conversionServiceFactoryBean.getObject().convert(content, TYPE_DESCRIPTOR, targetType);
|
||||
ReflectionUtils.makeAccessible(field);
|
||||
ReflectionUtils.setField(field, instance, value);
|
||||
} catch (Exception e) {
|
||||
throw new RunException(e, "无法将Excel资源[class:{}]中的[content:{}]转换为属性[field:{}]", instance.getClass().getSimpleName(), content, field.getName());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 只读取代码里写的字段
|
||||
private Collection<FieldInfo> getFieldInfos(JsonResource resource, Class<?> clazz) {
|
||||
var fieldRow = resource.getColumns();
|
||||
if (fieldRow == null) {
|
||||
throw new RunException("无法获取资源[class:{}]的Excel文件的属性控制列", clazz.getSimpleName());
|
||||
}
|
||||
|
||||
var cellFieldMap = new HashMap<String, Integer>();
|
||||
for (var i = 0; i < fieldRow.size(); i++) {
|
||||
var cell = fieldRow.get(i);
|
||||
if (Objects.isNull(cell)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = cell.getName();
|
||||
if (StringUtils.isEmpty(name)) {
|
||||
continue;
|
||||
}
|
||||
var previousValue = cellFieldMap.put(name, i);
|
||||
if (Objects.nonNull(previousValue)) {
|
||||
throw new RunException("资源[class:{}]的Excel文件出现重复的属性控制列[field:{}]", clazz.getSimpleName(), name);
|
||||
}
|
||||
}
|
||||
|
||||
var fieldList = Arrays.stream(clazz.getDeclaredFields())
|
||||
.filter(it -> !Modifier.isTransient(it.getModifiers()))
|
||||
.filter(it -> !Modifier.isStatic(it.getModifiers()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
for (var field : fieldList) {
|
||||
if (!cellFieldMap.containsKey(field.getName())) {
|
||||
throw new RunException("资源类[class:{}]的声明属性[filed:{}]无法获取,请检查配置表的格式", clazz, field.getName());
|
||||
}
|
||||
|
||||
if (field.isAnnotationPresent(Id.class)) {
|
||||
var cellIndex = cellFieldMap.get(field.getName());
|
||||
if (cellIndex != 0) {
|
||||
throw new RunException("资源类[class:{}]的主键[Id:{}]必须放在Excel配置表的第一列,请检查配置表的格式", clazz, field.getName());
|
||||
}
|
||||
}
|
||||
|
||||
if (Modifier.isPublic(field.getModifiers())) {
|
||||
throw new RunException("因为静态资源类是不能被修改的,所以资源类[class:{}]的属性[filed:{}]不能被public修饰,请改为private修饰", clazz, field.getName());
|
||||
}
|
||||
|
||||
var setMethodName = StringUtils.EMPTY;
|
||||
try {
|
||||
setMethodName = ReflectionUtils.fieldToSetMethod(clazz, field);
|
||||
} catch (Exception e) {
|
||||
// 没有setMethod是正确的
|
||||
}
|
||||
if (StringUtils.isNotBlank(setMethodName)) {
|
||||
throw new RunException("因为静态资源类是不能被修改的,所以资源类[class:{}]的属性[filed:{}]不能含有set方法[{}]", clazz, field.getName(), setMethodName);
|
||||
}
|
||||
}
|
||||
|
||||
return fieldList.stream().map(it -> new FieldInfo(cellFieldMap.get(it.getName()), it)).collect(Collectors.toList());
|
||||
|
||||
}
|
||||
|
||||
private String readTxt(InputStream input, Class<?> clazz) {
|
||||
try {
|
||||
return IOUtils.toString(input, StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new RunException("静态资源[{}]异常,无法读取文件", clazz.getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
private static class FieldInfo {
|
||||
public final int index;
|
||||
public final Field field;
|
||||
|
||||
public FieldInfo(int index, Field field) {
|
||||
this.index = index;
|
||||
this.field = field;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import com.zfoo.protocol.util.DomUtils;
|
||||
import com.zfoo.protocol.util.StringUtils;
|
||||
import com.zfoo.storage.StorageContext;
|
||||
import com.zfoo.storage.interpreter.ExcelResourceReader;
|
||||
import com.zfoo.storage.interpreter.JsonResourceReader;
|
||||
import com.zfoo.storage.manager.StorageManager;
|
||||
import com.zfoo.storage.model.config.StorageConfig;
|
||||
import org.springframework.beans.factory.support.AbstractBeanDefinition;
|
||||
@@ -72,6 +73,17 @@ public class StorageDefinitionParser implements BeanDefinitionParser {
|
||||
resolvePlaceholder("suffix", "resourceSuffix", builder, resourceElement, parserContext);
|
||||
|
||||
parserContext.getRegistry().registerBeanDefinition(clazz.getCanonicalName(), builder.getBeanDefinition());
|
||||
|
||||
// 注册ExcelResourceReader
|
||||
Class<?> readerClazz = ExcelResourceReader.class;
|
||||
var resourceSuffix = resourceElement.getAttribute("suffix");
|
||||
if (resourceSuffix.equals("txt")) {
|
||||
readerClazz = JsonResourceReader.class;
|
||||
}
|
||||
|
||||
var readerName = StringUtils.uncapitalize(readerClazz.getName());
|
||||
builder = BeanDefinitionBuilder.rootBeanDefinition(readerClazz);
|
||||
parserContext.getRegistry().registerBeanDefinition(readerName, builder.getBeanDefinition());
|
||||
}
|
||||
|
||||
private void registerBeanDefinition(ParserContext parserContext) {
|
||||
@@ -88,10 +100,10 @@ public class StorageDefinitionParser implements BeanDefinitionParser {
|
||||
registry.registerBeanDefinition(name, builder.getBeanDefinition());
|
||||
|
||||
// 注册ExcelResourceReader
|
||||
clazz = ExcelResourceReader.class;
|
||||
name = StringUtils.uncapitalize(clazz.getName());
|
||||
builder = BeanDefinitionBuilder.rootBeanDefinition(clazz);
|
||||
registry.registerBeanDefinition(name, builder.getBeanDefinition());
|
||||
// clazz = ExcelResourceReader.class;
|
||||
// name = StringUtils.uncapitalize(clazz.getName());
|
||||
// builder = BeanDefinitionBuilder.rootBeanDefinition(clazz);
|
||||
// registry.registerBeanDefinition(name, builder.getBeanDefinition());
|
||||
}
|
||||
|
||||
private void resolvePlaceholder(String attributeName, String fieldName, BeanDefinitionBuilder builder, Element element, ParserContext parserContext) {
|
||||
|
||||
@@ -39,7 +39,8 @@ public class JsonToMapConverter implements ConditionalGenericConverter {
|
||||
@Override
|
||||
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||
String content = (String) source;
|
||||
return JsonUtils.string2Object(content, targetType.getType());
|
||||
return JsonUtils.string2Map(content, targetType.getMapKeyTypeDescriptor().getType(), targetType.getMapValueTypeDescriptor().getType());
|
||||
// return JsonUtils.string2Object(content, targetType.getType());
|
||||
// return JsonUtil.string2Map(content, targetType.getMapKeyTypeDescriptor().getType()
|
||||
// , targetType.getMapValueTypeDescriptor().getType());
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.support.ClassPathXmlApplicationContext;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author jaysunxiao
|
||||
* @version 3.0
|
||||
@@ -61,4 +63,26 @@ public class ApplicationTest {
|
||||
var valuesByIndex = studentResources.getIndex("name", "james0");
|
||||
logger.info(JsonUtils.object2String(valuesByIndex));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startStorageTestTxt() {
|
||||
// 加载配置文件,配置文件中必须引入storage
|
||||
// 配置文件中scan,需要映射Excel的类所在位置,会自动搜索文件夹下的Excel文件,Excel文件可以放在指定文件夹的任意目录
|
||||
// 配置文件中resource,需要映射Excel的文件所在位置
|
||||
var context = new ClassPathXmlApplicationContext("application.xml");
|
||||
|
||||
// Excel的映射内容需要在被Spring管理的bean的方法上加上@ResInjection注解,即可自动注入Excel对应的对象
|
||||
var testManager = context.getBean(TestManager.class);
|
||||
var testResources = testManager.testResources;
|
||||
for (com.zfoo.storage.resource.TestResource resource : testResources.getAll()) {
|
||||
Map<Integer, String> map = resource.getType9();
|
||||
logger.info(map.get(1));
|
||||
// logger.info(JsonUtils.object2String(resource));
|
||||
}
|
||||
// 通过id找到对应的行
|
||||
var id = 2;
|
||||
var valueById = testResources.get(id);
|
||||
logger.info(JsonUtils.object2String(valueById));
|
||||
logger.info(StringUtils.MULTIPLE_HYPHENS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import org.springframework.stereotype.Component;
|
||||
* @author jaysunxiao
|
||||
* @version 3.0
|
||||
*/
|
||||
@Component
|
||||
//@Component
|
||||
public class StudentManager {
|
||||
|
||||
@ResInjection
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The zfoo Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
package com.zfoo.storage;
|
||||
|
||||
import com.zfoo.storage.model.anno.ResInjection;
|
||||
import com.zfoo.storage.model.vo.Storage;
|
||||
import com.zfoo.storage.resource.StudentResource;
|
||||
import com.zfoo.storage.resource.TestResource;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* @author jaysunxiao
|
||||
* @version 3.0
|
||||
*/
|
||||
@Component
|
||||
public class TestManager {
|
||||
|
||||
@ResInjection
|
||||
public Storage<Integer, TestResource> testResources;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The zfoo Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
package com.zfoo.storage.resource;
|
||||
|
||||
/**
|
||||
* @author jaysunxiao
|
||||
* @version 3.0
|
||||
*/
|
||||
public class Item {
|
||||
|
||||
private int id;
|
||||
private int attr;
|
||||
private String name;
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public int getAttr() {
|
||||
return attr;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import com.zfoo.storage.model.anno.Resource;
|
||||
* @author jaysunxiao
|
||||
* @version 3.0
|
||||
*/
|
||||
@Resource
|
||||
//@Resource
|
||||
public class StudentResource {
|
||||
|
||||
@Id
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The zfoo Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
package com.zfoo.storage.resource;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.zfoo.storage.model.anno.Id;
|
||||
import com.zfoo.storage.model.anno.Index;
|
||||
import com.zfoo.storage.model.anno.Resource;
|
||||
|
||||
/**
|
||||
* @author jaysunxiao
|
||||
* @version 3.0
|
||||
*/
|
||||
@Resource
|
||||
public class TestResource {
|
||||
|
||||
@Id
|
||||
private int Id;
|
||||
private long Type0;
|
||||
private String Type1;
|
||||
private String[] Type2;
|
||||
private Integer[] Type3;
|
||||
private Long[] Type4;
|
||||
private Item[] Type5;
|
||||
private Map<Integer, Integer> Type8;
|
||||
private Map<Integer, String> Type9;
|
||||
private Map<String, String> Type10;
|
||||
|
||||
public int getId() {
|
||||
return Id;
|
||||
}
|
||||
public long getType0() {
|
||||
return Type0;
|
||||
}
|
||||
public String getType1() {
|
||||
return Type1;
|
||||
}
|
||||
public String[] getType2() {
|
||||
return Type2;
|
||||
}
|
||||
public Integer[] getType3() {
|
||||
return Type3;
|
||||
}
|
||||
public Long[] getType4() {
|
||||
return Type4;
|
||||
}
|
||||
public Item[] getType5() {
|
||||
return Type5;
|
||||
}
|
||||
public Map<Integer, Integer> getType8() {
|
||||
return Type8;
|
||||
}
|
||||
public Map<Integer, String> getType9() {
|
||||
return Type9;
|
||||
}
|
||||
public Map<String, String> getType10() {
|
||||
return Type10;
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,8 @@
|
||||
<storage:storage id="resourceManager">
|
||||
<storage:scan package="com.zfoo.**.resource"/>
|
||||
<!-- 如果是类路径一classpath开头,如果是其它目录则以file开头-->
|
||||
<storage:resource location="classpath:/excel" suffix="xlsx"/>
|
||||
<storage:resource location="classpath:/excel" suffix="txt"/>
|
||||
<!--<storage:resource location="classpath:/excel" suffix="xlsx"/>-->
|
||||
<!-- <storage:resource location="file:C:\Users\jaysunxiao\Desktop\excel" suffix="xlsx"/>-->
|
||||
</storage:storage>
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"name": "Test",
|
||||
"columns": [
|
||||
{
|
||||
"name": "Id",
|
||||
"type": "int"
|
||||
},
|
||||
{
|
||||
"name": "Type0",
|
||||
"type": "long"
|
||||
},
|
||||
{
|
||||
"name": "Type1",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "Type2",
|
||||
"type": "list\u003cstring\u003e"
|
||||
},
|
||||
{
|
||||
"name": "Type3",
|
||||
"type": "list\u003cint\u003e"
|
||||
},
|
||||
{
|
||||
"name": "Type4",
|
||||
"type": "list\u003clong\u003e"
|
||||
},
|
||||
{
|
||||
"name": "Type5",
|
||||
"type": "array\u003cattr,id,name\u003e"
|
||||
},
|
||||
{
|
||||
"name": "Type8",
|
||||
"type": "map\u003cint,int\u003e"
|
||||
},
|
||||
{
|
||||
"name": "Type9",
|
||||
"type": "map\u003cint,string\u003e"
|
||||
},
|
||||
{
|
||||
"name": "Type10",
|
||||
"type": "map\u003cstring,string\u003e"
|
||||
}
|
||||
],
|
||||
"data": [
|
||||
[
|
||||
"1",
|
||||
"0",
|
||||
"1",
|
||||
"[\"\\\"sada\\\"\"]",
|
||||
"[21321]",
|
||||
"[100000000000000,30000000000000]",
|
||||
"[{\"name\":\"num\",\"id\":5,\"attr\":10},{\"name\":\"num\",\"id\":5,\"attr\":10}]",
|
||||
"{\"1\":10}",
|
||||
"{\"1\":\"\\\"cn\\\"\"}",
|
||||
"{\"\\\"chinese\\\"\":\"\\\"cn\\\"\"}"
|
||||
],
|
||||
[
|
||||
"2",
|
||||
"0",
|
||||
"3",
|
||||
"[\"\\\"sada\\\"\",\"\\\"sader\\\"\"]",
|
||||
"[21,321]",
|
||||
"[100000000000000,30000000000000]",
|
||||
"[{\"name\":\"num\",\"id\":5,\"attr\":10},{\"name\":\"num\",\"id\":5,\"attr\":10}]",
|
||||
"{\"1\":10,\"2\":50}",
|
||||
"{\"1\":\"\\\"cn\\\"\",\"2\":\"\\\"en\\\"\"}",
|
||||
"{\"\\\"english\\\"\":\"\\\"en\\\"\",\"\\\"chinese\\\"\":\"\\\"cn\\\"\"}"
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"0",
|
||||
"2",
|
||||
"[\"\\\"sada\\\"\",\"\\\"s\\\"\",\"\\\"ader\\\"\"]",
|
||||
"[21,3,21]",
|
||||
"[100000000000000,30000000000000]",
|
||||
"[{\"name\":\"num\",\"id\":5,\"attr\":10},{\"name\":\"num\",\"id\":5,\"attr\":10}]",
|
||||
"{\"1\":10,\"2\":50,\"3\":100}",
|
||||
"{\"1\":\"\\\"cn\\\"\",\"2\":\"\\\"en\\\"\",\"3\":\"\\\"ru\\\"\"}",
|
||||
"{\"\\\"Ruisn\\\"\":\"\\\"ru\\\"\",\"\\\"english\\\"\":\"\\\"en\\\"\",\"\\\"chinese\\\"\":\"\\\"cn\\\"\"}"
|
||||
]
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user