From b88ce592d62e5496adef91b8949d91146776b331 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Tue, 15 Oct 2024 17:36:06 +0200 Subject: [PATCH] Add support for Value Expressions in `@Query` methods. Closes #453 Original pull request: #505 --- pom.xml | 15 + .../query/AbstractLdapRepositoryQuery.java | 1 + .../query/AnnotatedLdapRepositoryQuery.java | 49 ++- .../ldap/repository/query/BindingContext.java | 177 +++++++++++ .../ContextualValueExpressionEvaluator.java | 44 +++ .../repository/query/LdapQueryMethod.java | 3 + .../repository/query/StringBasedQuery.java | 293 ++++++++++++++++++ .../support/LdapRepositoryFactory.java | 28 +- .../ldap/config/EmbeddedLdapProperties.java | 47 +++ .../config/InMemoryLdapConfiguration.java | 145 +++++++++ .../repository/SimpleLdapRepositoryTests.java | 2 - .../AnnotatedLdapRepositoryQueryTests.java | 83 +++++ .../ldap/repository/query/SchemaEntry.java | 48 +++ ...lueExpressionLdapRepositoryQueryTests.java | 167 ++++++++++ src/test/resources/schema.ldif | 71 +++++ 15 files changed, 1149 insertions(+), 24 deletions(-) create mode 100644 src/main/java/org/springframework/data/ldap/repository/query/BindingContext.java create mode 100644 src/main/java/org/springframework/data/ldap/repository/query/ContextualValueExpressionEvaluator.java create mode 100644 src/main/java/org/springframework/data/ldap/repository/query/StringBasedQuery.java create mode 100644 src/test/java/org/springframework/data/ldap/config/EmbeddedLdapProperties.java create mode 100644 src/test/java/org/springframework/data/ldap/config/InMemoryLdapConfiguration.java create mode 100644 src/test/java/org/springframework/data/ldap/repository/query/AnnotatedLdapRepositoryQueryTests.java create mode 100644 src/test/java/org/springframework/data/ldap/repository/query/SchemaEntry.java create mode 100644 src/test/java/org/springframework/data/ldap/repository/query/ValueExpressionLdapRepositoryQueryTests.java create mode 100644 src/test/resources/schema.ldif diff --git a/pom.xml b/pom.xml index dfdc03c..cd39cda 100644 --- a/pom.xml +++ b/pom.xml @@ -21,6 +21,7 @@ 3.2.8 3.5.0-SNAPSHOT spring.data.ldap + 7.0.1 @@ -109,6 +110,20 @@ test + + org.springframework.ldap + spring-ldap-test + ${spring-ldap} + test + + + + com.unboundid + unboundid-ldapsdk + ${unboundid-ldapsdk} + test + + diff --git a/src/main/java/org/springframework/data/ldap/repository/query/AbstractLdapRepositoryQuery.java b/src/main/java/org/springframework/data/ldap/repository/query/AbstractLdapRepositoryQuery.java index 4c4d1f9..d35e6fe 100644 --- a/src/main/java/org/springframework/data/ldap/repository/query/AbstractLdapRepositoryQuery.java +++ b/src/main/java/org/springframework/data/ldap/repository/query/AbstractLdapRepositoryQuery.java @@ -26,6 +26,7 @@ import org.springframework.data.mapping.model.EntityInstantiators; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.ldap.core.LdapOperations; import org.springframework.ldap.query.LdapQuery; import org.springframework.util.Assert; diff --git a/src/main/java/org/springframework/data/ldap/repository/query/AnnotatedLdapRepositoryQuery.java b/src/main/java/org/springframework/data/ldap/repository/query/AnnotatedLdapRepositoryQuery.java index 49d721b..708fc9f 100644 --- a/src/main/java/org/springframework/data/ldap/repository/query/AnnotatedLdapRepositoryQuery.java +++ b/src/main/java/org/springframework/data/ldap/repository/query/AnnotatedLdapRepositoryQuery.java @@ -17,11 +17,14 @@ package org.springframework.data.ldap.repository.query; import static org.springframework.ldap.query.LdapQueryBuilder.*; +import org.springframework.data.expression.ValueEvaluationContext; +import org.springframework.data.expression.ValueEvaluationContextProvider; import org.springframework.data.ldap.repository.Query; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.EntityInstantiators; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.ldap.core.LdapOperations; import org.springframework.ldap.query.LdapQuery; import org.springframework.util.Assert; @@ -31,10 +34,14 @@ import org.springframework.util.Assert; * * @author Mattias Hellborg Arthursson * @author Mark Paluch + * @author Marcin Grzejszczak */ public class AnnotatedLdapRepositoryQuery extends AbstractLdapRepositoryQuery { private final Query queryAnnotation; + private final ValueExpressionDelegate valueExpressionDelegate; + private final StringBasedQuery stringBasedQuery; + private final StringBasedQuery stringBasedBase; /** * Construct a new instance. @@ -44,26 +51,64 @@ public class AnnotatedLdapRepositoryQuery extends AbstractLdapRepositoryQuery { * @param ldapOperations the LdapOperations instance to use. * @param mappingContext must not be {@literal null}. * @param instantiators must not be {@literal null}. + * @deprecated use the constructor with {@link ValueExpressionDelegate} */ + @Deprecated(since = "3.4") public AnnotatedLdapRepositoryQuery(LdapQueryMethod queryMethod, Class entityType, LdapOperations ldapOperations, MappingContext, ? extends PersistentProperty> mappingContext, EntityInstantiators instantiators) { + this(queryMethod, entityType, ldapOperations, mappingContext, instantiators, ValueExpressionDelegate.create()); + } + + /** + * Construct a new instance. + * + * @param queryMethod the QueryMethod. + * @param entityType the managed class. + * @param ldapOperations the LdapOperations instance to use. + * @param mappingContext must not be {@literal null}. + * @param instantiators must not be {@literal null}. + * @param valueExpressionDelegate must not be {@literal null} + * @since 3.4 + */ + public AnnotatedLdapRepositoryQuery(LdapQueryMethod queryMethod, Class entityType, LdapOperations ldapOperations, + MappingContext, ? extends PersistentProperty> mappingContext, + EntityInstantiators instantiators, ValueExpressionDelegate valueExpressionDelegate) { + super(queryMethod, entityType, ldapOperations, mappingContext, instantiators); Assert.notNull(queryMethod.getQueryAnnotation(), "Annotation must be present"); Assert.hasLength(queryMethod.getQueryAnnotation().value(), "Query filter must be specified"); queryAnnotation = queryMethod.getRequiredQueryAnnotation(); + this.valueExpressionDelegate = valueExpressionDelegate; + stringBasedQuery = new StringBasedQuery(queryAnnotation.value(), queryMethod.getParameters(), valueExpressionDelegate); + stringBasedBase = new StringBasedQuery(queryAnnotation.base(), queryMethod.getParameters(), valueExpressionDelegate); } @Override protected LdapQuery createQuery(LdapParameterAccessor parameters) { - return query().base(queryAnnotation.base()) // + ValueEvaluationContextProvider valueContextProvider = valueExpressionDelegate + .createValueContextProvider(getQueryMethod().getParameters()); + + String boundQuery = bind(parameters, valueContextProvider, stringBasedQuery); + + String boundBase = bind(parameters, valueContextProvider, stringBasedBase); + + return query().base(boundBase) // .searchScope(queryAnnotation.searchScope()) // .countLimit(queryAnnotation.countLimit()) // .timeLimit(queryAnnotation.timeLimit()) // - .filter(queryAnnotation.value(), parameters.getBindableParameterValues()); + .filter(boundQuery); } + + private String bind(LdapParameterAccessor parameters, ValueEvaluationContextProvider valueContextProvider, StringBasedQuery query) { + ValueEvaluationContext evaluationContext = valueContextProvider + .getEvaluationContext(parameters.getBindableParameterValues(), query.getExpressionDependencies()); + return query.bindQuery(parameters, + new ContextualValueExpressionEvaluator(valueExpressionDelegate, evaluationContext)); + } + } diff --git a/src/main/java/org/springframework/data/ldap/repository/query/BindingContext.java b/src/main/java/org/springframework/data/ldap/repository/query/BindingContext.java new file mode 100644 index 0000000..2782acc --- /dev/null +++ b/src/main/java/org/springframework/data/ldap/repository/query/BindingContext.java @@ -0,0 +1,177 @@ +/* + * Copyright 2020-2024 the original author or authors. + * + * 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 + * + * https://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.springframework.data.ldap.repository.query; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.data.mapping.model.ValueExpressionEvaluator; +import org.springframework.data.repository.query.Parameter; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.Parameters; +import org.springframework.lang.Nullable; +import org.springframework.ldap.support.LdapEncoder; +import org.springframework.util.Assert; + +/** + * Value object capturing the binding context to provide {@link #getBindingValues() binding values} for queries. + * + * @author Mark Paluch + * @since 3.4 + */ +class BindingContext { + + private final Parameters parameters; + + private final ParameterAccessor parameterAccessor; + + private final List bindings; + + private final ValueExpressionEvaluator evaluator; + + /** + * Create new {@link BindingContext}. + */ + BindingContext(Parameters parameters, ParameterAccessor parameterAccessor, + List bindings, ValueExpressionEvaluator evaluator) { + + this.parameters = parameters; + this.parameterAccessor = parameterAccessor; + this.bindings = bindings; + this.evaluator = evaluator; + } + + /** + * @return {@literal true} when list of bindings is not empty. + */ + private boolean hasBindings() { + return !bindings.isEmpty(); + } + + /** + * Bind values provided by {@link LdapParameterAccessor} to placeholders in {@link BindingContext} while + * considering potential conversions and parameter types. + * + * @return {@literal null} if given {@code raw} value is empty. + */ + public List getBindingValues() { + + if (!hasBindings()) { + return Collections.emptyList(); + } + + List parameters = new ArrayList<>(bindings.size()); + + for (ParameterBinding binding : bindings) { + Object parameterValueForBinding = getParameterValueForBinding(binding); + parameters.add(parameterValueForBinding); + } + + return parameters; + } + + /** + * Return the value to be used for the given {@link ParameterBinding}. + * + * @param binding must not be {@literal null}. + * @return the value used for the given {@link ParameterBinding}. + */ + @Nullable + private Object getParameterValueForBinding(ParameterBinding binding) { + + if (binding.isExpression()) { + return evaluator.evaluate(binding.getRequiredExpression()); + } + + Object value = binding.isNamed() ? + parameterAccessor.getBindableValue(getParameterIndex(parameters, binding.getRequiredParameterName())) : + parameterAccessor.getBindableValue(binding.getParameterIndex()); + return value == null ? null : LdapEncoder.filterEncode(value.toString()); + } + + private int getParameterIndex(Parameters parameters, String parameterName) { + + return parameters.stream() // + .filter(parameter -> parameter // + .getName().filter(s -> s.equals(parameterName)) // + .isPresent()) // + .mapToInt(Parameter::getIndex) // + .findFirst() // + .orElseThrow(() -> new IllegalArgumentException( + String.format("Invalid parameter name; Cannot resolve parameter [%s]", parameterName))); + } + + /** + * A generic parameter binding with name or position information. + * + * @author Mark Paluch + */ + static class ParameterBinding { + + private final int parameterIndex; + private final @Nullable String expression; + private final @Nullable String parameterName; + + private ParameterBinding(int parameterIndex, @Nullable String expression, @Nullable String parameterName) { + + this.parameterIndex = parameterIndex; + this.expression = expression; + this.parameterName = parameterName; + } + + static ParameterBinding expression(String expression, boolean quoted) { + return new ParameterBinding(-1, expression, null); + } + + static ParameterBinding indexed(int parameterIndex) { + return new ParameterBinding(parameterIndex, null, null); + } + + static ParameterBinding named(String name) { + return new ParameterBinding(-1, null, name); + } + + boolean isNamed() { + return (parameterName != null); + } + + int getParameterIndex() { + return parameterIndex; + } + + String getParameter() { + return ("?" + (isExpression() ? "expr" : "") + parameterIndex); + } + + String getRequiredExpression() { + + Assert.state(expression != null, "ParameterBinding is not an expression"); + return expression; + } + + boolean isExpression() { + return (this.expression != null); + } + + String getRequiredParameterName() { + + Assert.state(parameterName != null, "ParameterBinding is not named"); + + return parameterName; + } + } +} diff --git a/src/main/java/org/springframework/data/ldap/repository/query/ContextualValueExpressionEvaluator.java b/src/main/java/org/springframework/data/ldap/repository/query/ContextualValueExpressionEvaluator.java new file mode 100644 index 0000000..66dd454 --- /dev/null +++ b/src/main/java/org/springframework/data/ldap/repository/query/ContextualValueExpressionEvaluator.java @@ -0,0 +1,44 @@ +/* + * Copyright 2024 the original author or authors. + * + * 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 + * + * https://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.springframework.data.ldap.repository.query; + +import org.springframework.data.expression.ValueEvaluationContext; +import org.springframework.data.expression.ValueExpression; +import org.springframework.data.expression.ValueExpressionParser; +import org.springframework.data.mapping.model.ValueExpressionEvaluator; + +/** + * @author Marcin Grzejszczak + * @author Mark Paluch + */ +class ContextualValueExpressionEvaluator implements ValueExpressionEvaluator { + + private final ValueExpressionParser parser; + + public ContextualValueExpressionEvaluator(ValueExpressionParser parser, ValueEvaluationContext evaluationContext) { + this.parser = parser; + this.evaluationContext = evaluationContext; + } + + private final ValueEvaluationContext evaluationContext; + + @SuppressWarnings("unchecked") + @Override + public T evaluate(String expressionString) { + ValueExpression expression = parser.parse(expressionString); + return (T) expression.evaluate(evaluationContext); + } +} diff --git a/src/main/java/org/springframework/data/ldap/repository/query/LdapQueryMethod.java b/src/main/java/org/springframework/data/ldap/repository/query/LdapQueryMethod.java index 96c2be9..e405e9c 100644 --- a/src/main/java/org/springframework/data/ldap/repository/query/LdapQueryMethod.java +++ b/src/main/java/org/springframework/data/ldap/repository/query/LdapQueryMethod.java @@ -21,6 +21,8 @@ import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.ldap.repository.Query; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.query.Parameters; +import org.springframework.data.repository.query.ParametersSource; import org.springframework.data.repository.query.QueryMethod; import org.springframework.lang.Nullable; @@ -85,4 +87,5 @@ public class LdapQueryMethod extends QueryMethod { throw new IllegalStateException("Required @Query annotation is not present"); } + } diff --git a/src/main/java/org/springframework/data/ldap/repository/query/StringBasedQuery.java b/src/main/java/org/springframework/data/ldap/repository/query/StringBasedQuery.java new file mode 100644 index 0000000..7e8aee3 --- /dev/null +++ b/src/main/java/org/springframework/data/ldap/repository/query/StringBasedQuery.java @@ -0,0 +1,293 @@ +/* + * Copyright 2016-2024 the original author or authors. + * + * 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 + * + * https://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.springframework.data.ldap.repository.query; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.data.ldap.repository.query.BindingContext.ParameterBinding; +import org.springframework.data.mapping.model.ValueExpressionEvaluator; +import org.springframework.data.repository.query.Parameters; +import org.springframework.data.repository.query.ValueExpressionDelegate; +import org.springframework.data.spel.ExpressionDependencies; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * String-based Query abstracting a query with parameter bindings. + * + * @author Marcin Grzejszczak + * @since 3.4 + */ +class StringBasedQuery { + + private final String query; + + private final Parameters parameters; + + private final ValueExpressionDelegate expressionParser; + + private final List queryParameterBindings = new ArrayList<>(); + + private final ExpressionDependencies expressionDependencies; + + /** + * Create a new {@link StringBasedQuery} given {@code query}, {@link Parameters} and {@link ValueExpressionDelegate}. + * + * @param query must not be empty. + * @param parameters must not be {@literal null}. + * @param expressionParser must not be {@literal null}. + */ + StringBasedQuery(String query, Parameters parameters, ValueExpressionDelegate expressionParser) { + + this.query = ParameterBindingParser.INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query, + this.queryParameterBindings); + this.parameters = parameters; + this.expressionParser = expressionParser; + this.expressionDependencies = createExpressionDependencies(); + } + + private ExpressionDependencies createExpressionDependencies() { + + if (queryParameterBindings.isEmpty()) { + return ExpressionDependencies.none(); + } + + List dependencies = new ArrayList<>(); + + for (ParameterBinding binding : queryParameterBindings) { + if (binding.isExpression()) { + dependencies + .add(expressionParser.parse(binding.getRequiredExpression()).getExpressionDependencies()); + } + } + + return ExpressionDependencies.merged(dependencies); + } + + /** + * Obtain {@link ExpressionDependencies} from the parsed query. + * + * @return the {@link ExpressionDependencies} from the parsed query. + */ + public ExpressionDependencies getExpressionDependencies() { + return expressionDependencies; + } + + /** + * Bind the query to actual parameters using {@link LdapParameterAccessor}, + * + * @param parameterAccessor must not be {@literal null}. + * @param evaluator must not be {@literal null}. + * @return the bound String query containing formatted parameters. + */ + String bindQuery(LdapParameterAccessor parameterAccessor, ValueExpressionEvaluator evaluator) { + + Assert.notNull(parameterAccessor, "LdapParameterAccessor must not be null"); + Assert.notNull(evaluator, "SpELExpressionEvaluator must not be null"); + + BindingContext bindingContext = new BindingContext(this.parameters, parameterAccessor, this.queryParameterBindings, + evaluator); + + List arguments = bindingContext.getBindingValues(); + + return ParameterBinder.INSTANCE.bind(this.query, arguments); + } + + /** + * A parser that extracts the parameter bindings from a given query string. + * + * @author Mark Paluch + */ + enum ParameterBinder { + + INSTANCE; + + private static final String ARGUMENT_PLACEHOLDER = "?_param_?"; + private static final Pattern ARGUMENT_PLACEHOLDER_PATTERN = Pattern.compile(Pattern.quote(ARGUMENT_PLACEHOLDER)); + + public String bind(String input, List parameters) { + + if (parameters.isEmpty()) { + return input; + } + + StringBuilder result = new StringBuilder(); + + int startIndex = 0; + int currentPosition = 0; + int parameterIndex = 0; + + Matcher matcher = ARGUMENT_PLACEHOLDER_PATTERN.matcher(input); + + while (currentPosition < input.length()) { + + if (!matcher.find()) { + break; + } + + int exprStart = matcher.start(); + + result.append(input.subSequence(startIndex, exprStart)).append(parameters.get(parameterIndex)); + + parameterIndex++; + currentPosition = matcher.end(); + startIndex = currentPosition; + } + + return result.append(input.subSequence(currentPosition, input.length())).toString(); + } + } + + /** + * A parser that extracts the parameter bindings from a given query string. + * + * @author Mark Paluch + */ + enum ParameterBindingParser { + + INSTANCE; + + private static final char CURRLY_BRACE_OPEN = '{'; + private static final char CURRLY_BRACE_CLOSE = '}'; + + private static final Pattern INDEX_PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)"); + private static final Pattern NAMED_PARAMETER_BINDING_PATTERN = Pattern.compile("\\:(\\w+)"); + private static final Pattern INDEX_BASED_EXPRESSION_PATTERN = Pattern.compile("\\?\\#\\{"); + private static final Pattern NAME_BASED_EXPRESSION_PATTERN = Pattern.compile("\\:\\#\\{"); + private static final Pattern INDEX_BASED_PROPERTY_PLACEHOLDER_PATTERN = Pattern.compile("\\?\\$\\{"); + private static final Pattern NAME_BASED_PROPERTY_PLACEHOLDER_PATTERN = Pattern.compile("\\:\\$\\{"); + + private static final Set VALUE_EXPRESSION_PATTERNS = Set.of(INDEX_BASED_EXPRESSION_PATTERN, NAME_BASED_EXPRESSION_PATTERN, INDEX_BASED_PROPERTY_PLACEHOLDER_PATTERN, NAME_BASED_PROPERTY_PLACEHOLDER_PATTERN); + + private static final String ARGUMENT_PLACEHOLDER = "?_param_?"; + + /** + * Returns a list of {@link ParameterBinding}s found in the given {@code input}. + * + * @param input can be {@literal null} or empty. + * @param bindings must not be {@literal null}. + * @return a list of {@link ParameterBinding}s found in the given {@code input}. + */ + public String parseAndCollectParameterBindingsFromQueryIntoBindings(String input, List bindings) { + + if (!StringUtils.hasText(input)) { + return input; + } + + Assert.notNull(bindings, "Parameter bindings must not be null"); + + return transformQueryAndCollectExpressionParametersIntoBindings(input, bindings); + } + + private static String transformQueryAndCollectExpressionParametersIntoBindings(String input, + List bindings) { + + StringBuilder result = new StringBuilder(); + + int startIndex = 0; + int currentPosition = 0; + + while (currentPosition < input.length()) { + + Matcher matcher = findNextBindingOrExpression(input, currentPosition); + + // no expression parameter found + if (matcher == null) { + break; + } + + int exprStart = matcher.start(); + currentPosition = exprStart; + + if (isValueExpression(matcher)) { + // eat parameter expression + int curlyBraceOpenCount = 1; + currentPosition += 3; + + while (curlyBraceOpenCount > 0 && currentPosition < input.length()) { + switch (input.charAt(currentPosition++)) { + case CURRLY_BRACE_OPEN: + curlyBraceOpenCount++; + break; + case CURRLY_BRACE_CLOSE: + curlyBraceOpenCount--; + break; + default: + } + } + result.append(input.subSequence(startIndex, exprStart)); + } else { + result.append(input.subSequence(startIndex, exprStart)); + } + + result.append(ARGUMENT_PLACEHOLDER); + + if (isValueExpression(matcher)) { + bindings.add( + ParameterBinding + .expression(input.substring(exprStart + 1, currentPosition), true)); + } else { + if (matcher.pattern() == INDEX_PARAMETER_BINDING_PATTERN) { + bindings + .add(ParameterBinding.indexed(Integer.parseInt(matcher.group(1)))); + } else { + bindings.add(ParameterBinding.named(matcher.group(1))); + } + + currentPosition = matcher.end(); + } + + startIndex = currentPosition; + } + + return result.append(input.subSequence(currentPosition, input.length())).toString(); + } + + private static boolean isValueExpression(Matcher matcher) { + return VALUE_EXPRESSION_PATTERNS.contains(matcher.pattern()); + } + + @Nullable + private static Matcher findNextBindingOrExpression(String input, int startPosition) { + + List matchers = new ArrayList<>(6); + + matchers.add(INDEX_PARAMETER_BINDING_PATTERN.matcher(input)); + matchers.add(NAMED_PARAMETER_BINDING_PATTERN.matcher(input)); + matchers.add(INDEX_BASED_EXPRESSION_PATTERN.matcher(input)); + matchers.add(NAME_BASED_EXPRESSION_PATTERN.matcher(input)); + matchers.add(INDEX_BASED_PROPERTY_PLACEHOLDER_PATTERN.matcher(input)); + matchers.add(NAME_BASED_PROPERTY_PLACEHOLDER_PATTERN.matcher(input)); + + Map matcherMap = new TreeMap<>(); + + for (Matcher matcher : matchers) { + if (matcher.find(startPosition)) { + matcherMap.put(matcher.start(), matcher); + } + } + + return (matcherMap.isEmpty() ? null : matcherMap.values().iterator().next()); + } + } +} diff --git a/src/main/java/org/springframework/data/ldap/repository/support/LdapRepositoryFactory.java b/src/main/java/org/springframework/data/ldap/repository/support/LdapRepositoryFactory.java index 6576474..85cbc6d 100644 --- a/src/main/java/org/springframework/data/ldap/repository/support/LdapRepositoryFactory.java +++ b/src/main/java/org/springframework/data/ldap/repository/support/LdapRepositoryFactory.java @@ -42,6 +42,7 @@ import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.lang.Nullable; import org.springframework.ldap.core.LdapOperations; import org.springframework.util.Assert; @@ -56,7 +57,6 @@ import org.springframework.util.Assert; */ public class LdapRepositoryFactory extends RepositoryFactorySupport { - private final LdapQueryLookupStrategy queryLookupStrategy; private final LdapOperations ldapOperations; private final MappingContext, ? extends PersistentProperty> mappingContext; private final EntityInstantiators instantiators = new EntityInstantiators(); @@ -72,7 +72,6 @@ public class LdapRepositoryFactory extends RepositoryFactorySupport { this.ldapOperations = ldapOperations; this.mappingContext = new LdapMappingContext(); - this.queryLookupStrategy = new LdapQueryLookupStrategy(ldapOperations, instantiators, mappingContext); } /** @@ -87,7 +86,6 @@ public class LdapRepositoryFactory extends RepositoryFactorySupport { Assert.notNull(ldapOperations, "LdapOperations must not be null"); Assert.notNull(mappingContext, "LdapMappingContext must not be null"); - this.queryLookupStrategy = new LdapQueryLookupStrategy(ldapOperations, instantiators, mappingContext); this.ldapOperations = ldapOperations; this.mappingContext = mappingContext; } @@ -154,9 +152,9 @@ public class LdapRepositoryFactory extends RepositoryFactorySupport { } @Override - protected Optional getQueryLookupStrategy(@Nullable Key key, - QueryMethodEvaluationContextProvider evaluationContextProvider) { - return Optional.of(queryLookupStrategy); + protected Optional getQueryLookupStrategy(Key key, + ValueExpressionDelegate valueExpressionDelegate) { + return Optional.of(new LdapQueryLookupStrategy(ldapOperations, instantiators, mappingContext, valueExpressionDelegate)); } /** @@ -185,19 +183,9 @@ public class LdapRepositoryFactory extends RepositoryFactorySupport { return acceptsMappingContext; } - private static final class LdapQueryLookupStrategy implements QueryLookupStrategy { - - private final LdapOperations ldapOperations; - private final EntityInstantiators instantiators; - private final MappingContext, ? extends PersistentProperty> mappingContext; - - public LdapQueryLookupStrategy(LdapOperations ldapOperations, EntityInstantiators instantiators, - MappingContext, ? extends PersistentProperty> mappingContext) { - - this.ldapOperations = ldapOperations; - this.instantiators = instantiators; - this.mappingContext = mappingContext; - } + private record LdapQueryLookupStrategy(LdapOperations ldapOperations, + EntityInstantiators instantiators, MappingContext, ? extends PersistentProperty> mappingContext, + ValueExpressionDelegate valueExpressionDelegate) implements QueryLookupStrategy { @Override public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, @@ -207,7 +195,7 @@ public class LdapRepositoryFactory extends RepositoryFactorySupport { Class domainType = metadata.getDomainType(); if (queryMethod.hasQueryAnnotation()) { - return new AnnotatedLdapRepositoryQuery(queryMethod, domainType, ldapOperations, mappingContext, instantiators); + return new AnnotatedLdapRepositoryQuery(queryMethod, domainType, ldapOperations, mappingContext, instantiators, valueExpressionDelegate); } else { return new PartTreeLdapRepositoryQuery(queryMethod, domainType, ldapOperations, mappingContext, instantiators); } diff --git a/src/test/java/org/springframework/data/ldap/config/EmbeddedLdapProperties.java b/src/test/java/org/springframework/data/ldap/config/EmbeddedLdapProperties.java new file mode 100644 index 0000000..f9ad3ae --- /dev/null +++ b/src/test/java/org/springframework/data/ldap/config/EmbeddedLdapProperties.java @@ -0,0 +1,47 @@ +package org.springframework.data.ldap.config; + +import java.util.ArrayList; +import java.util.List; + +public class EmbeddedLdapProperties { + + /** + * Embedded LDAP port. + */ + private int port = 0; + + /** + * List of base DNs. + */ + private List baseDn = new ArrayList<>(); + + /** + * Schema (LDIF) script resource reference. + */ + private String ldif = "classpath:schema.ldif"; + + public int getPort() { + return this.port; + } + + public void setPort(int port) { + this.port = port; + } + + public List getBaseDn() { + return this.baseDn; + } + + public void setBaseDn(List baseDn) { + this.baseDn = baseDn; + } + + public String getLdif() { + return this.ldif; + } + + public void setLdif(String ldif) { + this.ldif = ldif; + } + +} diff --git a/src/test/java/org/springframework/data/ldap/config/InMemoryLdapConfiguration.java b/src/test/java/org/springframework/data/ldap/config/InMemoryLdapConfiguration.java new file mode 100644 index 0000000..9db2450 --- /dev/null +++ b/src/test/java/org/springframework/data/ldap/config/InMemoryLdapConfiguration.java @@ -0,0 +1,145 @@ +/* + * Copyright 2024 the original author or authors. + * + * 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 + * + * https://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.springframework.data.ldap.config; + +import jakarta.annotation.PreDestroy; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; +import org.springframework.core.io.Resource; +import org.springframework.ldap.core.LdapTemplate; +import org.springframework.ldap.core.support.LdapContextSource; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import com.unboundid.ldap.listener.InMemoryDirectoryServer; +import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; +import com.unboundid.ldap.listener.InMemoryListenerConfig; +import com.unboundid.ldap.sdk.LDAPException; +import com.unboundid.ldif.LDIFReader; + +/** + * Taken from Spring Boot + */ +@Configuration(proxyBeanMethods = false) +public class InMemoryLdapConfiguration { + + private static final String PROPERTY_SOURCE_NAME = "ldap.ports"; + + private InMemoryDirectoryServer server; + + private final EmbeddedLdapProperties embeddedProperties; + + public InMemoryLdapConfiguration(EmbeddedLdapProperties embeddedLdapProperties) { + this.embeddedProperties = embeddedLdapProperties; + } + + @Bean + public InMemoryDirectoryServer directoryServer(ApplicationContext applicationContext) + throws LDAPException { + String[] baseDn = StringUtils.toStringArray(this.embeddedProperties.getBaseDn()); + InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(baseDn); + setSchema(config); + InMemoryListenerConfig listenerConfig = InMemoryListenerConfig.createLDAPConfig("LDAP", + this.embeddedProperties.getPort()); + config.setListenerConfigs(listenerConfig); + this.server = new InMemoryDirectoryServer(config); + importLdif(applicationContext); + this.server.startListening(); + setPortProperty(applicationContext, this.server.getListenPort()); + return this.server; + } + + + @Bean + @DependsOn("directoryServer") + LdapContextSource ldapContextSource(Environment environment, EmbeddedLdapProperties properties, + EmbeddedLdapProperties embeddedProperties) { + LdapContextSource source = new LdapContextSource(); + Assert.notEmpty(properties.getBaseDn(), "Base DN must be set with at least one value"); + source.setBase(properties.getBaseDn().get(0)); + source.setUrls(determineUrls(environment, properties.getPort())); + return source; + } + + @Bean + LdapTemplate ldapTemplate(LdapContextSource ldapContextSource) { + return new LdapTemplate(ldapContextSource); + } + + private String[] determineUrls(Environment environment, int port) { + return new String[] { "ldap://localhost:" + (port != 0 ? port : environment.getProperty("local.ldap.port")) }; + } + + private void setSchema(InMemoryDirectoryServerConfig config) { + config.setSchema(null); + } + + private void importLdif(ApplicationContext applicationContext) { + String location = this.embeddedProperties.getLdif(); + if (StringUtils.hasText(location)) { + try { + Resource resource = applicationContext.getResource(location); + if (resource.exists()) { + try (InputStream inputStream = resource.getInputStream()) { + this.server.importFromLDIF(true, new LDIFReader(inputStream)); + } + } + } + catch (Exception ex) { + throw new IllegalStateException("Unable to load LDIF " + location, ex); + } + } + } + + private void setPortProperty(ApplicationContext context, int port) { + if (context instanceof ConfigurableApplicationContext configurableContext) { + MutablePropertySources sources = configurableContext.getEnvironment().getPropertySources(); + getLdapPorts(sources).put("local.ldap.port", port); + } + if (context.getParent() != null) { + setPortProperty(context.getParent(), port); + } + } + + @SuppressWarnings("unchecked") + private Map getLdapPorts(MutablePropertySources sources) { + PropertySource propertySource = sources.get(PROPERTY_SOURCE_NAME); + if (propertySource == null) { + propertySource = new MapPropertySource(PROPERTY_SOURCE_NAME, new HashMap<>()); + sources.addFirst(propertySource); + } + return (Map) propertySource.getSource(); + } + + @PreDestroy + public void close() { + if (this.server != null) { + this.server.shutDown(true); + } + } +} diff --git a/src/test/java/org/springframework/data/ldap/repository/SimpleLdapRepositoryTests.java b/src/test/java/org/springframework/data/ldap/repository/SimpleLdapRepositoryTests.java index 921c8ce..4ddec8c 100644 --- a/src/test/java/org/springframework/data/ldap/repository/SimpleLdapRepositoryTests.java +++ b/src/test/java/org/springframework/data/ldap/repository/SimpleLdapRepositoryTests.java @@ -60,8 +60,6 @@ class SimpleLdapRepositoryTests { @BeforeEach void prepareTestedInstance() { tested = new SimpleLdapRepository<>(ldapOperationsMock, odmMock, Object.class); - - } @Test diff --git a/src/test/java/org/springframework/data/ldap/repository/query/AnnotatedLdapRepositoryQueryTests.java b/src/test/java/org/springframework/data/ldap/repository/query/AnnotatedLdapRepositoryQueryTests.java new file mode 100644 index 0000000..d9fc9c5 --- /dev/null +++ b/src/test/java/org/springframework/data/ldap/repository/query/AnnotatedLdapRepositoryQueryTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2016-2024 the original author or authors. + * + * 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 + * + * https://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.springframework.data.ldap.repository.query; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.data.ldap.core.mapping.LdapMappingContext; +import org.springframework.data.ldap.repository.LdapRepository; +import org.springframework.data.ldap.repository.Query; +import org.springframework.data.mapping.model.EntityInstantiators; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.repository.query.Param; +import org.springframework.data.repository.query.ValueExpressionDelegate; +import org.springframework.ldap.core.LdapOperations; +import org.springframework.ldap.query.LdapQuery; + +import static org.assertj.core.api.Assertions.assertThat; + +class AnnotatedLdapRepositoryQueryTests { + + LdapOperations ldapOperations = Mockito.mock(); + + ValueExpressionDelegate valueExpressionDelegate = ValueExpressionDelegate.create(); + + @Test + void shouldEncodeQuery() throws NoSuchMethodException { + LdapQueryMethod method = queryMethod("namedParameters"); + AnnotatedLdapRepositoryQuery query = repositoryQuery(method); + + LdapQuery ldapQuery = query.createQuery( + new LdapParametersParameterAccessor(method, new Object[] { "John)(cn=Doe)", "foo" })); + + assertThat(ldapQuery.filter().encode()).isEqualTo("(cn=John\\29\\28cn=Doe\\29)"); + } + + @Test + void shouldEncodeBase() throws NoSuchMethodException { + LdapQueryMethod method = queryMethod("baseNamedParameters"); + AnnotatedLdapRepositoryQuery query = repositoryQuery(method); + + LdapQuery ldapQuery = query.createQuery( + new LdapParametersParameterAccessor(method, new Object[] { "foo", "cn=John)" })); + + assertThat(ldapQuery.base()).hasToString("cn=John\\29"); + } + + private LdapQueryMethod queryMethod(String methodName) throws NoSuchMethodException { + return new LdapQueryMethod(QueryRepository.class.getMethod(methodName, String.class, String.class), + new DefaultRepositoryMetadata(QueryRepository.class), new SpelAwareProxyProjectionFactory()); + } + + private AnnotatedLdapRepositoryQuery repositoryQuery(LdapQueryMethod method) { + return new AnnotatedLdapRepositoryQuery(method, SchemaEntry.class, ldapOperations, new LdapMappingContext(), + new EntityInstantiators(), valueExpressionDelegate); + } + + interface QueryRepository extends LdapRepository { + + @Query(value = "(cn=:fullName)") + List namedParameters(@Param("fullName") String fullName, @Param("lastName") String lastName); + + @Query(base = ":dc", value = "(cn=:fullName)") + List baseNamedParameters(@Param("fullName") String fullName, @Param("dc") String dc); + + } +} \ No newline at end of file diff --git a/src/test/java/org/springframework/data/ldap/repository/query/SchemaEntry.java b/src/test/java/org/springframework/data/ldap/repository/query/SchemaEntry.java new file mode 100644 index 0000000..aa3aca7 --- /dev/null +++ b/src/test/java/org/springframework/data/ldap/repository/query/SchemaEntry.java @@ -0,0 +1,48 @@ +/* + * Copyright 2016-2024 the original author or authors. + * + * 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 + * + * https://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.springframework.data.ldap.repository.query; + +import javax.naming.Name; + +import org.springframework.ldap.odm.annotations.Attribute; +import org.springframework.ldap.odm.annotations.DnAttribute; +import org.springframework.ldap.odm.annotations.Entry; +import org.springframework.ldap.odm.annotations.Id; + +/** + * @author Marcin Grzejszczak + */ +@Entry(objectClasses = { "inetOrgPerson", "organizationalPerson", "person", "top" }) + +public class SchemaEntry { + @Id + Name dn; + + @Attribute(name = "cn") + String fullName; + + @Attribute + String lastName; + + public SchemaEntry() {} + + public SchemaEntry(Name dn, String fullName, String lastName) { + this.dn = dn; + this.fullName = fullName; + this.lastName = lastName; + } +} diff --git a/src/test/java/org/springframework/data/ldap/repository/query/ValueExpressionLdapRepositoryQueryTests.java b/src/test/java/org/springframework/data/ldap/repository/query/ValueExpressionLdapRepositoryQueryTests.java new file mode 100644 index 0000000..291103e --- /dev/null +++ b/src/test/java/org/springframework/data/ldap/repository/query/ValueExpressionLdapRepositoryQueryTests.java @@ -0,0 +1,167 @@ +/* + * Copyright 2016-2024 the original author or authors. + * + * 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 + * + * https://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.springframework.data.ldap.repository.query; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.ldap.config.EmbeddedLdapProperties; +import org.springframework.data.ldap.config.InMemoryLdapConfiguration; +import org.springframework.data.ldap.repository.LdapRepository; +import org.springframework.data.ldap.repository.Query; +import org.springframework.data.ldap.repository.config.EnableLdapRepositories; +import org.springframework.data.repository.query.Param; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +/** + * Integration tests for Repositories using value expressions. + * + * @author Marcin Grzejszczak + */ +@SpringJUnitConfig +@TestPropertySource(properties = { "full.name=John Doe", "dc.name=memorynotfound" }) +class ValueExpressionLdapRepositoryQueryTests { + + @Autowired private QueryRepository queryRepository; + + @Test + void shouldWorkWithNamedParameters() { + + List objects = queryRepository.namedParameters("John Doe", "Bar"); + + assertThatReturnedObjectIsJohnDoe(objects); + } + + @Test + void usingQueryLanguageCharsShouldNotFail() { + + List objects = queryRepository.namedParameters("John)(cn=Doe)", "Bar"); + + assertThat(objects).isEmpty(); + } + + @Test + void shouldWorkWithIndexParameters() { + + List objects = queryRepository.indexedParameters("John Doe", "Bar"); + + assertThatReturnedObjectIsJohnDoe(objects); + } + + @Test + void shouldWorkWithSpelExpressions() { + + List objects = queryRepository.spelParameters(); + + assertThatReturnedObjectIsJohnDoe(objects); + } + + @Test + void shouldWorkWithPropertyPlaceholders() { + + List objects = queryRepository.propertyPlaceholderParameters(); + + assertThatReturnedObjectIsJohnDoe(objects); + } + + @Test + void shouldWorkWithNamedParametersForBase() { + + List objects = queryRepository.baseNamedParameters("John Doe", "dc=memorynotfound"); + + assertThatReturnedObjectIsJohnDoe(objects); + } + + @Test + void shouldWorkWithIndexParametersForBase() { + + List objects = queryRepository.baseIndexedParameters("John Doe", "memorynotfound"); + + assertThatReturnedObjectIsJohnDoe(objects); + } + + @Test + void shouldWorkWithSpelExpressionsForBase() { + + List objects = queryRepository.baseSpelParameters(); + + assertThatReturnedObjectIsJohnDoe(objects); + } + + @Test + void shouldWorkWithPropertyPlaceholdersForBase() { + + List objects = queryRepository.basePropertyPlaceholderParameters(); + + assertThatReturnedObjectIsJohnDoe(objects); + } + + private static void assertThatReturnedObjectIsJohnDoe(List objects) { + + assertThat(objects).hasSize(1); + assertThat(objects.get(0).fullName).isEqualTo("John Doe"); + assertThat(objects.get(0).lastName).isEqualTo("Doe"); + } + + @Configuration(proxyBeanMethods = false) + @Import(InMemoryLdapConfiguration.class) + @EnableLdapRepositories(considerNestedRepositories = true) + static class TestConfig { + + @Bean + EmbeddedLdapProperties embeddedLdapProperties() { + EmbeddedLdapProperties embeddedLdapProperties = new EmbeddedLdapProperties(); + embeddedLdapProperties.setBaseDn(Arrays.asList("dc=com", "dc=memorynotfound")); + return embeddedLdapProperties; + } + } + + interface QueryRepository extends LdapRepository { + + @Query(value = "(cn=:fullName)") + List namedParameters(@Param("fullName") String fullName, @Param("lastName") String lastName); + + @Query(value = "(cn=?0)") + List indexedParameters(String fullName, String lastName); + + @Query(value = "(cn=:#{'John ' + 'Doe'})") + List spelParameters(); + + @Query(value = "(cn=?${full.name})") + List propertyPlaceholderParameters(); + + @Query(base = ":dc", value = "(cn=:fullName)") + List baseNamedParameters(@Param("fullName") String fullName, @Param("dc") String dc); + + @Query(base = "dc=?1", value = "(cn=?0)") + List baseIndexedParameters(String fullName, String dc); + + @Query(base = "dc=:#{'memory' + 'notfound'}", value = "(cn=:#{'John ' + 'Doe'})") + List baseSpelParameters(); + + @Query(base = "dc=?${dc.name}", value = "(cn=?${full.name})") + List basePropertyPlaceholderParameters(); + } +} diff --git a/src/test/resources/schema.ldif b/src/test/resources/schema.ldif new file mode 100644 index 0000000..e98b78f --- /dev/null +++ b/src/test/resources/schema.ldif @@ -0,0 +1,71 @@ +dn: dc=com +objectclass: top +objectclass: domain +dc: com + +dn: dc=memorynotfound,dc=com +objectclass: top +objectclass: domain +objectclass: extensibleObject +dc: memorynotfound + +# Organizational Units +dn: ou=groups,dc=memorynotfound,dc=com +objectclass: top +objectclass: organizationalUnit +ou: groups + +dn: ou=people,dc=memorynotfound,dc=com +objectclass: top +objectclass: organizationalUnit +ou: people + +# Create People +dn: uid=john,ou=people,dc=memorynotfound,dc=com +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +cn: John Doe +sn: John +uid: john +fullName: John Doe +lastName: Doe + +dn: uid=jihn,ou=people,dc=memorynotfound,dc=com +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +cn: Jihn Die +sn: Jihn +uid: jihn +fullName: Jihn Die +lastName: Die + +dn: uid=jahn,ou=people,dc=memorynotfound,dc=com +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +cn: Jahn Dae +sn: Jahn +uid: jahn +fullName: Jahn Die +lastName: Dae + +# Create Groups +dn: cn=developers,ou=groups,dc=memorynotfound,dc=com +objectclass: top +objectclass: groupOfUniqueNames +cn: developers +ou: developer +uniqueMember: uid=john,ou=people,dc=memorynotfound,dc=com +uniqueMember: uid=jihn,ou=people,dc=memorynotfound,dc=com + +dn: cn=managers,ou=groups,dc=memorynotfound,dc=com +objectclass: top +objectclass: groupOfUniqueNames +cn: managers +ou: manager +uniqueMember: uid=jahn,ou=people,dc=memorynotfound,dc=com