/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
* or http://forgerock.org/license/CDDLv1.0.html.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at legal-notices/CDDLv1_0.txt.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
* Copyright 2013 ForgeRock AS.
*/
package org.forgerock.opendj.ldap;
import static org.forgerock.opendj.ldap.Attributes.singletonAttribute;
import static org.forgerock.opendj.ldap.Entries.modifyEntry;
import static org.forgerock.opendj.ldap.ErrorResultException.newErrorResult;
import static org.forgerock.opendj.ldap.responses.Responses.newBindResult;
import static org.forgerock.opendj.ldap.responses.Responses.newCompareResult;
import static org.forgerock.opendj.ldap.responses.Responses.newResult;
import static org.forgerock.opendj.ldap.responses.Responses.newSearchResultEntry;
import java.io.IOException;
import java.util.NavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
import org.forgerock.i18n.LocalizedIllegalArgumentException;
import org.forgerock.opendj.ldap.controls.AssertionRequestControl;
import org.forgerock.opendj.ldap.controls.PostReadRequestControl;
import org.forgerock.opendj.ldap.controls.PostReadResponseControl;
import org.forgerock.opendj.ldap.controls.PreReadRequestControl;
import org.forgerock.opendj.ldap.controls.PreReadResponseControl;
import org.forgerock.opendj.ldap.controls.SubtreeDeleteRequestControl;
import org.forgerock.opendj.ldap.requests.AddRequest;
import org.forgerock.opendj.ldap.requests.BindRequest;
import org.forgerock.opendj.ldap.requests.CompareRequest;
import org.forgerock.opendj.ldap.requests.DeleteRequest;
import org.forgerock.opendj.ldap.requests.ExtendedRequest;
import org.forgerock.opendj.ldap.requests.GenericBindRequest;
import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
import org.forgerock.opendj.ldap.requests.ModifyRequest;
import org.forgerock.opendj.ldap.requests.Request;
import org.forgerock.opendj.ldap.requests.SearchRequest;
import org.forgerock.opendj.ldap.requests.SimpleBindRequest;
import org.forgerock.opendj.ldap.responses.BindResult;
import org.forgerock.opendj.ldap.responses.CompareResult;
import org.forgerock.opendj.ldap.responses.ExtendedResult;
import org.forgerock.opendj.ldap.responses.Result;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.opendj.ldif.EntryReader;
/**
* A simple in memory back-end which can be used for testing. It is not intended
* for production use due to various limitations. The back-end implementations
* supports the following:
*
* - add, bind (simple), compare, delete, modify, and search operations, but
* not modifyDN nor extended operations
*
- assertion, pre-, and post- read controls, subtree delete control, and
* permissive modify control
*
- thread safety - supports concurrent operations
*
* It does not support the following:
*
* - high performance
*
- secure password storage
*
- schema checking
*
- persistence
*
- indexing
*
* This class can be used in conjunction with the factories defined in
* {@link Connections} to create simpler servers as well as mock LDAP
* connections. For example, to create a mock LDAP connection factory:
*
*
* MemoryBackend backend = new MemoryBackend();
* Connection connection = newInternalConnectionFactory(newServerConnectionFactory(backend), null)
* .getConnection();
*
*/
public final class MemoryBackend implements RequestHandler {
private final DecodeOptions decodeOptions;
private final ConcurrentSkipListMap entries = new ConcurrentSkipListMap();
private final Object writeLock = new Object();
private final Schema schema;
/**
* Creates a new empty memory backend which will use the default schema.
*/
public MemoryBackend() {
this(Schema.getDefaultSchema());
}
/**
* Creates a new memory backend which will use the default schema, and will
* contain the entries read from the provided entry reader.
*
* @param reader
* The entry reader.
* @throws IOException
* If an unexpected IO error occurred while reading the entries.
*/
public MemoryBackend(final EntryReader reader) throws IOException {
this(Schema.getDefaultSchema(), reader);
}
/**
* Creates a new empty memory backend which will use the provided schema.
*
* @param schema
* The schema to use for decoding filters, etc.
*/
public MemoryBackend(final Schema schema) {
this.schema = schema;
this.decodeOptions = new DecodeOptions().setSchema(schema);
}
/**
* Creates a new memory backend which will use the provided schema, and will
* contain the entries read from the provided entry reader.
*
* @param schema
* The schema to use for decoding filters, etc.
* @param reader
* The entry reader.
* @throws IOException
* If an unexpected IO error occurred while reading the entries.
*/
public MemoryBackend(final Schema schema, final EntryReader reader) throws IOException {
this.schema = schema;
this.decodeOptions = new DecodeOptions().setSchema(schema);
if (reader != null) {
try {
while (reader.hasNext()) {
final Entry entry = reader.readEntry();
final DN dn = entry.getName();
if (entries.containsKey(dn)) {
throw new ErrorResultIOException(newErrorResult(
ResultCode.ENTRY_ALREADY_EXISTS, "Attempted to add the entry '"
+ dn.toString() + "' multiple times"));
} else {
entries.put(dn, entry);
}
}
} finally {
reader.close();
}
}
}
@Override
public void handleAdd(final RequestContext requestContext, final AddRequest request,
final IntermediateResponseHandler intermediateResponseHandler,
final ResultHandler super Result> resultHandler) {
try {
synchronized (writeLock) {
final DN dn = request.getName();
final DN parent = dn.parent();
if (entries.containsKey(dn)) {
throw newErrorResult(ResultCode.ENTRY_ALREADY_EXISTS, "The entry '"
+ dn.toString() + "' already exists");
} else if (!entries.containsKey(parent)) {
noSuchObject(parent);
} else {
entries.put(dn, request);
}
}
resultHandler.handleResult(getResult(request, null, request));
} catch (final ErrorResultException e) {
resultHandler.handleErrorResult(e);
}
}
@Override
public void handleBind(final RequestContext requestContext, final int version,
final BindRequest request,
final IntermediateResponseHandler intermediateResponseHandler,
final ResultHandler super BindResult> resultHandler) {
try {
final Entry entry;
synchronized (writeLock) {
final DN username = DN.valueOf(request.getName(), schema);
final byte[] password;
if (request instanceof SimpleBindRequest) {
password = ((SimpleBindRequest) request).getPassword();
} else if (request instanceof GenericBindRequest
&& request.getAuthenticationType() == BindRequest.AUTHENTICATION_TYPE_SIMPLE) {
password = ((GenericBindRequest) request).getAuthenticationValue();
} else {
throw newErrorResult(ResultCode.PROTOCOL_ERROR,
"non-SIMPLE authentication not supported: "
+ request.getAuthenticationType());
}
entry = getRequiredEntry(null, username);
if (!entry.containsAttribute("userPassword", password)) {
throw newErrorResult(ResultCode.INVALID_CREDENTIALS, "Wrong password");
}
}
resultHandler.handleResult(getBindResult(request, entry, entry));
} catch (final LocalizedIllegalArgumentException e) {
resultHandler.handleErrorResult(newErrorResult(ResultCode.PROTOCOL_ERROR, e));
} catch (final EntryNotFoundException e) {
/*
* Usually you would not include a diagnostic message, but we'll add
* one here because the memory back-end is not intended for
* production use.
*/
resultHandler.handleErrorResult(newErrorResult(ResultCode.INVALID_CREDENTIALS,
"Unknown user"));
} catch (final ErrorResultException e) {
resultHandler.handleErrorResult(e);
}
}
@Override
public void handleCompare(final RequestContext requestContext, final CompareRequest request,
final IntermediateResponseHandler intermediateResponseHandler,
final ResultHandler super CompareResult> resultHandler) {
try {
final Entry entry;
final Attribute assertion;
synchronized (writeLock) {
final DN dn = request.getName();
entry = getRequiredEntry(request, dn);
assertion =
singletonAttribute(request.getAttributeDescription(), request
.getAssertionValue());
}
resultHandler.handleResult(getCompareResult(request, entry, entry.containsAttribute(
assertion, null)));
} catch (final ErrorResultException e) {
resultHandler.handleErrorResult(e);
}
}
@Override
public void handleDelete(final RequestContext requestContext, final DeleteRequest request,
final IntermediateResponseHandler intermediateResponseHandler,
final ResultHandler super Result> resultHandler) {
try {
final Entry entry;
synchronized (writeLock) {
final DN dn = request.getName();
entry = getRequiredEntry(request, dn);
if (request.getControl(SubtreeDeleteRequestControl.DECODER, decodeOptions) != null) {
// Subtree delete.
entries.subMap(dn, dn.child(RDN.maxValue())).clear();
} else {
// Must be leaf.
final DN next = entries.higherKey(dn);
if (next == null || !next.isChildOf(dn)) {
entries.remove(dn);
} else {
throw newErrorResult(ResultCode.NOT_ALLOWED_ON_NONLEAF);
}
}
}
resultHandler.handleResult(getResult(request, entry, null));
} catch (final DecodeException e) {
resultHandler.handleErrorResult(newErrorResult(ResultCode.PROTOCOL_ERROR, e));
} catch (final ErrorResultException e) {
resultHandler.handleErrorResult(e);
}
}
@Override
public void handleExtendedRequest(
final RequestContext requestContext, final ExtendedRequest request,
final IntermediateResponseHandler intermediateResponseHandler,
final ResultHandler super R> resultHandler) {
resultHandler.handleErrorResult(newErrorResult(ResultCode.UNWILLING_TO_PERFORM,
"Extended request operation not supported"));
}
@Override
public void handleModify(final RequestContext requestContext, final ModifyRequest request,
final IntermediateResponseHandler intermediateResponseHandler,
final ResultHandler super Result> resultHandler) {
try {
final Entry entry;
final Entry newEntry;
synchronized (writeLock) {
final DN dn = request.getName();
entry = getRequiredEntry(request, dn);
newEntry = new LinkedHashMapEntry(entry);
entries.put(dn, modifyEntry(newEntry, request));
}
resultHandler.handleResult(getResult(request, entry, newEntry));
} catch (final ErrorResultException e) {
resultHandler.handleErrorResult(e);
}
}
@Override
public void handleModifyDN(final RequestContext requestContext, final ModifyDNRequest request,
final IntermediateResponseHandler intermediateResponseHandler,
final ResultHandler super Result> resultHandler) {
resultHandler.handleErrorResult(newErrorResult(ResultCode.UNWILLING_TO_PERFORM,
"ModifyDN request operation not supported"));
}
@Override
public void handleSearch(final RequestContext requestContext, final SearchRequest request,
final IntermediateResponseHandler intermediateResponseHandler,
final SearchResultHandler resultHandler) {
try {
final DN dn = request.getName();
final Entry baseEntry = getRequiredEntry(request, dn);
final SearchScope scope = request.getScope();
final Filter filter = request.getFilter();
final Matcher matcher = filter.matcher(schema);
final AttributeFilter attributeFilter =
new AttributeFilter(request.getAttributes(), schema).typesOnly(request
.isTypesOnly());
if (scope.equals(SearchScope.BASE_OBJECT)) {
if (matcher.matches(baseEntry).toBoolean()) {
sendEntry(attributeFilter, resultHandler, baseEntry);
}
} else if (scope.equals(SearchScope.SINGLE_LEVEL)) {
final NavigableMap subtree =
entries.subMap(dn, dn.child(RDN.maxValue()));
for (final Entry entry : subtree.values()) {
// Check for cancellation.
requestContext.checkIfCancelled(false);
final DN childDN = entry.getName();
if (childDN.isChildOf(dn)) {
if (matcher.matches(entry).toBoolean()
&& !sendEntry(attributeFilter, resultHandler, entry)) {
// Caller has asked to stop sending results.
break;
}
}
}
} else if (scope.equals(SearchScope.WHOLE_SUBTREE)) {
final NavigableMap subtree =
entries.subMap(dn, dn.child(RDN.maxValue()));
for (final Entry entry : subtree.values()) {
// Check for cancellation.
requestContext.checkIfCancelled(false);
if (matcher.matches(entry).toBoolean()
&& !sendEntry(attributeFilter, resultHandler, entry)) {
// Caller has asked to stop sending results.
break;
}
}
} else {
throw newErrorResult(ResultCode.PROTOCOL_ERROR,
"Search request contains an unsupported search scope");
}
resultHandler.handleResult(newResult(ResultCode.SUCCESS));
} catch (final ErrorResultException e) {
resultHandler.handleErrorResult(e);
}
}
private R addResultControls(final Request request, final Entry before,
final Entry after, final R result) throws ErrorResultException {
try {
// Add pre-read response control if requested.
final PreReadRequestControl preRead =
request.getControl(PreReadRequestControl.DECODER, decodeOptions);
if (preRead != null) {
if (preRead.isCritical() && before == null) {
throw newErrorResult(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION);
} else {
final AttributeFilter filter =
new AttributeFilter(preRead.getAttributes(), schema);
result.addControl(PreReadResponseControl.newControl(filter
.filteredViewOf(before)));
}
}
// Add post-read response control if requested.
final PostReadRequestControl postRead =
request.getControl(PostReadRequestControl.DECODER, decodeOptions);
if (postRead != null) {
if (postRead.isCritical() && after == null) {
throw newErrorResult(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION);
} else {
final AttributeFilter filter =
new AttributeFilter(postRead.getAttributes(), schema);
result.addControl(PostReadResponseControl.newControl(filter
.filteredViewOf(after)));
}
}
return result;
} catch (final DecodeException e) {
throw newErrorResult(ResultCode.PROTOCOL_ERROR, e);
}
}
private BindResult getBindResult(final BindRequest request, final Entry before,
final Entry after) throws ErrorResultException {
return addResultControls(request, before, after, newBindResult(ResultCode.SUCCESS));
}
private CompareResult getCompareResult(final CompareRequest request, final Entry entry,
final boolean compareResult) throws ErrorResultException {
return addResultControls(
request,
entry,
entry,
newCompareResult(compareResult ? ResultCode.COMPARE_TRUE : ResultCode.COMPARE_FALSE));
}
private Entry getRequiredEntry(final Request request, final DN dn) throws ErrorResultException {
final Entry entry = entries.get(dn);
if (entry == null) {
noSuchObject(dn);
} else if (request != null) {
AssertionRequestControl control;
try {
control = request.getControl(AssertionRequestControl.DECODER, decodeOptions);
} catch (final DecodeException e) {
throw newErrorResult(ResultCode.PROTOCOL_ERROR, e);
}
if (control != null) {
final Filter filter = control.getFilter();
final Matcher matcher = filter.matcher(schema);
if (!matcher.matches(entry).toBoolean()) {
throw newErrorResult(ResultCode.ASSERTION_FAILED, "The filter '"
+ filter.toString() + "' did not match the entry '"
+ entry.getName().toString() + "'");
}
}
}
return entry;
}
private Result getResult(final Request request, final Entry before, final Entry after)
throws ErrorResultException {
return addResultControls(request, before, after, newResult(ResultCode.SUCCESS));
}
private void noSuchObject(final DN dn) throws ErrorResultException {
throw newErrorResult(ResultCode.NO_SUCH_OBJECT, "The entry '" + dn.toString()
+ "' does not exist");
}
private boolean sendEntry(final AttributeFilter filter,
final SearchResultHandler resultHandler, final Entry entry) {
return resultHandler.handleEntry(newSearchResultEntry(filter.filteredViewOf(entry)));
}
}