HHH-15959 add TypeBinders + fix multiple AttributeBinders on a single field

This commit is contained in:
Gavin 2023-01-01 18:21:07 +01:00 committed by Gavin King
parent e48a8120a9
commit df5980226c
14 changed files with 219 additions and 53 deletions

View File

@ -24,6 +24,8 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
* will be called when the annotation is discovered by Hibernate. * will be called when the annotation is discovered by Hibernate.
* *
* @author Gavin King * @author Gavin King
*
* @see TypeBinderType
*/ */
@Target(ANNOTATION_TYPE) @Target(ANNOTATION_TYPE)
@Retention(RUNTIME) @Retention(RUNTIME)

View File

@ -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();
}

View File

@ -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);
}

View File

@ -37,6 +37,5 @@ public class AttributeAccessorBinder implements AttributeBinder<AttributeAccesso
else { else {
throw new AnnotationException("'@AttributeAccessor' annotation must specify a 'strategy'"); throw new AnnotationException("'@AttributeAccessor' annotation must specify a 'strategy'");
} }
} }
} }

View File

@ -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; package org.hibernate.binder.internal;

View File

@ -6,13 +6,23 @@
*/ */
/** /**
* This package defines an easy way to extend Hibernate with * This package defines an easy way to extend Hibernate with user-defined
* user-defined annotations that define customized O/R mappings * annotations that define customized O/R mappings of annotated entities
* of annotated entity attributes. * and annotated entity attributes.
* <p> * <ul>
* The meta-annotation * <li>The meta-annotation
* {@link org.hibernate.annotations.AttributeBinderType} * {@link org.hibernate.annotations.TypeBinderType @TypeBinderType}
* associates an {@link org.hibernate.binder.AttributeBinder} * associates a {@link org.hibernate.binder.TypeBinder} with a
* with a user-written annotation. * 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; package org.hibernate.binder;

View File

@ -19,10 +19,12 @@ import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne; import jakarta.persistence.OneToOne;
import org.hibernate.AnnotationException; import org.hibernate.AnnotationException;
import org.hibernate.annotations.Instantiator; import org.hibernate.annotations.Instantiator;
import org.hibernate.annotations.TypeBinderType;
import org.hibernate.annotations.common.reflection.XAnnotatedElement; import org.hibernate.annotations.common.reflection.XAnnotatedElement;
import org.hibernate.annotations.common.reflection.XClass; import org.hibernate.annotations.common.reflection.XClass;
import org.hibernate.annotations.common.reflection.XMethod; import org.hibernate.annotations.common.reflection.XMethod;
import org.hibernate.annotations.common.reflection.XProperty; import org.hibernate.annotations.common.reflection.XProperty;
import org.hibernate.binder.TypeBinder;
import org.hibernate.boot.spi.AccessType; import org.hibernate.boot.spi.AccessType;
import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.boot.spi.MetadataBuildingContext;
import org.hibernate.boot.spi.PropertyData; 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.resource.beans.spi.ManagedBeanRegistry;
import org.hibernate.usertype.CompositeUserType; import org.hibernate.usertype.CompositeUserType;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.ArrayList; import java.util.ArrayList;
@ -45,6 +48,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import static org.hibernate.boot.model.internal.BinderHelper.isGlobalGeneratorNameGlobal; 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.addElementsOfClass;
import static org.hibernate.boot.model.internal.PropertyBinder.processElementAnnotations; import static org.hibernate.boot.model.internal.PropertyBinder.processElementAnnotations;
import static org.hibernate.boot.model.internal.GeneratorBinder.buildGenerators; import static org.hibernate.boot.model.internal.GeneratorBinder.buildGenerators;
@ -109,22 +113,31 @@ public class EmbeddableBinder {
actualColumns = null; actualColumns = null;
} }
return bindEmbeddable( return createEmbeddedProperty(
inferredData, inferredData,
propertyHolder, propertyHolder,
entityBinder.getPropertyAccessor( property ),
entityBinder, entityBinder,
isIdentifierMapper,
context, context,
isComponentEmbedded, isComponentEmbedded,
propertyBinder.isId(), propertyBinder.isId(),
inheritanceStatePerClass, inheritanceStatePerClass,
referencedEntityName, bindEmbeddable(
propertyName, inferredData,
determineCustomInstantiator( property, returnedClass, context ), propertyHolder,
compositeUserType, entityBinder.getPropertyAccessor( property ),
actualColumns, entityBinder,
columns 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 ); || returnedClass.isAnnotationPresent( Embeddable.class );
} }
private static PropertyBinder bindEmbeddable( private static Component bindEmbeddable(
PropertyData inferredData, PropertyData inferredData,
PropertyHolder propertyHolder, PropertyHolder propertyHolder,
AccessType propertyAccessor, AccessType propertyAccessor,
@ -189,18 +202,44 @@ public class EmbeddableBinder {
component.setKey( true ); component.setKey( true );
checkEmbeddedId( inferredData, propertyHolder, referencedEntityName, component ); 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(); final PropertyBinder binder = new PropertyBinder();
binder.setDeclaringClass( inferredData.getDeclaringClass() ); binder.setDeclaringClass( inferredData.getDeclaringClass() );
binder.setName( inferredData.getPropertyName() ); binder.setName( inferredData.getPropertyName() );
binder.setValue( component ); binder.setValue(component);
binder.setProperty( inferredData.getProperty() ); binder.setProperty( inferredData.getProperty() );
binder.setAccessType( inferredData.getDefaultAccess() ); binder.setAccessType( inferredData.getDefaultAccess() );
binder.setEmbedded( isComponentEmbedded ); binder.setEmbedded(isComponentEmbedded);
binder.setHolder( propertyHolder ); binder.setHolder(propertyHolder);
binder.setId( isId ); binder.setId(isId);
binder.setEntityBinder( entityBinder ); binder.setEntityBinder(entityBinder);
binder.setInheritanceStatePerClass( inheritanceStatePerClass ); binder.setInheritanceStatePerClass(inheritanceStatePerClass);
binder.setBuildingContext( context ); binder.setBuildingContext(context);
binder.makePropertyAndBind(); binder.makePropertyAndBind();
return binder; return binder;
} }

View File

@ -77,10 +77,12 @@ import org.hibernate.annotations.SelectBeforeUpdate;
import org.hibernate.annotations.Subselect; import org.hibernate.annotations.Subselect;
import org.hibernate.annotations.Synchronize; import org.hibernate.annotations.Synchronize;
import org.hibernate.annotations.Tables; import org.hibernate.annotations.Tables;
import org.hibernate.annotations.TypeBinderType;
import org.hibernate.annotations.Where; import org.hibernate.annotations.Where;
import org.hibernate.annotations.common.reflection.ReflectionManager; import org.hibernate.annotations.common.reflection.ReflectionManager;
import org.hibernate.annotations.common.reflection.XAnnotatedElement; import org.hibernate.annotations.common.reflection.XAnnotatedElement;
import org.hibernate.annotations.common.reflection.XClass; import org.hibernate.annotations.common.reflection.XClass;
import org.hibernate.binder.TypeBinder;
import org.hibernate.boot.model.IdentifierGeneratorDefinition; import org.hibernate.boot.model.IdentifierGeneratorDefinition;
import org.hibernate.boot.model.NamedEntityGraphDefinition; import org.hibernate.boot.model.NamedEntityGraphDefinition;
import org.hibernate.boot.model.internal.InheritanceState.ElementsToProcess; 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.toAliasEntityMap;
import static org.hibernate.boot.model.internal.BinderHelper.toAliasTableMap; 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.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.InheritanceState.getInheritanceStateOfSuperEntity;
import static org.hibernate.boot.model.internal.PropertyBinder.addElementsOfClass; 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.PropertyBinder.processElementAnnotations;
@ -233,6 +236,20 @@ public class EntityBinder {
// comment, checkConstraint, and indexes are processed here // comment, checkConstraint, and indexes are processed here
entityBinder.processComplementaryTableDefinitions(); entityBinder.processComplementaryTableDefinitions();
bindCallbacks( clazzToProcess, persistentClass, context ); 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( private void handleIdentifier(

View File

@ -9,6 +9,8 @@ package org.hibernate.boot.model.internal;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement; import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Member; import java.lang.reflect.Member;
import java.util.ArrayList;
import java.util.List;
import org.hibernate.Internal; import org.hibernate.Internal;
import org.hibernate.annotations.common.reflection.XAnnotatedElement; import org.hibernate.annotations.common.reflection.XAnnotatedElement;
@ -156,21 +158,20 @@ public final class HCANNHelper {
* *
* @implNote Searches only one level deep * @implNote Searches only one level deep
*/ */
public static <A extends Annotation, T extends Annotation> A findContainingAnnotation( public static List<Annotation> findContainingAnnotations(
XAnnotatedElement annotatedElement, XAnnotatedElement annotatedElement,
Class<T> annotationType) { Class<? extends Annotation> annotationType) {
final List<Annotation> result = new ArrayList<>();
final Annotation[] annotations = annotatedElement.getAnnotations(); final Annotation[] annotations = annotatedElement.getAnnotations();
for ( Annotation annotation : annotations ) { for ( Annotation annotation : annotations ) {
// annotation = @Sequence final Annotation metaAnn = annotation.annotationType().getAnnotation( annotationType );
final T metaAnn = annotation.annotationType().getAnnotation( annotationType );
if ( metaAnn != null ) { if ( metaAnn != null ) {
//noinspection unchecked result.add( annotation );
return (A) annotation;
} }
} }
return null; return result;
} }
} }

