001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2012-2015 ForgeRock AS.
015 */
016package org.forgerock.opendj.rest2ldap;
017
018import java.util.ArrayList;
019import java.util.LinkedHashSet;
020import java.util.LinkedList;
021import java.util.List;
022import java.util.Set;
023import java.util.concurrent.atomic.AtomicInteger;
024import java.util.concurrent.atomic.AtomicReference;
025
026import org.forgerock.json.fluent.JsonPointer;
027import org.forgerock.json.fluent.JsonValue;
028import org.forgerock.json.resource.BadRequestException;
029import org.forgerock.json.resource.ResourceException;
030import org.forgerock.json.resource.ResultHandler;
031import org.forgerock.opendj.ldap.Attribute;
032import org.forgerock.opendj.ldap.AttributeDescription;
033import org.forgerock.opendj.ldap.ByteString;
034import org.forgerock.opendj.ldap.DN;
035import org.forgerock.opendj.ldap.Entry;
036import org.forgerock.opendj.ldap.EntryNotFoundException;
037import org.forgerock.opendj.ldap.LdapException;
038import org.forgerock.opendj.ldap.Filter;
039import org.forgerock.opendj.ldap.LinkedAttribute;
040import org.forgerock.opendj.ldap.MultipleEntriesFoundException;
041import org.forgerock.opendj.ldap.ResultCode;
042import org.forgerock.opendj.ldap.SearchResultHandler;
043import org.forgerock.opendj.ldap.SearchScope;
044import org.forgerock.opendj.ldap.requests.SearchRequest;
045import org.forgerock.opendj.ldap.responses.Result;
046import org.forgerock.opendj.ldap.responses.SearchResultEntry;
047import org.forgerock.opendj.ldap.responses.SearchResultReference;
048import org.forgerock.util.Function;
049import org.forgerock.util.promise.NeverThrowsException;
050import org.forgerock.util.promise.ExceptionHandler;
051
052import static org.forgerock.opendj.ldap.LdapException.*;
053import static org.forgerock.opendj.ldap.requests.Requests.*;
054import static org.forgerock.opendj.rest2ldap.Rest2LDAP.*;
055import static org.forgerock.opendj.rest2ldap.Utils.*;
056
057/**
058 * An attribute mapper which provides a mapping from a JSON value to a single DN
059 * valued LDAP attribute.
060 */
061public final class ReferenceAttributeMapper extends AbstractLDAPAttributeMapper<ReferenceAttributeMapper> {
062    /**
063     * The maximum number of candidate references to allow in search filters.
064     */
065    private static final int SEARCH_MAX_CANDIDATES = 1000;
066
067    private final DN baseDN;
068    private Filter filter;
069    private final AttributeMapper mapper;
070    private final AttributeDescription primaryKey;
071    private SearchScope scope = SearchScope.WHOLE_SUBTREE;
072
073    ReferenceAttributeMapper(final AttributeDescription ldapAttributeName, final DN baseDN,
074        final AttributeDescription primaryKey, final AttributeMapper mapper) {
075        super(ldapAttributeName);
076        this.baseDN = baseDN;
077        this.primaryKey = primaryKey;
078        this.mapper = mapper;
079    }
080
081    /**
082     * Sets the filter which should be used when searching for referenced LDAP
083     * entries. The default is {@code (objectClass=*)}.
084     *
085     * @param filter
086     *            The filter which should be used when searching for referenced
087     *            LDAP entries.
088     * @return This attribute mapper.
089     */
090    public ReferenceAttributeMapper searchFilter(final Filter filter) {
091        this.filter = ensureNotNull(filter);
092        return this;
093    }
094
095    /**
096     * Sets the filter which should be used when searching for referenced LDAP
097     * entries. The default is {@code (objectClass=*)}.
098     *
099     * @param filter
100     *            The filter which should be used when searching for referenced
101     *            LDAP entries.
102     * @return This attribute mapper.
103     */
104    public ReferenceAttributeMapper searchFilter(final String filter) {
105        return searchFilter(Filter.valueOf(filter));
106    }
107
108    /**
109     * Sets the search scope which should be used when searching for referenced
110     * LDAP entries. The default is {@link SearchScope#WHOLE_SUBTREE}.
111     *
112     * @param scope
113     *            The search scope which should be used when searching for
114     *            referenced LDAP entries.
115     * @return This attribute mapper.
116     */
117    public ReferenceAttributeMapper searchScope(final SearchScope scope) {
118        this.scope = ensureNotNull(scope);
119        return this;
120    }
121
122    @Override
123    public String toString() {
124        return "reference(" + ldapAttributeName + ")";
125    }
126
127    @Override
128    void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath, final FilterType type,
129        final String operator, final Object valueAssertion, final ResultHandler<Filter> h) {
130        // Construct a filter which can be used to find referenced resources.
131        mapper.getLDAPFilter(c, path, subPath, type, operator, valueAssertion, new ResultHandler<Filter>() {
132            @Override
133            public void handleError(final ResourceException error) {
134                h.handleError(error); // Propagate.
135            }
136
137            @Override
138            public void handleResult(final Filter result) {
139                // Search for all referenced entries and construct a filter.
140                final SearchRequest request = createSearchRequest(result);
141                final List<Filter> subFilters = new LinkedList<>();
142
143                final ExceptionHandler<LdapException> exceptionHandler = new ExceptionHandler<LdapException>() {
144                    @Override
145                    public void handleException(LdapException exception) {
146                        h.handleError(asResourceException(exception)); // Propagate.
147                    }
148                };
149
150                c.getConnection().searchAsync(request, new SearchResultHandler() {
151                    @Override
152                    public boolean handleEntry(final SearchResultEntry entry) {
153                        if (subFilters.size() < SEARCH_MAX_CANDIDATES) {
154                            subFilters.add(Filter.equality(ldapAttributeName.toString(), entry.getName()));
155                            return true;
156                        } else {
157                            // No point in continuing - maximum candidates reached.
158                            return false;
159                        }
160                    }
161
162                    @Override
163                    public boolean handleReference(final SearchResultReference reference) {
164                        // Ignore references.
165                        return true;
166                    }
167                }).thenOnResult(new org.forgerock.util.promise.ResultHandler<Result>() {
168                    @Override
169                    public void handleResult(Result result) {
170                        if (subFilters.size() >= SEARCH_MAX_CANDIDATES) {
171                            exceptionHandler.handleException(newLdapException(ResultCode.ADMIN_LIMIT_EXCEEDED));
172                        } else if (subFilters.size() == 1) {
173                            h.handleResult(subFilters.get(0));
174                        } else {
175                            h.handleResult(Filter.or(subFilters));
176                        }
177                    }
178                }).thenOnException(exceptionHandler);
179            }
180        });
181    }
182
183    @Override
184    void getNewLDAPAttributes(final Context c, final JsonPointer path, final List<Object> newValues,
185        final ResultHandler<Attribute> h) {
186        /*
187         * For each value use the subordinate mapper to obtain the LDAP primary
188         * key, the perform a search for each one to find the corresponding entries.
189         */
190        final Attribute newLDAPAttribute = new LinkedAttribute(ldapAttributeName);
191        final AtomicInteger pendingSearches = new AtomicInteger(newValues.size());
192        final AtomicReference<ResourceException> exception = new AtomicReference<>();
193
194        for (final Object value : newValues) {
195            mapper.create(c, path, new JsonValue(value), new ResultHandler<List<Attribute>>() {
196
197                @Override
198                public void handleError(final ResourceException error) {
199                    h.handleError(error);
200                }
201
202                @Override
203                public void handleResult(final List<Attribute> result) {
204                    Attribute primaryKeyAttribute = null;
205                    for (final Attribute attribute : result) {
206                        if (attribute.getAttributeDescription().equals(primaryKey)) {
207                            primaryKeyAttribute = attribute;
208                            break;
209                        }
210                    }
211
212                    if (primaryKeyAttribute == null || primaryKeyAttribute.isEmpty()) {
213                        h.handleError(new BadRequestException(i18n(
214                            "The request cannot be processed because the reference "
215                                + "field '%s' contains a value which does not contain " + "a primary key", path)));
216                        return;
217                    }
218
219                    if (primaryKeyAttribute.size() > 1) {
220                        h.handleError(new BadRequestException(i18n(
221                            "The request cannot be processed because the reference "
222                                + "field '%s' contains a value which contains multiple " + "primary keys", path)));
223                        return;
224                    }
225
226                    // Now search for the referenced entry in to get its DN.
227                    final ByteString primaryKeyValue = primaryKeyAttribute.firstValue();
228                    final Filter filter = Filter.equality(primaryKey.toString(), primaryKeyValue);
229                    final SearchRequest search = createSearchRequest(filter);
230                    c.getConnection().searchSingleEntryAsync(search).thenOnResult(
231                            new org.forgerock.util.promise.ResultHandler<SearchResultEntry>() {
232                            @Override
233                            public void handleResult(final SearchResultEntry result) {
234                                synchronized (newLDAPAttribute) {
235                                    newLDAPAttribute.add(result.getName());
236                                }
237                                completeIfNecessary();
238                            }
239                        }).thenOnException(new ExceptionHandler<LdapException>() {
240                            @Override
241                            public void handleException(final LdapException error) {
242                                ResourceException re;
243                                try {
244                                    throw error;
245                                } catch (final EntryNotFoundException e) {
246                                    re = new BadRequestException(i18n(
247                                            "The request cannot be processed " + "because the resource '%s' "
248                                                    + "referenced in field '%s' does " + "not exist",
249                                            primaryKeyValue.toString(), path));
250                                } catch (final MultipleEntriesFoundException e) {
251                                    re = new BadRequestException(i18n(
252                                            "The request cannot be processed " + "because the resource '%s' "
253                                                    + "referenced in field '%s' is " + "ambiguous",
254                                            primaryKeyValue.toString(), path));
255                                } catch (final LdapException e) {
256                                    re = asResourceException(e);
257                                }
258                                exception.compareAndSet(null, re);
259                                completeIfNecessary();
260                            }
261                        });
262                }
263
264                private void completeIfNecessary() {
265                    if (pendingSearches.decrementAndGet() == 0) {
266                        if (exception.get() != null) {
267                            h.handleError(exception.get());
268                        } else {
269                            h.handleResult(newLDAPAttribute);
270                        }
271                    }
272                }
273            });
274        }
275    }
276
277    @Override
278    ReferenceAttributeMapper getThis() {
279        return this;
280    }
281
282    @Override
283    void read(final Context c, final JsonPointer path, final Entry e, final ResultHandler<JsonValue> h) {
284        final Attribute attribute = e.getAttribute(ldapAttributeName);
285        if (attribute == null || attribute.isEmpty()) {
286            h.handleResult(null);
287        } else if (attributeIsSingleValued()) {
288            try {
289                final DN dn = attribute.parse().usingSchema(c.getConfig().schema()).asDN();
290                readEntry(c, path, dn, h);
291            } catch (final Exception ex) {
292                // The LDAP attribute could not be decoded.
293                h.handleError(asResourceException(ex));
294            }
295        } else {
296            try {
297                final Set<DN> dns = attribute.parse().usingSchema(c.getConfig().schema()).asSetOfDN();
298                final ResultHandler<JsonValue> handler =
299                    accumulate(dns.size(), transform(new Function<List<JsonValue>, JsonValue, NeverThrowsException>() {
300                        @Override
301                        public JsonValue apply(final List<JsonValue> value) {
302                            if (value.isEmpty()) {
303                                /*
304                                 * No values, so omit the entire JSON object
305                                 * from the resource.
306                                 */
307                                return null;
308                            } else {
309                                // Combine values into a single JSON array.
310                                final List<Object> result = new ArrayList<>(value.size());
311                                for (final JsonValue e : value) {
312                                    result.add(e.getObject());
313                                }
314                                return new JsonValue(result);
315                            }
316                        }
317                    }, h));
318                for (final DN dn : dns) {
319                    readEntry(c, path, dn, handler);
320                }
321            } catch (final Exception ex) {
322                // The LDAP attribute could not be decoded.
323                h.handleError(asResourceException(ex));
324            }
325        }
326    }
327
328    private SearchRequest createSearchRequest(final Filter result) {
329        final Filter searchFilter = filter != null ? Filter.and(filter, result) : result;
330        return newSearchRequest(baseDN, scope, searchFilter, "1.1");
331    }
332
333    private void readEntry(final Context c, final JsonPointer path, final DN dn,
334        final ResultHandler<JsonValue> handler) {
335        final Set<String> requestedLDAPAttributes = new LinkedHashSet<>();
336        mapper.getLDAPAttributes(c, path, new JsonPointer(), requestedLDAPAttributes);
337        c.getConnection().readEntryAsync(dn, requestedLDAPAttributes)
338                .thenOnResult(new org.forgerock.util.promise.ResultHandler<SearchResultEntry>() {
339                    @Override
340                    public void handleResult(final SearchResultEntry result) {
341                        mapper.read(c, path, result, handler);
342                    }
343                }).thenOnException(new ExceptionHandler<LdapException>() {
344                    @Override
345                    public void handleException(final LdapException error) {
346                        if (!(error instanceof EntryNotFoundException)) {
347                            handler.handleError(asResourceException(error));
348                        } else {
349                            /*
350                             * The referenced entry does not exist so ignore it
351                             * since it cannot be mapped.
352                             */
353                            handler.handleResult(null);
354                        }
355                    }
356                });
357    }
358
359}