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-2010 Sun Microsystems, Inc.
015 * Portions Copyright 2011-2017 ForgeRock AS.
016 */
017package org.opends.server.backends.pluggable;
018
019import static org.opends.messages.BackendMessages.*;
020import static org.opends.server.util.StaticUtils.*;
021
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.Collections;
025import java.util.List;
026import java.util.Set;
027import java.util.concurrent.ConcurrentHashMap;
028import java.util.concurrent.ConcurrentMap;
029import java.util.concurrent.atomic.AtomicLong;
030
031import org.forgerock.i18n.LocalizableMessage;
032import org.forgerock.i18n.slf4j.LocalizedLogger;
033import org.forgerock.opendj.config.server.ConfigChangeResult;
034import org.forgerock.opendj.config.server.ConfigException;
035import org.forgerock.opendj.ldap.ResultCode;
036import org.forgerock.opendj.config.server.ConfigurationChangeListener;
037import org.forgerock.opendj.server.config.server.PluggableBackendCfg;
038import org.opends.server.api.CompressedSchema;
039import org.opends.server.backends.pluggable.spi.AccessMode;
040import org.opends.server.backends.pluggable.spi.ReadOperation;
041import org.opends.server.backends.pluggable.spi.ReadableTransaction;
042import org.opends.server.backends.pluggable.spi.Storage;
043import org.opends.server.backends.pluggable.spi.StorageRuntimeException;
044import org.opends.server.backends.pluggable.spi.StorageStatus;
045import org.opends.server.backends.pluggable.spi.WriteOperation;
046import org.opends.server.backends.pluggable.spi.WriteableTransaction;
047import org.opends.server.core.DirectoryServer;
048import org.opends.server.core.SearchOperation;
049import org.opends.server.core.ServerContext;
050import org.forgerock.opendj.ldap.DN;
051import org.opends.server.types.DirectoryException;
052import org.opends.server.types.InitializationException;
053import org.opends.server.types.Operation;
054import org.opends.server.types.Privilege;
055
056/**
057 * Wrapper class for a backend "container". Root container holds all the entry
058 * containers for each base DN. It also maintains all the openings and closings
059 * of the entry containers.
060 */
061public class RootContainer implements ConfigurationChangeListener<PluggableBackendCfg>
062{
063  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
064
065  /** The tree storage. */
066  private final Storage storage;
067
068  /** The ID of the backend to which this entry root container belongs. */
069  private final String backendId;
070  /** The backend configuration. */
071  private volatile PluggableBackendCfg config;
072  /** The monitor for this backend. */
073  private BackendMonitor monitor;
074
075  /** The base DNs contained in this root container. */
076  private final ConcurrentMap<DN, EntryContainer> entryContainers = new ConcurrentHashMap<>();
077
078  /** Value of the next entryID to be assigned. */
079  private AtomicLong nextEntryID = new AtomicLong(1);
080
081  /** The compressed schema manager for this backend. */
082  private PersistentCompressedSchema compressedSchema;
083
084  private final ServerContext serverContext;
085
086  /**
087   * Creates a new RootContainer object representing a storage.
088   *
089   * @param backendID
090   *          A reference to the backend that is creating this root
091   *          container.
092   * @param serverContext
093   *          The server context.
094   * @param config
095   *          The configuration of the backend.
096   */
097  RootContainer(String backendID, ServerContext serverContext, Storage storage, PluggableBackendCfg config)
098  {
099    this.backendId = backendID;
100    this.serverContext = serverContext;
101    this.storage = storage;
102    this.config = config;
103
104    getMonitorProvider().enableFilterUseStats(config.isIndexFilterAnalyzerEnabled());
105    getMonitorProvider().setMaxEntries(config.getIndexFilterAnalyzerMaxFilters());
106
107    config.addPluggableChangeListener(this);
108  }
109
110  /**
111   * Returns the underlying storage engine.
112   *
113   * @return the underlying storage engine
114   */
115  Storage getStorage()
116  {
117    return storage;
118  }
119
120  /**
121   * Opens the root container.
122   *
123   * @param accessMode specifies how the container has to be opened (read-write or read-only)
124   *
125   * @throws StorageRuntimeException
126   *           If an error occurs when opening the storage.
127   * @throws ConfigException
128   *           If an configuration error occurs while opening the storage.
129   */
130  void open(final AccessMode accessMode) throws StorageRuntimeException, ConfigException
131  {
132    try
133    {
134      storage.open(accessMode);
135      storage.write(new WriteOperation()
136      {
137        @Override
138        public void run(WriteableTransaction txn) throws Exception
139        {
140          compressedSchema = new PersistentCompressedSchema(serverContext, storage, txn, accessMode);
141          openAndRegisterEntryContainers(txn, config.getBaseDN(), accessMode);
142        }
143      });
144    }
145    catch(StorageRuntimeException e)
146    {
147      throw e;
148    }
149    catch (Exception e)
150    {
151      throw new StorageRuntimeException(e);
152    }
153  }
154
155  /**
156   * Opens the entry container for a base DN. If the entry container does not
157   * exist for the base DN, it will be created. The entry container will be
158   * opened with the same mode as the root container. Any entry containers
159   * opened in a read only root container will also be read only. Any entry
160   * containers opened in a non transactional root container will also be non
161   * transactional.
162   *
163   * @param baseDN
164   *          The base DN of the entry container to open.
165   * @param txn
166   *          The transaction
167   * @param accessMode specifies how the container has to be opened (read-write or read-only)
168   * @return The opened entry container.
169   * @throws StorageRuntimeException
170   *           If an error occurs while opening the entry container.
171   * @throws ConfigException
172   *           If an configuration error occurs while opening the entry container.
173   */
174  EntryContainer openEntryContainer(DN baseDN, WriteableTransaction txn, AccessMode accessMode)
175      throws StorageRuntimeException, ConfigException
176  {
177    EntryContainer ec = new EntryContainer(baseDN, backendId, config, storage, this, serverContext);
178    ec.open(txn, accessMode);
179    return ec;
180  }
181
182  /**
183   * Registers the entry container for a base DN.
184   *
185   * @param baseDN
186   *          The base DN of the entry container to close.
187   * @param entryContainer
188   *          The entry container to register for the baseDN.
189   * @throws InitializationException
190   *           If an error occurs while opening the entry container.
191   */
192  void registerEntryContainer(DN baseDN, EntryContainer entryContainer) throws InitializationException
193  {
194    EntryContainer ec = this.entryContainers.get(baseDN);
195    if (ec != null)
196    {
197      throw new InitializationException(ERR_ENTRY_CONTAINER_ALREADY_REGISTERED.get(ec.getTreePrefix(), baseDN));
198    }
199    this.entryContainers.put(baseDN, entryContainer);
200  }
201
202  /**
203   * Opens the entry containers for multiple base DNs.
204   *
205   * @param baseDNs
206   *          The base DNs of the entry containers to open.
207   * @param accessMode specifies how the containers have to be opened (read-write or read-only)
208   *
209   * @throws StorageRuntimeException
210   *           If an error occurs while opening the entry container.
211   * @throws InitializationException
212   *           If an initialization error occurs while opening the entry
213   *           container.
214   * @throws ConfigException
215   *           If a configuration error occurs while opening the entry
216   *           container.
217   */
218  private void openAndRegisterEntryContainers(WriteableTransaction txn, Set<DN> baseDNs, AccessMode accessMode)
219      throws StorageRuntimeException, InitializationException, ConfigException
220  {
221    EntryID highestID = null;
222    for (DN baseDN : baseDNs)
223    {
224      EntryContainer ec = openEntryContainer(baseDN, txn, accessMode);
225      EntryID id = ec.getHighestEntryID(txn);
226      registerEntryContainer(baseDN, ec);
227      if (highestID == null || id.compareTo(highestID) > 0)
228      {
229        highestID = id;
230      }
231    }
232
233    nextEntryID = new AtomicLong(highestID.longValue() + 1);
234  }
235
236  /**
237   * Unregisters the entry container for a base DN.
238   *
239   * @param baseDN
240   *          The base DN of the entry container to close.
241   * @return The entry container that was unregistered or NULL if a entry
242   *         container for the base DN was not registered.
243   */
244  EntryContainer unregisterEntryContainer(DN baseDN)
245  {
246    return entryContainers.remove(baseDN);
247  }
248
249  /**
250   * Retrieves the compressed schema manager for this backend.
251   *
252   * @return The compressed schema manager for this backend.
253   */
254  CompressedSchema getCompressedSchema()
255  {
256    return compressedSchema;
257  }
258
259  /**
260   * Get the BackendMonitor object used by this root container.
261   *
262   * @return The BackendMonitor object.
263   */
264  BackendMonitor getMonitorProvider()
265  {
266    if (monitor == null)
267    {
268      monitor = new BackendMonitor(backendId + " Storage", this);
269    }
270    return monitor;
271  }
272
273  /**
274   * Preload the tree cache. There is no preload if the configured preload
275   * time limit is zero.
276   *
277   * @param timeLimit
278   *          The time limit for the preload process.
279   */
280  void preload(long timeLimit)
281  {
282    if (timeLimit > 0)
283    {
284      // Get a list of all the tree used by the backend.
285      final List<Tree> trees = new ArrayList<>();
286      for (EntryContainer ec : entryContainers.values())
287      {
288        ec.sharedLock.lock();
289        try
290        {
291          trees.addAll(ec.listTrees());
292        }
293        finally
294        {
295          ec.sharedLock.unlock();
296        }
297      }
298
299      // Sort the list in order of priority.
300      Collections.sort(trees, new TreePreloadComparator());
301
302      // Preload each tree until we reach the time limit or the cache is filled.
303      try
304      {
305        throw new UnsupportedOperationException("Not implemented exception");
306      }
307      catch (StorageRuntimeException e)
308      {
309        logger.error(ERR_CACHE_PRELOAD, backendId,
310            stackTraceToSingleLineString(e.getCause() != null ? e.getCause() : e));
311      }
312    }
313  }
314
315  /**
316   * Closes this root container.
317   *
318   * @throws StorageRuntimeException
319   *           If an error occurs while attempting to close the root container.
320   */
321  void close() throws StorageRuntimeException
322  {
323    for (DN baseDN : entryContainers.keySet())
324    {
325      EntryContainer ec = unregisterEntryContainer(baseDN);
326      ec.exclusiveLock.lock();
327      try
328      {
329        ec.close();
330      }
331      finally
332      {
333        ec.exclusiveLock.unlock();
334      }
335    }
336    config.removePluggableChangeListener(this);
337    if (storage != null)
338    {
339      storage.close();
340    }
341  }
342
343  /**
344   * Return all the entry containers in this root container.
345   *
346   * @return The entry containers in this root container.
347   */
348  public Collection<EntryContainer> getEntryContainers()
349  {
350    return entryContainers.values();
351  }
352
353  /**
354   * Returns all the baseDNs this root container stores.
355   *
356   * @return The set of DNs this root container stores.
357   */
358  Set<DN> getBaseDNs()
359  {
360    return entryContainers.keySet();
361  }
362
363  /**
364   * Return the entry container for a specific base DN.
365   *
366   * @param baseDN
367   *          The base DN of the entry container to retrieve.
368   * @return The entry container for the base DN.
369   */
370  EntryContainer getEntryContainer(DN baseDN)
371  {
372    EntryContainer ec = null;
373    DN nodeDN = baseDN;
374
375    while (ec == null && nodeDN != null)
376    {
377      ec = entryContainers.get(nodeDN);
378      if (ec == null)
379      {
380        nodeDN = DirectoryServer.getParentDNInSuffix(nodeDN);
381      }
382    }
383
384    return ec;
385  }
386
387  /**
388   * Get the total number of entries in this root container.
389   *
390   * @return The number of entries in this root container
391   * @throws StorageRuntimeException
392   *           If an error occurs while retrieving the entry count.
393   */
394  long getEntryCount() throws StorageRuntimeException
395  {
396    try
397    {
398      return storage.read(new ReadOperation<Long>()
399      {
400        @Override
401        public Long run(ReadableTransaction txn) throws Exception
402        {
403          long entryCount = 0;
404          for (EntryContainer ec : entryContainers.values())
405          {
406            ec.sharedLock.lock();
407            try
408            {
409              entryCount += ec.getNumberOfEntriesInBaseDN0(txn);
410            }
411            finally
412            {
413              ec.sharedLock.unlock();
414            }
415          }
416          return entryCount;
417        }
418      });
419    }
420    catch (Exception e)
421    {
422      throw new StorageRuntimeException(e);
423    }
424  }
425
426  /**
427   * Assign the next entry ID.
428   *
429   * @return The assigned entry ID.
430   */
431  EntryID getNextEntryID()
432  {
433    return new EntryID(nextEntryID.getAndIncrement());
434  }
435
436  /** Resets the next entry ID counter to zero. This should only be used after clearing all trees. */
437  public void resetNextEntryID()
438  {
439    nextEntryID.set(1);
440  }
441
442  @Override
443  public boolean isConfigurationChangeAcceptable(PluggableBackendCfg configuration,
444      List<LocalizableMessage> unacceptableReasons)
445  {
446    // Storage has also registered a change listener, delegate to it.
447    return true;
448  }
449
450  @Override
451  public ConfigChangeResult applyConfigurationChange(PluggableBackendCfg configuration)
452  {
453    config = configuration;
454    getMonitorProvider().enableFilterUseStats(config.isIndexFilterAnalyzerEnabled());
455    getMonitorProvider().setMaxEntries(config.getIndexFilterAnalyzerMaxFilters());
456
457    return new ConfigChangeResult();
458  }
459
460  /**
461   * Checks the storage has enough resources for an operation.
462   *
463   * @param operation the current operation
464   * @throws DirectoryException if resources are in short supply
465   */
466  void checkForEnoughResources(Operation operation) throws DirectoryException
467  {
468    StorageStatus status = storage.getStorageStatus();
469    if (status.isUnusable()
470        || (status.isLockedDown() && hasBypassLockdownPrivileges(operation)))
471    {
472      final DirectoryException e =
473        new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_NOT_ENOUGH_RESOURCES.get());
474      e.setMaskedMessage(status.getReason());
475      throw e;
476    }
477  }
478
479  private boolean hasBypassLockdownPrivileges(Operation operation)
480  {
481    return operation != null
482          // Read operations are always allowed in lock down mode
483          && !(operation instanceof SearchOperation)
484          && !operation.getClientConnection().hasPrivilege(
485              Privilege.BYPASS_LOCKDOWN, operation);
486  }
487}