HHH-15959 add TypeBinders + fix multiple AttributeBinders on a single field
This commit is contained in:
parent
e48a8120a9
commit
df5980226c
|
@ -24,6 +24,8 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
|||
* will be called when the annotation is discovered by Hibernate.
|
||||
*
|
||||
* @author Gavin King
|
||||
*
|
||||
* @see TypeBinderType
|
||||
*/
|
||||
@Target(ANNOTATION_TYPE)
|
||||
@Retention(RUNTIME)
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
import org.hibernate.Incubating;
|
||||
import org.hibernate.binder.TypeBinder;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
/**
|
||||
* Associates a user-defined annotation with a {@link TypeBinder},
|
||||
* allowing the annotation to drive some custom model binding.
|
||||
* <p>
|
||||
* The user-defined annotation may be used to annotate entity and
|
||||
* embeddable classes. The {@code TypeBinder} will be called when
|
||||
* the annotation is discovered by Hibernate.
|
||||
*
|
||||
* @author Gavin King
|
||||
*
|
||||
* @see AttributeBinderType
|
||||
*/
|
||||
@Target(ANNOTATION_TYPE)
|
||||
@Retention(RUNTIME)
|
||||
@Incubating
|
||||
public @interface TypeBinderType {
|
||||
/**
|
||||
* @return a type which implements {@link TypeBinder}
|
||||
*/
|
||||
Class<? extends TypeBinder<?>> binder();
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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.binder;
|
||||
|
||||
import org.hibernate.Incubating;
|
||||
import org.hibernate.boot.spi.MetadataBuildingContext;
|
||||
import org.hibernate.mapping.Component;
|
||||
import org.hibernate.mapping.PersistentClass;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
|
||||
/**
|
||||
* Allows a user-written annotation to drive some customized model binding.
|
||||
* <p>
|
||||
* An implementation of this interface interacts directly with model objects
|
||||
* like {@link PersistentClass} and {@link Component} to implement the
|
||||
* semantics of some {@link org.hibernate.annotations.TypeBinderType
|
||||
* custom mapping annotation}.
|
||||
*
|
||||
* @see org.hibernate.annotations.TypeBinderType
|
||||
*
|
||||
* @author Gavin King
|
||||
*/
|
||||
@Incubating
|
||||
public interface TypeBinder<A extends Annotation> {
|
||||
/**
|
||||
* Perform some custom configuration of the model relating to the given annotated
|
||||
* {@link PersistentClass entity class}.
|
||||
*
|
||||
* @param annotation an annotation of the entity class that is declared as an
|
||||
* {@link org.hibernate.annotations.TypeBinderType}
|
||||
* @param persistentClass the entity class
|
||||
*/
|
||||
void bind(A annotation, MetadataBuildingContext buildingContext, PersistentClass persistentClass);
|
||||
/**
|
||||
* Perform some custom configuration of the model relating to the given annotated
|
||||
* {@link Component embeddable class}.
|
||||
*
|
||||
* @param annotation an annotation of the embeddable class that is declared as an
|
||||
* {@link org.hibernate.annotations.TypeBinderType}
|
||||
* @param embeddableClass the embeddable class
|
||||
*/
|
||||
void bind(A annotation, MetadataBuildingContext buildingContext, Component embeddableClass);
|
||||
}
|
|
@ -37,6 +37,5 @@ public class AttributeAccessorBinder implements AttributeBinder<AttributeAccesso
|
|||
else {
|
||||
throw new AnnotationException("'@AttributeAccessor' annotation must specify a 'strategy'");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
/**
|
||||
* Built-in implementations of {@link org.hibernate.binder.AttributeBinder}.
|
||||
* Built-in implementations of {@link org.hibernate.binder.AttributeBinder}
|
||||
* and {@link org.hibernate.binder.TypeBinder}.
|
||||
*/
|
||||
package org.hibernate.binder.internal;
|
||||
|
|
|
@ -6,13 +6,23 @@
|
|||
*/
|
||||
|
||||
/**
|
||||
* This package defines an easy way to extend Hibernate with
|
||||
* user-defined annotations that define customized O/R mappings
|
||||
* of annotated entity attributes.
|
||||
* <p>
|
||||
* The meta-annotation
|
||||
* {@link org.hibernate.annotations.AttributeBinderType}
|
||||
* associates an {@link org.hibernate.binder.AttributeBinder}
|
||||
* with a user-written annotation.
|
||||
* This package defines an easy way to extend Hibernate with user-defined
|
||||
* annotations that define customized O/R mappings of annotated entities
|
||||
* and annotated entity attributes.
|
||||
* <ul>
|
||||
* <li>The meta-annotation
|
||||
* {@link org.hibernate.annotations.TypeBinderType @TypeBinderType}
|
||||
* associates a {@link org.hibernate.binder.TypeBinder} with a
|
||||
* user-written annotation which targets entity and embeddable
|
||||
* {@linkplain java.lang.annotation.ElementType#TYPE types}.
|
||||
* <li>The meta-annotation
|
||||
* {@link org.hibernate.annotations.AttributeBinderType @AttributeBinderType}
|
||||
* associates an {@link org.hibernate.binder.AttributeBinder}
|
||||
* with a user-written annotation which targets
|
||||
* {@linkplain java.lang.annotation.ElementType#FIELD fields} and
|
||||
* properties of entity types and embeddable classes.
|
||||
*
|
||||
* @see org.hibernate.binder.AttributeBinder
|
||||
* @see org.hibernate.binder.TypeBinder
|
||||
*/
|
||||
package org.hibernate.binder;
|
||||
|
|
|
@ -19,10 +19,12 @@ import jakarta.persistence.OneToMany;
|
|||
import jakarta.persistence.OneToOne;
|
||||
import org.hibernate.AnnotationException;
|
||||
import org.hibernate.annotations.Instantiator;
|
||||
import org.hibernate.annotations.TypeBinderType;
|
||||
import org.hibernate.annotations.common.reflection.XAnnotatedElement;
|
||||
import org.hibernate.annotations.common.reflection.XClass;
|
||||
import org.hibernate.annotations.common.reflection.XMethod;
|
||||
import org.hibernate.annotations.common.reflection.XProperty;
|
||||
import org.hibernate.binder.TypeBinder;
|
||||
import org.hibernate.boot.spi.AccessType;
|
||||
import org.hibernate.boot.spi.MetadataBuildingContext;
|
||||
import org.hibernate.boot.spi.PropertyData;
|
||||
|
@ -37,6 +39,7 @@ import org.hibernate.property.access.spi.PropertyAccessStrategy;
|
|||
import org.hibernate.resource.beans.spi.ManagedBeanRegistry;
|
||||
import org.hibernate.usertype.CompositeUserType;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
|
@ -45,6 +48,7 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
|
||||
import static org.hibernate.boot.model.internal.BinderHelper.isGlobalGeneratorNameGlobal;
|
||||
import static org.hibernate.boot.model.internal.HCANNHelper.findContainingAnnotations;
|
||||
import static org.hibernate.boot.model.internal.PropertyBinder.addElementsOfClass;
|
||||
import static org.hibernate.boot.model.internal.PropertyBinder.processElementAnnotations;
|
||||
import static org.hibernate.boot.model.internal.GeneratorBinder.buildGenerators;
|
||||
|
@ -109,22 +113,31 @@ public class EmbeddableBinder {
|
|||
actualColumns = null;
|
||||
}
|
||||
|
||||
return bindEmbeddable(
|
||||
return createEmbeddedProperty(
|
||||
inferredData,
|
||||
propertyHolder,
|
||||
entityBinder.getPropertyAccessor( property ),
|
||||
entityBinder,
|
||||
isIdentifierMapper,
|
||||
context,
|
||||
isComponentEmbedded,
|
||||
propertyBinder.isId(),
|
||||
inheritanceStatePerClass,
|
||||
referencedEntityName,
|
||||
propertyName,
|
||||
determineCustomInstantiator( property, returnedClass, context ),
|
||||
compositeUserType,
|
||||
actualColumns,
|
||||
columns
|
||||
bindEmbeddable(
|
||||
inferredData,
|
||||
propertyHolder,
|
||||
entityBinder.getPropertyAccessor( property ),
|
||||
entityBinder,
|
||||
isIdentifierMapper,
|
||||
context,
|
||||
isComponentEmbedded,
|
||||
propertyBinder.isId(),
|
||||
inheritanceStatePerClass,
|
||||
referencedEntityName,
|
||||
propertyName,
|
||||
determineCustomInstantiator( property, returnedClass, context ),
|
||||
compositeUserType,
|
||||
actualColumns,
|
||||
columns
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -134,7 +147,7 @@ public class EmbeddableBinder {
|
|||
|| returnedClass.isAnnotationPresent( Embeddable.class );
|
||||
}
|
||||
|
||||
private static PropertyBinder bindEmbeddable(
|
||||
private static Component bindEmbeddable(
|
||||
PropertyData inferredData,
|
||||
PropertyHolder propertyHolder,
|
||||
AccessType propertyAccessor,
|
||||
|
@ -189,18 +202,44 @@ public class EmbeddableBinder {
|
|||
component.setKey( true );
|
||||
checkEmbeddedId( inferredData, propertyHolder, referencedEntityName, component );
|
||||
}
|
||||
callTypeBinders( component, context, inferredData.getPropertyClass() );
|
||||
return component;
|
||||
}
|
||||
|
||||
private static void callTypeBinders(Component component, MetadataBuildingContext context, XClass annotatedClass ) {
|
||||
for ( Annotation containingAnnotation : findContainingAnnotations( annotatedClass, TypeBinderType.class) ) {
|
||||
final TypeBinderType binderType = containingAnnotation.annotationType().getAnnotation( TypeBinderType.class );
|
||||
try {
|
||||
final TypeBinder binder = binderType.binder().newInstance();
|
||||
binder.bind( containingAnnotation, context, component );
|
||||
}
|
||||
catch ( Exception e ) {
|
||||
throw new AnnotationException( "error processing @TypeBinderType annotation '" + containingAnnotation + "'", e );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static PropertyBinder createEmbeddedProperty(
|
||||
PropertyData inferredData,
|
||||
PropertyHolder propertyHolder,
|
||||
EntityBinder entityBinder,
|
||||
MetadataBuildingContext context,
|
||||
boolean isComponentEmbedded,
|
||||
boolean isId,
|
||||
Map<XClass, InheritanceState> inheritanceStatePerClass,
|
||||
Component component) {
|
||||
final PropertyBinder binder = new PropertyBinder();
|
||||
binder.setDeclaringClass( inferredData.getDeclaringClass() );
|
||||
binder.setName( inferredData.getPropertyName() );
|
||||
binder.setValue( component );
|
||||
binder.setValue(component);
|
||||
binder.setProperty( inferredData.getProperty() );
|
||||
binder.setAccessType( inferredData.getDefaultAccess() );
|
||||
binder.setEmbedded( isComponentEmbedded );
|
||||
binder.setHolder( propertyHolder );
|
||||
binder.setId( isId );
|
||||
binder.setEntityBinder( entityBinder );
|
||||
binder.setInheritanceStatePerClass( inheritanceStatePerClass );
|
||||
binder.setBuildingContext( context );
|
||||
binder.setEmbedded(isComponentEmbedded);
|
||||
binder.setHolder(propertyHolder);
|
||||
binder.setId(isId);
|
||||
binder.setEntityBinder(entityBinder);
|
||||
binder.setInheritanceStatePerClass(inheritanceStatePerClass);
|
||||
binder.setBuildingContext(context);
|
||||
binder.makePropertyAndBind();
|
||||
return binder;
|
||||
}
|
||||
|
|
|
@ -77,10 +77,12 @@ import org.hibernate.annotations.SelectBeforeUpdate;
|
|||
import org.hibernate.annotations.Subselect;
|
||||
import org.hibernate.annotations.Synchronize;
|
||||
import org.hibernate.annotations.Tables;
|
||||
import org.hibernate.annotations.TypeBinderType;
|
||||
import org.hibernate.annotations.Where;
|
||||
import org.hibernate.annotations.common.reflection.ReflectionManager;
|
||||
import org.hibernate.annotations.common.reflection.XAnnotatedElement;
|
||||
import org.hibernate.annotations.common.reflection.XClass;
|
||||
import org.hibernate.binder.TypeBinder;
|
||||
import org.hibernate.boot.model.IdentifierGeneratorDefinition;
|
||||
import org.hibernate.boot.model.NamedEntityGraphDefinition;
|
||||
import org.hibernate.boot.model.internal.InheritanceState.ElementsToProcess;
|
||||
|
@ -131,6 +133,7 @@ import static org.hibernate.boot.model.internal.GeneratorBinder.makeIdGenerator;
|
|||
import static org.hibernate.boot.model.internal.BinderHelper.toAliasEntityMap;
|
||||
import static org.hibernate.boot.model.internal.BinderHelper.toAliasTableMap;
|
||||
import static org.hibernate.boot.model.internal.EmbeddableBinder.fillEmbeddable;
|
||||
import static org.hibernate.boot.model.internal.HCANNHelper.findContainingAnnotations;
|
||||
import static org.hibernate.boot.model.internal.InheritanceState.getInheritanceStateOfSuperEntity;
|
||||
import static org.hibernate.boot.model.internal.PropertyBinder.addElementsOfClass;
|
||||
import static org.hibernate.boot.model.internal.PropertyBinder.processElementAnnotations;
|
||||
|
@ -233,6 +236,20 @@ public class EntityBinder {
|
|||
// comment, checkConstraint, and indexes are processed here
|
||||
entityBinder.processComplementaryTableDefinitions();
|
||||
bindCallbacks( clazzToProcess, persistentClass, context );
|
||||
entityBinder.callTypeBinders( persistentClass );
|
||||
}
|
||||
|
||||
private void callTypeBinders(PersistentClass persistentClass) {
|
||||
for ( Annotation containingAnnotation : findContainingAnnotations( annotatedClass, TypeBinderType.class ) ) {
|
||||
final TypeBinderType binderType = containingAnnotation.annotationType().getAnnotation( TypeBinderType.class );
|
||||
try {
|
||||
final TypeBinder binder = binderType.binder().newInstance();
|
||||
binder.bind( containingAnnotation, context, persistentClass );
|
||||
}
|
||||
catch ( Exception e ) {
|
||||
throw new AnnotationException( "error processing @TypeBinderType annotation '" + containingAnnotation + "'", e );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleIdentifier(
|
||||
|
|
|
@ -9,6 +9,8 @@ package org.hibernate.boot.model.internal;
|
|||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.lang.reflect.Member;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.hibernate.Internal;
|
||||
import org.hibernate.annotations.common.reflection.XAnnotatedElement;
|
||||
|
@ -156,21 +158,20 @@ public final class HCANNHelper {
|
|||
*
|
||||
* @implNote Searches only one level deep
|
||||
*/
|
||||
public static <A extends Annotation, T extends Annotation> A findContainingAnnotation(
|
||||
public static List<Annotation> findContainingAnnotations(
|
||||
XAnnotatedElement annotatedElement,
|
||||
Class<T> annotationType) {
|
||||
Class<? extends Annotation> annotationType) {
|
||||
|
||||
final List<Annotation> result = new ArrayList<>();
|
||||
|
||||
final Annotation[] annotations = annotatedElement.getAnnotations();
|
||||
for ( Annotation annotation : annotations ) {
|
||||
// annotation = @Sequence
|
||||
|
||||
final T metaAnn = annotation.annotationType().getAnnotation( annotationType );
|
||||
final Annotation metaAnn = annotation.annotationType().getAnnotation( annotationType );
|
||||
if ( metaAnn != null ) {
|
||||
//noinspection unchecked
|
||||
return (A) annotation;
|
||||
result.add( annotation );
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,11 +84,12 @@ import static org.hibernate.boot.model.internal.EmbeddableBinder.createComposite
|
|||
import static org.hibernate.boot.model.internal.EmbeddableBinder.createEmbeddable;
|
||||
import static org.hibernate.boot.model.internal.EmbeddableBinder.isEmbedded;
|
||||
import static org.hibernate.boot.model.internal.HCANNHelper.findAnnotation;
|
||||
import static org.hibernate.boot.model.internal.HCANNHelper.findContainingAnnotation;
|
||||
import static org.hibernate.boot.model.internal.HCANNHelper.findContainingAnnotations;
|
||||
import static org.hibernate.boot.model.internal.TimeZoneStorageHelper.resolveTimeZoneStorageCompositeUserType;
|
||||
import static org.hibernate.boot.model.internal.ToOneBinder.bindManyToOne;
|
||||
import static org.hibernate.boot.model.internal.ToOneBinder.bindOneToOne;
|
||||
import static org.hibernate.internal.util.StringHelper.qualify;
|
||||
import static org.hibernate.internal.util.collections.CollectionHelper.combine;
|
||||
import static org.hibernate.mapping.Constraint.hashedName;
|
||||
|
||||
/**
|
||||
|
@ -269,15 +270,14 @@ public class PropertyBinder {
|
|||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
private void callAttributeBinders(Property prop) {
|
||||
final Annotation containingAnnotation = findContainingAnnotation( property, AttributeBinderType.class);
|
||||
if ( containingAnnotation != null ) {
|
||||
for ( Annotation containingAnnotation : findContainingAnnotations(property, AttributeBinderType.class ) ) {
|
||||
final AttributeBinderType binderType = containingAnnotation.annotationType().getAnnotation( AttributeBinderType.class );
|
||||
try {
|
||||
final AttributeBinder binder = binderType.binder().newInstance();
|
||||
binder.bind( containingAnnotation, buildingContext, entityBinder.getPersistentClass(), prop );
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new AnnotationException( "error processing @AttributeBinderType annotation", e );
|
||||
catch ( Exception e ) {
|
||||
throw new AnnotationException( "error processing @AttributeBinderType annotation '" + containingAnnotation + "'", e );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1270,16 +1270,21 @@ public class PropertyBinder {
|
|||
+ "' belongs to an '@IdClass' and may not be annotated '@Id' or '@EmbeddedId'" );
|
||||
}
|
||||
final XProperty idProperty = inferredData.getProperty();
|
||||
final Annotation idGeneratorAnnotation = findContainingAnnotation( idProperty, IdGeneratorType.class );
|
||||
final Annotation generatorAnnotation = findContainingAnnotation( idProperty, ValueGenerationType.class );
|
||||
//TODO: validate that we don't have too many generator annotations and throw
|
||||
if ( idGeneratorAnnotation != null ) {
|
||||
idValue.setCustomIdGeneratorCreator( identifierGeneratorCreator( idProperty, idGeneratorAnnotation ) );
|
||||
final List<Annotation> idGeneratorAnnotations = findContainingAnnotations( idProperty, IdGeneratorType.class );
|
||||
final List<Annotation> generatorAnnotations = findContainingAnnotations( idProperty, ValueGenerationType.class );
|
||||
generatorAnnotations.removeAll( idGeneratorAnnotations );
|
||||
if ( idGeneratorAnnotations.size() + generatorAnnotations.size() > 1 ) {
|
||||
throw new AnnotationException( "Property '"+ getPath( propertyHolder, inferredData )
|
||||
+ "' has too many generator annotations " + combine( idGeneratorAnnotations, generatorAnnotations ) );
|
||||
}
|
||||
else if ( generatorAnnotation != null ) {
|
||||
if ( !idGeneratorAnnotations.isEmpty() ) {
|
||||
idValue.setCustomIdGeneratorCreator( identifierGeneratorCreator( idProperty, idGeneratorAnnotations.get(0) ) );
|
||||
}
|
||||
else if ( !generatorAnnotations.isEmpty() ) {
|
||||
// idValue.setCustomGeneratorCreator( generatorCreator( idProperty, generatorAnnotation ) );
|
||||
throw new AnnotationException( "Property '"+ getPath( propertyHolder, inferredData )
|
||||
+ "' is annotated '" + generatorAnnotation.annotationType() + "' which is not an '@IdGeneratorType'" );
|
||||
+ "' is annotated '" + generatorAnnotations.get(0).annotationType()
|
||||
+ "' which is not an '@IdGeneratorType'" );
|
||||
}
|
||||
else {
|
||||
final XClass entityClass = inferredData.getClassOrElement();
|
||||
|
|
|
@ -51,9 +51,9 @@ import static org.hibernate.id.IdentifierGeneratorHelper.POST_INSERT_INDICATOR;
|
|||
|
||||
/**
|
||||
* A mapping model object that represents an {@linkplain jakarta.persistence.Embeddable embeddable class}.
|
||||
* <p>
|
||||
* Note that the name of this class is historical and unfortunate. An embeddable class holds a "component"
|
||||
* of the state of an entity. It has absolutely nothing to do with modularity in software engineering.
|
||||
*
|
||||
* @apiNote The name of this class is historical and unfortunate. An embeddable class holds a "component"
|
||||
* of the state of an entity. It has absolutely nothing to do with modularity in software engineering.
|
||||
*
|
||||
* @author Gavin King
|
||||
* @author Steve Ebersole
|
||||
|
|
|
@ -73,7 +73,7 @@ public class Table implements Serializable, ContributableDatabaseObject {
|
|||
|
||||
private List<Function<SqlStringGenerationContext, InitCommand>> initCommandProducers;
|
||||
|
||||
@Deprecated(since="6.2") @Remove
|
||||
@Deprecated(since="6.2", forRemoval = true)
|
||||
public Table() {
|
||||
this( "orm" );
|
||||
}
|
||||
|
|
|
@ -133,7 +133,10 @@
|
|||
</li>
|
||||
<li>
|
||||
{@link org.hibernate.context.spi} defines support for context-bound "current" sessions
|
||||
and contextual multi-tenancy,
|
||||
and contextual multi-tenancy, and
|
||||
</li>
|
||||
<li>
|
||||
{@link org.hibernate.binder} allows for user-defined mapping annotations.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
|
|
|
@ -133,7 +133,10 @@
|
|||
</li>
|
||||
<li>
|
||||
{@link org.hibernate.context.spi} defines support for context-bound "current" sessions
|
||||
and contextual multi-tenancy,
|
||||
and contextual multi-tenancy, and
|
||||
</li>
|
||||
<li>
|
||||
{@link org.hibernate.binder} allows for user-defined mapping annotations.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
|
|
Loading…
Reference in New Issue