001/*
002 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003 *
004 * Copyright (c) 2005 Sun Microsystems Inc. All Rights Reserved
005 *
006 * The contents of this file are subject to the terms
007 * of the Common Development and Distribution License
008 * (the License). You may not use this file except in
009 * compliance with the License.
010 *
011 * You can obtain a copy of the License at
012 * https://opensso.dev.java.net/public/CDDLv1.0.html or
013 * opensso/legal/CDDLv1.0.txt
014 * See the License for the specific language governing
015 * permission and limitations under the License.
016 *
017 * When distributing Covered Code, include this CDDL
018 * Header Notice in each file and include the License file
019 * at opensso/legal/CDDLv1.0.txt.
020 * If applicable, add the following below the CDDL Header,
021 * with the fields enclosed by brackets [] replaced by
022 * your own identifying information:
023 * "Portions Copyrighted [year] [name of copyright owner]"
024 *
025 * $Id: SearchResults.java,v 1.7 2009/01/28 05:34:51 ww203982 Exp $
026 *
027 * Portions Copyrighted 2011-2015 ForgeRock AS.
028 */
029package com.iplanet.ums;
030
031import com.iplanet.services.ldap.Attr;
032import com.iplanet.services.ldap.AttrSet;
033import com.iplanet.services.util.I18n;
034import com.sun.identity.shared.debug.Debug;
035import java.security.Principal;
036import java.util.ArrayList;
037import java.util.Collection;
038import java.util.Hashtable;
039import java.util.List;
040import java.util.NoSuchElementException;
041import org.forgerock.openam.utils.IOUtils;
042import org.forgerock.opendj.ldap.Attribute;
043import org.forgerock.opendj.ldap.Connection;
044import org.forgerock.opendj.ldap.LdapException;
045import org.forgerock.opendj.ldap.SearchResultReferenceIOException;
046import org.forgerock.opendj.ldap.controls.Control;
047import org.forgerock.opendj.ldap.controls.VirtualListViewResponseControl;
048import org.forgerock.opendj.ldap.responses.SearchResultEntry;
049import org.forgerock.opendj.ldif.ConnectionEntryReader;
050
051/**
052 * Represents search results. Each search result is a PersistentObject
053 * 
054 * @supported.api
055 */
056public class SearchResults implements java.io.Serializable {
057
058    private static Debug debug;
059    static {
060        debug = Debug.getInstance(IUMSConstants.UMS_DEBUG);
061    }
062
063    private static I18n i18n = I18n.getInstance(IUMSConstants.UMS_PKG);
064
065    /**
066     * Attribute name used with Object get( String ). Expected return object is
067     * Integer getting the content count from the VirtualListResponse control
068     * returned by server after a search
069     * 
070     * @supported.api
071     */
072    public static final String VLVRESPONSE_CONTENT_COUNT = "vlvContentCount";
073
074    /**
075     * Attribute name used with Object get( String ). Expected return object is
076     * Integer getting the index of first position from VirtualListResponse
077     * control returned by server after a search
078     * 
079     * @supported.api
080     */
081    public static final String VLVRESPONSE_FIRST_POSITION = "vlvFirstPosition";
082
083    /**
084     * Attribute name used with Object get( String ). Expected return object is
085     * Integer getting the result code from from VirtualListResponse control
086     * returned by server after a search.
087     * 
088     * @supported.api
089     */
090    public static final String VLVRESPONSE_RESULT_CODE = "vlvResultCode";
091
092    /**
093     * Attribute name used with Object get( String ). Expected return object is
094     * String getting the context cookie from VirtualListResponse control
095     * returned by server after a search.
096     * 
097     * @supported.api
098     */
099    public static final String VLVRESPONSE_CONTEXT = "vlvContext";
100
101    static final String EXPECT_VLV_RESPONSE = "expectVlvResponse";
102
103    static final String BASE_ID = "baseID";
104
105    static final String SEARCH_FILTER = "searchFilter";
106
107    static final String SORT_KEYS = "sortKeys";
108
109    static final String SEARCH_SCOPE = "searchScope";
110
111    /**
112     * Constructs SearchResults from <code>ldapSearchResult</code>.
113     * 
114     * @param ldapSearchResult <code>LDAPSearchResults</code> to construct from
115     * @param conn <code>LDAPConnection</code> assosciated with the search
116     *        results.
117     * @param dataLayer Data Layer assosciated with the connection.
118     */
119    protected SearchResults(Connection connection, ConnectionEntryReader ldapSearchResult, Connection conn,
120            DataLayer dataLayer) {
121        // TODO: SearchResults is tightly coupled with DataLayer and
122        // PersistentObject. That could make it harder to separate them
123        // in the future.
124        //
125        this.connection = connection;
126        m_ldapSearchResults = ldapSearchResult;
127        m_conn = conn;
128        m_dataLayer = dataLayer;
129        if (debug.messageEnabled()) {
130            debug.message("Constructing SearchResults: " + this
131                    + " with connection : " + conn);
132        }
133    }
134
135    /**
136     * Constructs Search Results from <code>ldapSearchResult</code>.
137     * 
138     * @param ldapSearchResult <code>LDAPSearchResults</code> to construct
139     *        from.
140     * @param conn <code>LDAPConnection</code> associated with the search
141     *        results.
142     */
143    protected SearchResults(Connection connection, ConnectionEntryReader ldapSearchResult, Connection conn) {
144        this(connection, ldapSearchResult, conn, null);
145    }
146
147    /**
148     * Secret constructor for iterating through the values of an entry.
149     * 
150     * @param attr
151     *            An attribute containing 0 or more DN values, to be treated as
152     *            individual results
153     */
154    SearchResults(Attr attr) {
155        if (attr == null) {
156            m_attrVals = new String[0];
157        } else {
158            m_attrVals = attr.getStringValues();
159        }
160    }
161
162    /**
163     * Checks whether there are entries available.
164     * 
165     * @return <code>true</code> if there is more to read
166     * @supported.api
167     */
168    public boolean hasMoreElements() {
169        boolean hasGotMoreElements = false;
170        try {
171            hasGotMoreElements = (m_attrVals != null) ?
172                    (m_attrIndex < m_attrVals.length)
173                    : m_ldapSearchResults.hasNext();
174            if (hasGotMoreElements) {
175                readEntry();
176            }
177            if (debug.messageEnabled()) {
178            if (!hasGotMoreElements && m_conn != null) {
179                    debug.message("Finishing SearchResults: " + this
180                            + "  with connection : " + m_conn);
181                    debug.message("SearchResults: " + this
182                            + "  releasing connection : " + m_conn);
183                }
184            }
185        } catch (LdapException | SearchResultReferenceIOException ignored) {
186        }
187        return hasGotMoreElements;
188    }
189
190    private void readEntry() throws SearchResultReferenceIOException, LdapException {
191        if (m_ldapSearchResults != null) {
192            if (m_ldapSearchResults.isReference()) {
193                //Ignoring references
194                m_ldapSearchResults.readReference();
195            }
196            currentEntry = m_ldapSearchResults.readEntry();
197        }
198    }
199
200    /**
201     * Returns the next entry in the search results.
202     * 
203     * @throws UMSException
204     *             No more entries in the search results.
205     * @supported.api
206     */
207    public PersistentObject next() throws UMSException {
208        // TODO: define detailed exception list (eg. referral, ...)
209        //
210
211        SearchResultEntry ldapEntry;
212
213        if (m_attrVals != null) {
214            if (m_attrIndex < m_attrVals.length) {
215                String dn = m_attrVals[m_attrIndex++];
216                PersistentObject pO = new PersistentObject();
217                pO.setGuid(new Guid(dn));
218                pO.setPrincipal(m_principal);
219                return pO;
220            } else {
221                throw new NoSuchElementException();
222            }
223        }
224
225        if ((ldapEntry = currentEntry) != null) {
226            String id = ldapEntry.getName().toString();
227            Collection<Attribute> attributes = new ArrayList<>();
228            for (Attribute attribute : ldapEntry.getAllAttributes()) {
229                attributes.add(attribute);
230            }
231            AttrSet attrSet = new AttrSet(attributes);
232            Class javaClass = TemplateManager.getTemplateManager()
233                    .getJavaClassForEntry(id, attrSet);
234            PersistentObject pO = null;
235            try {
236                pO = (PersistentObject) javaClass.newInstance();
237            } catch (Exception e) {
238                String args[] = new String[1];
239
240                args[0] = e.toString();
241                String msg = i18n.getString(
242                        IUMSConstants.NEW_INSTANCE_FAILED, args);
243                throw new UMSException(msg);
244            }
245            // Make it a live object
246            pO.setAttrSet(attrSet);
247            pO.setGuid(new Guid(ldapEntry.getName().toString()));
248            pO.setPrincipal(m_principal);
249            return pO;
250        }
251        return null;
252    }
253
254    /**
255     * Assert if the search result contains one and only one entry.
256     * 
257     * @return Entry if and only if there is one single entry
258     * @throws EntryNotFoundException
259     *             if there is no entry at all
260     * 
261     * @supported.api
262     */
263    public PersistentObject assertOneEntry() throws EntryNotFoundException,
264            UMSException {
265        PersistentObject entry = null;
266        while (hasMoreElements()) {
267            entry = next();
268            break;
269        }
270
271        if (entry == null) {
272            throw new EntryNotFoundException();
273        }
274
275        if (hasMoreElements()) {
276            abandon();
277            // TODO: to be replaced by new exception
278            //
279            throw new UMSException(
280                        i18n.getString(IUMSConstants.MULTIPLE_ENTRY));
281        }
282
283        return entry;
284    }
285
286    /**
287     * Get search result attributes related to the search operation performed.
288     * 
289     * @param name
290     *            Name of attribute to return, null if attribute is unknown or
291     *            not found
292     * @throws UMSException
293     *             from accessor methods on LDAPVirtualListResponse control
294     * 
295     * @supported.api
296     */
297    public Object get(String name) throws UMSException {
298
299        // For non vlv related attributes, get it
300        // from the generic hash table
301        //
302        if (!isVLVAttrs(name)) {
303            return m_attrHash == null ? null : m_attrHash.get(name);
304        }
305
306        // The rest is related to vlv response control
307        //
308        if (currentEntry == null)
309            return null;
310
311        List<Control> ctrls = currentEntry.getControls();
312
313        if (ctrls == null && expectVlvResponse() == true) {
314
315            //
316            // Code to deal with response controls not being returned yet. It
317            // instructs a small search with vlv ranage that expect one result
318            // to
319            // return so that the response is returned. This probe is only
320            // launched if EXPECT_VLV_RESPONSE is set for true in SearchResults
321            //
322
323            PersistentObject parent = getParentContainer();
324
325            synchronized (this) {
326                // The following code fragment uses a test control that only
327                // asks
328                // one result to return. This is done so that the response
329                // control
330                // can be queried for the vlvContentCount. This is a search
331                // probe to
332                // get the vlvCount
333                //
334                String[] sortAttrNames = { "objectclass" };
335                SortKey[] sortKeys = (SortKey[]) get(SearchResults.SORT_KEYS);
336                String filter = (String) get(SearchResults.SEARCH_FILTER);
337                Integer scopeVal = (Integer) get(SearchResults.SEARCH_SCOPE);
338                int scope = scopeVal == null ? SearchControl.SCOPE_SUB
339                        : scopeVal.intValue();
340
341                SearchControl testControl = new SearchControl();
342                testControl.setVLVRange(1, 0, 0);
343
344                if (sortKeys == null) {
345                    testControl.setSortKeys(sortAttrNames);
346                } else {
347                    testControl.setSortKeys(sortKeys);
348                }
349
350                testControl.setSearchScope(scope);
351
352                SearchResults testResults = parent.search(filter,
353                        sortAttrNames, testControl);
354                while (testResults.hasMoreElements()) {
355                    // This while loop is required to
356                    // enumerate the result set to get the response control
357                    testResults.next();
358                }
359
360                // After all the hazzle, now the response should be in after the
361                // search probe, use the probe's search results to get the vlv
362                // related attribute(s). Set the internal flag not to launch
363                // the probe again in subsequent get.
364                //
365                testResults.set(SearchResults.EXPECT_VLV_RESPONSE, Boolean.FALSE);
366                return testResults.get(name);
367            }
368        }
369
370        // the control can be null
371        if (ctrls == null)
372            return null;
373
374        VirtualListViewResponseControl vlvResponse = null;
375
376        // Find the VLV response control recorded in SearchResults
377        //
378        for (Control control : ctrls) {
379            if (VirtualListViewResponseControl.OID.equals(control.getOID())) {
380                vlvResponse = (VirtualListViewResponseControl) control;
381            }
382        }
383
384        // Check on the attribute to return and
385        // return the value from the response control
386        // Currently only expose the VirtualListResponse control
387        // returned after a search operation
388        //
389        if (vlvResponse != null) {
390            if (name.equalsIgnoreCase(VLVRESPONSE_CONTENT_COUNT)) {
391                return vlvResponse.getContentCount();
392            } else if (name.equalsIgnoreCase(VLVRESPONSE_FIRST_POSITION)) {
393                return vlvResponse.getTargetPosition();
394            } else if (name.equalsIgnoreCase(VLVRESPONSE_RESULT_CODE)) {
395                return vlvResponse.getResult().intValue();
396            } else if (name.equalsIgnoreCase(VLVRESPONSE_CONTEXT)) {
397                return vlvResponse.getValue().toString();
398            }
399        }
400
401        // For all other unknown attribute names,
402        // just return a null object
403        //
404        return null;
405
406    }
407
408    /**
409     * Abandons a current search operation, notifying the server not to send
410     * additional search results.
411     * 
412     * @throws UMSException
413     *             from abandoning a search operation from LDAP
414     * @supported.api
415     */
416    public void abandon() throws UMSException {
417        //Nothing to do
418        IOUtils.closeIfNotNull(connection, m_ldapSearchResults);
419    }
420
421    /**
422     * Sets the principal with which to associate search results.
423     * 
424     * @param principal Authenticated principal.
425     */
426    protected void setPrincipal(Principal principal) {
427        m_principal = principal;
428    }
429
430    /**
431     * Set attribute internal to search result. This set function is explicitly
432     * coded for package scope.
433     * 
434     * @param name
435     *            Name of attribute to set.
436     * @param value
437     *            Value of attribute to set
438     */
439    void set(String name, Object value) {
440
441        if (m_attrHash == null) {
442            synchronized (this) {
443                if (m_attrHash == null) {
444                    m_attrHash = new Hashtable();
445                }
446            }
447        }
448
449        m_attrHash.put(name, value);
450    }
451
452    /**
453     * Check if this search result expects a VLV response control
454     * 
455     * @return <code>true</code> if search result expects a VLV response
456     *         control and <code>false</code> otherwise
457     */
458    private boolean expectVlvResponse() {
459        Boolean expected = Boolean.FALSE;
460
461        try {
462            expected = (Boolean) get(EXPECT_VLV_RESPONSE);
463        } catch (Exception e) {
464        }
465
466        return expected == null ? false : expected.booleanValue();
467    }
468
469    /**
470     * Gets the original container that the search result is originated from
471     * 
472     * @return PersistentObject The parent container object.
473     * @throws UMSException
474     *             If an exception occurs.
475     */
476    private PersistentObject getParentContainer() throws UMSException {
477        String parentID = null;
478        PersistentObject parent = null;
479
480        try {
481            parentID = (String) get(BASE_ID);
482            Guid parentGuid = new Guid(parentID);
483            parent = new PersistentObject(m_principal, parentGuid);
484        } catch (UMSException e) {
485            throw new UMSException(e.getMessage());
486        }
487
488        return parent;
489    }
490
491    /**
492     * Check if attribute name is related to vlv response attributes
493     * 
494     */
495    private boolean isVLVAttrs(String name) {
496
497        for (int i = 0; i < vlvAttrNames.length; i++) {
498            if (name.equalsIgnoreCase(vlvAttrNames[i])) {
499                return true;
500            }
501        }
502        return false;
503    }
504
505    /*
506     * Iterator iterator() { return new Iterator() { public boolean hasNext() {
507     * return SearchResults.this.hasMoreElements(); }
508     * 
509     * public Object next() { PersistentObject po = null; try { po =
510     * SearchResults.this.next(); } catch ( Exception ignored) { } return po; }
511     * 
512     * public void remove() { throw new UnsupportedOperationException(); } }; }
513     */
514
515    private SearchResultEntry currentEntry = null;
516
517    private Connection connection;
518
519    private ConnectionEntryReader m_ldapSearchResults = null;
520
521    private Connection m_conn = null;
522
523    private Principal m_principal = null;
524
525    private Hashtable m_attrHash = null;
526
527    private static String[] vlvAttrNames = { VLVRESPONSE_CONTENT_COUNT,
528            VLVRESPONSE_FIRST_POSITION, VLVRESPONSE_RESULT_CODE,
529            VLVRESPONSE_CONTEXT };
530
531    //
532    // These are only used for the tricky constructor with SearchResults(Attr)
533    //
534    private String[] m_attrVals = null;
535
536    private int m_attrIndex = 0;
537
538    private DataLayer m_dataLayer = null;
539
540}