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: DataLayer.java,v 1.19 2009/11/20 23:52:52 ww203982 Exp $
026 *
027 * Portions Copyrighted 2011-2016 ForgeRock AS.
028 */
029
030package com.iplanet.ums;
031
032import static org.forgerock.opendj.ldap.LDAPConnectionFactory.AUTHN_BIND_REQUEST;
033
034import com.iplanet.am.util.SystemProperties;
035import com.iplanet.services.ldap.Attr;
036import com.iplanet.services.ldap.AttrSet;
037import com.iplanet.services.ldap.DSConfigMgr;
038import com.iplanet.services.ldap.LDAPServiceException;
039import com.iplanet.services.ldap.LDAPUser;
040import com.iplanet.services.ldap.ServerInstance;
041import com.iplanet.services.ldap.event.EventService;
042import com.iplanet.services.util.I18n;
043import com.sun.identity.common.configuration.ConfigurationListener;
044import com.sun.identity.common.configuration.ConfigurationObserver;
045import com.sun.identity.security.ServerInstanceAction;
046import com.sun.identity.shared.Constants;
047import com.sun.identity.shared.debug.Debug;
048
049import java.io.IOException;
050import java.security.AccessController;
051import java.security.Principal;
052import java.util.ArrayList;
053import java.util.Collection;
054import java.util.Collections;
055import java.util.HashSet;
056import java.util.List;
057import java.util.Set;
058import java.util.StringTokenizer;
059import java.util.concurrent.TimeUnit;
060
061import org.forgerock.openam.ldap.LDAPRequests;
062import org.forgerock.opendj.ldap.Attribute;
063import org.forgerock.opendj.ldap.Attributes;
064import org.forgerock.opendj.ldap.ByteString;
065import org.forgerock.opendj.ldap.Connection;
066import org.forgerock.opendj.ldap.ConnectionFactory;
067import org.forgerock.opendj.ldap.ConnectionPool;
068import org.forgerock.opendj.ldap.Connections;
069import org.forgerock.opendj.ldap.DN;
070import org.forgerock.opendj.ldap.Entry;
071import org.forgerock.opendj.ldap.LDAPConnectionFactory;
072import org.forgerock.opendj.ldap.LdapException;
073import org.forgerock.opendj.ldap.Modification;
074import org.forgerock.opendj.ldap.ModificationType;
075import org.forgerock.opendj.ldap.ResultCode;
076import org.forgerock.opendj.ldap.SearchScope;
077import org.forgerock.opendj.ldap.controls.Control;
078import org.forgerock.opendj.ldap.controls.ProxiedAuthV1RequestControl;
079import org.forgerock.opendj.ldap.controls.ServerSideSortRequestControl;
080import org.forgerock.opendj.ldap.controls.VirtualListViewRequestControl;
081import org.forgerock.opendj.ldap.requests.AddRequest;
082import org.forgerock.opendj.ldap.requests.DeleteRequest;
083import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
084import org.forgerock.opendj.ldap.requests.ModifyRequest;
085import org.forgerock.opendj.ldap.requests.SearchRequest;
086import org.forgerock.opendj.ldap.requests.SimpleBindRequest;
087import org.forgerock.opendj.ldap.responses.SearchResultEntry;
088import org.forgerock.opendj.ldap.schema.Schema;
089import org.forgerock.opendj.ldif.ConnectionEntryReader;
090import org.forgerock.util.Options;
091import org.forgerock.util.thread.listener.ShutdownListener;
092import org.forgerock.util.thread.listener.ShutdownManager;
093
094/**
095 * DataLayer (A PACKAGE SCOPE CLASS) to access LDAP or other database
096 * 
097 * TODO: 1. Needs to subclass and isolate the current implementation of
098 * DataLayer as DSLayer for ldap specific operations 2. Improvements needed for
099 * _ldapPool: destroy(), initial bind user, tunning for MIN and MAX initial
100 * settings etc 3. May choose to extend implementation of _ldapPool from
101 * LDAPConnectionPool so that there is load balance between connections. Also
102 * _ldapPool may be implemented with a HashTable of (host,port) for mulitple
103 * pools of connections for mulitple (host,port) to DS servers instead of single
104 * host and port.
105 * 
106 * @supported.api
107 */
108public class DataLayer implements java.io.Serializable {
109
110    private static final String RETRIES_KEY = "com.iplanet.am.replica.num.retries";
111    private static final String RETRIES_DELAY_KEY = "com.iplanet.am.replica.delay.between.retries";
112
113    /**
114     * Static section to retrieve the debug object.
115     */
116    private static Debug debug;
117
118    private static I18n i18n = I18n.getInstance(IUMSConstants.UMS_PKG);
119
120    private static DataLayerConfigListener configListener;
121
122    /**
123     * Default minimal connections if none is defined in configuration
124     */
125
126    /**
127     * Default maximum connections if none is defined in configuration
128     */
129    static final int MAX_CONN = 20;
130
131    /**
132     * Default maximum backlog queue size
133     */
134    static final int MAX_BACKLOG = 100;
135
136    static final String LDAP_MAXBACKLOG = "maxbacklog";
137
138    static final String LDAP_RELEASECONNBEFORESEARCH =
139        "releaseconnectionbeforesearchcompletes";
140
141    static final String LDAP_REFERRAL = "referral";
142
143    private static int replicaRetryNum = 1;
144
145    private static long replicaRetryInterval = 1000;
146
147    private static final String LDAP_CONNECTION_NUM_RETRIES = 
148        "com.iplanet.am.ldap.connection.num.retries";
149
150    private static final String LDAP_CONNECTION_RETRY_INTERVAL = 
151        "com.iplanet.am.ldap.connection.delay.between.retries";
152
153    private static final String LDAP_CONNECTION_ERROR_CODES = 
154        "com.iplanet.am.ldap.connection.ldap.error.codes.retries";
155
156    private static int connNumRetry = 3;
157
158    private static int connRetryInterval = 1000;
159
160    private static Set<ResultCode> retryErrorCodes = new HashSet<>();
161    
162    static {
163        debug = Debug.getInstance(IUMSConstants.UMS_DEBUG);
164        initConnectionParams();
165    }
166    
167    public static void initConnectionParams() {
168        String numRetryStr = SystemProperties.get(LDAP_CONNECTION_NUM_RETRIES);
169        if (numRetryStr != null) {
170            try {
171                connNumRetry = Integer.parseInt(numRetryStr);
172            } catch (NumberFormatException e) {
173                if (debug.warningEnabled()) {
174                    debug.warning("Invalid value for "
175                            + LDAP_CONNECTION_NUM_RETRIES);
176                }
177            }
178        }
179
180        String retryIntervalStr = SystemProperties
181                .get(LDAP_CONNECTION_RETRY_INTERVAL);
182        if (retryIntervalStr != null) {
183            try {
184                connRetryInterval = Integer.parseInt(retryIntervalStr);
185            } catch (NumberFormatException e) {
186                if (debug.warningEnabled()) {
187                    debug.warning("Invalid value for "
188                            + LDAP_CONNECTION_RETRY_INTERVAL);
189                }
190            }
191        }
192
193        String retryErrs = SystemProperties.get(LDAP_CONNECTION_ERROR_CODES);
194        if (retryErrs != null) {
195            StringTokenizer stz = new StringTokenizer(retryErrs, ",");
196            while (stz.hasMoreTokens()) {
197                retryErrorCodes.add(ResultCode.valueOf(Integer.parseInt(stz.nextToken().trim())));
198            }
199        }
200
201        if (debug.messageEnabled()) {
202            debug.message("DataLayer: number of retry = " + connNumRetry);
203            debug.message("DataLayer: retry interval = " + connRetryInterval);
204            debug.message("DataLayer: retry error codes = " + retryErrorCodes);
205        }
206    }
207
208    /**
209     * DataLayer constructor
210     */
211    private DataLayer() {
212    }
213
214    /**
215     * Constructor given the extra parameter of guid and pwd identifying an
216     * authenticated principal
217     * 
218     * @param host
219     *            LDAP host
220     * @param port
221     *            LDAP port
222     * @param pwd
223     *            Password for the user
224     */
225    private DataLayer(String id, String pwd, String host, int port)
226        throws UMSException {
227        m_proxyUser = id;
228        m_proxyPassword = pwd;
229        m_host = host;
230        m_port = port;
231        configListener = new DataLayerConfigListener();
232
233        initReplicaProperties();
234        initLdapPool();
235    }
236
237    /**
238     * Create the singleton DataLayer object if it doesn't exist already.
239     *
240     * @supported.api
241     */
242    public synchronized static DataLayer getInstance(ServerInstance serverCfg)
243        throws UMSException {
244        // Make sure only one instance of this class is created.
245        if (m_instance == null) {
246            String host = "localhost";
247            int port = 389;
248            String pUser = "";
249            String pPwd = "";
250
251            if (serverCfg != null) {
252                host = serverCfg.getServerName();
253                port = serverCfg.getPort();
254                pUser = serverCfg.getAuthID();
255                pPwd = (String) AccessController
256                        .doPrivileged(new ServerInstanceAction(serverCfg));
257            }
258            m_instance = new DataLayer(pUser, pPwd, host, port);
259
260            ConfigurationObserver.getInstance().addListener(configListener);
261
262            // Start the EventService thread if it has not already started.
263            initializeEventService();
264        }
265        return m_instance;
266    }
267
268    /**
269     * Create the singleton DataLayer object if it doesn't exist already.
270     * Assumes the server instance for "LDAPUser.Type.AUTH_PROXY".
271     *
272     * @supported.api
273     */
274    public static DataLayer getInstance() throws UMSException {
275        // Make sure only one instance of this class is created.
276        if (m_instance == null) {
277            try {
278                DSConfigMgr cfgMgr = DSConfigMgr.getDSConfigMgr();
279                ServerInstance serverCfg = cfgMgr.getServerInstance(LDAPUser.Type.AUTH_PROXY);
280                m_instance = getInstance(serverCfg);
281            } catch (LDAPServiceException ex) {
282                debug.error("Error:  Unable to get server config instance "
283                        + ex.getMessage());
284            }
285        }
286        return m_instance;
287    }
288
289    /**
290     * Get connection from pool. Reauthenticate if necessary
291     * 
292     * @return connection that is available to use.
293     *
294     * @supported.api
295     */
296    public Connection getConnection(java.security.Principal principal) throws LdapException {
297        if (_ldapPool == null)
298            return null;
299
300        if (debug.messageEnabled()) {
301            debug.message("Invoking _ldapPool.getConnection()");
302        }
303
304        // proxy as given principal
305        ProxiedAuthV1RequestControl.newControl(principal.getName());
306        Connection conn = _ldapPool.getConnection();
307        if (debug.messageEnabled()) {
308            debug.message("Got Connection : " + conn);
309        }
310
311        return conn;
312    }
313
314    /**
315     * Returns String values of the attribute.
316     * 
317     * @param principal Authentication Principal.
318     * @param guid distinguished name.
319     * @param attrName attribute name.
320     *
321     * @supported.api
322     */
323    public String[] getAttributeString(Principal principal, Guid guid, String attrName) {
324        String id = guid.getDn();
325        SearchRequest request = LDAPRequests.newSearchRequest(id, SearchScope.BASE_OBJECT, "(objectclass=*)");
326        try {
327            try (ConnectionEntryReader reader = readLDAPEntry(principal, request)) {
328                Attribute attribute = reader.readEntry().getAttribute(attrName);
329                Collection<String> values = new ArrayList<>();
330                for (ByteString byteString : attribute) {
331                    values.add(byteString.toString());
332                }
333                return values.toArray(new String[0]);
334            }
335        } catch (Exception e) {
336            if (debug.warningEnabled()) {
337                debug.warning(
338                        "Exception in DataLayer.getAttributeString for DN: "
339                                + id, e);
340            }
341            return null;
342        }
343    }
344
345    /**
346     * Returns <code>Attr</code> from the given attribute name.
347     * 
348     * @param principal Authentication Principal.
349     * @param guid Distinguished name.
350     * @param attrName Attribute name.
351     *
352     * @supported.api
353     */
354    public Attr getAttribute(Principal principal, Guid guid, String attrName) {
355        String id = guid.getDn();
356        try {
357            SearchRequest request = LDAPRequests.newSearchRequest(id, SearchScope.BASE_OBJECT, "(objectclass=*)",
358                    attrName);
359            try (ConnectionEntryReader reader = readLDAPEntry(principal, request)) {
360                Attribute attribute = reader.readEntry().getAttribute(attrName);
361                if (attribute == null) {
362                    return null;
363                } else {
364                    return new Attr(attribute);
365                }
366            }
367        } catch (Exception e) {
368            if (debug.warningEnabled()) {
369                debug.warning("Exception in DataLayer.getAttribute for DN: "
370                        + id, e);
371            }
372            return null;
373        }
374    }
375
376    /**
377     * Returns attributes for the given attribute names.
378     * 
379     * @param principal Authentication Principal.
380     * @param guid Distinguished name.
381     * @param attrNames Attribute names.
382     * @return collection of Attr.
383     *
384     * @supported.api
385     */
386    public Collection<Attr> getAttributes(Principal principal, Guid guid, Collection<String> attrNames) {
387        String id = guid.getDn();
388        SearchRequest request = LDAPRequests.newSearchRequest(id, SearchScope.BASE_OBJECT, "(objectclass=*)",
389                attrNames.toArray(EMPTY_STRING_ARRAY));
390        ConnectionEntryReader ldapEntry;
391        try {
392            ldapEntry = readLDAPEntry(principal, request);
393
394            if (ldapEntry == null) {
395                debug.warning("No attributes returned may not have permission to read");
396                return Collections.emptySet();
397            }
398
399            Collection<Attr> attributes = new ArrayList<>();
400            while (ldapEntry.hasNext()) {
401                if (ldapEntry.isEntry()) {
402                    SearchResultEntry entry = ldapEntry.readEntry();
403                    for (Attribute attr : entry.getAllAttributes()) {
404                        attributes.add(new Attr(attr));
405                    }
406                }
407            }
408            return attributes;
409        } catch(Exception e) {
410            debug.warning("Exception in DataLayer.getAttributes for DN: {}", id, e);
411            return null;
412        }
413    }
414
415    /**
416     * Adds entry to the server.
417     * 
418     * @param principal Authenticated Principal.
419     * @param guid Distinguished name.
420     * @param attrSet attribute set containing name/value pairs.
421     * @exception AccessRightsException if insufficient access>
422     * @exception EntryAlreadyExistsException if the entry already exists.
423     * @exception UMSException if fail to add entry.
424     *
425     * @supported.api
426     */
427    public void addEntry(
428        java.security.Principal principal,
429        Guid guid,
430        AttrSet attrSet
431    ) throws UMSException {
432        String id = guid.getDn();
433        ResultCode errorCode;
434
435        try {
436            AddRequest request = LDAPRequests.newAddRequest(id);
437            for (Attribute attribute : attrSet.toLDAPAttributeSet()) {
438                request.addAttribute(attribute);
439            }
440
441            int retry = 0;
442            while (retry <= connNumRetry) {
443                if (debug.messageEnabled()) {
444                    debug.message("DataLayer.addEntry retry: " + retry);
445                }
446
447                try (Connection conn = getConnection(principal)) {
448                    conn.add(request);
449                    return;
450                } catch (LdapException e) {
451                    errorCode = e.getResult().getResultCode();
452                    if (!retryErrorCodes.contains(errorCode) || retry == connNumRetry) {
453                        throw e;
454                    }
455                    retry++;
456                    try {
457                        Thread.sleep(connRetryInterval);
458                    } catch (InterruptedException ex) {
459                    }
460                }
461            }
462        } catch (LdapException e) {
463            if (debug.warningEnabled()) {
464                debug.warning("Exception in DataLayer.addEntry for DN: " + id,
465                        e);
466            }
467            errorCode = e.getResult().getResultCode();
468            String[] args = {id};
469            if (ResultCode.ENTRY_ALREADY_EXISTS.equals(errorCode)) {
470                throw new EntryAlreadyExistsException(i18n.getString(IUMSConstants.ENTRY_ALREADY_EXISTS, args), e);
471            } else if (ResultCode.INSUFFICIENT_ACCESS_RIGHTS.equals(errorCode)) {
472                throw new AccessRightsException(i18n.getString(IUMSConstants.INSUFFICIENT_ACCESS_ADD, args), e);
473            } else {
474                throw new UMSException(i18n.getString(IUMSConstants.UNABLE_TO_ADD_ENTRY, args), e);
475            }
476        }
477    }
478
479    /**
480     * Delete entry from the server
481     * 
482     * @param guid
483     *            globally unique identifier for the entry
484     * @exception AccessRightsException
485     *                insufficient access
486     * @exception EntryNotFoundException
487     *                if the entry is not found
488     * @exception UMSException
489     *                Fail to delete the entry
490     *
491     * @supported.api
492     */
493    public void deleteEntry(java.security.Principal principal, Guid guid)
494            throws UMSException {
495        if (guid == null) {
496            String msg = i18n.getString(IUMSConstants.BAD_ID);
497            throw new IllegalArgumentException(msg);
498        }
499        String id = guid.getDn();
500        ResultCode errorCode;
501
502        try {
503            DeleteRequest request = LDAPRequests.newDeleteRequest(id);
504            int retry = 0;
505            while (retry <= connNumRetry) {
506                if (debug.messageEnabled()) {
507                    debug.message("DataLayer.deleteEntry retry: " + retry);
508                }
509
510                try (Connection conn = getConnection(principal)) {
511                    conn.delete(request);
512                    return;
513                } catch (LdapException e) {
514                    if (!retryErrorCodes.contains(e.getResult().getResultCode())
515                            || retry == connNumRetry) {
516                        throw e;
517                    }
518                    retry++;
519                    try {
520                        Thread.sleep(connRetryInterval);
521                    } catch (InterruptedException ex) {
522                    }
523                }
524            }
525        } catch (LdapException e) {
526            debug.error("Exception in DataLayer.deleteEntry for DN: " + id, e);
527            errorCode = e.getResult().getResultCode();
528            String[] args = { id };
529            if (ResultCode.NO_SUCH_OBJECT.equals(errorCode)) {
530                throw new EntryNotFoundException(i18n.getString(IUMSConstants.ENTRY_NOT_FOUND, args), e);
531            } else if (ResultCode.INSUFFICIENT_ACCESS_RIGHTS.equals(errorCode)) {
532                throw new AccessRightsException(i18n.getString(IUMSConstants.INSUFFICIENT_ACCESS_DELETE, args), e);
533            } else {
534                throw new UMSException(i18n.getString(IUMSConstants.UNABLE_TO_DELETE_ENTRY, args), e);
535            }
536        }
537    }
538
539    /**
540     * Read an ldap entry
541     * 
542     * @param guid
543     *            globally unique identifier for the entry
544     * @return an attribute set representing the entry in ldap, all non
545     *         operational attributes are read
546     * @exception EntryNotFoundException
547     *                if the entry is not found
548     * @exception UMSException
549     *                Fail to read the entry
550     *
551     * @supported.api
552     */
553    public AttrSet read(java.security.Principal principal, Guid guid)
554            throws UMSException {
555        return read(principal, guid, null);
556    }
557
558    /**
559     * Reads an ldap entry.
560     * 
561     * @param principal Authentication Principal.
562     * @param guid Globally unique identifier for the entry.
563     * @param attrNames Attributes to read.
564     * @return an attribute set representing the entry in LDAP.
565     * @exception EntryNotFoundException if the entry is not found.
566     * @exception UMSException if fail to read the entry.
567     *
568     * @supported.api
569     */
570    public AttrSet read(
571        java.security.Principal principal,
572        Guid guid,
573        String attrNames[]
574    ) throws UMSException {
575        String id = guid.getDn();
576        ConnectionEntryReader entryReader;
577        SearchRequest request = LDAPRequests.newSearchRequest(id, SearchScope.BASE_OBJECT, "(objectclass=*)",
578                attrNames);
579
580        entryReader = readLDAPEntry(principal, request);
581
582        if (entryReader == null) {
583            throw new AccessRightsException(id);
584        }
585
586        Collection<Attribute> attrs = new ArrayList<>();
587        try (ConnectionEntryReader reader = entryReader) {
588            while (reader.hasNext()) {
589                if (reader.isReference()) {
590                    reader.readReference();
591                    //TODO AME-7017
592                }
593                SearchResultEntry entry = entryReader.readEntry();
594                for (Attribute attr : entry.getAllAttributes()) {
595                    attrs.add(attr);
596                }
597            }
598            if (attrs.isEmpty()) {
599                throw new EntryNotFoundException(i18n.getString(IUMSConstants.ENTRY_NOT_FOUND, new String[]{id}));
600            }
601            return new AttrSet(attrs);
602        } catch (IOException e) {
603            throw new UMSException(i18n.getString(IUMSConstants.UNABLE_TO_READ_ENTRY, new String[]{id}), e);
604        }
605    }
606
607    public void rename(java.security.Principal principal, Guid guid,
608            String newName, boolean deleteOldName)
609            throws UMSException {
610        String id = guid.getDn();
611        ResultCode errorCode;
612
613        try {
614            ModifyDNRequest request = LDAPRequests.newModifyDNRequest(id, newName);
615            int retry = 0;
616            while (retry <= connNumRetry) {
617                if (debug.messageEnabled()) {
618                    debug.message("DataLayer.rename retry: " + retry);
619                }
620
621                try (Connection conn = getConnection(principal)) {
622                    conn.applyChange(request);
623                    return;
624                } catch (LdapException e) {
625                    errorCode = e.getResult().getResultCode();
626                    if (!retryErrorCodes.contains(errorCode) || retry == connNumRetry) {
627                        throw e;
628                    }
629                    retry++;
630                    try {
631                        Thread.sleep(connRetryInterval);
632                    } catch (InterruptedException ex) {
633                    }
634                }
635            }
636        } catch (LdapException e) {
637            if (debug.warningEnabled()) {
638                debug.warning("Exception in DataLayer.rename for DN: " + id, e);
639            }
640            errorCode = e.getResult().getResultCode();
641            if (ResultCode.NO_SUCH_OBJECT.equals(errorCode)) {
642                throw new EntryNotFoundException(id, e);
643            } else if (ResultCode.INSUFFICIENT_ACCESS_RIGHTS.equals(errorCode)) {
644                throw new AccessRightsException(id, e);
645            } else {
646                throw new UMSException(id, e);
647            }
648        }
649    }
650
651    /**
652     * Modifies an ldap entry.
653     * 
654     * @param principal Authentication Principal.
655     * @param guid globally unique identifier for the entry.
656     * @param modifications Set of modifications for the entry.
657     * @exception AccessRightsException if insufficient access
658     * @exception EntryNotFoundException if the entry is not found.
659     * @exception UMSException if failure
660     *
661     * @supported.api
662     */
663    public void modify(Principal principal, Guid guid, Collection<Modification> modifications)
664            throws UMSException {
665        String id = guid.getDn();
666        ResultCode errorCode;
667
668        try {
669            ModifyRequest request = LDAPRequests.newModifyRequest(id);
670            for (Modification modification : modifications) {
671                request.addModification(modification);
672            }
673            int retry = 0;
674            while (retry <= connNumRetry) {
675                if (debug.messageEnabled()) {
676                    debug.message("DataLayer.modify retry: " + retry);
677                }
678
679                try (Connection conn = getConnection(principal)) {
680                    conn.modify(request);
681                    return;
682                } catch (LdapException e) {
683                    if (!retryErrorCodes.contains("" + e.getResult().getResultCode().toString())
684                            || retry == connNumRetry) {
685                        throw e;
686                    }
687                    retry++;
688                    try {
689                        Thread.sleep(connRetryInterval);
690                    } catch (InterruptedException ex) {
691                    }
692                }
693            }
694        } catch (LdapException e) {
695            if (debug.warningEnabled()) {
696                debug.warning("Exception in DataLayer.modify for DN: " + id, e);
697            }
698            errorCode = e.getResult().getResultCode();
699            if (ResultCode.NO_SUCH_OBJECT.equals(errorCode)) {
700                throw new EntryNotFoundException(id, e);
701            } else if (ResultCode.INSUFFICIENT_ACCESS_RIGHTS.equals(errorCode)) {
702                throw new AccessRightsException(id, e);
703            } else {
704                throw new UMSException(id, e);
705            }
706        }
707    }
708
709    /**
710     * Changes user password.
711     * 
712     * @param guid globally unique identifier for the entry.
713     * @param attrName password attribute name
714     * @param oldPassword old password
715     * @param newPassword new password
716     * @exception AccessRightsException if insufficient access
717     * @exception EntryNotFoundException if the entry is not found.
718     * @exception UMSException if failure
719     *
720     * @supported.api
721     */
722    public void changePassword(Guid guid, String attrName, String oldPassword, String newPassword)
723            throws UMSException {
724
725        Modification modification = new Modification(ModificationType.REPLACE,
726                Attributes.singletonAttribute(attrName, newPassword));
727
728        String id = guid.getDn();
729
730        try {
731            DSConfigMgr dsCfg = DSConfigMgr.getDSConfigMgr();
732            String hostAndPort = dsCfg.getHostName("default");
733
734            // All connections will use authentication
735            SimpleBindRequest bindRequest = LDAPRequests.newSimpleBindRequest(id, oldPassword.toCharArray());
736            Options options = Options.defaultOptions()
737                    .set(AUTHN_BIND_REQUEST, bindRequest);
738
739            try (ConnectionFactory factory = new LDAPConnectionFactory(hostAndPort, 389, options)) {
740                Connection ldc = factory.getConnection();
741                ldc.modify(LDAPRequests.newModifyRequest(id).addModification(modification));
742            } catch (LdapException ldex) {
743                if (debug.warningEnabled()) {
744                    debug.warning("DataLayer.changePassword:", ldex);
745                }
746                ResultCode errorCode = ldex.getResult().getResultCode();
747                if (ResultCode.NO_SUCH_OBJECT.equals(errorCode)) {
748                    throw new EntryNotFoundException(id, ldex);
749                } else if (ResultCode.INSUFFICIENT_ACCESS_RIGHTS.equals(errorCode)) {
750                    throw new AccessRightsException(id, ldex);
751                } else {
752                    throw new UMSException(id, ldex);
753                }
754            }
755        } catch (LDAPServiceException ex) {
756            debug.error("DataLayer.changePassword:", ex);
757            throw new UMSException(id, ex);
758        }
759    }
760
761    /**
762     * Adds value for an attribute and saves the change in the database.
763     * 
764     * @param principal Authenticated Principal.
765     * @param guid ID of the entry to which to add the attribute value.
766     * @param name name of the attribute to which value is being added.
767     * @param value Value to be added to the attribute.
768     * @throws UMSException if there is any error while adding the value.
769     *
770     * @supported.api
771     */
772    public void addAttributeValue(Principal principal, Guid guid, String name, String value) throws UMSException {
773        // Delegate to the other modify() method.
774        modifyAttributeValue(ModificationType.ADD, principal, guid, name, value);
775    }
776
777    /**
778     * Removes value for an attribute and saves the change in the database.
779     * 
780     * @param principal Authenticated Principal.
781     * @param guid the id of the entry from which to remove the attribute value.
782     * @param name Name of the attribute from which value is being removed
783     * @param value Value to be removed from the attribute.
784     * @throws UMSException if there is any error while removing the value.
785     *
786     * @supported.api
787     */
788    public void removeAttributeValue(Principal principal, Guid guid, String name, String value) throws UMSException {
789        // Delegate to the other modify() method.
790        modifyAttributeValue(ModificationType.DELETE, principal, guid, name, value);
791    }
792
793    private void modifyAttributeValue(ModificationType modType, Principal principal, Guid guid, String name,
794            String value) throws UMSException {
795        // Delegate to the other modify() method.
796        modify(principal, guid, Collections.singleton(
797                new Modification(modType, Attributes.singletonAttribute(name, value))));
798    }
799
800    private List<Control> getSearchControls(SearchControl searchControl) throws LdapException {
801        if (searchControl != null) {
802            int[] vlvRange = searchControl.getVLVRange();
803            SortKey[] sortKeys = searchControl.getSortKeys();
804            Collection<org.forgerock.opendj.ldap.SortKey> ldapSortKeys;
805            List<Control> ctrls = new ArrayList<>(); // will hold all server controls
806
807            if (sortKeys != null) {
808                ldapSortKeys = new ArrayList<>(sortKeys.length);
809                for (SortKey sortKey : sortKeys) {
810                    ldapSortKeys.add(new org.forgerock.opendj.ldap.SortKey(sortKey.attributeName, sortKey.reverse));
811                }
812
813                ctrls.add(ServerSideSortRequestControl.newControl(false, ldapSortKeys));
814
815                if (vlvRange != null) {
816                    if (searchControl.getVLVJumpTo() == null) {
817                        ctrls.add(VirtualListViewRequestControl.newOffsetControl(false, vlvRange[0], 0, vlvRange[1],
818                                vlvRange[2], null));
819                    } else {
820                        ctrls.add(VirtualListViewRequestControl.newAssertionControl(false,
821                                ByteString.valueOfUtf8(searchControl.getVLVJumpTo()), vlvRange[1], vlvRange[2], null));
822                    }
823                }
824            }
825            return ctrls;
826        }
827        return null;
828    }
829
830    /**
831     * Performs synchronous search based on specified ldap filter. This is low
832     * level API which assumes caller knows how to construct a data store filer.
833     * 
834     * @param principal Authenticated Principal.
835     * @param guid Unique identifier for the entry.
836     * @param scope Scope can be either <code>SCOPE_ONE</code>,
837     *        <code>SCOPE_SUB</code> or <code>SCOPE_BASE</code>.
838     * @param searchFilter Search filter for this search.
839     * @param attrNames Attribute name for retrieving.
840     * @param attrOnly if true, returns the names but not the values of the
841     *        attributes found.
842     * @param searchControl Search Control.
843     * @exception UMSException if failure.
844     * @exception InvalidSearchFilterException if failure
845     *
846     * @supported.api
847     */
848    public SearchResults search(
849        java.security.Principal principal,
850        Guid guid,
851        int scope,
852        String searchFilter,
853        String attrNames[],
854        boolean attrOnly,
855        SearchControl searchControl
856    ) throws UMSException {
857        String id = guid.getDn();
858
859        // always add "objectclass" to attributes to get, to find the right java
860        // class
861        String[] attrNames1 = null;
862        if (attrNames != null) {
863            attrNames1 = new String[attrNames.length + 1];
864            System.arraycopy(attrNames, 0, attrNames1, 0, attrNames.length);
865            attrNames1[attrNames1.length - 1] = "objectclass";
866        } else {
867            attrNames1 = new String[] { "objectclass" };
868        }
869
870        ConnectionEntryReader ldapResults = null;
871
872        // if searchFilter is null, search for everything under the base
873        if (searchFilter == null) {
874            searchFilter = "(objectclass=*)";
875        }
876        ResultCode errorCode;
877
878        try {
879            Connection conn = getConnection(principal);
880            List<Control> controls = getSearchControls(searchControl);
881            // call readLDAPEntry() only in replica case, save one LDAP search
882            // assume replica case when replicaRetryNum is not 0
883            if (replicaRetryNum != 0) {
884                readLDAPEntry(conn, id, null);
885            }
886
887            SearchRequest request = null;
888            int retry = 0;
889            while (retry <= connNumRetry) {
890                if (debug.messageEnabled()) {
891                    debug.message("DataLayer.search retry: " + retry);
892                }
893
894                if (searchControl != null && searchControl.isGetAllReturnAttributesEnabled()) {
895                    /*
896                     * The array {"*"} is used, because LDAPv3 defines
897                     * "*" as a special string indicating all
898                     * attributes. This gets all the attributes.
899                     */
900                    attrNames1 = new String[] { "*" };
901                }
902                request = LDAPRequests.newSearchRequest(id, SearchScope.valueOf(scope), searchFilter, attrNames1);
903                break;
904            }
905            for (Control control : controls) {
906                request.addControl(control);
907            }
908
909            ldapResults = conn.search(request);
910
911            // TODO: need review and see if conn is recorded properly for
912            // subsequent use
913            //
914            SearchResults result = new SearchResults(conn, ldapResults, conn, this);
915            result.set(SearchResults.BASE_ID, id);
916            result.set(SearchResults.SEARCH_FILTER, searchFilter);
917            result.set(SearchResults.SEARCH_SCOPE, scope);
918
919            if ((searchControl != null)
920                    && (searchControl.contains(SearchControl.KeyVlvRange) 
921                       || searchControl.contains(SearchControl.KeyVlvJumpTo))) {
922                result.set(SearchResults.EXPECT_VLV_RESPONSE, Boolean.TRUE);
923
924            }
925
926            if (searchControl != null
927                    && searchControl.contains(SearchControl.KeySortKeys)) {
928                SortKey[] sortKeys = searchControl.getSortKeys();
929                if (sortKeys != null && sortKeys.length > 0) {
930                    result.set(SearchResults.SORT_KEYS, sortKeys);
931                }
932            }
933
934            return result;
935
936        } catch (LdapException e) {
937            errorCode = e.getResult().getResultCode();
938            if (debug.warningEnabled()) {
939                debug.warning("Exception in DataLayer.search: ", e);
940            }
941            String msg = i18n.getString(IUMSConstants.SEARCH_FAILED);
942            if (ResultCode.TIME_LIMIT_EXCEEDED.equals(errorCode)) {
943                int timeLimit = searchControl != null ? searchControl.getTimeOut() : 0;
944                throw new TimeLimitExceededException(String.valueOf(timeLimit), e);
945            } else if (ResultCode.SIZE_LIMIT_EXCEEDED.equals(errorCode)) {
946                int sizeLimit = searchControl != null ? searchControl.getMaxResults() : 0;
947                throw new SizeLimitExceededException(String.valueOf(sizeLimit), e);
948            } else if (ResultCode.CLIENT_SIDE_PARAM_ERROR.equals(errorCode)
949                    || ResultCode.PROTOCOL_ERROR.equals(errorCode)) {
950                throw new InvalidSearchFilterException(searchFilter, e);
951            } else {
952                throw new UMSException(msg, e);
953            }
954        }
955    }
956
957    /**
958     * Perform synchronous search based on specified ldap filter. This is low
959     * level API which assumes caller knows how to construct a data store filer.
960     * 
961     * @param principal Authenticated Principal.
962     * @param guid Unique identifier for the entry
963     * @param scope Scope can be either <code>SCOPE_ONE</code>,
964     *        <code>SCOPE_SUB</code>, <code>SCOBE_BASE</code>
965     * @param searchFilter Search filter for this search.
966     * @param searchControl Search Control.
967     * @exception UMSException if failure.
968     * @exception InvalidSearchFilterException if failure.
969     *
970     * @supported.api
971     */
972    public SearchResults searchIDs(
973        java.security.Principal principal,
974        Guid guid,
975        int scope,
976        String searchFilter,
977        SearchControl searchControl
978    ) throws InvalidSearchFilterException, UMSException {
979        // TODO: support LDAP referral
980        String attrNames[] = { "objectclass" };
981        return search(principal, guid, scope, searchFilter, attrNames, false,
982                searchControl);
983    }
984
985    /**
986     * Fetches the schema from the LDAP directory server. Retrieve the entire
987     * schema from the root of a Directory Server.
988     * 
989     * @return the schema in the LDAP directory server
990     * @exception AccessRightsException
991     *                insufficient access
992     * @exception UMSException
993     *                Fail to fetch the schema.
994     * @exception LdapException
995     *                Error with LDAP connection.
996     *
997     * @supported.api
998     */
999    public Schema getSchema(java.security.Principal principal) throws UMSException {
1000        ResultCode errorCode;
1001
1002        try (Connection conn = getConnection(principal)) {
1003            int retry = 0;
1004            while (retry <= connNumRetry) {
1005                if (debug.messageEnabled()) {
1006                    debug.message("DataLayer.getSchema retry: " + retry);
1007                }
1008
1009                try {
1010                    return Schema.readSchemaForEntry(conn, DN.valueOf("cn=schema"));
1011                } catch (LdapException e) {
1012                    if (!retryErrorCodes.contains(e.getResult().getResultCode()) || retry == connNumRetry) {
1013                        throw e;
1014                    }
1015                    retry++;
1016                    try {
1017                        Thread.sleep(connRetryInterval);
1018                    } catch (InterruptedException ex) {
1019                    }
1020                }
1021            }
1022        } catch (LdapException e) {
1023            debug.error("Exception in DataLayer.getSchema: ", e);
1024            errorCode = e.getResult().getResultCode();
1025            if (ResultCode.INSUFFICIENT_ACCESS_RIGHTS.equals(errorCode)) {
1026                throw new AccessRightsException(m_host, e);
1027            } else {
1028                throw new UMSException(m_host, e);
1029            }
1030        }
1031
1032        return null;
1033    }
1034
1035    private synchronized void initReplicaProperties() {
1036        int retries = SystemProperties.getAsInt(RETRIES_KEY, 0);
1037        if (retries < 0) {
1038            retries = 0;
1039            debug.warning("Invalid value for replica retry num, set to 0");
1040        }
1041
1042        replicaRetryNum = retries;
1043
1044        long interval = SystemProperties.getAsLong(RETRIES_DELAY_KEY, 0);
1045        if (interval < 0) {
1046            interval = 0;
1047            debug.warning("Invalid value for replica interval, set to 0");
1048        }
1049
1050        replicaRetryInterval = interval;
1051    }
1052
1053    public Entry readLDAPEntry(Connection ld, String dn,
1054            String[] attrnames) throws LdapException {
1055
1056        LdapException ldapEx = null;
1057        int retry = 0;
1058        int connRetry = 0;
1059        while (retry <= replicaRetryNum && connRetry <= connNumRetry) {
1060            if (debug.messageEnabled()) {
1061                debug.message("DataLayer.readLDAPEntry: connRetry: "
1062                        + connRetry);
1063                debug.message("DataLayer.readLDAPEntry: retry: " + retry);
1064            }
1065            try {
1066                if (attrnames == null) {
1067                    return ld.searchSingleEntry(LDAPRequests.newSingleEntrySearchRequest(dn));
1068                } else {
1069                    return ld.searchSingleEntry(LDAPRequests.newSingleEntrySearchRequest(dn, attrnames));
1070                }
1071            } catch (LdapException e) {
1072                ResultCode errorCode = e.getResult().getResultCode();
1073                if (ResultCode.NO_SUCH_OBJECT.equals(errorCode)) {
1074                    if (debug.messageEnabled()) {
1075                        debug.message("Replica: entry not found: " + dn
1076                                + " retry: " + retry);
1077                    }
1078                    if (retry == replicaRetryNum) {
1079                        ldapEx = e;
1080                    } else {
1081                        try {
1082                            Thread.sleep(replicaRetryInterval);
1083                        } catch (Exception ignored) {
1084                        }
1085                    }
1086                    retry++;
1087                } else if (retryErrorCodes.contains("" + errorCode)) {
1088                    if (connRetry == connNumRetry) {
1089                        ldapEx = e;
1090                    } else {
1091                        try {
1092                            Thread.sleep(connRetryInterval);
1093                        } catch (Exception ignored) {
1094                        }
1095                    }
1096                    connRetry++;
1097                } else {
1098                    throw e;
1099                }
1100            }
1101        }
1102
1103        throw ldapEx;
1104    }
1105
1106    public ConnectionEntryReader readLDAPEntry(Principal principal, SearchRequest request) throws UMSException {
1107
1108        LdapException ldapEx = null;
1109        int retry = 0;
1110        int connRetry = 0;
1111        while (retry <= replicaRetryNum && connRetry <= connNumRetry) {
1112            if (debug.messageEnabled()) {
1113                debug.message("DataLayer.readLDAPEntry: connRetry: "
1114                        + connRetry);
1115                debug.message("DataLayer.readLDAPEntry: retry: " + retry);
1116            }
1117            try (Connection conn = getConnection(principal)) {
1118                return conn.search(request);
1119            } catch (LdapException e) {
1120                ResultCode errorCode = e.getResult().getResultCode();
1121                if (ResultCode.NO_SUCH_OBJECT.equals(errorCode)) {
1122                    if (debug.messageEnabled()) {
1123                        debug.message("Replica: entry not found: " +
1124                            request.getName().toString() + " retry: " + retry);
1125                    }
1126                    if (retry == replicaRetryNum) {
1127                        ldapEx = e;
1128                    } else {
1129                        try {
1130                            Thread.sleep(replicaRetryInterval);
1131                        } catch (Exception ex) {
1132                        }
1133                    }
1134                    retry++;
1135                } else if (retryErrorCodes.contains("" + errorCode)) {
1136                    if (connRetry == connNumRetry) {
1137                        ldapEx = e;
1138                    } else {
1139                        try {
1140                            Thread.sleep(connRetryInterval);
1141                        } catch (Exception ex) {
1142                        }
1143                    }
1144                    connRetry++;
1145                } else {
1146                    throw new UMSException(e.getMessage(), e);
1147                }
1148            }
1149        }
1150
1151        throw new UMSException(ldapEx.getMessage(), ldapEx);
1152    }
1153
1154
1155    /**
1156     * Initialize the pool shared by all DataLayer object(s).
1157     */
1158    private synchronized void initLdapPool() throws UMSException {
1159        // Don't do anything if pool is already initialized
1160        if (_ldapPool != null)
1161            return;
1162
1163        /*
1164         * Initialize the pool with minimum and maximum connections settings
1165         * retrieved from configuration
1166         */
1167        ServerInstance svrCfg = null;
1168        String hostName = null;
1169
1170        try {
1171            DSConfigMgr dsCfg = DSConfigMgr.getDSConfigMgr();
1172            hostName = dsCfg.getHostName("default");
1173            baseFactory = dsCfg.getNewProxyConnectionFactory();
1174
1175            svrCfg = dsCfg.getServerInstance(LDAPUser.Type.AUTH_PROXY);
1176        } catch (LDAPServiceException ex) {
1177            debug.error("Error initializing connection pool " + ex.getMessage());
1178        }
1179        
1180        // Check if svrCfg was successfully obtained
1181        if (svrCfg == null) {
1182            debug.error("Error getting server config.");
1183            String args[] = new String[1];
1184            args[0] = hostName == null ? "default" : hostName;
1185            throw new UMSException(i18n.getString(IUMSConstants.NEW_INSTANCE_FAILED, args));
1186        }
1187
1188        int poolMin = svrCfg.getMinConnections();
1189        int poolMax = svrCfg.getMaxConnections();
1190        m_releaseConnectionBeforeSearchCompletes = svrCfg.getBooleanValue(LDAP_RELEASECONNBEFORESEARCH, false);
1191
1192        if (debug.messageEnabled()) {
1193            debug.message("Creating ldap connection pool with: poolMin {}, poolMax {}", poolMin, poolMax);
1194        }
1195
1196        int idleTimeout = SystemProperties.getAsInt(Constants.LDAP_CONN_IDLE_TIME_IN_SECS, 0);
1197        if (idleTimeout == 0) {
1198            debug.warning("Idle timeout not set. Defaulting to 0.");
1199        }
1200
1201        _ldapPool = Connections.newCachedConnectionPool(
1202                Connections.newNamedConnectionFactory(baseFactory, "DataLayer"), poolMin, poolMax, idleTimeout,
1203                TimeUnit.SECONDS);
1204
1205        ShutdownManager shutdownMan = com.sun.identity.common.ShutdownManager.getInstance();
1206        shutdownMan.addShutdownListener(
1207            new ShutdownListener() {
1208                public void shutdown() {
1209                    if (_ldapPool != null) {
1210                        _ldapPool.close();
1211                    }
1212                }
1213            }
1214        );
1215    }
1216
1217    public static int getConnNumRetry() {
1218        return connNumRetry;
1219    }
1220
1221    public static int getConnRetryInterval() {
1222        return connRetryInterval;
1223    }
1224
1225    public static Set<ResultCode> getRetryErrorCodes() {
1226        return retryErrorCodes;
1227    }
1228    
1229    private static void initializeEventService() {
1230        // Initialize event service. This is to make sure that EventService
1231        // thread is started. The other place where it is also tried to start
1232        // is: com.iplanet.am.sdk.ldap.AMEventManager which is
1233        // initialized in com.iplanet.am.sdk.ldap.DirectoryManager
1234        if (!EventService.isStarted()) {
1235            // Use a separate thread to start the EventService thread.
1236            // This will prevent deadlocks associated in the system because
1237            // of EventService related dependencies.
1238            InitEventServiceThread th = new InitEventServiceThread();
1239            Thread initEventServiceThread = new Thread(th,
1240                "InitEventServiceThread");
1241            initEventServiceThread.setDaemon(true);
1242            initEventServiceThread.start();
1243        }
1244    }
1245
1246    private static class InitEventServiceThread implements Runnable {
1247        public void run() {
1248            debug.message("InitEventServiceThread:initializeEventService() - "
1249                + "EventService thread getting  initialized ");
1250            try {
1251                EventService es = EventService.getEventService();
1252                synchronized (es) {
1253                    if (!EventService.isStarted()) {
1254                        es.restartPSearches();
1255                    }
1256                }
1257            } catch (Exception e) {
1258                // An Error occurred while initializing EventService
1259                debug.error("InitEventServiceThread:run() Unable to start EventService!!", e);
1260            }
1261        }
1262    }    
1263
1264    static private ConnectionPool _ldapPool = null;
1265
1266    static private ConnectionFactory baseFactory = null;
1267
1268    static private DataLayer m_instance = null;
1269
1270    private String m_host = null;
1271
1272    private int m_port;
1273
1274    private String m_proxyUser = "";
1275
1276    private String m_proxyPassword = "";
1277
1278    private boolean m_releaseConnectionBeforeSearchCompletes = false;
1279
1280    private static final String[] EMPTY_STRING_ARRAY = new String[0];
1281
1282    private class DataLayerConfigListener implements ConfigurationListener {
1283
1284        @Override
1285        public synchronized void notifyChanges() {
1286            final int retries = SystemProperties.getAsInt(RETRIES_KEY, 0);
1287            final long delay = SystemProperties.getAsLong(RETRIES_DELAY_KEY, 0);
1288
1289            if (retries != replicaRetryNum || delay != replicaRetryInterval) {
1290                initReplicaProperties();
1291            }
1292        }
1293    }
1294}