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