Compare commits
4 Commits
record-bui
...
record-bui
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c4baa015f | ||
|
|
81b7b93a5b | ||
|
|
c39983e342 | ||
|
|
400caa2943 |
20
README.md
20
README.md
@@ -57,8 +57,11 @@ var r1 = new NameAndAge("foo", 123);
|
||||
var r2 = r1.withName("bar");
|
||||
var r3 = r2.withAge(456);
|
||||
|
||||
// access the builder as well:
|
||||
// access the builder as well
|
||||
var r4 = r3.with().age(101).name("baz").build();
|
||||
|
||||
// alternate method of accessing the builder (note: no need to call "build()")
|
||||
var r5 = r3.with(b -> b.age(200).name("whatever"));
|
||||
```
|
||||
|
||||
_Hat tip to [Benji Weber](https://benjiweber.co.uk/blog/2020/09/19/fun-with-java-records/) for the Withers idea._
|
||||
@@ -161,19 +164,11 @@ public class NameAndAgeBuilder {
|
||||
* Add withers to {@code NameAndAge}
|
||||
*/
|
||||
public interface With {
|
||||
/**
|
||||
* Cast this to the record type
|
||||
*/
|
||||
default NameAndAge internalGetThis() {
|
||||
Object obj = this;
|
||||
return (PersonRecord)obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new record builder using the current values
|
||||
*/
|
||||
default NameAndAgeBuilder with() {
|
||||
NameAndAge r = internalGetThis();
|
||||
var r = (NameAndAge)(Object)this;
|
||||
return NameAndAgeBuilder.builder(r);
|
||||
}
|
||||
|
||||
@@ -181,7 +176,7 @@ public class NameAndAgeBuilder {
|
||||
* Return a new instance of {@code NameAndAge} with a new value for {@code name}
|
||||
*/
|
||||
default NameAndAge withName(String name) {
|
||||
NameAndAge r = internalGetThis();
|
||||
var r = (NameAndAge)(Object)this;
|
||||
return new NameAndAge(name, r.age());
|
||||
}
|
||||
|
||||
@@ -189,7 +184,7 @@ public class NameAndAgeBuilder {
|
||||
* Return a new instance of {@code NameAndAge} with a new value for {@code age}
|
||||
*/
|
||||
default NameAndAge withAge(int age) {
|
||||
NameAndAge r = internalGetThis();
|
||||
var r = (NameAndAge)(Object)this;
|
||||
return new NameAndAge(r.name(), age);
|
||||
}
|
||||
}
|
||||
@@ -327,7 +322,6 @@ Alternatively, you can provide values for each individual meta data (or combinat
|
||||
- `javac ... -AcomponentsMethodName=foo`
|
||||
- `javac ... -AwithClassName=foo`
|
||||
- `javac ... -AwithClassMethodPrefix=foo`
|
||||
- `javac ... -AwithClassGetThisMethodName=foo`
|
||||
- `javac ... -AfileComment=foo`
|
||||
- `javac ... -AfileIndent=foo`
|
||||
- `javac ... -AprefixEnclosingClassNames=foo`
|
||||
|
||||
4
pom.xml
4
pom.xml
@@ -5,7 +5,7 @@
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<version>1.8.ea</version>
|
||||
<version>1.9.ea</version>
|
||||
|
||||
<modules>
|
||||
<module>record-builder-core</module>
|
||||
@@ -71,7 +71,7 @@
|
||||
<url>https://github.com/randgalt/record-builder</url>
|
||||
<connection>scm:git:https://github.com/randgalt/record-builder.git</connection>
|
||||
<developerConnection>scm:git:git@github.com:randgalt/record-builder.git</developerConnection>
|
||||
<tag>record-builder-1.8.ea</tag>
|
||||
<tag>record-builder-1.9.ea</tag>
|
||||
</scm>
|
||||
|
||||
<issueManagement>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder</artifactId>
|
||||
<version>1.8.ea</version>
|
||||
<version>1.9.ea</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@@ -105,15 +105,6 @@ public interface RecordBuilderMetaData {
|
||||
return "with";
|
||||
}
|
||||
|
||||
/**
|
||||
* The name to use for the method that returns "this" cast to the record type
|
||||
*
|
||||
* @return method name
|
||||
*/
|
||||
default String withClassGetThisMethodName() {
|
||||
return "internalGetThis";
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the comment to place at the top of generated files. Return null or an empty string for no comment.
|
||||
*
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder</artifactId>
|
||||
<version>1.8.ea</version>
|
||||
<version>1.9.ea</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@@ -15,15 +15,25 @@
|
||||
*/
|
||||
package io.soabase.recordbuilder.processor;
|
||||
|
||||
import com.squareup.javapoet.*;
|
||||
import com.squareup.javapoet.ClassName;
|
||||
import com.squareup.javapoet.CodeBlock;
|
||||
import com.squareup.javapoet.FieldSpec;
|
||||
import com.squareup.javapoet.MethodSpec;
|
||||
import com.squareup.javapoet.ParameterSpec;
|
||||
import com.squareup.javapoet.ParameterizedTypeName;
|
||||
import com.squareup.javapoet.TypeName;
|
||||
import com.squareup.javapoet.TypeSpec;
|
||||
import com.squareup.javapoet.TypeVariableName;
|
||||
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
|
||||
|
||||
import javax.lang.model.element.Modifier;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
|
||||
import java.util.AbstractMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
@@ -32,7 +42,8 @@ import static io.soabase.recordbuilder.processor.ElementUtils.getBuilderName;
|
||||
import static io.soabase.recordbuilder.processor.ElementUtils.getWithMethodName;
|
||||
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.generatedRecordBuilderAnnotation;
|
||||
|
||||
class InternalRecordBuilderProcessor {
|
||||
class InternalRecordBuilderProcessor
|
||||
{
|
||||
private final RecordBuilderMetaData metaData;
|
||||
private final ClassType recordClassType;
|
||||
private final String packageName;
|
||||
@@ -42,7 +53,8 @@ class InternalRecordBuilderProcessor {
|
||||
private final TypeSpec builderType;
|
||||
private final TypeSpec.Builder builder;
|
||||
|
||||
InternalRecordBuilderProcessor(TypeElement record, RecordBuilderMetaData metaData) {
|
||||
InternalRecordBuilderProcessor(TypeElement record, RecordBuilderMetaData metaData)
|
||||
{
|
||||
this.metaData = metaData;
|
||||
recordClassType = ElementUtils.getClassType(record, record.getTypeParameters());
|
||||
packageName = ElementUtils.getPackageName(record);
|
||||
@@ -72,19 +84,23 @@ class InternalRecordBuilderProcessor {
|
||||
builderType = builder.build();
|
||||
}
|
||||
|
||||
String packageName() {
|
||||
String packageName()
|
||||
{
|
||||
return packageName;
|
||||
}
|
||||
|
||||
ClassType builderClassType() {
|
||||
ClassType builderClassType()
|
||||
{
|
||||
return builderClassType;
|
||||
}
|
||||
|
||||
TypeSpec builderType() {
|
||||
TypeSpec builderType()
|
||||
{
|
||||
return builderType;
|
||||
}
|
||||
|
||||
private void addWithNestedClass() {
|
||||
private void addWithNestedClass()
|
||||
{
|
||||
/*
|
||||
Adds a nested interface that adds withers similar to:
|
||||
|
||||
@@ -94,45 +110,61 @@ class InternalRecordBuilderProcessor {
|
||||
}
|
||||
}
|
||||
*/
|
||||
TypeSpec.Builder classBuilder = TypeSpec.interfaceBuilder(metaData.withClassName())
|
||||
var classBuilder = TypeSpec.interfaceBuilder(metaData.withClassName())
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addJavadoc("Add withers to {@code $L}\n", recordClassType.name())
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.addTypeVariables(typeVariables);
|
||||
addWithGetThisMethod(classBuilder);
|
||||
addWithBuilderMethod(classBuilder);
|
||||
addWithSuppliedBuilderMethod(classBuilder);
|
||||
IntStream.range(0, recordComponents.size()).forEach(index -> add1WithMethod(classBuilder, recordComponents.get(index), index));
|
||||
builder.addType(classBuilder.build());
|
||||
}
|
||||
|
||||
private void addWithGetThisMethod(TypeSpec.Builder classBuilder) {
|
||||
private void addWithSuppliedBuilderMethod(TypeSpec.Builder classBuilder)
|
||||
{
|
||||
/*
|
||||
Adds a method that returns "this" cast to the record similar to:
|
||||
Adds a method that returns a pre-filled copy builder similar to:
|
||||
|
||||
default MyRecord internalGetThis() {
|
||||
Object obj = this;
|
||||
return (MyRecord)obj;
|
||||
default MyRecord with(Consumer<MyRecordBuilder> consumer) {
|
||||
MyRecord r = (MyRecord)(Object)this;
|
||||
MyRecordBuilder builder MyRecordBuilder.builder(r);
|
||||
consumer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
*/
|
||||
CodeBlock codeBlock = CodeBlock.builder()
|
||||
.add("Object obj = this;\n")
|
||||
.add("return ($T)obj;", recordClassType.typeName())
|
||||
.build();
|
||||
MethodSpec methodSpec = MethodSpec.methodBuilder(metaData.withClassGetThisMethodName())
|
||||
var codeBlockBuilder = CodeBlock.builder()
|
||||
.add("var r = ($T)(Object)this;\n", recordClassType.typeName())
|
||||
.add("$T builder = $L.$L(r);\n", builderClassType.typeName(), builderClassType.name(), metaData.copyMethodName())
|
||||
.add("consumer.accept(builder);\n")
|
||||
.add("return builder.build();\n");
|
||||
var consumerType = ParameterizedTypeName.get(ClassName.get(Consumer.class), builderClassType.typeName());
|
||||
var parameter = ParameterSpec.builder(consumerType, "consumer").build();
|
||||
var methodSpec = MethodSpec.methodBuilder(metaData.withClassMethodPrefix())
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addJavadoc("Cast this to the record type")
|
||||
.addJavadoc("Return a new record built from the builder passed to the given consumer")
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
|
||||
.addParameter(parameter)
|
||||
.returns(recordClassType.typeName())
|
||||
.addCode(codeBlock)
|
||||
.addCode(codeBlockBuilder.build())
|
||||
.build();
|
||||
classBuilder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addWithBuilderMethod(TypeSpec.Builder classBuilder) {
|
||||
CodeBlock.Builder codeBlockBuilder = CodeBlock.builder()
|
||||
.add("$T r = $L();\n", recordClassType.typeName(), metaData.withClassGetThisMethodName())
|
||||
private void addWithBuilderMethod(TypeSpec.Builder classBuilder)
|
||||
{
|
||||
/*
|
||||
Adds a method that returns a pre-filled copy builder similar to:
|
||||
|
||||
default MyRecordBuilder with() {
|
||||
MyRecord r = (MyRecord)(Object)this;
|
||||
return MyRecordBuilder.builder(r);
|
||||
}
|
||||
*/
|
||||
var codeBlockBuilder = CodeBlock.builder()
|
||||
.add("var r = ($T)(Object)this;\n", recordClassType.typeName())
|
||||
.add("return $L.$L(r);", builderClassType.name(), metaData.copyMethodName());
|
||||
MethodSpec methodSpec = MethodSpec.methodBuilder(metaData.withClassMethodPrefix())
|
||||
var methodSpec = MethodSpec.methodBuilder(metaData.withClassMethodPrefix())
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addJavadoc("Return a new record builder using the current values")
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
|
||||
@@ -142,17 +174,18 @@ class InternalRecordBuilderProcessor {
|
||||
classBuilder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void add1WithMethod(TypeSpec.Builder classBuilder, ClassType component, int index) {
|
||||
private void add1WithMethod(TypeSpec.Builder classBuilder, ClassType component, int index)
|
||||
{
|
||||
/*
|
||||
Adds a with method for the component similar to:
|
||||
|
||||
default MyRecord withName(String name) {
|
||||
MyRecord r = internalGetThis();
|
||||
MyRecord r = (MyRecord)(Object)this;
|
||||
return new MyRecord(name, r.age());
|
||||
}
|
||||
*/
|
||||
CodeBlock.Builder codeBlockBuilder = CodeBlock.builder()
|
||||
.add("$T r = $L();\n", recordClassType.typeName(), metaData.withClassGetThisMethodName())
|
||||
var codeBlockBuilder = CodeBlock.builder()
|
||||
.add("var r = ($T)(Object)this;\n", recordClassType.typeName())
|
||||
.add("return new $T(", recordClassType.typeName());
|
||||
IntStream.range(0, recordComponents.size()).forEach(parameterIndex -> {
|
||||
if (parameterIndex > 0) {
|
||||
@@ -161,15 +194,16 @@ class InternalRecordBuilderProcessor {
|
||||
ClassType parameterComponent = recordComponents.get(parameterIndex);
|
||||
if (parameterIndex == index) {
|
||||
codeBlockBuilder.add(parameterComponent.name());
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
codeBlockBuilder.add("r.$L()", parameterComponent.name());
|
||||
}
|
||||
});
|
||||
codeBlockBuilder.add(");");
|
||||
|
||||
String methodName = getWithMethodName(component, metaData.withClassMethodPrefix());
|
||||
var methodName = getWithMethodName(component, metaData.withClassMethodPrefix());
|
||||
var parameterSpec = ParameterSpec.builder(component.typeName(), component.name()).build();
|
||||
MethodSpec methodSpec = MethodSpec.methodBuilder(methodName)
|
||||
var methodSpec = MethodSpec.methodBuilder(methodName)
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addJavadoc("Return a new instance of {@code $L} with a new value for {@code $L}\n", recordClassType.name(), component.name())
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
|
||||
@@ -180,7 +214,8 @@ class InternalRecordBuilderProcessor {
|
||||
classBuilder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addDefaultConstructor() {
|
||||
private void addDefaultConstructor()
|
||||
{
|
||||
/*
|
||||
Adds a default constructor similar to:
|
||||
|
||||
@@ -194,7 +229,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(constructor);
|
||||
}
|
||||
|
||||
private void addAllArgsConstructor() {
|
||||
private void addAllArgsConstructor()
|
||||
{
|
||||
/*
|
||||
Adds an all-args constructor similar to:
|
||||
|
||||
@@ -215,7 +251,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(constructorBuilder.build());
|
||||
}
|
||||
|
||||
private void addToStringMethod() {
|
||||
private void addToStringMethod()
|
||||
{
|
||||
/*
|
||||
add a toString() method similar to:
|
||||
|
||||
@@ -244,7 +281,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addHashCodeMethod() {
|
||||
private void addHashCodeMethod()
|
||||
{
|
||||
/*
|
||||
add a hashCode() method similar to:
|
||||
|
||||
@@ -272,7 +310,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addEqualsMethod() {
|
||||
private void addEqualsMethod()
|
||||
{
|
||||
/*
|
||||
add an equals() method similar to:
|
||||
|
||||
@@ -290,7 +329,8 @@ class InternalRecordBuilderProcessor {
|
||||
String name = recordComponent.name();
|
||||
if (recordComponent.typeName().isPrimitive()) {
|
||||
codeBuilder.add("\n&& ($L == b.$L)", name, name);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
codeBuilder.add("\n&& $T.equals($L, b.$L)", Objects.class, name, name);
|
||||
}
|
||||
});
|
||||
@@ -307,7 +347,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addBuildMethod() {
|
||||
private void addBuildMethod()
|
||||
{
|
||||
/*
|
||||
Adds the build method that generates the record similar to:
|
||||
|
||||
@@ -334,7 +375,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addStaticCopyBuilderMethod() {
|
||||
private void addStaticCopyBuilderMethod()
|
||||
{
|
||||
/*
|
||||
Adds a copy builder method that pre-fills the builder with existing values similar to:
|
||||
|
||||
@@ -363,7 +405,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addStaticDefaultBuilderMethod() {
|
||||
private void addStaticDefaultBuilderMethod()
|
||||
{
|
||||
/*
|
||||
Adds a the default builder method similar to:
|
||||
|
||||
@@ -382,7 +425,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addStaticComponentsMethod() {
|
||||
private void addStaticComponentsMethod()
|
||||
{
|
||||
/*
|
||||
Adds a static method that converts a record instance into a stream of its component parts
|
||||
|
||||
@@ -416,7 +460,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void add1Field(ClassType component) {
|
||||
private void add1Field(ClassType component)
|
||||
{
|
||||
/*
|
||||
For a single record component, add a field similar to:
|
||||
|
||||
@@ -426,7 +471,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addField(fieldSpec);
|
||||
}
|
||||
|
||||
private void add1GetterMethod(ClassType component) {
|
||||
private void add1GetterMethod(ClassType component)
|
||||
{
|
||||
/*
|
||||
For a single record component, add a getter similar to:
|
||||
|
||||
@@ -444,7 +490,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void add1SetterMethod(ClassType component) {
|
||||
private void add1SetterMethod(ClassType component)
|
||||
{
|
||||
/*
|
||||
For a single record component, add a setter similar to:
|
||||
|
||||
|
||||
@@ -75,11 +75,6 @@ public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
|
||||
*/
|
||||
public static final String OPTION_WITH_CLASS_METHOD_PREFIX = "withClassMethodPrefix";
|
||||
|
||||
/**
|
||||
* @see #withClassGetThisMethodName()
|
||||
*/
|
||||
public static final String OPTION_WITH_CLASS_GET_THIS_METHOD_NAME = "withClassGetThisMethodName";
|
||||
|
||||
private final String suffix;
|
||||
private final String interfaceSuffix;
|
||||
private final String copyMethodName;
|
||||
@@ -88,7 +83,6 @@ public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
|
||||
private final String componentsMethodName;
|
||||
private final String withClassName;
|
||||
private final String withClassMethodPrefix;
|
||||
private final String withClassGetThisMethodName;
|
||||
private final String fileComment;
|
||||
private final String fileIndent;
|
||||
private final boolean prefixEnclosingClassNames;
|
||||
@@ -102,7 +96,6 @@ public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
|
||||
componentsMethodName = options.getOrDefault(OPTION_COMPONENTS_METHOD_NAME, DEFAULT.componentsMethodName());
|
||||
withClassName = options.getOrDefault(OPTION_WITH_CLASS_NAME, DEFAULT.withClassName());
|
||||
withClassMethodPrefix = options.getOrDefault(OPTION_WITH_CLASS_METHOD_PREFIX, DEFAULT.withClassMethodPrefix());
|
||||
withClassGetThisMethodName = options.getOrDefault(OPTION_WITH_CLASS_GET_THIS_METHOD_NAME, DEFAULT.withClassGetThisMethodName());
|
||||
fileComment = options.getOrDefault(OPTION_FILE_COMMENT, DEFAULT.fileComment());
|
||||
fileIndent = options.getOrDefault(OPTION_FILE_INDENT, DEFAULT.fileIndent());
|
||||
String prefixenclosingclassnamesopt = options.getOrDefault(OPTION_PREFIX_ENCLOSING_CLASS_NAMES, String.valueOf(DEFAULT.prefixEnclosingClassNames()));
|
||||
@@ -148,11 +141,6 @@ public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
|
||||
return withClassMethodPrefix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String withClassGetThisMethodName() {
|
||||
return withClassGetThisMethodName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String fileComment() {
|
||||
return fileComment;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder</artifactId>
|
||||
<version>1.8.ea</version>
|
||||
<version>1.9.ea</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@@ -46,4 +46,17 @@ class TestWithers {
|
||||
Assertions.assertEquals(20, r3.i());
|
||||
Assertions.assertEquals("changed", r3.s());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWitherBuilderConsumer() {
|
||||
var r1 = new SimpleGenericRecord<>(10, "ten");
|
||||
var r2 = r1.with(r -> r.i(15));
|
||||
var r3 = r1.with(r -> r.s("twenty").i(20));
|
||||
Assertions.assertEquals(10, r1.i());
|
||||
Assertions.assertEquals("ten", r1.s());
|
||||
Assertions.assertEquals(15, r2.i());
|
||||
Assertions.assertEquals("ten", r2.s());
|
||||
Assertions.assertEquals(20, r3.i());
|
||||
Assertions.assertEquals("twenty", r3.s());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user