HHH-16633 add ability to generate @Find methods
This commit is contained in:
parent
3969c74963
commit
0c40711563
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Hibernate, Relational Persistence for Idiomatic Java
|
||||
*
|
||||
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
|
||||
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
|
||||
*/
|
||||
package org.hibernate.annotations.processing;
|
||||
|
||||
import org.hibernate.Incubating;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.METHOD;
|
||||
import static java.lang.annotation.RetentionPolicy.CLASS;
|
||||
|
||||
/**
|
||||
* Identifies a method of an abstract class or interface as defining
|
||||
* the signature of a finder method, and is generated automatically by
|
||||
* the Hibernate Metamodel Generator.
|
||||
*
|
||||
* @author Gavin King
|
||||
* @since 6.3
|
||||
*/
|
||||
@Target(METHOD)
|
||||
@Retention(CLASS)
|
||||
@Incubating
|
||||
public @interface Find {}
|
|
@ -30,6 +30,8 @@ import org.hibernate.jpamodelgen.util.TypeUtils;
|
|||
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
import static org.hibernate.jpamodelgen.util.TypeUtils.containsAnnotation;
|
||||
|
||||
/**
|
||||
* Helper class to write the actual meta model class using the {@link javax.annotation.processing.Filer} API.
|
||||
*
|
||||
|
@ -123,8 +125,10 @@ public final class ClassWriter {
|
|||
}
|
||||
pw.println();
|
||||
for ( MetaAttribute metaMember : members ) {
|
||||
if ( metaMember.hasStringAttribute() ) {
|
||||
pw.println( '\t' + metaMember.getAttributeNameDeclarationString() );
|
||||
}
|
||||
}
|
||||
|
||||
pw.println();
|
||||
pw.println("}");
|
||||
|
@ -190,8 +194,8 @@ public final class ClassWriter {
|
|||
// to allow for the case that the metamodel class for the super entity is for example contained in another
|
||||
// jar file we use reflection. However, we need to consider the fact that there is xml configuration
|
||||
// and annotations should be ignored
|
||||
if ( !entityMetaComplete && ( TypeUtils.containsAnnotation( superClassElement, Constants.ENTITY )
|
||||
|| TypeUtils.containsAnnotation( superClassElement, Constants.MAPPED_SUPERCLASS ) ) ) {
|
||||
if ( !entityMetaComplete && ( containsAnnotation( superClassElement, Constants.ENTITY )
|
||||
|| containsAnnotation( superClassElement, Constants.MAPPED_SUPERCLASS ) ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,6 @@ import javax.lang.model.type.DeclaredType;
|
|||
import javax.lang.model.type.ExecutableType;
|
||||
import javax.lang.model.type.TypeKind;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
import javax.lang.model.util.ElementFilter;
|
||||
import javax.lang.model.util.SimpleTypeVisitor8;
|
||||
import javax.tools.Diagnostic;
|
||||
|
||||
|
@ -38,6 +37,8 @@ import org.hibernate.jpamodelgen.xml.JpaDescriptorParser;
|
|||
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
import static javax.lang.model.util.ElementFilter.fieldsIn;
|
||||
import static javax.lang.model.util.ElementFilter.methodsIn;
|
||||
import static org.hibernate.jpamodelgen.util.Constants.HQL;
|
||||
import static org.hibernate.jpamodelgen.util.Constants.SQL;
|
||||
import static org.hibernate.jpamodelgen.util.TypeUtils.containsAnnotation;
|
||||
|
@ -56,6 +57,7 @@ import static org.hibernate.jpamodelgen.util.TypeUtils.isAnnotationMirrorOfType;
|
|||
Constants.EMBEDDABLE,
|
||||
Constants.HQL,
|
||||
Constants.SQL,
|
||||
Constants.FIND,
|
||||
Constants.NAMED_QUERY,
|
||||
Constants.NAMED_NATIVE_QUERY,
|
||||
Constants.NAMED_ENTITY_GRAPH,
|
||||
|
@ -247,7 +249,7 @@ public class JPAMetaModelEntityProcessor extends AbstractProcessor {
|
|||
if ( entity.equals( containedEntity ) ) {
|
||||
continue;
|
||||
}
|
||||
for ( Element subElement : ElementFilter.fieldsIn( entity.getElement().getEnclosedElements() ) ) {
|
||||
for ( Element subElement : fieldsIn( entity.getElement().getEnclosedElements() ) ) {
|
||||
TypeMirror mirror = subElement.asType();
|
||||
if ( TypeKind.DECLARED == mirror.getKind() ) {
|
||||
if ( mirror.accept( visitor, subElement ) ) {
|
||||
|
@ -255,7 +257,7 @@ public class JPAMetaModelEntityProcessor extends AbstractProcessor {
|
|||
}
|
||||
}
|
||||
}
|
||||
for ( Element subElement : ElementFilter.methodsIn( entity.getElement().getEnclosedElements() ) ) {
|
||||
for ( Element subElement : methodsIn( entity.getElement().getEnclosedElements() ) ) {
|
||||
TypeMirror mirror = subElement.asType();
|
||||
if ( TypeKind.DECLARED == mirror.getKind() ) {
|
||||
if ( mirror.accept( visitor, subElement ) ) {
|
||||
|
|
|
@ -39,6 +39,11 @@ public abstract class AnnotationMetaAttribute implements MetaAttribute {
|
|||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasStringAttribute() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAttributeDeclarationString() {
|
||||
return new StringBuilder()
|
||||
|
|
|
@ -233,7 +233,7 @@ public class AnnotationMetaEntity extends AnnotationMeta {
|
|||
if ( isGetterOrSetter( rawMethodOfClass ) ) {
|
||||
gettersAndSettersOfClass.add( rawMethodOfClass );
|
||||
}
|
||||
else if ( containsAnnotation( rawMethodOfClass, Constants.HQL, Constants.SQL ) ) {
|
||||
else if ( containsAnnotation( rawMethodOfClass, Constants.HQL, Constants.SQL, Constants.FIND ) ) {
|
||||
queryMethods.add( rawMethodOfClass );
|
||||
}
|
||||
}
|
||||
|
@ -341,35 +341,33 @@ public class AnnotationMetaEntity extends AnnotationMeta {
|
|||
}
|
||||
|
||||
private void addQueryMethod(ExecutableElement method) {
|
||||
final String methodName = method.getSimpleName().toString();
|
||||
final TypeMirror returnType = method.getReturnType();
|
||||
if ( returnType instanceof DeclaredType ) {
|
||||
if ( returnType.getKind() == TypeKind.DECLARED ) {
|
||||
final DeclaredType declaredType = (DeclaredType) returnType;
|
||||
final TypeElement typeElement = (TypeElement) declaredType.asElement();
|
||||
final List<? extends TypeMirror> typeArguments = declaredType.getTypeArguments();
|
||||
if ( typeArguments.size() == 0 ) {
|
||||
final String typeName = declaredType.toString();
|
||||
if ( containsAnnotation( declaredType.asElement(), Constants.ENTITY ) ) {
|
||||
addQueryMethod( method, methodName, typeName, null );
|
||||
addQueryMethod( method, declaredType, null );
|
||||
}
|
||||
else {
|
||||
if ( isLegalRawResultType( typeName ) ) {
|
||||
addQueryMethod( method, methodName, null, typeName );
|
||||
if ( isLegalRawResultType( typeElement.getQualifiedName().toString() ) ) {
|
||||
addQueryMethod( method, null, typeElement );
|
||||
}
|
||||
else {
|
||||
// probably a projection
|
||||
addQueryMethod( method, methodName, typeName, null );
|
||||
addQueryMethod( method, declaredType, null );
|
||||
}
|
||||
}
|
||||
}
|
||||
else if ( typeArguments.size() == 1 ) {
|
||||
final String containerTypeName = declaredType.asElement().toString();
|
||||
final String returnTypeName = typeArguments.get(0).toString();
|
||||
if ( isLegalGenericResultType( containerTypeName ) ) {
|
||||
addQueryMethod( method, methodName, returnTypeName, containerTypeName );
|
||||
final Element containerType = declaredType.asElement();
|
||||
if ( isLegalGenericResultType( containerType.toString() ) ) {
|
||||
addQueryMethod( method, typeArguments.get(0), typeElement );
|
||||
}
|
||||
else {
|
||||
context.message( method,
|
||||
"incorrect return type '" + containerTypeName + "'",
|
||||
"incorrect return type '" + containerType + "'",
|
||||
Diagnostic.Kind.ERROR );
|
||||
}
|
||||
}
|
||||
|
@ -396,46 +394,121 @@ public class AnnotationMetaEntity extends AnnotationMeta {
|
|||
|
||||
private void addQueryMethod(
|
||||
ExecutableElement method,
|
||||
String methodName,
|
||||
@Nullable String returnTypeName,
|
||||
@Nullable String containerTypeName) {
|
||||
@Nullable TypeMirror returnType,
|
||||
@Nullable TypeElement containerType) {
|
||||
final AnnotationMirror hql = getAnnotationMirror( method, Constants.HQL );
|
||||
if ( hql != null ) {
|
||||
addQueryMethod( method, methodName, returnTypeName, containerTypeName, hql, false );
|
||||
addQueryMethod( method, returnType, containerType, hql, false );
|
||||
}
|
||||
final AnnotationMirror sql = getAnnotationMirror( method, Constants.SQL );
|
||||
if ( sql != null ) {
|
||||
addQueryMethod( method, methodName, returnTypeName, containerTypeName, sql, true );
|
||||
addQueryMethod( method, returnType, containerType, sql, true );
|
||||
}
|
||||
final AnnotationMirror find = getAnnotationMirror( method, Constants.FIND );
|
||||
if ( find != null ) {
|
||||
addFinderMethod( method, returnType, containerType );
|
||||
}
|
||||
}
|
||||
|
||||
private void addFinderMethod(
|
||||
ExecutableElement method,
|
||||
@Nullable TypeMirror returnType,
|
||||
@Nullable TypeElement containerType) {
|
||||
if ( containerType != null ) {
|
||||
context.message( method,
|
||||
"incorrect return type '" + containerType.getQualifiedName() + "' is not an entity type",
|
||||
Diagnostic.Kind.ERROR );
|
||||
}
|
||||
else if ( returnType == null || returnType.getKind() != TypeKind.DECLARED ) {
|
||||
context.message( method,
|
||||
"incorrect return type '" + returnType + "' is not an entity type",
|
||||
Diagnostic.Kind.ERROR );
|
||||
}
|
||||
else {
|
||||
final DeclaredType declaredType = (DeclaredType) returnType;
|
||||
final TypeElement entity = (TypeElement) declaredType.asElement();
|
||||
if ( !containsAnnotation( entity, Constants.ENTITY ) ) {
|
||||
context.message( method,
|
||||
"incorrect return type '" + returnType + "' is not annotated '@Entity'",
|
||||
Diagnostic.Kind.ERROR );
|
||||
}
|
||||
else {
|
||||
switch ( method.getParameters().size() ) {
|
||||
case 0:
|
||||
context.message( method,
|
||||
"missing parameter",
|
||||
Diagnostic.Kind.ERROR );
|
||||
break;
|
||||
case 1:
|
||||
final VariableElement parameter = method.getParameters().get(0);
|
||||
validateFinderParameter( entity, parameter);
|
||||
final String methodName = method.getSimpleName().toString();
|
||||
putMember( methodName,
|
||||
new FinderMethod(
|
||||
this,
|
||||
methodName,
|
||||
returnType.toString(),
|
||||
parameter.getSimpleName().toString(),
|
||||
parameter.asType().toString(),
|
||||
dao
|
||||
)
|
||||
);
|
||||
break;
|
||||
default:
|
||||
context.message( method,
|
||||
"too many parameters ('@IdClass' not yet supported)",
|
||||
Diagnostic.Kind.ERROR );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateFinderParameter(TypeElement entity, VariableElement param) {
|
||||
entity.getEnclosedElements().stream()
|
||||
.filter(member -> member.getSimpleName().contentEquals( param.getSimpleName() )
|
||||
&& member.getAnnotationMirrors().stream()
|
||||
.anyMatch(annotation -> {
|
||||
final TypeElement annotationType = (TypeElement)
|
||||
annotation.getAnnotationType().asElement();
|
||||
final Name annotatioName = annotationType.getQualifiedName();
|
||||
return annotatioName.contentEquals(Constants.ID)
|
||||
|| annotatioName.contentEquals(Constants.EMBEDDED_ID);
|
||||
}))
|
||||
.findAny()
|
||||
.ifPresentOrElse(
|
||||
member -> {
|
||||
final String memberType = member.asType().toString();
|
||||
final String paramType = param.asType().toString();
|
||||
if ( !memberType.equals(paramType)) {
|
||||
context.message( param,
|
||||
"matching '@Id' field in entity class has type '" + memberType + "'",
|
||||
Diagnostic.Kind.ERROR );
|
||||
}
|
||||
},
|
||||
() -> context.message( param,
|
||||
"no matching '@Id' field in entity class",
|
||||
Diagnostic.Kind.ERROR )
|
||||
);
|
||||
}
|
||||
|
||||
private void addQueryMethod(
|
||||
ExecutableElement method,
|
||||
String methodName,
|
||||
@Nullable
|
||||
String returnTypeName,
|
||||
@Nullable
|
||||
String containerTypeName,
|
||||
@Nullable TypeMirror returnType,
|
||||
@Nullable TypeElement containerType,
|
||||
AnnotationMirror mirror,
|
||||
boolean isNative) {
|
||||
final Object queryString = getAnnotationValue( mirror, "value" );
|
||||
if ( queryString instanceof String ) {
|
||||
final List<String> paramNames =
|
||||
method.getParameters().stream()
|
||||
.map( param -> param.getSimpleName().toString() )
|
||||
.collect( toList() );
|
||||
final List<String> paramTypes =
|
||||
method.getParameters().stream()
|
||||
.map( param -> param.asType().toString() )
|
||||
.collect( toList() );
|
||||
final List<String> paramNames = parameterNames( method );
|
||||
final List<String> paramTypes = parameterTypes( method );
|
||||
final String hql = (String) queryString;
|
||||
final QueryMethod attribute =
|
||||
new QueryMethod(
|
||||
this,
|
||||
methodName,
|
||||
method.getSimpleName().toString(),
|
||||
hql,
|
||||
returnTypeName,
|
||||
containerTypeName,
|
||||
returnType == null ? null : returnType.toString(),
|
||||
containerType == null ? null : containerType.getQualifiedName().toString(),
|
||||
paramNames,
|
||||
paramTypes,
|
||||
isNative,
|
||||
|
@ -457,6 +530,18 @@ public class AnnotationMetaEntity extends AnnotationMeta {
|
|||
}
|
||||
}
|
||||
|
||||
private static List<String> parameterTypes(ExecutableElement method) {
|
||||
return method.getParameters().stream()
|
||||
.map(param -> param.asType().toString())
|
||||
.collect(toList());
|
||||
}
|
||||
|
||||
private static List<String> parameterNames(ExecutableElement method) {
|
||||
return method.getParameters().stream()
|
||||
.map(param -> param.getSimpleName().toString())
|
||||
.collect(toList());
|
||||
}
|
||||
|
||||
private void checkParameters(ExecutableElement method, List<String> paramNames, List<String> paramTypes, AnnotationMirror mirror, String hql) {
|
||||
for (int i = 1; i <= paramNames.size(); i++) {
|
||||
final String param = paramNames.get(i-1);
|
||||
|
|
|
@ -9,6 +9,9 @@ package org.hibernate.jpamodelgen.annotation;
|
|||
import org.hibernate.jpamodelgen.model.MetaAttribute;
|
||||
import org.hibernate.jpamodelgen.model.Metamodel;
|
||||
|
||||
/**
|
||||
* @author Gavin King
|
||||
*/
|
||||
public class DaoConstructor implements MetaAttribute {
|
||||
private final Metamodel annotationMetaEntity;
|
||||
private final String constructorName;
|
||||
|
@ -29,9 +32,18 @@ public class DaoConstructor implements MetaAttribute {
|
|||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasStringAttribute() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAttributeDeclarationString() {
|
||||
return new StringBuilder()
|
||||
.append("\nprivate final ")
|
||||
.append(annotationMetaEntity.importType(returnTypeName))
|
||||
.append(" entityManager;")
|
||||
.append("\n")
|
||||
.append(inject ? "\n@" + annotationMetaEntity.importType("jakarta.inject.Inject") : "")
|
||||
.append("\npublic ")
|
||||
.append(constructorName)
|
||||
|
@ -53,11 +65,7 @@ public class DaoConstructor implements MetaAttribute {
|
|||
|
||||
@Override
|
||||
public String getAttributeNameDeclarationString() {
|
||||
return new StringBuilder()
|
||||
.append("\n\tprivate final ")
|
||||
.append(annotationMetaEntity.importType(returnTypeName))
|
||||
.append(" entityManager;")
|
||||
.toString();
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* Hibernate, Relational Persistence for Idiomatic Java
|
||||
*
|
||||
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
|
||||
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
|
||||
*/
|
||||
package org.hibernate.jpamodelgen.annotation;
|
||||
|
||||
import org.hibernate.jpamodelgen.model.MetaAttribute;
|
||||
import org.hibernate.jpamodelgen.model.Metamodel;
|
||||
|
||||
/**
|
||||
* @author Gavin King
|
||||
*/
|
||||
public class FinderMethod implements MetaAttribute {
|
||||
private final Metamodel annotationMetaEntity;
|
||||
private final String methodName;
|
||||
private final String entity;
|
||||
private final String paramName;
|
||||
private final String paramType;
|
||||
private final boolean belongsToDao;
|
||||
|
||||
public FinderMethod(Metamodel annotationMetaEntity, String methodName, String entity, String paramName, String paramType, boolean belongsToDao) {
|
||||
this.annotationMetaEntity = annotationMetaEntity;
|
||||
this.methodName = methodName;
|
||||
this.entity = entity;
|
||||
this.paramName = paramName;
|
||||
this.paramType = paramType;
|
||||
this.belongsToDao = belongsToDao;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasTypedAttribute() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasStringAttribute() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAttributeDeclarationString() {
|
||||
StringBuilder declaration = new StringBuilder();
|
||||
declaration
|
||||
.append("\n/**\n * @see ")
|
||||
.append(annotationMetaEntity.getQualifiedName())
|
||||
.append("#")
|
||||
.append(methodName)
|
||||
.append("(")
|
||||
.append(annotationMetaEntity.importType(paramType))
|
||||
.append(")")
|
||||
.append("\n **/\n");
|
||||
if ( belongsToDao ) {
|
||||
declaration
|
||||
.append("@Override\npublic ");
|
||||
}
|
||||
else {
|
||||
declaration
|
||||
.append("public static ");
|
||||
}
|
||||
declaration
|
||||
.append(annotationMetaEntity.importType(entity));
|
||||
declaration
|
||||
.append(" ")
|
||||
.append(methodName)
|
||||
.append("(");
|
||||
if ( !belongsToDao ) {
|
||||
declaration
|
||||
.append(annotationMetaEntity.importType("jakarta.persistence.EntityManager"))
|
||||
.append(" entityManager, ");
|
||||
}
|
||||
declaration
|
||||
.append(annotationMetaEntity.importType(paramType))
|
||||
.append(" ")
|
||||
.append(paramName)
|
||||
.append(") {")
|
||||
.append("\n\treturn entityManager.find(")
|
||||
.append(annotationMetaEntity.importType(entity))
|
||||
.append(".class, ")
|
||||
.append(paramName)
|
||||
.append(");")
|
||||
.append("\n}");
|
||||
return declaration.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAttributeNameDeclarationString() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMetaType() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPropertyName() {
|
||||
return methodName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeDeclaration() {
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Metamodel getHostingEntity() {
|
||||
return annotationMetaEntity;
|
||||
}
|
||||
}
|
|
@ -29,6 +29,11 @@ class NameMetaAttribute implements MetaAttribute {
|
|||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasStringAttribute() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAttributeDeclarationString() {
|
||||
throw new UnsupportedOperationException();
|
||||
|
|
|
@ -62,6 +62,11 @@ public class QueryMethod implements MetaAttribute {
|
|||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasStringAttribute() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAttributeDeclarationString() {
|
||||
List<String> paramTypes = this.paramTypes.stream()
|
||||
|
|
|
@ -13,6 +13,8 @@ public interface MetaAttribute {
|
|||
|
||||
boolean hasTypedAttribute();
|
||||
|
||||
boolean hasStringAttribute();
|
||||
|
||||
String getAttributeDeclarationString();
|
||||
|
||||
String getAttributeNameDeclarationString();
|
||||
|
|
|
@ -53,6 +53,7 @@ public final class Constants {
|
|||
|
||||
public static final String HQL = "org.hibernate.annotations.processing.HQL";
|
||||
public static final String SQL = "org.hibernate.annotations.processing.SQL";
|
||||
public static final String FIND = "org.hibernate.annotations.processing.Find";
|
||||
|
||||
public static final String CHECK_HQL = "org.hibernate.annotations.processing.CheckHQL";
|
||||
|
||||
|
|
|
@ -30,6 +30,11 @@ public abstract class XmlMetaAttribute implements MetaAttribute {
|
|||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasStringAttribute() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAttributeDeclarationString() {
|
||||
return "public static volatile " + hostingEntity.importType( getMetaType() )
|
||||
|
|
|
@ -2,6 +2,7 @@ package org.hibernate.jpamodelgen.test.dao;
|
|||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.TypedQuery;
|
||||
import org.hibernate.annotations.processing.Find;
|
||||
import org.hibernate.annotations.processing.HQL;
|
||||
import org.hibernate.annotations.processing.SQL;
|
||||
|
||||
|
@ -11,6 +12,9 @@ public interface Dao {
|
|||
|
||||
EntityManager getEntityManager();
|
||||
|
||||
@Find
|
||||
Book getBook(String isbn);
|
||||
|
||||
@HQL("from Book where title like ?1")
|
||||
TypedQuery<Book> findByTitle(String title);
|
||||
|
||||
|
|
Loading…
Reference in New Issue