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: EventService.java,v 1.19 2009/09/28 21:47:33 ww203982 Exp $
026 *
027 * Portions Copyrighted 2010-2017 ForgeRock AS.
028 */
029
030package com.iplanet.services.ldap.event;
031
032import java.math.BigInteger;
033import java.security.AccessController;
034import java.util.ArrayList;
035import java.util.Collection;
036import java.util.HashMap;
037import java.util.Iterator;
038import java.util.List;
039import java.util.Map;
040import java.util.Random;
041import java.util.Set;
042
043import com.iplanet.am.sdk.ldap.ACIEventListener;
044import com.iplanet.am.sdk.ldap.EntryEventListener;
045import com.iplanet.am.util.SystemProperties;
046import com.iplanet.services.ldap.DSConfigMgr;
047import com.iplanet.services.ldap.LDAPServiceException;
048import com.iplanet.services.ldap.LDAPUser;
049import com.iplanet.services.util.I18n;
050import com.iplanet.sso.SSOException;
051import com.iplanet.ums.IUMSConstants;
052import com.sun.identity.idm.IdConstants;
053import com.sun.identity.security.AdminTokenAction;
054import com.sun.identity.shared.Constants;
055import com.sun.identity.shared.debug.Debug;
056import com.sun.identity.sm.SMSException;
057import com.sun.identity.sm.ServiceSchema;
058import com.sun.identity.sm.ServiceSchemaManager;
059import com.sun.identity.sm.ldap.LDAPEventManager;
060import org.forgerock.opendj.ldap.ConnectionFactory;
061import org.forgerock.opendj.ldap.DN;
062import org.forgerock.opendj.ldap.Entry;
063import org.forgerock.opendj.ldap.Filter;
064import org.forgerock.opendj.ldap.LdapException;
065import org.forgerock.opendj.ldap.SearchScope;
066import org.forgerock.opendj.ldap.controls.PersistentSearchChangeType;
067import org.forgerock.opendj.ldap.responses.SearchResultEntry;
068import org.forgerock.util.thread.listener.ShutdownListener;
069import org.forgerock.util.thread.listener.ShutdownManager;
070
071/**
072 *
073 *
074 * @supported.api
075 */
076public class EventService {
077
078    private static Debug logger = Debug.getInstance("amEventService");
079    private static I18n i18n = I18n.getInstance(IUMSConstants.UMS_PKG);
080    private static DSConfigMgr cm = null;
081    private static final String EVENT_CONNECTION_RETRY_INTERVAL =
082            "com.iplanet.am.event.connection.delay.between.retries";
083    private static final int RETRY_INTERVAL = SystemProperties.getAsInt(EVENT_CONNECTION_RETRY_INTERVAL, 3000);
084    private static final String EVENT_LISTENER_DISABLE_LIST = "com.sun.am.event.connection.disable.list";
085    private static final Class<? extends IDSEventListener> ACI_EVENT_LISTENER_CLASS_NAME = ACIEventListener.class;
086    private static final Class<? extends IDSEventListener> ENTRY_EVENT_LISTENER_CLASS_NAME = EntryEventListener.class;
087    private static final Class<? extends IDSEventListener> LDAP_EVENT_LISTENER_CLASS_NAME = LDAPEventManager.class;
088
089    private static volatile boolean isShutdownCalled = false;
090    private static volatile boolean isRunning = false;
091
092    private ConnectionFactory adminConnectionFactory;
093    private ConnectionFactory smsConnectionFactory;
094    private final Map<Class<? extends IDSEventListener>, ListenerSearch> persistentSearches = new HashMap<>();
095
096    private static final class ListenerSearch {
097        private final IDSEventListener listener;
098        private final EventServicePersistentSearch search;
099        private ListenerSearch(IDSEventListener listener, EventServicePersistentSearch search) {
100            this.listener = listener;
101            this.search = search;
102        }
103    }
104
105    private enum Singleton {
106        INSTANCE;
107
108        private EventService eventService;
109        private EventException eventException;
110
111        Singleton() {
112            try {
113                eventService = new EventService();
114                ShutdownManager shutdownMan = com.sun.identity.common.ShutdownManager.getInstance();
115                shutdownMan.addShutdownListener(
116                        new ShutdownListener() {
117                            public void shutdown() {
118                                if (eventService != null) {
119                                    eventService.stopPSearches();
120                                }
121                            }
122                        });
123            } catch (EventException e) {
124                eventException = e;
125            }
126        }
127
128        private EventService getEventService() throws EventException {
129            if (eventException != null) {
130                throw eventException;
131            }
132            return eventService;
133        }
134    }
135
136    private EventService() throws EventException {
137        try {
138            cm = DSConfigMgr.getDSConfigMgr();
139        } catch (LDAPServiceException lse) {
140            logger.error("EventService.getConfigManager() - Failed to get handle to Configuration Manager", lse);
141            throw new EventException(i18n.getString(IUMSConstants.DSCFG_NOCFGMGR), lse);
142        }
143    }
144
145    /**
146     *
147     *
148     * @supported.api
149     */
150    public synchronized static EventService getEventService() throws EventException, LdapException {
151        if (isShutdownCalled) {
152            return null;
153        }
154        return Singleton.INSTANCE.getEventService();
155    }
156
157    //Question: is ok to not actually restart running psearches if the listener is still enabled?
158    public synchronized void restartPSearches() {
159        List<Class<? extends IDSEventListener>> listenersClasses = getEnabledListenersClasses();
160
161        for (Iterator<Class<? extends IDSEventListener>> iterator = persistentSearches.keySet().iterator();
162             iterator.hasNext();) {
163            Class<? extends IDSEventListener> pSearchListenerClass = iterator.next();
164            //remove any running psearches that do not have enabled listeners
165            if (!listenersClasses.contains(pSearchListenerClass)) {
166                persistentSearches.get(pSearchListenerClass).search.stopSearch();
167                iterator.remove();
168            } else {
169                listenersClasses.remove(pSearchListenerClass);
170            }
171        }
172
173        for (Iterator<Class<? extends IDSEventListener>> iterator = listenersClasses.iterator(); iterator.hasNext();) {
174            Class<? extends IDSEventListener> listenerClass = iterator.next();
175
176            try {
177                IDSEventListener listener = listenerClass.newInstance();
178
179                EventServicePersistentSearch pSearch = new EventServicePersistentSearch(RETRY_INTERVAL,
180                        DN.valueOf(listener.getBase()), Filter.valueOf(listener.getFilter()),
181                        SearchScope.valueOf(listener.getScope()), getConnectionFactory(listener.getClass()),
182                        "objectclass");
183
184                pSearch.addListener(listener, new BigInteger(130, new Random()).toString());
185
186                pSearch.startSearch();
187                persistentSearches.put(listenerClass, new ListenerSearch(listener, pSearch));
188                logger.message("EventService.restartPSearches() - successfully initialized: {}", listenerClass);
189                iterator.remove();
190            } catch (Exception e) {
191                logger.error("EventService.restartPSearches() Unable to start listener {}", listenerClass, e);
192            }
193        }
194
195        if (!listenersClasses.isEmpty()) {
196            for (Class<? extends IDSEventListener> listenerClass : listenersClasses) {
197                logger.error("EventService.restartPSearches(): unable add listener: {}", listenerClass);
198            }
199        }
200        isRunning = true;
201    }
202
203    public synchronized void stopPSearches() {
204        isShutdownCalled = true;
205        for (ListenerSearch pSearch : persistentSearches.values()) {
206            pSearch.search.removeListener(pSearch.listener);
207            pSearch.search.stopSearch();
208        }
209    }
210
211    public static boolean isStarted() {
212        return isRunning && !isShutdownCalled;
213    }
214
215    public IDSEventListener getListener(Class<? extends IDSEventListener> listenerClass) {
216        return persistentSearches.get(listenerClass).listener;
217    }
218
219    private static List<Class<? extends IDSEventListener>> getEnabledListenersClasses() {
220
221        Collection<String> disabledListeners = getDisabledListeners();
222        boolean disableACI = disabledListeners.contains("aci");
223        boolean disableUM = disabledListeners.contains("um");
224        boolean disableSM = disabledListeners.contains("sm");
225
226        if (!disableUM || !disableACI) {
227            // Check if AMSDK is configured
228            if (!isAMSDKConfigured()) {
229                disableUM = true;
230                disableACI = true;
231                if (logger.messageEnabled()) {
232                    logger.message("EventService.getListenerList(): AMSDK is not configured or config time. "
233                            + "Disabling UM and ACI event listeners");
234                }
235            }
236        }
237
238        //psearch terminated if you disable the DB notifications, or add 'sm' to the list of disabled
239        if (!disableSM) {
240            disableSM = !Boolean.parseBoolean(SystemProperties.get(Constants.SMS_ENABLE_DB_NOTIFICATION));
241        }
242
243        if (logger.messageEnabled()) {
244            logger.message("EventService.getListenerList(): SMS listener is enabled: {}", !disableSM);
245        }
246
247        List<Class<? extends IDSEventListener>> listeners = new ArrayList<>();
248        // Disable the selected listeners
249        if (!disableACI) {
250            listeners.add(ACI_EVENT_LISTENER_CLASS_NAME);
251        }
252        if (!disableUM) {
253            listeners.add(ENTRY_EVENT_LISTENER_CLASS_NAME);
254        }
255        if (!disableSM) {
256            listeners.add(LDAP_EVENT_LISTENER_CLASS_NAME);
257        }
258
259        if (disableACI && disableUM && disableSM) {
260            logger.message("EventService.getListenerList() - all listeners are disabled, EventService won't start");
261        }
262
263        return listeners;
264    }
265
266    private ConnectionFactory getConnectionFactory(Class<? extends IDSEventListener> listenerClass)
267            throws LDAPServiceException {
268        if (LDAPEventManager.class.equals(listenerClass) && cm.getServerGroup("sms") != null) {
269            return getSmsConnectionFactory();
270        } else {
271            return getAdminConnectionFactory();
272        }
273    }
274
275    private ConnectionFactory getAdminConnectionFactory() throws LDAPServiceException {
276        if (adminConnectionFactory == null) {
277            adminConnectionFactory = DSConfigMgr.getDSConfigMgr().getNewAdminConnectionFactory();
278        }
279        return adminConnectionFactory;
280    }
281
282    private ConnectionFactory getSmsConnectionFactory() throws LDAPServiceException {
283        if (smsConnectionFactory == null) {
284            smsConnectionFactory = DSConfigMgr.getDSConfigMgr()
285                    .getNewConnectionFactory("sms", LDAPUser.Type.AUTH_ADMIN);
286        }
287        return smsConnectionFactory;
288    }
289
290    private void dispatchException(Exception e, String requestId, IDSEventListener listener) {
291        logger.error("EventService.dispatchException() - dispatching exception to the listener: {} Listener: {}",
292                requestId, listener, e);
293        listener.eventError(e.toString());
294    }
295
296    private void dispatchEvent(DSEvent dirEvent, IDSEventListener listener) {
297        listener.entryChanged(dirEvent);
298    }
299
300    private DSEvent createDSEvent(Entry entry, PersistentSearchChangeType changeType, String requestId,
301            IDSEventListener listener) throws Exception {
302        DSEvent dsEvent = new DSEvent();
303
304        logger.message("EventService.createDSEvent() - Notifying event to: {}", listener);
305
306        // Get the dn from the entry
307        String dn = entry.getName().toString();
308        dsEvent.setID(dn);
309
310        // Get information on the type of change made
311        dsEvent.setEventType(changeType.intValue());
312
313        // Pass the search ID as the event's change info
314        dsEvent.setSearchID(requestId);
315
316        // set the object class name
317        String className = entry.getAttribute("objectclass").toString();
318        dsEvent.setClassName(className);
319
320        return dsEvent;
321    }
322
323    private static Collection<String> getDisabledListeners() {
324        List<String> disabledListeners = new ArrayList<>();
325
326        String list = SystemProperties.get(EVENT_LISTENER_DISABLE_LIST, "");
327        logger.message("EventService.getListenerList(): {}: {}", EVENT_LISTENER_DISABLE_LIST, list);
328
329        for (String disabledListener : list.split(",")) {
330            disabledListeners.add(disabledListener.trim());
331        }
332
333        return disabledListeners;
334    }
335
336    private static boolean isDuringConfigurationTime() {
337        return Boolean.parseBoolean(SystemProperties.get(Constants.SYS_PROPERTY_INSTALL_TIME));
338    }
339
340    private static boolean isAMSDKConfigured() {
341        boolean isAMSDKConfigured = false;
342
343        boolean configTime = isDuringConfigurationTime();
344        logger.message("EventService.getListenerList(): {}: {}", Constants.SYS_PROPERTY_INSTALL_TIME, configTime);
345        if (!configTime) {
346            try {
347                ServiceSchemaManager scm = new ServiceSchemaManager(
348                        AccessController.doPrivileged(AdminTokenAction.getInstance()), IdConstants.REPO_SERVICE, "1.0");
349                ServiceSchema idRepoSubSchema = scm.getOrganizationSchema();
350                Set idRepoPlugins = idRepoSubSchema.getSubSchemaNames();
351                if (idRepoPlugins.contains("amSDK")) {
352                    isAMSDKConfigured = true;
353                }
354            } catch (SMSException ex) {
355                logger.warning("EventService.getListenerList() - Unable to obtain idrepo service", ex);
356            } catch (SSOException ex) {
357                // Should not happen, ignore the exception
358            }
359        }
360        return isAMSDKConfigured;
361    }
362
363    private final class EventServicePersistentSearch extends LDAPv3PersistentSearch<IDSEventListener, String> {
364
365        private final SearchResultEntryHandler resultEntryHandler = new PSearchResultEntryHandler();
366
367        public EventServicePersistentSearch(int retryInterval, DN pSearchBaseDN, Filter pSearchFilter,
368                SearchScope pSearchScope, ConnectionFactory factory, String... attributeNames) {
369            super(retryInterval, pSearchBaseDN, pSearchFilter, pSearchScope, factory, attributeNames);
370        }
371
372        @Override
373        protected void clearCaches() {
374            for (IDSEventListener listener : getListeners().keySet()) {
375                listener.allEntriesChanged();
376            }
377        }
378
379        @Override
380        protected void connectionLost() {
381            //this section intentionally left blank
382        }
383
384        @Override
385        protected void connectionReestablished() {
386            //this section intentionally left blank
387        }
388
389        @Override
390        protected SearchResultEntryHandler getSearchResultEntryHandler() {
391            return resultEntryHandler;
392        }
393
394        private final class PSearchResultEntryHandler implements LDAPv3PersistentSearch.SearchResultEntryHandler {
395
396            private final Exception EXCEPTION =
397                    new Exception("EventService - Cannot create NamingEvent, no change control info");
398
399            @Override
400            public boolean handle(SearchResultEntry entry, String dn, DN previousDn, PersistentSearchChangeType type) {
401                for (Map.Entry<IDSEventListener, String> listener : getListeners().entrySet()) {
402                    if (type != null) {
403                        logger.message("EventService.processSearchResultMessage() changeCtrl = {}", type.toString());
404                        // Convert control into a DSEvent and dispatch to listeners
405                        try {
406                            DSEvent event = createDSEvent(entry, type, listener.getValue(), listener.getKey());
407                            dispatchEvent(event, listener.getKey());
408                        } catch (Exception ex) {
409                            dispatchException(ex, listener.getValue(), listener.getKey());
410                        }
411                    } else {
412                        dispatchException(EXCEPTION, listener.getValue(), listener.getKey());
413                    }
414                }
415                return true;
416            }
417        }
418    }
419}