View File

@ -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.createEmbeddable;
import static org.hibernate.boot.model.internal.EmbeddableBinder.isEmbedded; 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.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.TimeZoneStorageHelper.resolveTimeZoneStorageCompositeUserType;
import static org.hibernate.boot.model.internal.ToOneBinder.bindManyToOne; import static org.hibernate.boot.model.internal.ToOneBinder.bindManyToOne;
import static org.hibernate.boot.model.internal.ToOneBinder.bindOneToOne; import static org.hibernate.boot.model.internal.ToOneBinder.bindOneToOne;
import static org.hibernate.internal.util.StringHelper.qualify; import static org.hibernate.internal.util.StringHelper.qualify;
import static org.hibernate.internal.util.collections.CollectionHelper.combine;
import static org.hibernate.mapping.Constraint.hashedName; import static org.hibernate.mapping.Constraint.hashedName;
/** /**
@ -269,15 +270,14 @@ public class PropertyBinder {
@SuppressWarnings({"rawtypes", "unchecked"}) @SuppressWarnings({"rawtypes", "unchecked"})
private void callAttributeBinders(Property prop) { private void callAttributeBinders(Property prop) {
final Annotation containingAnnotation = findContainingAnnotation( property, AttributeBinderType.class); for ( Annotation containingAnnotation : findContainingAnnotations(property, AttributeBinderType.class ) ) {
if ( containingAnnotation != null ) {
final AttributeBinderType binderType = containingAnnotation.annotationType().getAnnotation( AttributeBinderType.class ); final AttributeBinderType binderType = containingAnnotation.annotationType().getAnnotation( AttributeBinderType.class );
try { try {
final AttributeBinder binder = binderType.binder().newInstance(); final AttributeBinder binder = binderType.binder().newInstance();
binder.bind( containingAnnotation, buildingContext, entityBinder.getPersistentClass(), prop ); binder.bind( containingAnnotation, buildingContext, entityBinder.getPersistentClass(), prop );
} }
catch (Exception e) { catch ( Exception e ) {
throw new AnnotationException( "error processing @AttributeBinderType annotation", 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'" ); + "' belongs to an '@IdClass' and may not be annotated '@Id' or '@EmbeddedId'" );
} }
final XProperty idProperty = inferredData.getProperty(); final XProperty idProperty = inferredData.getProperty();
final Annotation idGeneratorAnnotation = findContainingAnnotation( idProperty, IdGeneratorType.class ); final List<Annotation> idGeneratorAnnotations = findContainingAnnotations( idProperty, IdGeneratorType.class );
final Annotation generatorAnnotation = findContainingAnnotation( idProperty, ValueGenerationType.class ); final List<Annotation> generatorAnnotations = findContainingAnnotations( idProperty, ValueGenerationType.class );
//TODO: validate that we don't have too many generator annotations and throw generatorAnnotations.removeAll( idGeneratorAnnotations );
if ( idGeneratorAnnotation != null ) { if ( idGeneratorAnnotations.size() + generatorAnnotations.size() > 1 ) {
idValue.setCustomIdGeneratorCreator( identifierGeneratorCreator( idProperty, idGeneratorAnnotation ) ); 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 ) ); // idValue.setCustomGeneratorCreator( generatorCreator( idProperty, generatorAnnotation ) );
throw new AnnotationException( "Property '"+ getPath( propertyHolder, inferredData ) 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 { else {
final XClass entityClass = inferredData.getClassOrElement(); final XClass entityClass = inferredData.getClassOrElement();

View File

@ -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}. * 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" * @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. * of the state of an entity. It has absolutely nothing to do with modularity in software engineering.
* *
* @author Gavin King * @author Gavin King
* @author Steve Ebersole * @author Steve Ebersole

View File

@ -73,7 +73,7 @@ public class Table implements Serializable, ContributableDatabaseObject {
private List<Function<SqlStringGenerationContext, InitCommand>> initCommandProducers; private List<Function<SqlStringGenerationContext, InitCommand>> initCommandProducers;
@Deprecated(since="6.2") @Remove @Deprecated(since="6.2", forRemoval = true)
public Table() { public Table() {
this( "orm" ); this( "orm" );
} }

View File

@ -133,7 +133,10 @@
</li> </li>
<li> <li>
{@link org.hibernate.context.spi} defines support for context-bound "current" sessions {@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> </li>
</ul> </ul>
<p> <p>

View File

@ -133,7 +133,10 @@
</li> </li>
<li> <li>
{@link org.hibernate.context.spi} defines support for context-bound "current" sessions {@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> </li>
</ul> </ul>
<p> <p>