#160 allow selection of update methods by refactoring MethodMatcher but stopping selection of update method at new filter

This commit is contained in:
sjaakd 2015-02-14 11:50:46 +01:00
parent 0c226f8388
commit 37e5942c86
11 changed files with 228 additions and 130 deletions

View File

@ -50,7 +50,7 @@ public abstract class MappingMethod extends ModelElement {
this.name = method.getName(); this.name = method.getName();
this.parameters = method.getParameters(); this.parameters = method.getParameters();
this.returnType = method.getReturnType(); this.returnType = method.getReturnType();
this.targetParameter = method.getTargetParameter(); this.targetParameter = method.getMappingTargetParameter();
this.accessibility = method.getAccessibility(); this.accessibility = method.getAccessibility();
this.thrownTypes = method.getThrownTypes(); this.thrownTypes = method.getThrownTypes();
this.isStatic = method.isStatic(); this.isStatic = method.isStatic();

View File

@ -71,19 +71,18 @@ public class ForgedMethod implements Method {
} }
@Override @Override
public boolean matches(List<Type> sourceTypes, Type targetType) { public boolean matches(Type sourceTypes, Type targetType) {
if ( !targetType.equals( returnType ) ) { if ( !targetType.equals( returnType ) ) {
return false; return false;
} }
if ( sourceTypes.size() == parameters.size() ) { if ( parameters.size() != 1 ) {
return false; return false;
} }
for ( int i = 0; i < sourceTypes.size(); i++ ) {
if ( !sourceTypes.get( i ).equals( parameters.get( i ).getType() ) ) { if ( !sourceTypes.equals( parameters.get( 0 ).getType() ) ) {
return false; return false;
}
} }
return true; return true;
@ -110,7 +109,12 @@ public class ForgedMethod implements Method {
} }
@Override @Override
public Parameter getTargetParameter() { public Parameter getMappingTargetParameter() {
return null;
}
@Override
public Parameter getTargetTypeParameter() {
return null; return null;
} }

View File

@ -40,12 +40,12 @@ public interface Method {
* Checks whether the provided sourceType and provided targetType match with the parameter respectively return type * Checks whether the provided sourceType and provided targetType match with the parameter respectively return type
* of the method. The check also should incorporate wild card and generic type variables * of the method. The check also should incorporate wild card and generic type variables
* *
* @param sourceTypes the sourceTypes to match to the parameter * @param sourceType the sourceType to match to the parameter
* @param targetType the targetType to match to the returnType * @param targetType the targetType to match to the returnType
* *
* @return true when match * @return true when match
*/ */
boolean matches(List<Type> sourceTypes, Type targetType); boolean matches(Type sourceType, Type targetType );
/** /**
* Returns the mapper type declaring this method if it is not declared by the mapper interface currently processed * Returns the mapper type declaring this method if it is not declared by the mapper interface currently processed
@ -71,18 +71,26 @@ public interface Method {
/** /**
* returns the list of 'true' source parameters excluding the parameter(s) that is designated as * returns the list of 'true' source parameters excluding the parameter(s) that is designated as
* target by means of the target annotation {@link #getTargetParameter() }. * target by means of the target annotation {@link #getMappingTargetParameter() }.
* *
* @return list of 'true' source parameters * @return list of 'true' source parameters
*/ */
List<Parameter> getSourceParameters(); List<Parameter> getSourceParameters();
/** /**
* Returns the parameter designated as target parameter (if present) {@link #getSourceParameters() } * Returns the parameter designated as mapping target (if present) {@link org.mapstruct.MappingTarget }
* *
* @return target parameter (when present) null otherwise. * @return mapping target parameter (when present) null otherwise.
*/ */
Parameter getTargetParameter(); Parameter getMappingTargetParameter();
/**
* Returns the parameter designated as target type (if present) {@link org.mapstruct.TargetType }
*
* @return target type parameter (when present) null otherwise.
*/
Parameter getTargetTypeParameter();
/** /**
* Returns the {@link Accessibility} of this method. * Returns the {@link Accessibility} of this method.

View File

@ -24,7 +24,6 @@ import java.util.Map;
import javax.lang.model.element.TypeElement; import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement; import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType; import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType; import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.PrimitiveType; import javax.lang.model.type.PrimitiveType;
@ -34,8 +33,10 @@ import javax.lang.model.type.TypeVariable;
import javax.lang.model.type.WildcardType; import javax.lang.model.type.WildcardType;
import javax.lang.model.util.SimpleTypeVisitor6; import javax.lang.model.util.SimpleTypeVisitor6;
import javax.lang.model.util.Types; import javax.lang.model.util.Types;
import org.mapstruct.ap.model.common.Parameter;
import org.mapstruct.ap.model.common.Type; import org.mapstruct.ap.model.common.Type;
import org.mapstruct.ap.model.common.TypeFactory;
import static org.mapstruct.ap.util.SpecificCompilerWorkarounds.isSubType; import static org.mapstruct.ap.util.SpecificCompilerWorkarounds.isSubType;
@ -68,80 +69,65 @@ public class MethodMatcher {
private final SourceMethod candidateMethod; private final SourceMethod candidateMethod;
private final Types typeUtils; private final Types typeUtils;
private final TypeFactory typeFactory;
MethodMatcher(Types typeUtils, SourceMethod candidateMethod) { MethodMatcher(Types typeUtils, TypeFactory typeFactory, SourceMethod candidateMethod) {
this.typeUtils = typeUtils; this.typeUtils = typeUtils;
this.candidateMethod = candidateMethod; this.candidateMethod = candidateMethod;
this.typeFactory = typeFactory;
} }
/** /**
* Whether the given source and target types are matched by this matcher's candidate method. * Whether the given source and target types are matched by this matcher's candidate method.
* *
* @param sourceTypes the source types * @param sourceType the source types
* @param targetType the target type * @param resultType the target type
* *
* @return {@code true} when both, source type and target types match the signature of this matcher's method; * @return {@code true} when both, source type and target types match the signature of this matcher's method;
* {@code false} otherwise. * {@code false} otherwise.
*/ */
boolean matches(List<Type> sourceTypes, Type targetType) { boolean matches(Type sourceType, Type resultType) {
// check & collect generic types.
List<? extends VariableElement> candidateParameters = candidateMethod.getExecutable().getParameters();
if ( candidateParameters.size() != sourceTypes.size() ) { // check & collect generic types.
Map<TypeVariable, TypeMirror> genericTypesMap = new HashMap<TypeVariable, TypeMirror>();
// only factory / mapping methods with zero or one source parameters qualify
if ( candidateMethod.getSourceParameters().size() > 1 ) {
return false; return false;
} }
Map<TypeVariable, TypeMirror> genericTypesMap = new HashMap<TypeVariable, TypeMirror>(); if ( sourceType != null ) {
// if the sourcetype is not null then only methods with one source parameter qualfiy
int i = 0; if ( candidateMethod.getSourceParameters().size() == 1 ) {
for ( VariableElement candidateParameter : candidateParameters ) { Parameter sourceParam = candidateMethod.getSourceParameters().iterator().next();
TypeMatcher parameterMatcher = new TypeMatcher( Assignability.VISITED_ASSIGNABLE_FROM, genericTypesMap ); if ( !matchSourceType( sourceType, sourceParam.getType(), genericTypesMap ) ) {
Type sourceType = sourceTypes.get( i++ );
if ( !parameterMatcher.visit( candidateParameter.asType(), sourceType.getTypeMirror() ) ) {
if (sourceType.isPrimitive() ) {
// the candidate source is primitive, so promote to its boxed type and check again (autobox)
TypeMirror boxedType = typeUtils.boxedClass( (PrimitiveType) sourceType.getTypeMirror() ).asType();
if ( !parameterMatcher.visit( candidateParameter.asType(), boxedType ) ) {
return false;
}
}
else {
// NOTE: unboxing is deliberately not considered here. This should be handled via type-conversion
// (for NPE safety).
return false; return false;
} }
} }
}
// check return type
TypeMirror candidateReturnType = candidateMethod.getExecutable().getReturnType();
TypeMatcher returnTypeMatcher = new TypeMatcher( Assignability.VISITED_ASSIGNABLE_TO, genericTypesMap );
if ( !returnTypeMatcher.visit( candidateReturnType, targetType.getTypeMirror() ) ) {
if ( targetType.isPrimitive() ) {
TypeMirror boxedType = typeUtils.boxedClass( (PrimitiveType) targetType.getTypeMirror() ).asType();
TypeMatcher boxedReturnTypeMatcher =
new TypeMatcher( Assignability.VISITED_ASSIGNABLE_TO, genericTypesMap );
if ( !boxedReturnTypeMatcher.visit( candidateReturnType, boxedType ) ) {
return false;
}
}
else if ( candidateReturnType.getKind().isPrimitive() ) {
TypeMirror boxedCandidateReturnType =
typeUtils.boxedClass( (PrimitiveType) candidateReturnType ).asType();
TypeMatcher boxedReturnTypeMatcher =
new TypeMatcher( Assignability.VISITED_ASSIGNABLE_TO, genericTypesMap );
if ( !boxedReturnTypeMatcher.visit( boxedCandidateReturnType, targetType.getTypeMirror() ) ) {
return false;
}
}
else { else {
return false; return false;
} }
} }
else {
// if the sourcetype is null then only methods with zero source parameters qualfiy
if ( !candidateMethod.getSourceParameters().isEmpty() ) {
return false;
}
}
// check if the method matches the proper result type to construct
Parameter targetTypeParameter = candidateMethod.getTargetTypeParameter();
if ( targetTypeParameter != null ) {
Type returnClassType = typeFactory.classTypeOf( resultType );
if ( !matchSourceType( returnClassType, targetTypeParameter.getType(), genericTypesMap ) ) {
return false;
}
}
// check result type
if ( !matchResultType( resultType, candidateMethod.getResultType(), genericTypesMap ) ) {
return false;
}
// check if all type parameters are indeed mapped // check if all type parameters are indeed mapped
if ( candidateMethod.getExecutable().getTypeParameters().size() != genericTypesMap.size() ) { if ( candidateMethod.getExecutable().getTypeParameters().size() != genericTypesMap.size() ) {
@ -158,6 +144,63 @@ public class MethodMatcher {
return true; return true;
} }
private boolean matchSourceType(Type sourceType,
Type candidateSourceType,
Map<TypeVariable, TypeMirror> genericTypesMap) {
TypeMatcher parameterMatcher = new TypeMatcher( Assignability.VISITED_ASSIGNABLE_FROM, genericTypesMap );
if ( !parameterMatcher.visit( candidateSourceType.getTypeMirror(), sourceType.getTypeMirror() ) ) {
if ( sourceType.isPrimitive() ) {
// the candidate source is primitive, so promote to its boxed type and check again (autobox)
TypeMirror boxedType = typeUtils.boxedClass( (PrimitiveType) sourceType.getTypeMirror() ).asType();
if ( !parameterMatcher.visit( candidateSourceType.getTypeMirror(), boxedType ) ) {
return false;
}
}
else {
// NOTE: unboxing is deliberately not considered here. This should be handled via type-conversion
// (for NPE safety).
return false;
}
}
return true;
}
private boolean matchResultType(Type resultType,
Type candidateResultType,
Map<TypeVariable, TypeMirror> genericTypesMap) {
TypeMatcher returnTypeMatcher = new TypeMatcher( Assignability.VISITED_ASSIGNABLE_TO, genericTypesMap );
if ( !returnTypeMatcher.visit( candidateResultType.getTypeMirror(), resultType.getTypeMirror() ) ) {
if ( resultType.isPrimitive() ) {
TypeMirror boxedType = typeUtils.boxedClass( (PrimitiveType) resultType.getTypeMirror() ).asType();
TypeMatcher boxedReturnTypeMatcher =
new TypeMatcher( Assignability.VISITED_ASSIGNABLE_TO, genericTypesMap );
if ( !boxedReturnTypeMatcher.visit( candidateResultType.getTypeMirror(), boxedType ) ) {
return false;
}
}
else if ( candidateResultType.getTypeMirror().getKind().isPrimitive() ) {
TypeMirror boxedCandidateReturnType =
typeUtils.boxedClass( (PrimitiveType) candidateResultType.getTypeMirror() ).asType();
TypeMatcher boxedReturnTypeMatcher =
new TypeMatcher( Assignability.VISITED_ASSIGNABLE_TO, genericTypesMap );
if ( !boxedReturnTypeMatcher.visit( boxedCandidateReturnType, resultType.getTypeMirror() ) ) {
return false;
}
}
else {
return false;
}
}
return true;
}
private enum Assignability { private enum Assignability {
VISITED_ASSIGNABLE_FROM, VISITED_ASSIGNABLE_TO VISITED_ASSIGNABLE_FROM, VISITED_ASSIGNABLE_TO
} }

View File

@ -55,7 +55,8 @@ public class SourceMethod implements Method {
private final Type declaringMapper; private final Type declaringMapper;
private final ExecutableElement executable; private final ExecutableElement executable;
private final List<Parameter> parameters; private final List<Parameter> parameters;
private final Parameter targetParameter; private final Parameter mappingTargetParameter;
private final Parameter targetTypeParameter;
private final Type returnType; private final Type returnType;
private final Accessibility accessibility; private final Accessibility accessibility;
private final List<Type> exceptionTypes; private final List<Type> exceptionTypes;
@ -185,7 +186,8 @@ public class SourceMethod implements Method {
this.mapMapping = mapMapping; this.mapMapping = mapMapping;
this.accessibility = Accessibility.fromModifiers( executable.getModifiers() ); this.accessibility = Accessibility.fromModifiers( executable.getModifiers() );
this.targetParameter = determineTargetParameter( parameters ); this.mappingTargetParameter = determineMappingTargetParameter( parameters );
this.targetTypeParameter = determineTargetTypeParameter( parameters );
this.typeUtils = typeUtils; this.typeUtils = typeUtils;
this.typeFactory = typeFactory; this.typeFactory = typeFactory;
@ -193,7 +195,7 @@ public class SourceMethod implements Method {
this.config = config; this.config = config;
} }
private Parameter determineTargetParameter(Iterable<Parameter> parameters) { private Parameter determineMappingTargetParameter(Iterable<Parameter> parameters) {
for ( Parameter parameter : parameters ) { for ( Parameter parameter : parameters ) {
if ( parameter.isMappingTarget() ) { if ( parameter.isMappingTarget() ) {
return parameter; return parameter;
@ -203,6 +205,16 @@ public class SourceMethod implements Method {
return null; return null;
} }
private Parameter determineTargetTypeParameter(Iterable<Parameter> parameters) {
for ( Parameter parameter : parameters ) {
if ( parameter.isTargetType() ) {
return parameter;
}
}
return null;
}
/** /**
* {@inheritDoc} {@link Method} * {@inheritDoc} {@link Method}
*/ */
@ -261,7 +273,7 @@ public class SourceMethod implements Method {
@Override @Override
public Type getResultType() { public Type getResultType() {
return targetParameter != null ? targetParameter.getType() : returnType; return mappingTargetParameter != null ? mappingTargetParameter.getType() : returnType;
} }
/** /**
@ -357,12 +369,14 @@ public class SourceMethod implements Method {
return equals( getResultType(), method.getResultType() ); return equals( getResultType(), method.getResultType() );
} }
/**
* {@inheritDoc} {@link Method}
*/
@Override @Override
public Parameter getTargetParameter() { public Parameter getMappingTargetParameter() {
return targetParameter; return mappingTargetParameter;
}
@Override
public Parameter getTargetTypeParameter() {
return targetTypeParameter;
} }
public boolean isIterableMapping() { public boolean isIterableMapping() {
@ -455,9 +469,9 @@ public class SourceMethod implements Method {
* {@inheritDoc} {@link Method} * {@inheritDoc} {@link Method}
*/ */
@Override @Override
public boolean matches(List<Type> sourceTypes, Type targetType) { public boolean matches(Type sourceType, Type targetType) {
MethodMatcher matcher = new MethodMatcher( typeUtils, this ); MethodMatcher matcher = new MethodMatcher( typeUtils, typeFactory, this );
return matcher.matches( sourceTypes, targetType ); return matcher.matches( sourceType, targetType );
} }
/** /**

View File

@ -71,11 +71,7 @@ public abstract class BuiltInMethod implements Method {
* excluding generic type variables. When the implementor sees a need for this, this method can be overridden. * excluding generic type variables. When the implementor sees a need for this, this method can be overridden.
*/ */
@Override @Override
public boolean matches(List<Type> sourceTypes, Type targetType) { public boolean matches(Type sourceType, Type targetType) {
if ( sourceTypes.size() > 1 ) {
return false;
}
Type sourceType = sourceTypes.iterator().next();
if ( getReturnType().isAssignableTo( targetType.erasure() ) if ( getReturnType().isAssignableTo( targetType.erasure() )
&& sourceType.erasure().isAssignableTo( getParameter().getType() ) ) { && sourceType.erasure().isAssignableTo( getParameter().getType() ) ) {
@ -118,12 +114,22 @@ public abstract class BuiltInMethod implements Method {
} }
/** /**
* target parameter mechanism not supported for built-in methods * mapping target parameter mechanism not supported for built-in methods
* *
* @return {@code null} * @return {@code null}
*/ */
@Override @Override
public Parameter getTargetParameter() { public Parameter getMappingTargetParameter() {
return null;
}
/**
* target type parameter mechanism not supported for built-in methods
*
* @return {@code null}
*/
@Override
public Parameter getTargetTypeParameter() {
return null; return null;
} }

View File

@ -0,0 +1,51 @@
/**
* Copyright 2012-2015 Gunnar Morling (http://www.gunnarmorling.de/)
* and/or other contributors as indicated by the @authors tag. See the
* copyright.txt file in the distribution for a full listing of all
* contributors.
*
* 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 org.mapstruct.ap.model.source.selector;
import java.util.ArrayList;
import java.util.List;
import org.mapstruct.ap.model.common.Type;
import org.mapstruct.ap.model.source.Method;
import org.mapstruct.ap.model.source.MethodMatcher;
/**
* Selects only create method candidates (so methods not containing {@link @MappingTarget} )
* {@link MethodMatcher}).
*
* @author Sjaak Derksen
*/
public class CreateOrUpdateSelector implements MethodSelector {
@Override
public <T extends Method> List<T> getMatchingMethods(Method mappingMethod, List<T> methods,
Type sourceType, Type targetType,
SelectionCriteria criteria) {
List<T> createCandidates = new ArrayList<T>();
for ( T method : methods ) {
boolean isCreateCandidate = method.getMappingTargetParameter() == null;
if ( isCreateCandidate ) {
createCandidates.add( method );
}
}
return createCandidates;
}
}

View File

@ -24,7 +24,6 @@ import java.util.List;
import javax.lang.model.util.Elements; import javax.lang.model.util.Elements;
import javax.lang.model.util.Types; import javax.lang.model.util.Types;
import org.mapstruct.ap.model.common.Parameter;
import org.mapstruct.ap.model.common.Type; import org.mapstruct.ap.model.common.Type;
import org.mapstruct.ap.model.common.TypeFactory; import org.mapstruct.ap.model.common.TypeFactory;
import org.mapstruct.ap.model.source.Method; import org.mapstruct.ap.model.source.Method;
@ -41,11 +40,12 @@ public class MethodSelectors implements MethodSelector {
public MethodSelectors(Types typeUtils, Elements elementUtils, TypeFactory typeFactory) { public MethodSelectors(Types typeUtils, Elements elementUtils, TypeFactory typeFactory) {
selectors = selectors =
Arrays.<MethodSelector>asList( Arrays.<MethodSelector>asList(
new TypeSelector( typeFactory ), new TypeSelector(),
new QualifierSelector( typeUtils, elementUtils ), new QualifierSelector( typeUtils, elementUtils ),
new TargetTypeSelector( typeUtils, elementUtils ), new TargetTypeSelector( typeUtils, elementUtils ),
new XmlElementDeclSelector( typeUtils ), new XmlElementDeclSelector( typeUtils ),
new InheritanceSelector() new InheritanceSelector(),
new CreateOrUpdateSelector()
); );
} }
@ -67,30 +67,4 @@ public class MethodSelectors implements MethodSelector {
} }
return candidates; return candidates;
} }
/**
* @param typeFactory the type factory to use
* @param parameters the parameters to map the types for
* @param sourceType the source type
* @param returnType the return type
*
* @return the list of actual parameter types
*/
public static List<Type> getParameterTypes(TypeFactory typeFactory, List<Parameter> parameters, Type sourceType,
Type returnType) {
List<Type> result = new ArrayList<Type>();
for ( Parameter param : parameters ) {
if ( param.isTargetType() ) {
result.add( typeFactory.classTypeOf( returnType ) );
}
else {
if ( sourceType != null ) {
/* for factory methods (sourceType==null), no parameter must be added */
result.add( sourceType );
}
}
}
return result;
}
} }

View File

@ -22,7 +22,6 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.mapstruct.ap.model.common.Type; import org.mapstruct.ap.model.common.Type;
import org.mapstruct.ap.model.common.TypeFactory;
import org.mapstruct.ap.model.source.Method; import org.mapstruct.ap.model.source.Method;
import org.mapstruct.ap.model.source.MethodMatcher; import org.mapstruct.ap.model.source.MethodMatcher;
@ -34,12 +33,6 @@ import org.mapstruct.ap.model.source.MethodMatcher;
*/ */
public class TypeSelector implements MethodSelector { public class TypeSelector implements MethodSelector {
private final TypeFactory typeFactory;
public TypeSelector(TypeFactory typeFactory) {
this.typeFactory = typeFactory;
}
@Override @Override
public <T extends Method> List<T> getMatchingMethods(Method mappingMethod, List<T> methods, public <T extends Method> List<T> getMatchingMethods(Method mappingMethod, List<T> methods,
Type sourceType, Type targetType, Type sourceType, Type targetType,
@ -47,13 +40,7 @@ public class TypeSelector implements MethodSelector {
List<T> result = new ArrayList<T>(); List<T> result = new ArrayList<T>();
for ( T method : methods ) { for ( T method : methods ) {
if ( method.getSourceParameters().size() > 1 ) { if ( method.matches( sourceType, targetType ) ) {
continue;
}
List<Type> parameterTypes =
MethodSelectors.getParameterTypes( typeFactory, method.getParameters(), sourceType, targetType );
if ( method.matches( parameterTypes, targetType ) ) {
result.add( method ); result.add( method );
} }
} }

View File

@ -204,6 +204,7 @@ public class MethodRetrievalProcessor implements ModelElementProcessor<Void, Lis
.setReturnType( returnType ) .setReturnType( returnType )
.setExceptionTypes( exceptionTypes ) .setExceptionTypes( exceptionTypes )
.setTypeUtils( typeUtils ) .setTypeUtils( typeUtils )
.setTypeFactory( typeFactory )
.createSourceMethod(); .createSourceMethod();
} }

View File

@ -444,9 +444,19 @@ public class MappingResolverImpl implements MappingResolver {
} }
private boolean isCandidateForMapping(Method methodCandidate) { private boolean isCandidateForMapping(Method methodCandidate) {
return isCreateMethodForMapping( methodCandidate ) || isUpdateMethodForMapping( methodCandidate );
}
private boolean isCreateMethodForMapping(Method methodCandidate) {
// a create method may not return void and has no target parameter
return methodCandidate.getSourceParameters().size() == 1 && !methodCandidate.getReturnType().isVoid() return methodCandidate.getSourceParameters().size() == 1 && !methodCandidate.getReturnType().isVoid()
&& methodCandidate.getTargetParameter() == null; // @MappingTarget is not yet supported for property && methodCandidate.getMappingTargetParameter() == null;
// mappings }
private boolean isUpdateMethodForMapping(Method methodCandidate) {
// an update method may, or may not return void and has a target parameter
return methodCandidate.getSourceParameters().size() == 1
&& methodCandidate.getMappingTargetParameter() != null;
} }
private <T extends Method> T getBestMatch(List<T> methods, Type sourceType, Type returnType) { private <T extends Method> T getBestMatch(List<T> methods, Type sourceType, Type returnType) {