I'm using spring-data-ldap with OOM and my own schema containing a boolean property. When I try to store a value in or read a value from openLDAP, I get the exception
javax.naming.directory.InvalidAttributeValueException: [LDAP: error code 21 - XXXActive: value #0 invalid per syntax]
here's my schema (the actual customer prefix has been replaced with "XXX"):
dn: cn=XXX,cn=schema,cn=config
objectClass: olcSchemaConfig
cn: XXX
olcAttributeTypes: ( 1.3.6.1.4.1.42691910.1.1.1.1 NAME 'XXXActive'
DESC 'whether the subscriber has been activated (default false, after self-registration)'
EQUALITY booleanMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
SINGLE-VALUE )
olcAttributeTypes: ( 1.3.6.1.4.1.42691910.1.1.1.2 NAME 'XXXLocale'
DESC 'the locale in which he wants to receive emails'
EQUALITY caseExactMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
olcAttributeTypes: ( 1.3.6.1.4.1.42691910.1.1.1.7 NAME 'XXXQueryFreshnessDate'
DESC 'freshness date ...'
EQUALITY generalizedTimeMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.24
SINGLE-VALUE )
olcObjectClasses: ( 1.3.6.1.4.1.42691910.1.1.2.1 NAME 'XXXSubscriber'
DESC 'a subscriber for ...'
SUP top
STRUCTURAL
MUST ( uid $ userPassword )
MAY ( XXXActive $ XXXLocale $ XXXQueryFreshnessDate $ telephoneNumber ) )
here's my entity class:
import lombok.*;
import org.springframework.ldap.odm.annotations.*;
import javax.naming.Name;
import java.io.Serializable;
import java.util.Date;
@Getter
@Setter
@Entry(objectClasses = {"XXXSubscriber", "top"})
@EqualsAndHashCode
@Builder
@AllArgsConstructor
@NoArgsConstructor
public final class XXXSubscriber implements Serializable {
@Id
private Name dn;
@Attribute(name = "uid")
@DnAttribute(value = "uid", index = 3)
private String email;
@Transient
@DnAttribute(value = "dc", index = 0)
private String env;
@Transient
@DnAttribute(value = "dc", index = 1)
private String application;
@Transient
@DnAttribute(value = "ou", index = 2)
private String orga;
@Attribute(name = "telephoneNumber")
private String phone;
@Attribute(name = "XXXActive", syntax="1.3.6.1.4.1.1466.115.121.1.7") //TODO throws an error due to invalid syntax false/FALSE
private boolean active;
@Attribute(name = "XXXQueryFreshnessDate", syntax = "1.3.6.1.4.1.1466.115.121.1.24") //TODO doesn't work either
private Date queryFreshnessDate;
@Attribute(name = "XXXLocale")
private String locale;
@Attribute(name = "userPassword", type = Attribute.Type.BINARY)
private byte[] password;
}
And the according Repo class (which is registered via @org.springframework.data.ldap.repository.config.EnableLdapRepositories()
):
import org.springframework.data.ldap.repository.LdapRepository;
public interface XXXSubscriberRepo extends LdapRepository<XXXSubscriber> {
XXXSubscriber findOneByEmail(String email);
XXXSubscriber findOneByEmailAndActive(String email, boolean active);
}
And here's an example entry from an ldif:
dn: uid=somebody@example.org,ou=subscribers,dc=applications,dc=test,dc=example,dc=org
objectclass: top
objectclass: XXXSubscriber
uid: somebody@example.org
telephoneNumber: 004940123456789
XXXActive: TRUE
XXXLocale: en
userPassword: {SCRYPT}$e0801$9KXJwk7Q0kFzj07LWKef4TgGmPll0sr1hWxL6kMAQzuluW/87EyaQ4lLkWHNdUInF1GMkm7DAefsa+wUOlMGJg==$3aCwqyWYcS70p6Ib1k/Wh7gKsyZwYq/D3ynZpUUvIfM=
XXXQueryFreshnessDate: 20221108164632.123Z
Is there a possibility, to use the org.springframework.data.ldap.repository.LdapRepository
, but register the correct converter for this class, so it can handle the boolean values properly? Per default the boolean values are converted to "true" / "false", but LDAP seems to expect "TRUE"/"FALSE" (MATCH booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
).
btw I'm pretty sure that the Date XXXQueryFreshnessDate
(MATCH generalizedTimeMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24
) will need to be converted accordingly.
The only examples I found in the internet were either with custom Repositories (not an LdapRepository
interface with @EnableLdapRepositories
) and Converters.
Is it possible? How?
Best regards,
Alexander.
Update
I've debugged into it - when debugging I stepped across org.springframework.ldap.odm.core.impl.DefaultObjectDirectoryMapper#populateSingleValueAttribute
- which calls org.springframework.ldap.odm.typeconversion.impl.ConversionServiceConverterManager#convert(Object source, String syntax, Class<T> toClass)
with
source
: falsesyntax
: "1.3.6.1.4.1.1466.115.121.1.7"toClass
: java.lang.String
(!) it's trying to convert the given Boolean to a String.
(!) It's completely ignoring the syntax:
@Override
public <T> T convert(Object source, String syntax, Class<T> toClass) {
return conversionService.convert(source, toClass);
}
(i) conversionService
is of type org.springframework.core.convert.support.DefaultConversionService
.
I managed to get this thing going by wiring my own ConversionService
/ ConversionServiceConverterManager
into the LdapTemplate like this:
@Bean
public DefaultConversionService myObjectDirectoryMapper(LdapTemplate ldapTemplate) {
DefaultObjectDirectoryMapper objectDirectoryMapper = (DefaultObjectDirectoryMapper) ldapTemplate.getObjectDirectoryMapper();
DefaultConversionService conversionService = new DefaultConversionService();
// own implementations
conversionService.addConverter(new BooleanToStringConverter());
conversionService.addConverter(new StringToBooleanConverter());
// implementations from https://mvnrepository.com/artifact/org.ldaptive/ldaptive-beans/2.1.1
conversionService.addConverter(new StringToZonedDateTimeConverter());
conversionService.addConverter(new ZonedDateTimeToStringConverter());
ConversionServiceConverterManager converterManager = new ConversionServiceConverterManager(conversionService);
objectDirectoryMapper.setConverterManager(converterManager);
return conversionService;
}
with my converter classes
class BooleanToStringConverter
implements org.springframework.core.convert.converter.Converter<Boolean, String> {
@Override
public String convert(Boolean source) {
return source.toString().toUpperCase();
}
}
and
class StringToBooleanConverter
implements org.springframework.core.convert.converter.Converter<String, Boolean> {
@Override
public Boolean convert(String source) {
return Boolean.parseBoolean(source.toLowerCase());
}
}