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 2006-2008 Sun Microsystems, Inc.
015 * Portions Copyright 2013-2016 ForgeRock AS.
016 */
017package org.opends.server.core;
018
019import java.util.*;
020
021import org.forgerock.i18n.LocalizableMessage;
022import org.forgerock.i18n.slf4j.LocalizedLogger;
023import org.forgerock.opendj.ldap.ByteString;
024import org.forgerock.util.Utils;
025import org.forgerock.opendj.config.ClassPropertyDefinition;
026import org.forgerock.opendj.config.server.ConfigurationAddListener;
027import org.forgerock.opendj.config.server.ConfigurationChangeListener;
028import org.forgerock.opendj.config.server.ConfigurationDeleteListener;
029import org.forgerock.opendj.server.config.meta.EntryCacheCfgDefn;
030import org.forgerock.opendj.server.config.server.EntryCacheCfg;
031import org.forgerock.opendj.server.config.server.EntryCacheMonitorProviderCfg;
032import org.forgerock.opendj.server.config.server.RootCfg;
033import org.opends.server.api.EntryCache;
034import org.opends.server.config.ConfigConstants;
035import org.opends.server.types.Entry;
036import org.forgerock.opendj.config.server.ConfigException;
037import org.opends.server.extensions.DefaultEntryCache;
038import org.opends.server.monitors.EntryCacheMonitorProvider;
039import org.forgerock.opendj.config.server.ConfigChangeResult;
040import org.forgerock.opendj.ldap.DN;
041import org.opends.server.types.InitializationException;
042
043import static org.opends.messages.ConfigMessages.*;
044import static org.opends.server.util.StaticUtils.*;
045
046/**
047 * This class defines a utility that will be used to manage the configuration
048 * for the Directory Server entry cache.  The default entry cache is always
049 * enabled.
050 */
051public class EntryCacheConfigManager
052       implements
053          ConfigurationChangeListener <EntryCacheCfg>,
054          ConfigurationAddListener    <EntryCacheCfg>,
055          ConfigurationDeleteListener <EntryCacheCfg>
056{
057  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
058
059  /** The default entry cache. */
060  private DefaultEntryCache _defaultEntryCache;
061
062  /** The entry cache order map sorted by the cache level. */
063  @SuppressWarnings("rawtypes")
064  private SortedMap<Integer, EntryCache> cacheOrderMap = new TreeMap<>();
065
066  /** The entry cache to level map. */
067  private Map<DN,Integer> cacheNameToLevelMap = new HashMap<>();
068
069  /** Global entry cache monitor provider name. */
070  private static final String
071    DEFAULT_ENTRY_CACHE_MONITOR_PROVIDER = "Entry Caches";
072
073  private final ServerContext serverContext;
074
075  /**
076   * Creates a new instance of this entry cache config manager.
077   *
078   * @param serverContext
079   *          The server context.
080   */
081  public EntryCacheConfigManager(ServerContext serverContext)
082  {
083    this.serverContext = serverContext;
084  }
085
086  /**
087   * Initializes the default entry cache.
088   * This should only be called at Directory Server startup.
089   *
090   * @throws  InitializationException  If a problem occurs while trying to
091   *                                   install the default entry cache.
092   */
093  public void initializeDefaultEntryCache()
094         throws InitializationException
095  {
096    try
097    {
098      DefaultEntryCache defaultCache = new DefaultEntryCache();
099      defaultCache.initializeEntryCache(null);
100      DirectoryServer.setEntryCache(defaultCache);
101      _defaultEntryCache = defaultCache;
102    }
103    catch (Exception e)
104    {
105      logger.traceException(e);
106
107      LocalizableMessage message = ERR_CONFIG_ENTRYCACHE_CANNOT_INSTALL_DEFAULT_CACHE.get(
108          stackTraceToSingleLineString(e));
109      throw new InitializationException(message, e);
110    }
111  }
112
113  /**
114   * Initializes the configuration associated with the Directory Server entry
115   * cache.  This should only be called at Directory Server startup.  If an
116   * error occurs, then a message will be logged for each entry cache that is
117   * failed to initialize.
118   *
119   * @throws  ConfigException  If a configuration problem causes the entry
120   *                           cache initialization process to fail.
121   */
122  public void initializeEntryCache()
123         throws ConfigException
124  {
125    // Default entry cache should be already installed with
126    // <CODE>initializeDefaultEntryCache()</CODE> method so
127    // that there will be one even if we encounter a problem later.
128
129    RootCfg rootConfiguration = serverContext.getRootConfig();
130    rootConfiguration.addEntryCacheAddListener(this);
131    rootConfiguration.addEntryCacheDeleteListener(this);
132
133    // Get the base entry cache configuration entry.
134    Entry entryCacheBase;
135    try {
136      DN configEntryDN = DN.valueOf(ConfigConstants.DN_ENTRY_CACHE_BASE);
137      entryCacheBase   = DirectoryServer.getConfigEntry(configEntryDN);
138    } catch (Exception e) {
139      logger.traceException(e);
140
141      logger.warn(WARN_CONFIG_ENTRYCACHE_NO_CONFIG_ENTRY);
142      return;
143    }
144
145    // If the configuration base entry is null, then assume it doesn't exist.
146    // At least that entry must exist in the configuration, even if there are
147    // no entry cache defined below it.
148    if (entryCacheBase == null)
149    {
150      logger.error(WARN_CONFIG_ENTRYCACHE_NO_CONFIG_ENTRY);
151      return;
152    }
153
154    // Initialize every entry cache configured.
155    for (String cacheName : rootConfiguration.listEntryCaches())
156    {
157      // Get the entry cache configuration.
158      EntryCacheCfg configuration = rootConfiguration.getEntryCache(cacheName);
159
160      // At this point, we have a configuration entry. Register a change
161      // listener with it so we can be notified of changes to it over time.
162      configuration.addChangeListener(this);
163
164      // Check if there is another entry cache installed at the same level.
165      if (!cacheOrderMap.isEmpty()
166          && cacheOrderMap.containsKey(configuration.getCacheLevel()))
167      {
168        // Log error and skip this cache.
169        logger.error(ERR_CONFIG_ENTRYCACHE_CONFIG_LEVEL_NOT_ACCEPTABLE,
170            configuration.dn(), configuration.getCacheLevel());
171        continue;
172      }
173
174      // Initialize the entry cache.
175      if (configuration.isEnabled()) {
176        // Load the entry cache implementation class and install the entry
177        // cache with the server.
178        String className = configuration.getJavaClass();
179        try {
180          loadAndInstallEntryCache(className, configuration);
181        } catch (InitializationException ie) {
182          logger.error(ie.getMessageObject());
183        }
184      }
185    }
186  }
187
188  @Override
189  public boolean isConfigurationChangeAcceptable(
190      EntryCacheCfg configuration,
191      List<LocalizableMessage> unacceptableReasons
192      )
193  {
194    // returned status -- all is fine by default
195    boolean status = true;
196
197    // Get the name of the class and make sure we can instantiate it as an
198    // entry cache.
199    String className = configuration.getJavaClass();
200    try {
201      // Load the class but don't initialize it.
202      loadEntryCache(className, configuration, false);
203    } catch (InitializationException ie) {
204      unacceptableReasons.add(ie.getMessageObject());
205      status = false;
206    }
207
208    if (!cacheOrderMap.isEmpty() && !cacheNameToLevelMap.isEmpty())
209    {
210      final ByteString normDN = configuration.dn().toNormalizedByteString();
211      if (cacheNameToLevelMap.containsKey(normDN)) {
212        int currentCacheLevel = cacheNameToLevelMap.get(normDN);
213
214        // Check if there any existing cache at the same level.
215        if (currentCacheLevel != configuration.getCacheLevel() &&
216          cacheOrderMap.containsKey(configuration.getCacheLevel())) {
217          unacceptableReasons.add(
218            ERR_CONFIG_ENTRYCACHE_CONFIG_LEVEL_NOT_ACCEPTABLE.get(
219              configuration.dn(), configuration.getCacheLevel()));
220          status = false;
221        }
222      }
223    }
224
225    return status;
226  }
227
228  @Override
229  public ConfigChangeResult applyConfigurationChange(
230      EntryCacheCfg configuration
231      )
232  {
233    EntryCache<? extends EntryCacheCfg> entryCache = null;
234
235    // If we this entry cache is already installed and active it
236    // should be present in the cache maps, if so use it.
237    if (!cacheOrderMap.isEmpty() && !cacheNameToLevelMap.isEmpty()) {
238      final DN dn = configuration.dn();
239      if (cacheNameToLevelMap.containsKey(dn))
240      {
241        int currentCacheLevel = cacheNameToLevelMap.get(dn);
242        entryCache = cacheOrderMap.get(currentCacheLevel);
243
244        // Check if the existing cache just shifted its level.
245        if (currentCacheLevel != configuration.getCacheLevel()) {
246          // Update the maps then.
247          cacheOrderMap.remove(currentCacheLevel);
248          cacheOrderMap.put(configuration.getCacheLevel(), entryCache);
249          cacheNameToLevelMap.put(dn, configuration.getCacheLevel());
250        }
251      }
252    }
253
254    final ConfigChangeResult changeResult = new ConfigChangeResult();
255
256    // If an entry cache was installed then remove it.
257    if (!configuration.isEnabled())
258    {
259      configuration.getCacheLevel();
260      if (entryCache != null)
261      {
262        EntryCacheMonitorProvider monitor = entryCache.getEntryCacheMonitor();
263        if (monitor != null)
264        {
265          DirectoryServer.deregisterMonitorProvider(monitor);
266          monitor.finalizeMonitorProvider();
267          entryCache.setEntryCacheMonitor(null);
268        }
269        entryCache.finalizeEntryCache();
270        cacheOrderMap.remove(configuration.getCacheLevel());
271        entryCache = null;
272      }
273      return changeResult;
274    }
275
276    // Push any changes made to the cache order map.
277    setCacheOrder(cacheOrderMap);
278
279    // At this point, new configuration is enabled...
280    // If the current entry cache is already enabled then we don't do
281    // anything unless the class has changed in which case we should
282    // indicate that administrative action is required.
283    String newClassName = configuration.getJavaClass();
284    if ( entryCache != null)
285    {
286      String curClassName = entryCache.getClass().getName();
287      boolean classIsNew = !newClassName.equals(curClassName);
288      if (classIsNew)
289      {
290        changeResult.setAdminActionRequired (true);
291      }
292      return changeResult;
293    }
294
295    // New entry cache is enabled and there were no previous one.
296    // Instantiate the new class and initialize it.
297    try
298    {
299      loadAndInstallEntryCache (newClassName, configuration);
300    }
301    catch (InitializationException ie)
302    {
303      changeResult.addMessage (ie.getMessageObject());
304      changeResult.setResultCode (DirectoryServer.getServerErrorResultCode());
305      return changeResult;
306    }
307
308    return changeResult;
309  }
310
311  @Override
312  public boolean isConfigurationAddAcceptable(
313      EntryCacheCfg configuration,
314      List<LocalizableMessage> unacceptableReasons
315      )
316  {
317    // returned status -- all is fine by default
318    // Check if there is another entry cache installed at the same level.
319    if (!cacheOrderMap.isEmpty()
320        && cacheOrderMap.containsKey(configuration.getCacheLevel()))
321    {
322      unacceptableReasons.add(ERR_CONFIG_ENTRYCACHE_CONFIG_LEVEL_NOT_ACCEPTABLE.get(
323          configuration.dn(), configuration.getCacheLevel()));
324      return false;
325    }
326
327    if (configuration.isEnabled())
328    {
329      // Get the name of the class and make sure we can instantiate it as
330      // an entry cache.
331      String className = configuration.getJavaClass();
332      try
333      {
334        // Load the class but don't initialize it.
335        loadEntryCache(className, configuration, false);
336      }
337      catch (InitializationException ie)
338      {
339        unacceptableReasons.add (ie.getMessageObject());
340        return false;
341      }
342    }
343
344    return true;
345  }
346
347  @Override
348  public ConfigChangeResult applyConfigurationAdd(EntryCacheCfg configuration)
349  {
350    final ConfigChangeResult changeResult = new ConfigChangeResult();
351
352    // Register a change listener with it so we can be notified of changes
353    // to it over time.
354    configuration.addChangeListener(this);
355
356    if (configuration.isEnabled())
357    {
358      // Instantiate the class as an entry cache and initialize it.
359      String className = configuration.getJavaClass();
360      try
361      {
362        loadAndInstallEntryCache (className, configuration);
363      }
364      catch (InitializationException ie)
365      {
366        changeResult.addMessage (ie.getMessageObject());
367        changeResult.setResultCode (DirectoryServer.getServerErrorResultCode());
368        return changeResult;
369      }
370    }
371
372    return changeResult;
373  }
374
375  @Override
376  public boolean isConfigurationDeleteAcceptable(
377      EntryCacheCfg configuration,
378      List<LocalizableMessage> unacceptableReasons
379      )
380  {
381    // If we've gotten to this point, then it is acceptable as far as we are
382    // concerned.  If it is unacceptable according to the configuration, then
383    // the entry cache itself will make that determination.
384    return true;
385  }
386
387  @Override
388  public ConfigChangeResult applyConfigurationDelete(
389      EntryCacheCfg configuration
390      )
391  {
392    EntryCache<? extends EntryCacheCfg> entryCache = null;
393
394    // If we this entry cache is already installed and active it
395    // should be present in the current cache order map, use it.
396    if (!cacheOrderMap.isEmpty()) {
397      entryCache = cacheOrderMap.get(configuration.getCacheLevel());
398    }
399
400    final ConfigChangeResult changeResult = new ConfigChangeResult();
401
402    // If the entry cache was installed then remove it.
403    if (entryCache != null)
404    {
405      EntryCacheMonitorProvider monitor = entryCache.getEntryCacheMonitor();
406      if (monitor != null)
407      {
408        DirectoryServer.deregisterMonitorProvider(monitor);
409        monitor.finalizeMonitorProvider();
410        entryCache.setEntryCacheMonitor(null);
411      }
412      entryCache.finalizeEntryCache();
413      cacheOrderMap.remove(configuration.getCacheLevel());
414      cacheNameToLevelMap.remove(configuration.dn().toNormalizedByteString());
415
416      // Push any changes made to the cache order map.
417      setCacheOrder(cacheOrderMap);
418
419      entryCache = null;
420    }
421
422    return changeResult;
423  }
424
425  /**
426   * Loads the specified class, instantiates it as an entry cache,
427   * and optionally initializes that instance. Any initialize entry
428   * cache is registered in the server.
429   *
430   * @param  className      The fully-qualified name of the entry cache
431   *                        class to load, instantiate, and initialize.
432   * @param  configuration  The configuration to use to initialize the
433   *                        entry cache, or {@code null} if the
434   *                        entry cache should not be initialized.
435   *
436   * @throws  InitializationException  If a problem occurred while attempting
437   *                                   to initialize the entry cache.
438   */
439  private void loadAndInstallEntryCache(
440    String        className,
441    EntryCacheCfg configuration
442    )
443    throws InitializationException
444  {
445    // Load the entry cache class...
446    EntryCache<? extends EntryCacheCfg> entryCache =
447      loadEntryCache (className, configuration, true);
448
449    // ... and install the entry cache in the server.
450
451    // Add this entry cache to the current cache config maps.
452    cacheOrderMap.put(configuration.getCacheLevel(), entryCache);
453    cacheNameToLevelMap.put(configuration.dn(), configuration.getCacheLevel());
454
455    // Push any changes made to the cache order map.
456    setCacheOrder(cacheOrderMap);
457
458    // Install and register the monitor for this cache.
459    EntryCacheMonitorProvider monitor =
460        new EntryCacheMonitorProvider(configuration.dn().
461            rdn().getFirstAVA().getAttributeValue().toString(), entryCache);
462    try {
463      RootCfg rootConfiguration = serverContext.getRootConfig();
464      monitor.initializeMonitorProvider((EntryCacheMonitorProviderCfg)
465        rootConfiguration.getMonitorProvider(DEFAULT_ENTRY_CACHE_MONITOR_PROVIDER));
466    } catch (ConfigException ce) {
467      // ConfigException here means that either the entry cache monitor
468      // config entry is not present or the monitor is not enabled. In
469      // either case that means no monitor provider for this cache.
470      return;
471    }
472    entryCache.setEntryCacheMonitor(monitor);
473    DirectoryServer.registerMonitorProvider(monitor);
474  }
475
476  @SuppressWarnings({ "rawtypes", "unchecked" })
477  private void setCacheOrder(SortedMap<Integer, EntryCache> cacheOrderMap)
478  {
479    _defaultEntryCache.setCacheOrder((SortedMap) cacheOrderMap);
480  }
481
482  /**
483   * Loads the specified class, instantiates it as an entry cache, and
484   * optionally initializes that instance.
485   *
486   * @param  className      The fully-qualified name of the entry cache class
487   *                        to load, instantiate, and initialize.
488   * @param  configuration  The configuration to use to initialize the entry
489   *                        cache.  It must not be {@code null}.
490   * @param  initialize     Indicates whether the entry cache instance should be
491   *                        initialized.
492   *
493   * @return  The possibly initialized entry cache.
494   *
495   * @throws  InitializationException  If a problem occurred while attempting
496   *                                   to initialize the entry cache.
497   */
498  private <T extends EntryCacheCfg> EntryCache<T> loadEntryCache(
499    String        className,
500    T configuration,
501    boolean initialize
502    )
503    throws InitializationException
504  {
505    // If we this entry cache is already installed and active it
506    // should be present in the current cache order map, use it.
507    EntryCache<T> entryCache = null;
508    if (!cacheOrderMap.isEmpty()) {
509      entryCache = cacheOrderMap.get(configuration.getCacheLevel());
510    }
511
512    try
513    {
514      EntryCacheCfgDefn definition = EntryCacheCfgDefn.getInstance();
515      ClassPropertyDefinition propertyDefinition = definition
516          .getJavaClassPropertyDefinition();
517      @SuppressWarnings("unchecked")
518      Class<? extends EntryCache<T>> cacheClass =
519          (Class<? extends EntryCache<T>>) propertyDefinition
520              .loadClass(className, EntryCache.class);
521
522      // If there is some entry cache instance already initialized work with
523      // it instead of creating a new one unless explicit init is requested.
524      EntryCache<T> cache;
525      if (initialize || entryCache == null) {
526        cache = cacheClass.newInstance();
527      } else {
528        cache = entryCache;
529      }
530
531      if (initialize)
532      {
533        cache.initializeEntryCache(configuration);
534      }
535      // This will check if configuration is acceptable on disabled
536      // and uninitialized cache instance that has no "acceptable"
537      // change listener registered to invoke and verify on its own.
538      else if (!configuration.isEnabled())
539      {
540        List<LocalizableMessage> unacceptableReasons = new ArrayList<>();
541        if (!cache.isConfigurationAcceptable(configuration, unacceptableReasons))
542        {
543          String buffer = Utils.joinAsString(".  ", unacceptableReasons);
544          throw new InitializationException(
545              ERR_CONFIG_ENTRYCACHE_CONFIG_NOT_ACCEPTABLE.get(configuration.dn(), buffer));
546        }
547      }
548
549      return cache;
550    }
551    catch (Exception e)
552    {
553      logger.traceException(e);
554
555      if (!initialize) {
556        if (e instanceof InitializationException) {
557          throw (InitializationException) e;
558        } else {
559          LocalizableMessage message = ERR_CONFIG_ENTRYCACHE_CONFIG_NOT_ACCEPTABLE.get(
560            configuration.dn(), e.getCause() != null ?
561              e.getCause().getMessage() : stackTraceToSingleLineString(e));
562          throw new InitializationException(message);
563        }
564      }
565      LocalizableMessage message = ERR_CONFIG_ENTRYCACHE_CANNOT_INITIALIZE_CACHE.get(
566        className, e.getCause() != null ? e.getCause().getMessage() :
567          stackTraceToSingleLineString(e));
568      throw new InitializationException(message, e);
569    }
570  }
571}